ML KIT — Современное решение для сканирования в Android приложениях

от автора

С чего бы начать?

Библиотека от компании Google ML Kit предлагает набор встроенных API, которые могут работать как на самом девайсе, так и в облаке.

ML Kit — это мощный инструмент для работы с камерой в Android и IOS приложениях.

Возможности ML Kit

Категория

API

Описание

Работа с изображениями

Face Detection

Обнаружение лиц, черт лица, направлений взгляда и т.д.

Text Recognition

Распознание текста, с возможность фильтровать и получать необходимый результат

Barcode Scanning

Сканирование QR-кодов и штрихкодов

Image Labeling

Определение объектов на изображении

Object Detection & Tracking

Обнаружение и отслеживание объектов

Работа с текстом

Smart Reply

Генерация ответов на сообщения

Translate

Перевод текста в реальном времени

Language Indetification

Определение языка текста

Как дела c Jetpack Compose?

С Jetpack Compose библиотека ML Kit отлично дружит и настоятельно рекомендую использовать в этой связке.

С чего начать?

Для примера я возьму две зависимости — одна для сканирования QR/штрихкодов, другая — для распознавания текста.

mlkit = { group = "com.google.mlkit", name = "barcode-scanning", version = "17.2.0" } mlkit-text = { group = "com.google.mlkit", name = "text-recognition", version = "16.0.0" }  implementation "androidx.camera:camera-core:1.4.0" // для работы с CameraX

1) Можем создать простой пример использования. Используем AndroidView, которая позволит нам работать с PreviewView в камере.

@Composable fun ScannerScreen(   scanType: ScanType,   onCodeScanned: ((String) -> Unit)? = null // для передачи результата на определенный экран ) {     val lifecycleOwner = LocalLifecycleOwner.current     val context = LocalContext.current     val analysisExecutor = remember { CoroutineScope(Dispatchers.Default) }     val cameraProviderFuture = remember { ProcessCameraProvider.getInstance(context) }      DisposableEffect(Unit) {         onDispose {             val cameraProvider = cameraProviderFuture.get()             cameraProvider.unbindAll()          }     }      Box(modifier = Modifier.fillMaxSize()) {    AndroidView(             factory = { ctx ->                 val previewView = PreviewView(ctx).apply {                     layoutParams = ViewGroup.LayoutParams(                         ViewGroup.LayoutParams.MATCH_PARENT,                         ViewGroup.LayoutParams.MATCH_PARENT                     )                     scaleType = PreviewView.ScaleType.FILL_CENTER                 }                    val cameraProviderFuture = ProcessCameraProvider.getInstance(ctx)                 cameraProviderFuture.addListener({                     val cameraProvider = cameraProviderFuture.get()                      val previewUseCase = Preview.Builder().build().also {                         it.surfaceProvider = previewView.surfaceProvider                     }                      val permissionGranted = ContextCompat.checkSelfPermission(                         ctx, Manifest.permission.CAMERA                     ) == PackageManager.PERMISSION_GRANTED                      val barcodeScanner = BarcodeScanning.getClient(                         BarcodeScannerOptions.Builder()                             .setBarcodeFormats(Barcode.FORMAT_QR_CODE)                             .build()                     )                      if (!permissionGranted) {                          // Нет доступа к камере                         return@addListener                     }                      val selector = when {                         cameraProvider.hasCamera(CameraSelector.DEFAULT_BACK_CAMERA) -> CameraSelector.DEFAULT_BACK_CAMERA                          cameraProvider.hasCamera(CameraSelector.DEFAULT_FRONT_CAMERA) -> CameraSelector.DEFAULT_FRONT_CAMERA                         else -> {                           return@addListener                         }                     }                      val analysisUseCase = ImageAnalysis.Builder()                         .setTargetResolution(Size(1280, 720))                         .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)                         .build()                         .also { analysis ->                             analysis.setAnalyzer(                                 { runnable -> analysisExecutor.launch { runnable.run() } }                             ) { imageProxy ->                                 val mediaImage = imageProxy.image                                  if (mediaImage != null) {                                     val rotation = imageProxy.imageInfo.rotationDegrees                                     val inputImage = InputImage.fromMediaImage(mediaImage, rotation)                                     when (scanType) {                                         ScanType.ScanQr -> {                                             barcodeScanner.process(inputImage)                                                 .addOnSuccessListener { barcodes ->                                                     barcodes.firstOrNull { barcode ->                                                         val box = barcode.boundingBox                                                         box != null                                                      }?.rawValue?.let { qrCode ->                                                         onCodeScanned?.let { it(qrCode) }                                                     }                                                 }                                                 .addOnCompleteListener {                                                     imageProxy.close()                                                 }                                         }                                          ScanType.ScanCard -> {                                             val textRecognizer = TextRecognition.getClient(                                                 TextRecognizerOptions.DEFAULT_OPTIONS                                             )                                               textRecognizer.process(inputImage)                                                 .addOnSuccessListener { visionText ->                                                      if (!visionText.isNullOrEmpty()) {                                                         onCodeScanned?.let {                                                             it(visionText)                                                         }                                                     }                                                 }                                                 .addOnCompleteListener {                                                     imageProxy.close()                                                 }                                         }                                          ScanType.ScanText -> {                                             val textRecognizer = TextRecognition.getClient(                                                 TextRecognizerOptions.DEFAULT_OPTIONS                                             )                                              textRecognizer.process(inputImage)                                                 .addOnSuccessListener { visionText ->                                                     val text = visionText.text                                                        if (!text.isNullOrEmpty()) {                                                         onCodeScanned?.let { result ->                                                             result(text)                                                         }                                                     }                                                 }                                                 .addOnCompleteListener {                                                     imageProxy.close()                                                 }                                          }                                          ScanType.None -> {}                                     }                                 } else {                                     imageProxy.close()                                 }                             }                         }                      try {                         cameraProvider.unbindAll()                         cameraProvider.bindToLifecycle(                             lifecycleOwner,                             selector,                             previewUseCase,                             analysisUseCase                         )                     } catch (exception: Exception) {                       // Ошибка привязки камеры                     }                  }, ContextCompat.getMainExecutor(ctx))                  previewView             },             modifier = Modifier.fillMaxSize()         )         when (scanType) {                                   ScanType.ScanQr -> { CameraScanQrOverlay() }              ScanType.ScanCard -> { CameraScanCardOverlay() }              ScanType.ScanText -> { CameraScanTextOverlay() }              ScanType.None -> {}         }     } }   sealed class ScanType() {   data object ScanQr: ScanType()   data object ScanText: ScanType()   data object ScanCard: ScanType()   data object None: ScanType() }

Что здесь происходит? Создаем PreviewView для отображения изображения с камеры (CameraX), далее в LayoutParams устанавливаем ширину и высоту для контейнера.

PreviewView.ScaleType.FILL_CENTER — отвечает за то, чтобы заполнить PreviewView, сохраняя при этому центрирование.

Какие есть варианты PreviewView.ScaleType?

  • FILL_START

  • FILL_CENTER

  • FILL_END

  • FIT_CENTER

    Детальный пример PreviewView.ScaleType

    Детальный пример PreviewView.ScaleType

2) Далее получаем CameraProvider, который отвечает за получение и настройки камеры

val cameraProviderFuture = ProcessCameraProvider.getInstance(ctx) cameraProviderFuture.addListener({ ... }, ContextCompat.getMainExecutor(ctx))

addListener позволяет безопасно получить cameraProvider, когда он будет доступен.

2.1) Так же можете добавить проверку разрешения камеры. При необходимости связать с навигацией и в случае чего либо возвращать пользователя на предыдущий экран или показывать соответствующее диалоговое окно

val permissionGranted = ContextCompat.checkSelfPermission(...) == PackageManager.PERMISSION_GRANTED if (!permissionGranted) return@addListener

3) Далее добавим выбор доступной камеры — сперва основная, потом фронтальная

val selector = when {     cameraProvider.hasCamera(CameraSelector.DEFAULT_BACK_CAMERA) -> ...     cameraProvider.hasCamera(CameraSelector.DEFAULT_FRONT_CAMERA) -> ...     else -> return@addListener }

4) Создаем image analysis use case для анализа кадров с камеры

val analysisUseCase = ImageAnalysis.Builder()     .setTargetResolution(Size(1280, 720))     .setBackpressureStrategy(...)     .build()

4.1) Обязательно! Если mediaImage == null, то освобождаем ресурсы камеры!

Почему это так важно?

  • Предотвращение утечек памяти

  • Избежание сбоев и зависания

  • Освобождения ресурсов камеры

 if (mediaImage != null) {         val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)          scanner.process(inputImage)             .addOnSuccessListener { barcodes ->                 // Обработка кодов             }             .addOnFailureListener {                 // Обработка ошибок             }             .addOnCompleteListener {                 // Важно! Закрываем imageProxy после обработки                 imageProxy.close()             }     } else {         imageProxy.close()     } }

5) Установка анализатора, анализатор передает кадры на обработку.

analysis.setAnalyzer({ runnable -> ... }) { imageProxy -> ... }

image proxy — объект изображения

6) Привязка use-cases к жизненому циклу. Удаляем все предыдущие use cases (unbindAll())

cameraProvider.unbindAll() cameraProvider.bindToLifecycle(     lifecycleOwner,     selector,     previewUseCase,     analysisUseCase )

7) Обрабатываем overlay UI по типу сканирования. Также вы можете передавать Rect области сканирования из самих Overlay в ScannerScreen.

when (scanType) {     ScanType.ScanQr -> CameraScanQrOverlay()     ScanType.ScanCard -> CameraScanCardOverlay()     ScanType.ScanText -> CameraScanTextOverlay()     ScanType.None -> {} }

Почему не zxing?

При всех своих плюсах в простоте и быстрой реализации Zxing имеет свои плюсы и минусы. Да, ML Kit зависит от Google сервисов, но при этом это современное и надежное решение с больших спектром настроек, если для вас критично, чтобы приложение имело малый размер и быстрое внедрение в проект, то собственно вам подойдет Zxing, но если вы хотите, чтобы решение было гибкое и современное, то однозначно ML Kit.

Итоги

Библиотека ML Kit имеет ряд преимуществ: высокая точность и распознание даже при плохом освещении, широкая поддержка форматов, работает оффлайн, поддерживается Google и активно обновляется, легко кастомизировать UI в связке c Jetpack Compose, можно объединить с другими ML Kit модулями.

Надеюсь статья привнесла пользу Вам, попытался изложить в кратной форме, если есть вопросы, то буду рад ответить на них. Cпасибо за внимание!


ссылка на оригинал статьи https://habr.com/ru/articles/908772/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *