androidperformancecameraqr-codegoogle-mlkit

Android ML Kit library for QR code scanning: How to increase detection performance by reducing image resolution


This is my stripped down sourcecode for barcode scanning

build.gradle

dependencies {
    .....
     // MLKit Dependencies
    implementation 'com.google.android.gms:play-services-vision:20.1.3'

    implementation 'com.google.mlkit:barcode-scanning:17.0.2'
    def camerax_version = "1.1.0-beta01"
    implementation "androidx.camera:camera-core:${camerax_version}"
    implementation "androidx.camera:camera-camera2:${camerax_version}"
    implementation "androidx.camera:camera-lifecycle:${camerax_version}"
    implementation "androidx.camera:camera-video:${camerax_version}" 
    ......
}

ScanCameraFragment.kt

class ScanCameraFragment : BaseFragment() {
    private lateinit var binding: FragmentScanCameraBinding
    private lateinit var cameraExecutor: ExecutorService

    //region Lifecycle Methods
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?): View? {
        binding = FragmentScanCameraBinding.inflate(inflater, container, false)
        cameraExecutor = Executors.newSingleThreadExecutor()

        startCamera()
        return binding.root
    }

    override fun onDestroyView() {
        super.onDestroyView()
        cameraExecutor.shutdown()
    }

    companion object {
        fun newInstance() = ScanCameraFragment().apply {}
    }

    private fun startCamera() {
        context?.let { context ->
            val cameraProviderFuture = ProcessCameraProvider.getInstance(context)

            cameraProviderFuture.addListener({
                val cameraProvider = cameraProviderFuture.get()

                // Preview
                val preview = Preview.Builder()
                    .build()
                    .also {
                        it.setSurfaceProvider(binding.previewView.surfaceProvider)
                    }

                // Image analyzer
                val imageAnalyzer = ImageAnalysis.Builder()
                    .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
                    .build()
                    .also {
                        it.setAnalyzer(cameraExecutor,
                            QrCodeAnalyzer(context, binding.barcodeBoxView,
                                      binding.previewView.width.toFloat(), 
                                      binding.previewView.height.toFloat()
                            )
                        )
                    }

                // Select back camera as a default
                val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

                try {
                    // Unbind use cases before rebinding
                    cameraProvider.unbindAll()

                    // Bind use cases to camera
                    var camera = cameraProvider.bindToLifecycle(this, cameraSelector,
                                      preview, imageAnalyzer)

                } catch (exc: Exception) {
                    exc.printStackTrace()
                }
            }, ContextCompat.getMainExecutor(context))
        }
    }
}

QRCodeAnalyzer.kt

class QrCodeAnalyzer(private val context: Context, 
    private val barcodeBoxView: BarcodeBoxView, private val previewViewWidth: Float,
    private val previewViewHeight: Float) : ImageAnalysis.Analyzer {

    private var scaleX = 1f
    private var scaleY = 1f

    private fun translateX(x: Float) = x * scaleX
    private fun translateY(y: Float) = y * scaleY

    private fun adjustBoundingRect(rect: Rect) = RectF(
        translateX(rect.left.toFloat()),
        translateY(rect.top.toFloat()),
        translateX(rect.right.toFloat()),
        translateY(rect.bottom.toFloat())
    )

    @SuppressLint("UnsafeOptInUsageError")
    override fun analyze(image: ImageProxy) {
        val img = image.image
        if (img != null) {
            // Update scale factors
            scaleX = previewViewWidth / img.height.toFloat()
            scaleY = previewViewHeight / img.width.toFloat()

            val inputImage = InputImage.fromMediaImage(img, 
                                image.imageInfo.rotationDegrees)

            // Process image searching for barcodes
            val options = BarcodeScannerOptions.Builder()
                .build()
            val scanner = BarcodeScanning.getClient(options)
            scanner.process(inputImage)
                .addOnSuccessListener { barcodes ->
                    for (barcode in barcodes) {
                        barcode?.rawValue?.let {
                            if (it.trim().isNotBlank()) {
                                Scanner.updateBarcode(it)
                                barcode.boundingBox?.let { rect ->
                                    barcodeBoxView.setRect(adjustBoundingRect(rect))
                                }
                            }
                            return@addOnSuccessListener
                        }
                    }
                    // coming here means no satisfiable barcode was found
                    barcodeBoxView.setRect(RectF())
                }
                .addOnFailureListener {
                    image.close()
                }
                .addOnFailureListener { }
        }

        image.close()
    }
}

This code works and I am able to scan barcodes. But sometimes, the barcode detection is slow. The documentation says one way to increase performance is to limit the image resolution.

Don't capture input at the camera’s native resolution. On some devices, capturing input at the native resolution produces extremely large (10+ megapixels) images, which results in very poor latency with no benefit to accuracy. Instead, only request the size from the camera that's required for barcode detection, which is usually no more than 2 megapixels.

If scanning speed is important, you can further lower the image capture resolution. However, bear in mind the minimum barcode size requirements outlined above.

Unfortunately, the documentation doesn't specify how to reduce the image resolution. And some of my end users are using high end devices with powerful camera, so we assume the poor performance is because of the image size.

How can I reduce the resolution of the image to a fixed value (something like 1024 x 768) rather than the default camera resolution?


Solution

  • User HarmenH's answer correctly tells how to set the image resolution, so I am not repeating it here.

    As it turns out, the performance issue on my end was not because of image resolution. It seems I was closing the imageProxy prematurely.

    override fun analyze(image: ImageProxy) {
        val img = image.image
        if (img != null) {
            // Update scale factors
            scaleX = previewViewWidth / img.height.toFloat()
            scaleY = previewViewHeight / img.width.toFloat()
    
            val inputImage = InputImage.fromMediaImage(img,
                image.imageInfo.rotationDegrees)
    
            // Process image searching for barcodes
            val options = BarcodeScannerOptions.Builder()
                .build()
            val scanner = BarcodeScanning.getClient(options)
            scanner.process(inputImage)
                .addOnSuccessListener { barcodes - >
                    for (barcode in barcodes) {
                        barcode?.rawValue?.let {
                           if (it.trim().isNotBlank()) {
                               Scanner.updateBarcode(it)
                               barcode.boundingBox?.let { rect - >
                                  barcodeBoxView.setRect(adjustBoundingRect(rect))
                               }
                           }
                           return @addOnSuccessListener
                        }
                    }
                    // coming here means no satisfiable barcode was found
                    barcodeBoxView.setRect(RectF())
                }
                .addOnFailureListener {
                    image.close()
                }
                .addOnFailureListener {
                    //added this here.
                    image.close()
                }
        }
        //Removed this because we don't close the 
        //imageProxy before analysis completes
        //image.close()
    }