Raj
Raj

Reputation: 1872

Android - Barcode scanning with MLKit not accurate for Barcode.FORMAT_CODE_39

I am Implementing Barcode Scanning functionality in my Android App. its working great for basic QR Code or Barcode FORMAT-128. But its not Accurate for Barcode FORMAT-39. Sometimes its captured correct value and some times wrong value. (especially when barcode value contains repeated values like A00000190) below are codes i tried and respective scanned values.

  1. Barcode value : AA00000029001

enter image description here

scan results randomly picking one of the following : AA00000029001, AA000029001, A100029001, AA00029001 .. etc

below is my code snippet.

class MainActivity3 : AppCompatActivity(), BarcodeScannerListener {

    companion object {
        private const val CAMERA_REQUEST_CODE = 101
        private const val TAG = "MAIN_ACTIVITY"
    }

    private lateinit var viewBinding: ActivityMain3Binding

    private val executorService: ExecutorService by lazy {
        Executors.newSingleThreadExecutor()
    }

    private val barcodeScanner by lazy {
        BarcodeScanner(this)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        viewBinding = ActivityMain3Binding.inflate(layoutInflater)
        setContentView(viewBinding.root)

        setupCamera()
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>,
                                            grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (requestCode == CAMERA_REQUEST_CODE && isCameraPermissionGranted()) {
            startCamera()
        }
    }

    private fun setupCamera() {
        if (isCameraPermissionGranted()) {
            startCamera()
        } else {
            requestPermission()
        }
    }

    private fun startCamera() {
        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

        cameraProviderFuture.addListener({
            val cameraProvider = cameraProviderFuture.get()
            val preview = Preview.Builder().build().apply {
                setSurfaceProvider(viewBinding.cameraPreview.surfaceProvider)
            }
            val imageAnalyzer = ImageAnalysis.Builder().build().apply {
                setAnalyzer(executorService, getImageAnalyzerListener())
            }

            try {
                cameraProvider.unbindAll()
                cameraProvider.bindToLifecycle(
                    this,
                    CameraSelector.DEFAULT_BACK_CAMERA,
                    preview,
                    imageAnalyzer
                )
            } catch (throwable: Throwable) {
                Log.e(TAG, "Use case binding failed", throwable)
            }
        }, ContextCompat.getMainExecutor(this))
    }

    @SuppressLint("UnsafeOptInUsageError")
    private fun getImageAnalyzerListener(): ImageAnalysis.Analyzer {
        return ImageAnalysis.Analyzer { imageProxy ->
            val image = imageProxy.image ?: return@Analyzer
            val inputImage = InputImage.fromMediaImage(image, imageProxy.imageInfo.rotationDegrees)
            barcodeScanner.scanImage(inputImage) {
                imageProxy.close()
            }
        }
    }

    override fun onSuccessScan(result: List<Barcode>) {
        result.forEachIndexed { index, barcode ->
            Log.e("MainActivity3","Barcode value : ${barcode.rawValue}")
            Toast.makeText(this, "Barcode value: ${barcode.rawValue}", Toast.LENGTH_SHORT).show()
        }
    }

    override fun onScanFailed() {
        Toast.makeText(this, "Fail", Toast.LENGTH_SHORT).show()
    }

    private fun isCameraPermissionGranted(): Boolean {
        return ContextCompat.checkSelfPermission(this,
            Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
    }

    private fun requestPermission() {
        ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CAMERA),
            CAMERA_REQUEST_CODE)
    }

    override fun onDestroy() {
        super.onDestroy()
        barcodeScanner.closeScanner()
        executorService.shutdown()
    }
}

Scanner Functionality :

class BarcodeScanner(private val barcodeScannerListener: BarcodeScannerListener) {

    private val barcodeScanner: BarcodeScanner by lazy {
        constructBarcodeScanner()
    }

    private val executorService: ExecutorService by lazy {
        Executors.newSingleThreadExecutor()
    }

    fun scanImage(inputImage: InputImage, onScanComplete: (() -> Unit)? = null) {
        barcodeScanner.process(inputImage)
            .addOnCompleteListener {
                onScanComplete?.invoke()
            }
            .addOnSuccessListener {
                barcodeScannerListener.onSuccessScan(it)
            }
            .addOnFailureListener {
                Log.e("Scanner fail", "caused:", it)
                barcodeScannerListener.onScanFailed()
            }
    }

    fun closeScanner() {
        barcodeScanner.close()
        executorService.shutdown()
    }

    private fun constructBarcodeScanner(): BarcodeScanner {
        val barcodeScannerOptions = BarcodeScannerOptions.Builder()
            .setExecutor(executorService)
            .setBarcodeFormats(
              //  Barcode.FORMAT_ALL_FORMATS,
                Barcode.FORMAT_CODE_39
            )
            .build()
        return BarcodeScanning.getClient(barcodeScannerOptions)
    }
}

Below are Some other Barcodes I tested and scan results

  1. Barcode value : R000000590

enter image description here

scan results randomly picking one of the following : R000000590, R00000590, V00000590, 40000590, 200000590 .. etc

  1. Barcode value : ABC-1200000034

enter image description here scan results randomly picking one of the following : ABC-1200000034, ABC-12000034, ABC-1200000034, BP712000034, BP-12000034, ABC712000034 .. etc

Can someone please help to resolve these Barcodes with format-39 issue. Other types like QR Code or format-128 are working fine.

Upvotes: 1

Views: 1818

Answers (2)

Pasha Oleynik
Pasha Oleynik

Reputation: 515

As far as I understand, this issue is a common issue for any Barcode Scanning library. At least, I found the same bug in Zxing as well.

But, you can create kind of workaround.

Let's assume, you are scanning one barcode at a time. Let's call it "scanning session". Scanning is continuous, so library will give us some scanned barcodes continuously. In such way, we could do the next:

  1. Put all the scanned barcodes into collection

  2. When collection reaches size of X entries, filter this collection by "similarity" check

  3. Take the most repetitive value from the filtered collection (it is your confident barcode), and clear collection (prepare for the next "scanning session")

    private class BarcodeScannerAnalyzer(
     val onBarcodeDetected: (Barcode) -> Unit
    ) : ImageAnalysis.Analyzer {
    
      companion object {
         private const val CONFIDENCE_SELECTION_SIZE = 20
         private const val SIMILARITY_COEFFICIENT: Double = 0.7
         private const val CONFIDENCE_SELECTION_CLEAR_THRESHOLD = 250 // ms
     }
    
     private val sessionBarcodes: MutableList<Barcode> = mutableListOf()
     private var lastSessionTimestamp = 0L
    
     private fun addSessionBarcode(barcode: Barcode?) {
         if (System.currentTimeMillis() - lastSessionTimestamp > CONFIDENCE_SELECTION_CLEAR_THRESHOLD) {
             sessionBarcodes.clear()
         }
         barcode?.let {
             lastSessionTimestamp = System.currentTimeMillis()
             sessionBarcodes.add(it)
         }
         if (sessionBarcodes.size >= CONFIDENCE_SELECTION_SIZE) {
             processSession()
         }
     }
    
     private fun processSession() {
         filterSimilar(sessionBarcodes).takeIf {
             it.isNotEmpty()
         }?.selectConfident()?.let(onBarcodeDetected)
         sessionBarcodes.clear()
     }
    
     private fun filterSimilar(barcodes: List<Barcode>, minSimilarity: Double = SIMILARITY_COEFFICIENT): List<Barcode> {
         if (barcodes.isEmpty()) return emptyList()
    
         val indexOfLongest = barcodes.map {
             it.rawValue ?: ""
         }.indexOfMaxLength().takeIf {
             it >= 0
         } ?: return emptyList()
    
         val referenceBarcode = try {
             barcodes[indexOfLongest]
         } catch (ex: Exception) {
             return emptyList()
         }
    
         val filteredBarcodes = mutableListOf<Barcode>()
    
         for (barcode in barcodes) {
             val similarity = calculateSimilarity(referenceBarcode.rawValue, barcode.rawValue)
             if (similarity >= minSimilarity) {
                 filteredBarcodes.add(barcode)
             }
         }
    
         return filteredBarcodes
     }
    
     fun calculateSimilarity(barcode1: String?, barcode2: String?): Double {
         if (barcode1 == null || barcode2 == null) return 0.0
         val maxLen = max(barcode1.length, barcode2.length)
         val commonChars = barcode1.zip(barcode2).count { it.first == it.second }
         return commonChars.toDouble() / maxLen
     }
    
     private fun List<String>.indexOfMaxLength(): Int {
         if (isEmpty()) return -1
    
         var maxLength = 0
         var maxIndex = 0
    
         for (i in indices) {
             val length = this[i].length
             if (length > maxLength) {
                 maxLength = length
                 maxIndex = i
             }
         }
    
         return maxIndex
     }
    
     fun List<Barcode>.selectConfident(): Barcode? {
         if (isEmpty()) return null
    
         val barcodeCounts = groupingBy { it.rawValue }.eachCount()
         val mostCommonBarcode = barcodeCounts.maxByOrNull { it.value }?.key
    
         return if ((barcodeCounts[mostCommonBarcode] ?: 0) > 1) find {
             it.rawValue == mostCommonBarcode
         }  else null
     }
    
     override fun analyze(imageProxy: ImageProxy) {
         val mediaImage = imageProxy.image ?: return
         val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
         barcodeScanner.process(image)
             .addOnSuccessListener { barcodes ->
                 barcodes.forEach { addSessionBarcode(it) }
             }.addOnFailureListener {
                 // Handle somehow?
             }.addOnCompleteListener {
                 imageProxy.close()
             }
     }
    
    }
    

Upvotes: 0

Hossam Waziry
Hossam Waziry

Reputation: 61

The data is repeated because the analyzer reads the image more than once. The image must be closed when the reading is complete

 fun scanImage(inputImage: InputImage, onScanComplete: (() -> Unit)? = null) {
        barcodeScanner.process(inputImage)
            .addOnCompleteListener {
                 barcodeScannerListener.onSuccessScan(it)
            //imageProxy should be closed here 
            }
            .addOnFailureListener {
                Log.e("Scanner fail", "caused:", it)
                barcodeScannerListener.onScanFailed()
            }
    }

Or try this and see the result:

 override fun onSuccessScan(result: List<Barcode>) {
        Log.e("MainActivity3","Barcode value : ${result.toString()}")
  
}

Upvotes: 0

Related Questions