Привет, меня зовут Виталий Беляев, я Android-разработчик в red_mad_robot. В этой статье я расскажу про опыт интеграции CameraX с ML Kit на замену библиотеки card.io, и что в итоге из этого получилось.
В приложении над которым я работаю, есть экран добавления банковской карты. Там можно заполнить всю информацию руками, а можно нажать «Сканировать», и с помощью камеры телефона распознать номер карты. Для этого у нас используется библиотека card.io.
Почему мы решили заменить card.io?
мы хотели заменить third-party library, которая уже находится в архиве, на что-то более актуальное от крупных компаний;
card.io
использует подход с созданием отдельной activity, а мы стараемся придерживаться single-activity подхода;мало возможностей кастомизации UI в
card.io
;интересно было попробовать
CameraX
иML Kit
;card.io
тянет много нативных библиотек. Если вы не используете App Bundle, то выпиливаниеcard.io
уменьшит ваш APK на 12 MB в размере.
Сравнение размеров проводилось на sample-проекте
Что такое ML Kit?
Сразу уточню, что такое ML Kit
. По сути это библиотека, которая предоставляет API для использования ML под разные задачи, такие как маркировка изображений, считывание штрихкода, распознавание текста, лиц, объектов, перевод текста, text to speech и так далее.
Всё это делается с помощью обученных моделей и может происходить как локально(on-device), так и удаленно на сервере(on-cloud).
И у Google, и у Huawei есть свой ML Kit
, которые очень похожи. Google ML Kit
зависит от GMS, а Huawei ML Kit
, соответственно, зависит от HMS.
Для задачи по распознаванию номера банковской карты нам подходит та часть ML Kit
, что связана с распознаванием текста. В обоих ML Kit
она называется Text Recognition
. В обоих ML Kit
данный Text Recogniton
может работать локально(on-device).
Используя on-device Text Recognition
мы получаем более высокую скорость работы, независимость от наличия интернета и отсутствие платы за использование, по сравнению с on-cloud решением.
В качестве входа, Text Recognition
принимает изображение, которое он обрабатывает и затем выдаёт результат в виде текста, который он распознал. Чтобы обеспечить Text Recognition
входными данными, нам нужно получить эти изображения(фреймы) с камеры устройства.
Получаем фреймы с камеры для анализа
Для этой задачи нам необходимо работать с Camera API, чтобы показывать preview и передавать с него фреймы на анализ в ML Kit
.
Google сделал CameraX — библиотеку для работы с камерой, часть Jetpack, которая инкапсулирует в себе работу с Camera1
и Camera2
API и предоставляет удобный lifecycle-aware интерфейс для работы с камерой.
В CameraX
есть так называемые use cases, их всего три:
ImageAnalysis
Preview
ImageCapture
По названию нетрудно догадаться, что и зачем используется. Нас интересуют Preview
и ImageAnalysis
.
Делаем настройку:
val preview = Preview.Builder()
.setTargetRotation(Surface.ROTATION_0)
.setTargetAspectRatio(screenAspectRatio)
.build()
.also { it.setSurfaceProvider(binding.cameraPreview.surfaceProvider) }
val imageAnalyzer = ImageAnalysis.Builder()
.setTargetRotation(Surface.ROTATION_0)
.setTargetAspectRatio(screenAspectRatio)
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
.also { it.setAnalyzer(cameraExecutor, framesAnalyzer) }
Не будем вдаваться в подробности каждой строчки, про это можно почитать хорошую документацию по CameraX
или пройти codelab. Сейчас же мы конфигурируем use cases
, и стоит отметить, что это довольно удобно и компактно выглядит.
Далее мы всё это привязываем к lifecycle и запускаем.
try {
cameraProvider.unbindAll()
camera = cameraProvider.bindToLifecycle(
viewLifecycleOwner,
CameraSelector.DEFAULT_BACK_CAMERA,
useCaseGroup
)
setupCameraMenuIcons()
} catch (t: Throwable) {
Timber.e(t, "Camera use cases binding failed")
}
Здесь мы получаем так называемый cameraProvider
— часть CameraX
интерфейса. Затем один раз выполняем bindToLifecycle
и всё. Далее, когда приложение уходит в background, CameraX
сама обрабатывает эти ситуации и релизит камеру, а когда приложение возвращается в foreground, запускает наши use cases
. И это очень круто: те, кто хоть раз сталкивался с Camera1
/Camera2
API, меня поймут.
При создании ImageAnalysis
use case мы передали ему framesAnalyzer
— это тоже сущность из CameraX
, по сути, это просто SAM-интерфейс ImageAnalysis.Analyzer
с одним методом analyze()
, в котором нам приходит картинка в виде ImageProxy
.
private val framesAnalyzer: ImageAnalysis.Analyzer by lazy {
ImageAnalysis.Analyzer(viewModel::onFrameReceived)
}
Вот таким образом мы получили картинку, которую можно передавать в ML Kit
на распознавание.
GMS ML Kit
У Google раньше была библиотека ML Kit for Firebase, где были собраны все ML-related вещи: те, что работают on-device (сканирование штрихкодов например) и те, что работают on-cloud (Image Labeling например).
Потом они вынесли все те части, которые можно использовать on-device, в отдельный артефакт и назвали его ML Kit.
Все части, которые используют on-cloud обработку, поместили в библиотеку Firebase ML.
Вот, как раз новый ML Kit
, который работает on-device и который полностью бесплатен, мы и будем использовать для распознавания номера карты.
Часть, отвечающая за распознавания текста в ML Kit
, называется Text Recognition, и подключается она таким образом:
implementation 'com.google.android.gms:play-services-mlkit-text-recognition:16.1.3'
В манифест внутри application tag нужно добавить:
<meta-data
android:name="com.google.mlkit.vision.DEPENDENCIES"
android:value="ocr" />
Это нужно, чтобы модели для ML Kit
скачались при установке вашего приложения. Если этого не сделать, то они загрузятся при первом использовании распознавания.
Далее всё достаточно просто, делаем всё по документации и получаем результат распознавания:
fun processFrame(frame: Image, rotationDegrees: Int): Task<List<RecognizedLine>> {
val inputImage = InputImage.fromMediaImage(frame, rotationDegrees)
return analyzer
.process(inputImage)
.continueWith { task->
task.result
.textBlocks
.flatMap { block -> block.lines }
.map { line -> line.toRecognizedLine() }
}
}
Библиотека отдаёт достаточно детализированный результат в виде Text
объекта, который содержит в себе список TextBlock
. Каждый TextBlock
, в свою очередь, содержит список Line
, а каждый Line
содержит список Element
.
Для наших тестовых целей, пока что подойдёт просто работать со списком строк, поэтому мы используем RecognizedLine
— это просто:
data class RecognizedLine(val text: String)
Отдельный класс нам нужен для того, чтобы иметь общую сущность, которую можно возвращать из GMS и из HMS ML Kit
.
HMS ML Kit
Так как наше приложение распространяется также в Huawei App Gallery, нам нужно использовать ML Kit
от Huawei.
В общем и целом, в HMS все составляющие имеют похожий на GMS интерфейс, ML Kit
в этом плане не исключение.
Но Huawei не делали никакой разбивки ML библиотек по признаку on-device и on-cloud, поэтому, с этим SDK можно запустить как on-device распознавание, так и on-cloud.
Подключаем HMS ML Kit Text Recognition SDK
согласно документации:
implementation 'com.huawei.hms:ml-computer-vision-ocr:2.0.5.300'
implementation 'com.huawei.hms:ml-computer-vision-ocr-latin-model:2.0.5.300'
И аналогично с GMS ML Kit
добавляем в манифест:
<meta-data
android:name="com.huawei.hms.ml.DEPENDENCY"
android:value="ocr" />
Руководствуясь документацией обрабатываем фрейм с камеры и получаем результат:
fun processFrame(frame: Image, rotationDegrees: Int): Task<List<RecognizedLine>> {
val mlFrame = MLFrame.fromMediaImage(frame, getHmsQuadrant(rotationDegrees))
return localAnalyzer
.asyncAnalyseFrame(mlFrame)
.continueWith { task ->
task.result
.blocks
.flatMap { block -> block.contents }
.map { line -> line.toRecognizedLine() }
}
}
Результаты тестов распознавания
Я был удивлён результатами — оказалось, что распознавание работает не так хорошо и стабильно, как я думал.
При дневном естественном освещении у меня получилось распознать номер карты длинной в 16 цифр на своей VISA, но на это ушло около минуты разного кручения, отдаления и приближения карты. При этом одна из цифр была неверной.
При искусственном освещении, а также при тусклом освещении со включеной вспышкой, мне вообще не удалось получить что-то вменяемое, похожее на номер карты.
В то же время, сard.io
даже в очень тёмном помещении со включенной вспышкой рапознаёт номер карты в среднем за 1-2 секунды.
Попытка использовать on-cloud распознавание
Раз on-device распознавание выдаёт неприемлемые результаты, то появилась идея попробовать on-cloud распознавание.
Сразу нужно понимать, что это будет платно, как в случае с GMS, так и в случае с HMS.
Как я ранее писал, Google разбил библиотеки на on-device и on-cloud. Поэтому вместо ML Kit
нам, нужно использовать Firebase ML
. Но не всё так просто, так как использовать его можно только если у вас Blaze-план для проекта в Firebase.
Поэтому я решил, что проще будет потестить on-cloud
распознавание на HMS ML Kit
. Для этого нам нужен проект в App Gallery Connect.
Нужно подключить agconnect
плагин:
classpath 'com.huawei.agconnect:agcp:1.4.1.300'
Также нужно скачать agconnect-services.json
и положить его в app-папку вашего проекта.
Text Recognition
SDK в данном случае тот же, и нам нужно использовать другой Analyzer
, в который необходимо передать apiKey
для вашего проекта из App Gallery Connect.
Создаём MLTextAnalyzer
согласно документации:
private val remoteAnalyzer: MLTextAnalyzer by lazy {
MLApplication.getInstance().apiKey = "Your apiToken here"
val settings = MLRemoteTextSetting.Factory()
.setTextDensityScene(MLRemoteTextSetting.OCR_COMPACT_SCENE)
.create()
MLAnalyzerFactory.getInstance().getRemoteTextAnalyzer(settings)
}
Далее обработка фрейма очень похожа на on-device:
fun processFrame(bitmap: Bitmap, rotationDegrees: Int): Task<List<RecognizedLine>> {
val mlFrame = MLFrame.fromBitmap(bitmap)
return remoteAnalyzer
.asyncAnalyseFrame(mlFrame)
.continueWith { task ->
task.result
.blocks
.flatMap { block -> block.contents }
.map { line -> line.toRecognizedLine() }
}
}
Нужно отметить, что мы здесь используем Bitmap
, а не Image
для создания MLFrame
, хоть мы и видели в случае с on-device, что можно создать MLFrame
из Image
. Мы это делаем потому, что MLTextAnalyzer
кидает NPE с сообщением от том, что внутренний Bitmap
null, если передавать ему MLFrame
, созданный из Image
. Если создавать из Bitmap
, то всё работает.
Так как on-cloud Text Recognition
платный (хоть и с бесплатным лимитом), я решил, что лучше перестрахуюсь и буду делать фото, то есть использовать ImageCapture
use case вместо ImageAnalysis
для on-cloud распознавания.
imageCapture = ImageCapture.Builder()
.setTargetRotation(Surface.ROTATION_0)
.setTargetAspectRatio(screenAspectRatio)
.setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
.build()
Результаты распознавания в этом случае неудовлетворительные: из трёх фото в отличном качестве (я их сохранял в память приложения и посмотрел после съемки) с ествественным дневным освещением, ни на одном номер карты не распознался корректно.
При этом, стоит ометить, что с платным on-cloud распознаванием не получится использовать тот же подход, который мы использовали с on-device распознаванием— то есть, передавать фреймы камеры с максимально доступной нам скоростью и пытаться на каждом из них распознать номер карты.
На каждом дейвайсе будет по-разному: на Pixel 3 XL это в среднем 5 fps, на Huawei Y8p — это 2 fps, но главное, что в среднем в секунду этих фреймов будет больше 1, и они будут передаваться на распознавание сразу, как пользователь откроет экран, даже если он ещё не навёл камеру на карту.
Получается весьма значительное количество запросов, поэтому придётся отдать немалую сумму денег.
Последний шанс
После неудач с on-device и on-cloud распознаванием текста, я решил поискать, может есть более специфичные части в ML Kit
, именно про распознавание номера карты. В GMS ML Kit
ничего такого не нашёл, а вот в HMS ML Kit
нашёл Bank Card Recognition.
Но есть 3 проблемы:
Он сам работает с камерой, нужно только передать ему Activity и callback для получения результатов. Соответственно, мы не можем использовать
CameraX
.У GMS
ML Kit
такого нет и соответственно, работать это будет только для приложений в Huawei App Gallery, а мы хотим, чтобы работало для всех.Не очень понятна цена для этой фичи: для on-device написано Free in the trial period, а для on-cloud N/A.
Покажите мне код
Все вставки кода в статье сделаны из кода sample-приложения, доступного в этом репозитории. Оно рабочее, можете запусить на своём девайсе и проверить качество распознавания. Помимо CameraX
+ML Kit
, там также добавлена card.io
, чтобы можно было сравнивать.
Итоги
Я рассказал про наш опыт замены card.io
на связку CameraX
+ML Kit
для распознавания номера карты. ML Kit
(GMS и HMS) справляется с задачей распознавания номера карты сильно хуже, по сравнению с card.io
.
В связи с этим было принято решение оставить card.io
в приложении и посмотреть в сторону считывания номера карты с помощью NFC, так как подавляющее большинство банковских карт сейчас — бесконтактные.
mike114
Жалко, что не получилось перейти. Тоже подумывал об этом пару лет как, обрадовался уже, что осталось только повторить, но нет. NFC делал с использованием github.com/devnied/EMV-NFC-Paycard-Enrollment, но там много зависит от самих карт — некоторые не возвращают часть полей, которые можно вытащить оптическим распознаванием.
vitaliybv Автор
Я когда изучал как сделать распознавание EMV карт с помощью NCF, также к этой библиотеке пришёл, но как верно подмечено, она не абсолютно на всех картах гарантирует работу. Как я понял, нужно понимать с какими картами будет работать ваше приложение, на них тестить и уже в зависимости от этого принимать решение использовать библиотеку или нет.
К слову в том же репозитории, где лежит сэмпл к статье, есть отдельная ветка с рабочим прототипом распознавания EMV карт с помощью этой библиотеки.