Search code examples
androidkotlinopencvbitmapbarcode-scanner

Multiple image analyses for same frame with cameraX


I use the cameraX API in Android to analyze multiple frames in a period of 5 up to 60 seconds. There are multiple conditional tasks I want to do with the images depending on what tasks the user selected. These include:

  1. scan for barcodes/qr codes (using google mlkit)
  2. scan for text (using google mlkit)
  3. custom edge detection using openCV in C++ with JNI
  4. save image as png file (losless)
  5. show frames in app (PreviewView or ImageView)

These tasks heavily vary in workload and time to finish, so instead of waiting for each task to finish until getting a new frame, I want to receive constant frames and let each task only start with the newest frame when it's finished with it's last workload.

while MLKit takes YUV images as input, openCV uses RGBA (or BGRA), so no matter which output format I choose, I will need to convert it some way. My choice was to use RGBA_8888 as output format and convert it into a bitmap since bitmap is supported from both MLKit and OpenCV and the conversion from RGBA to bitmap is much quicker than from YUV to bitmap. But using bitmaps I get huge problems with memory to the extend of the app just getting closed by Android. Using the Android Studio Profiler, I noticed the native part of ram usage going up constantly, staying that high even after workload is done and the camera is unbound.

I read online that it is heavily suggested to recycle bitmaps after use to free up their memory space. Problem here is that all these tasks run and finish independently and I couldn't come up with a good solution for recycling the bitmap as soon as possible without heavily increasing memory usage by keeping them in memory for a certain time (like 10 seconds).

I thought about using jobs for each task and to recycle when all jobs are done, but this doesn't work for the MLKit analyses because they return using a listener, resulting in the jobs ending before the task is actually done.

I appreciate any input for how to efficiently recycle the bitmaps, using something different than bitmaps, reducing memory consumption or any code improvements in general!

Here are code samples for the image analysis and for the barcode scanner. They should suffice for giving a general idea of the running code.

val imageAnalysisBuilder =
            ImageAnalysis
                .Builder()
                .setTargetResolution(android.util.Size(720, 1280))
                .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
                .setOutputImageFormat(OUTPUT_IMAGE_FORMAT_RGBA_8888)


val imageAnalysis = imageAnalysisBuilder.build()

imageAnalysis.setAnalyzer(Executors.newSingleThreadExecutor()) { imageProxy ->

            //bitmap conversion from https://github.com/android/camera-samples
            var bitmap = Bitmap.createBitmap(imageProxy.width, imageProxy.height, Bitmap.Config.ARGB_8888)
            imageProxy.use { bitmap.copyPixelsFromBuffer(it.planes[0].buffer) }

            val rotationDegrees = imageProxy.imageInfo.rotationDegrees
            imageProxy.close()

            if (!barcodeScannerBusy) {
                        CoroutineScope.launch { startMlKitBarcodeScanner(bitmap, rotationDegrees) }
            }

            if (!textRecognitionBusy) {
                        CoroutineScope.launch { startMlKitTextRecognition(bitmap, rotationDegrees) }
            }
            //more tasks with same pattern
            //when to recycle bitmap???
}
    private fun startMlKitBarcodeScanner(bitmap: Bitmap, rotationDegrees: Int) {

        barcodeScannerBusy = true

        val inputImage = InputImage.fromBitmap(bitmap, rotationDegrees)

        barcodeScanner?.process(inputImage)
            ?.addOnSuccessListener { barcodes ->
                //do stuff with barcodes
            }
            ?.addOnFailureListener {
                //failure handling
            }
            ?.addOnCompleteListener {
                barcodeScannerBusy = false
                //can't recycle bitmap here since other tasks might still use it
            }
    }

Solution

  • I solved the issue by now. Mainly by using a bitmap buffer variable for each task working with the image. Downside is that in the worst case, I create the same bitmap multiple times in a row. Upside is that each task can use its own bitmap independently of any other task. Also since the device I use is not the most powerful (quite the contrary in fact), I decided to split up some of the tasks into multiple analyzers and assign a new analyzer to the camera when needing it. Also if copying the planes of the imageProxy multiple times the way I do it here, you need to use the rewind() method before creating a new bitmap with it.

    lateinit var barcodeScannerBitmapBuffer: Bitmap
    lateinit var textRecognitionBitmapBuffer: Bitmap
    
    val imageAnalysisBuilder =
                ImageAnalysis
                    .Builder()
                    .setTargetResolution(android.util.Size(720, 1280))
                    .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
                    .setOutputImageFormat(OUTPUT_IMAGE_FORMAT_RGBA_8888)
    
    
    val imageAnalysis = imageAnalysisBuilder.build()
    
    imageAnalysis.setAnalyzer(Executors.newSingleThreadExecutor()) { imageProxy ->
    
    
                if (barcodeScannerBusy && textRecognitionBusy) {
                    imageProxy.close()
                    return@Analyzer
                }
    
                if (!::barcodeScannerBitmapBuffer.isInitialized) {
                    barcodeScannerBitmapBuffer = Bitmap.createBitmap(
                        imageProxy.width,
                        imageProxy.height,
                        Bitmap.Config.ARGB_8888
                    )
                }
    
                if (!::textRecognitionBitmapBuffer.isInitialized) {
                    textRecognitionBitmapBuffer = Bitmap.createBitmap(
                        imageProxy.width,
                        imageProxy.height,
                        Bitmap.Config.ARGB_8888
                    )
                }
    
                if (!barcodeScannerBusy) {
                    imageProxy.use {
                        //bitmap conversion from https://github.com/android/camera-samples
                        barcodeScannerBitmapBuffer.copyPixelsFromBuffer(it.planes[0].buffer)
                        it.planes[0].buffer.rewind()
                    }
                }
    
                if (!textRecognitionBusy) {
                    imageProxy.use { textRecognitionBitmapBuffer.copyPixelsFromBuffer(it.planes[0].buffer) }
                }
    
                val rotationDegrees = imageProxy.imageInfo.rotationDegrees
                imageProxy.close()
    
                if (::barcodeScannerBitmapBuffer.isInitialized &&!barcodeScannerBusy) {
                        startMlKitBarcodeScanner(barcodeScannerBitmapBuffer, rotationDegrees)
                }
    
                if (::textRecognitionBitmapBuffer.isInitialized && !textRecognitionBusy) {
                        startMlKitTextRecognition(textRecognitionBitmapBuffer, rotationDegrees)
                }
    }