# CameraX+ML Kit для распознавания номера карты в действии Привет, меня зовут Виталий Беляев, я Android-разработчик в red_mad_robot. В этой статье я расскажу про опыт интеграции `CameraX` с `ML Kit` на замену библиотеки `card.io`, и что в итоге из этого получилось. В приложении над которым я работаю, есть экран добавления банковской карты. Там можно заполнить всю информацию руками, а можно нажать *«Сканировать»*, и с помощью камеры телефона распознать номер карты. Для этого у нас используется библиотека [`card.io`](https://github.com/card-io/card.io-Android-SDK). ### Почему мы решили заменить `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 проекте](https://github.com/redmadrobot-spb/android-camerax-mlkit-article) ![](https://i.imgur.com/Lhm9O4Y.png) ### Что такое `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`](https://developer.android.com/training/camerax) — библиотеку для работы с камерой, часть Jetpack, которая инкапсулирует в себе работу с `Camera1` и `Camera2` API и предоставляет удобный lifecycle-aware интерфейс для работы с камерой. В `CameraX` есть так называемые use cases, их всего три: - `ImageAnalysis` - `Preview` - `ImageCapture` По названию нетрудно догадаться, что и зачем используется. Нас интересуют `Preview` и `ImageAnalysis`. Делаем настройку: ```kotlin= 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) } ``` Не будем вдаваться в подробности каждой строчки, про это можно почитать [хорошую документацию](https://developer.android.com/training/camerax) по `CameraX` или пройти [codelab](https://codelabs.developers.google.com/codelabs/camerax-getting-started#0). Сейчас же мы конфигурируем `use cases`, и стоит отметить, что это довольно удобно и компактно выглядит. Далее мы всё это привязываем к lifecycle и запускаем. ```kotlin= 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`. ```kotlin= private val framesAnalyzer: ImageAnalysis.Analyzer by lazy { ImageAnalysis.Analyzer(viewModel::onFrameReceived) } ``` Вот таким образом мы получили картинку, которую можно передавать в `ML Kit` на распознавание. ### GMS `ML Kit` У Google раньше была библиотека [`ML Kit for Firebase`](https://firebase.google.com/docs/ml-kit), где были собраны все ML-related вещи: те, что работают *on-device* (сканирование штрихкодов например) и те, что работают *on-cloud* (Image Labeling например). Потом они вынесли все те части, которые можно использовать *on-device*, в отдельный артефакт и назвали его [`ML Kit`](https://developers.google.com/ml-kit). Все части, которые используют *on-cloud* обработку, поместили в библиотеку [`Firebase ML`](https://firebase.google.com/docs/ml). Вот, как раз новый `ML Kit`, который работает *on-device* и который полностью бесплатен, мы и будем использовать для распознавания номера карты. Часть, отвечающая за распознавания текста в `ML Kit`, называется [`Text Recognition`](https://developers.google.com/ml-kit/vision/text-recognition/android), и подключается она таким образом: ```groovy= implementation 'com.google.android.gms:play-services-mlkit-text-recognition:16.1.3' ``` В манифест внутри application tag нужно добавить: ```xml= <meta-data android:name="com.google.mlkit.vision.DEPENDENCIES" android:value="ocr" /> ``` Это нужно, чтобы модели для `ML Kit` скачались при установке вашего приложения. Если этого не сделать, то они загрузятся при первом использовании распознавания. Далее всё достаточно просто, делаем всё по [документации](https://developers.google.com/ml-kit/vision/text-recognition/android) и получаем результат распознавания: ```kotlin= 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() } } } ``` Библиотека отдаёт достаточно [детализированный результат](https://developers.google.com/ml-kit/vision/text-recognition/android#4.-extract-text-from-blocks-of-recognized-text) в виде `Text` объекта, который содержит в себе список `TextBlock`. Каждый `TextBlock`, в свою очередь, содержит список `Line`, а каждый `Line` содержит список `Element`. Для наших тестовых целей, пока что подойдёт просто работать со списком строк, поэтому мы используем `RecognizedLine` — это просто: ```kotlin= 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` согласно [документации](https://developer.huawei.com/consumer/en/doc/development/HMSCore-Guides-V5/text-sdk-0000001050040033-V5): ```groovy= 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` добавляем в манифест: ```xml= <meta-data android:name="com.huawei.hms.ml.DEPENDENCY" android:value="ocr" /> ``` Руководствуясь [документацией](https://developer.huawei.com/consumer/en/doc/development/HMSCore-Guides-V5/text-recognition-0000001050040053-V5#EN-US_TOPIC_0000001050750207__section16220018134717) обрабатываем фрейм с камеры и получаем результат: ```kotlin= 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](https://cloud.google.com/vision/pricing), так и в случае с [HMS](https://developer.huawei.com/consumer/en/doc/development/HMSCore-Guides/ml-service-billing-0000001051010023). Как я ранее писал, Google разбил библиотеки на *on-device* и *on-cloud*. Поэтому вместо `ML Kit` нам, нужно использовать `Firebase ML`. Но не всё так просто, так как использовать его можно только если у вас Blaze-план для проекта в Firebase. Поэтому я решил, что проще будет потестить `on-cloud` распознавание на HMS `ML Kit`. Для этого нам нужен проект в App Gallery Connect. Нужно подключить `agconnect` плагин: ```groovy= classpath 'com.huawei.agconnect:agcp:1.4.1.300' ``` Также нужно скачать `agconnect-services.json` и положить его в app-папку вашего проекта. `Text Recognition` SDK в данном случае тот же, и нам нужно использовать другой `Analyzer`, в который необходимо передать `apiKey` для вашего проекта из App Gallery Connect. Создаём `MLTextAnalyzer` согласно [документации](https://developer.huawei.com/consumer/en/doc/development/HMSCore-Guides-V5/text-recognition-0000001050040053-V5#EN-US_TOPIC_0000001050750207__section1341510143119): ```kotlin= 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*: ```kotlin= 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* распознавания. ```kotlin= 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`](https://developer.huawei.com/consumer/en/doc/development/HMSCore-Guides-V5/bank-card-recognition-0000001050038118-V5#EN-US_TOPIC_0000001051070154__section1593515577819). Но есть 3 проблемы: 1. Он сам работает с камерой, нужно только передать ему Activity и callback для получения результатов. Соответственно, мы не можем использовать `CameraX`. 2. У GMS `ML Kit` такого нет и соответственно, работать это будет только для приложений в Huawei App Gallery, а мы хотим, чтобы работало для всех. 3. Не очень понятна [цена](https://developer.huawei.com/consumer/en/doc/development/HMSCore-Guides/ml-service-billing-0000001051010023) для этой фичи: для *on-device* написано **Free in the trial period**, а для *on-cloud* **N/A**. ### Покажите мне код Все вставки кода в статье сделаны из кода sample-приложения, доступного в этом [репозитории](https://github.com/redmadrobot-spb/android-camerax-mlkit-article). Оно рабочее, можете запусить на своём девайсе и проверить качество распознавания. Помимо `CameraX`+`ML Kit`, там также добавлена `card.io`, чтобы можно было сравнивать. ### Итоги Я рассказал про наш опыт замены `card.io` на связку `CameraX`+`ML Kit` для распознавания номера карты. `ML Kit`(GMS и HMS) справляется с задачей распознавания номера карты сильно хуже, по сравнению с `card.io`. В связи с этим было принято решение оставить `card.io` в приложении и посмотреть в сторону считывания номера карты с помощью NFC, так как подавляющее большинство банковских карт сейчас — бесконтактные. ### Все ссылки 1. Sample app для этой статьи https://github.com/redmadrobot-spb/android-camerax-mlkit-article. 2. `card.io` https://github.com/card-io/card.io-Android-SDK. 3. CameraX https://developer.android.com/training/camerax. 4. CameraX codelab https://codelabs.developers.google.com/codelabs/camerax-getting-started#0. 6. GMS Text Recognition https://developers.google.com/ml-kit/vision/text-recognition/android. 7. GMS ML Kit Pricing https://cloud.google.com/vision/pricing. 8. Firebase ML https://firebase.google.com/docs/ml. 9. HMS Text Recognition https://developer.huawei.com/consumer/en/doc/development/HMSCore-Guides-V5/text-sdk-0000001050040033-V5. 10. HMS ML Kit Pricing https://developer.huawei.com/consumer/en/doc/development/HMSCore-Guides/ml-service-billing-0000001051010023. 11. HMS Bank Card Recognition https://developer.huawei.com/consumer/en/doc/development/HMSCore-Guides-V5/bank-card-recognition-0000001050038118-V5#EN-US_TOPIC_0000001051070154__section1593515577819.