---
# System prepended metadata

title: CameraX+ML Kit для распознавания номера карты в действии

---

# 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.