Первую в мире покупку по штрихкоду относят к 26 июня 1974 года – это была упаковка жевательной резинки в одном из супермаркетов США. Считывая информацию со штрихкода, по различным оценкам, можно ускорить операции с товарами в среднем на 30%. Сейчас штрихкоды сканируют и продавцы, и работники склада, и покупатели – например, если они хотят сделать покупку на кассе самообслуживания.
В статье рассмотрим некоторые особенности распознавания штрихкодов с помощью библиотеки ML Kit. Материал может быть полезен как начинающим разработчикам с базовыми навыками, так и опытным специалистам, которые хотят изучить новый инструмент.
ML Kit – это бесплатный мобильный SDK от Google, который позволяет использовать машинное обучение на устройствах с операционными системами Android, iOS и Flutter. В мобильной разработке это, пожалуй, простейший способ для добавления нейронных сетей в приложение. В свою очередь, это позволяет упростить реализацию некоторых функций.
Ключевые возможности ML Kit:
• Распознавание текста (в том числе и рукописного)
• Перевод текста между языками (офлайн)
• Распознавание лиц (и эмоций)
• Распознавание объектов
А также менее известные:
• Распознавание поз (определяет местоположение головы)
• Сканирование штрихкодов
Такие функции могут быть полезны во многих приложениях, например, в туристических гидах – для перевода вывесок и указателей и вывода информации о достопримечательностях. Как пример, мы однажды участвовали в создании приложения, в котором туристы могли сфотографировать и распознать данные, чтобы не вводить их вручную.
Итак, перейдем к практике работы с ML Kit. В одном из проектов у нашего партнера была потребность заменить библиотеку для сканирования штрихкодов. Ранее заказчик использовал платную библиотеку Scandit и столкнулся с некоторыми ограничениями. На тот момент, в частности, требовалось выводить логотип библиотеки на экран сканирования кодов. Также лицензионное соглашение не исключало возможности того, что производитель может отозвать лицензию. В качестве альтернативного решения команда разработки выбрала ML Kit Barcode scanning.
Пример работы barcode scanning (Android)
Прежде всего, перед началом работы с ML Kit необходимо подключить необходимые библиотеки в gradle:
implementation 'com.google.mlkit:barcode-scanning:17.0.0'
Или же:
implementation 'com.google.android.gms:play-services-mlkit-barcode-scanning:16.2.1'
А также в manifest:
<meta-data
android:name="com.google.mlkit.vision.DEPENDENCIES"
android:value="barcode" />
В первом случае все необходимое добавляется в установочный файл, а во втором динамически скачивается. Также в первом случае можно рассчитывать на чуть большую производительность.
Далее необходимо подготовить Detector. Это основной интерфейс в ML Kit, имеющий важнейшие методы process и close:
process производит всю обработку изображений и возвращает результаты, которые зависят от конкретной реализации интерфейса;
с помощью сlose мы высвобождаем занятые ресурсы.
Рассмотрим процесс подготовки BarcodeScanner – одного из наследников Detector:
val detector = BarcodeScanning.getClient(
BarcodeScannerOptions.Builder()
.setBarcodeFormats(Barcode.FORMAT_QR_CODE)
.build()
)
BarcodeScanning – вариация порождающего паттерна. На вход единственного метода getClient принимает параметры нужного объекта, на выходе выдает экземпляр BarcodeScanner. В свою очередь BarcodeScannerOptions создается через стандартный Builder. В данном случае мы указали, что желаем распознавать только QR коды. Этот подход относится и к остальным функциям ML Kit.
После этого можно использовать данный Detector, ниже простой пример:
detector.process(image).addOnSuccessListener { barcodes ->
barcodes.firstOrNull()?.let {
Toast.makeText(context, it.rawValue, Toast.LENGTH_SHORT).show()
}
}
Возможные трудности
1) Realtime
Хотя ML Kit достаточно удобен в использовании, мы обнаружили некоторые «подводные камни». Основные вопросы оказались связаны с работой в режиме realtime. Во время реализации проекта у нас не было официальных примеров, поэтому мы изучали неофициальные примеры, и в некоторых из них были ошибки.
Так, первоначально мы рассматривали следующий пример (особенности получения данных с камеры рассмотрим ниже).
private var imageAnalysis = ImageAnalysis.Analyzer { imageProxy ->
val image = imageProxy.image ?: return@Analyzer
val inputImage = InputImage.fromMediaImage(imageProxy.image!!, imageProxy.imageInfo.rotationDegrees)
detector?.process(inputImage)?.addOnSuccessListener { barcodes ->
barcodes.firstOrNull()?.let {
Toast.makeText(context, it.rawValue, Toast.LENGTH_SHORT).show()
}
}
imageProxy.close()
}
Хотя на большинстве устройств этот способ работал, на менее мощных он приводил к переполнению памяти. Так как detector обрабатывает кадры не мгновенно, был риск серьезной утечки памяти. Например, если каждое фото “весит” по 2 мегабайта, а в памяти одновременно находится несколько сотен кадров, это приведет к крашу приложения.
Изучив документацию ImageAnalysis, мы выяснили, что одна из причин вызова imageProxy.close() – необходимость сообщить системе о том, что приложение готово к обработке следующего кадра.
В результате мы изменили код следующим образом:
private var imageAnalysis = ImageAnalysis.Analyzer { imageProxy ->
val image = imageProxy.image ?: return@Analyzer
val inputImage = InputImage.fromMediaImage(imageProxy.image!!, imageProxy.imageInfo.rotationDegrees)
detector?.process(inputImage)?.addOnSuccessListener { barcodes ->
barcodes.firstOrNull()?.let {
Toast.makeText(context, it.rawValue, Toast.LENGTH_SHORT).show()
}
}?.addOnCompleteListener {
imageProxy.close()
}
}
При такой реализации в памяти всегда находился только один кадр, и проблема с крашем на малопроизводительных устройствах была решена.
2) Адаптация
Также одной из наших задач была адаптация ML Kit к потребностям конкретного проекта. В частности, предыдущая библиотека умела обрабатывать как черно-белые, так и бело-черные штрихкоды. В свою очередь, ML Kit на старте работы негативы не понимал.
Для решения проблемы мы изменили код. Предыдущий вариант:
InputImage.fromMediaImage(imageProxy.image!!, imageProxy.imageInfo.rotationDegrees)
Новый вариант стал более сложным, с предварительной обработкой:
private fun getByteDataFromImage(image: Image): ByteArray? {
var data: ByteArray? = null
val planes: Array<Image.Plane> = image.planes
if (planes.isNotEmpty()) {
val buffer: ByteBuffer = planes[0].buffer
data = ByteArray(buffer.remaining())
buffer.get(data)
}
return data
}
getByteDataFromImage(image)?.let { byteArray ->
processImage(byteArray, imageProxy)
val inverted = ByteArray(byteArray.size)
byteArray.forEachIndexed { index, byte ->
inverted[index] = byte.inv()
}
processImage(inverted, imageProxy)
}
Здесь мы получали картинку как массив байтов и разделяли ее на позитив и негатив, которые отправляли по отдельности в detector.
3) CameraX
Ещё один баг, с которым мы столкнулись, касался неправильного использования разрешения в CameraX. Мы ставили максимальное разрешение 1920x1080.
val camProfile = CamcorderProfile.get(CamcorderProfile.QUALITY_1080P)
analysisBuilder.setTargetResolution(Size(camProfile.videoFrameWidth,
camProfile.videoFrameHeight))
Однако, в CameraX на выходе получались дефолтные 320x640. Мы выяснили, что порядок width и height зависит от ориентации, и для “портретного” вывода в нашем случае нужно было следующее:
val camProfile = CamcorderProfile.get(CamcorderProfile.QUALITY_1080P)
analysisBuilder.setTargetResolution(Size(camProfile.videoFrameHeight,
camProfile.videoFrameWidth))
Заключение
После настройки и внедрения в проект мы убедились, что ML Kit соответствует потребностям приложения и может заменить предыдущую библиотеку. В некоторых случаях платная библиотека была эффективнее, например, на небольшой доле «смазанных» штрихкодов. В свою очередь, библиотека ML Kit полностью бесплатная и не требует добавления своего логотипа на экран сканирования. В результате после тестирования владелец приложения решил полностью перейти на ML Kit.
Спасибо за внимание! Надеемся, что материал был вам полезен.