Работа с ML Kit: как распознавать штрихкоды

Первую в мире покупку по штрихкоду относят к 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.

Спасибо за внимание! Надеемся, что материал был вам полезен.