Pull to refresh
76.88
red_mad_robot
№1 в разработке цифровых решений для бизнеса

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

Reading time8 min
Views7.7K

Привет, меня зовут Виталий Беляев, я 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 проблемы:

  1. Он сам работает с камерой, нужно только передать ему Activity и callback для получения результатов. Соответственно, мы не можем использовать CameraX.

  2. У GMS ML Kit такого нет и соответственно, работать это будет только для приложений в Huawei App Gallery, а мы хотим, чтобы работало для всех.

  3. Не очень понятна цена для этой фичи: для 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, так как подавляющее большинство банковских карт сейчас — бесконтактные.

Все ссылки

  1. Sample app для этой статьи

  2. card.io

  3. CameraX

  4. CameraX codelab

  5. GMS Text Recognition

  6. GMS ML Kit Pricing

  7. Firebase ML

  8. HMS Text Recognition

  9. HMS ML Kit Pricing

  10. HMS Bank Card Recognition

Tags:
Hubs:
+6
Comments7

Articles

Information

Website
redmadrobot.ru
Registered
Founded
Employees
1,001–5,000 employees
Location
Россия