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

Библиотека от компании 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пасибо за внимание!

Комментарии (0)