NiceToMytyuk
NiceToMytyuk

Reputation: 4307

How to prevent barcode from being read twice in MlKit?

I'm having an issue with my MlKit barcode reader, I'm reading one barcode per time, like once a barcode is read, I open a dialog and stop the camera.

The issue is that the reader is too fast and reads the barcode value twice and i would prevent it.

I've yet tried to set a variable like "isReading" and to check for it in the onBitmapPrepared() but that had no results.

The only thing that works is to call close() after a barcode read, but that will prevent the next barcode from being read.

Here is the Analyzer:

class BarcodeAnalyzer(overlay: ViewFinderOverlay, private val prefix: String)
    : BaseAnalyzer<String>(overlay) {

    private var scanner: BarcodeScanner = run {
        val options = BarcodeScannerOptions.Builder()
            .setBarcodeFormats(
                Barcode.FORMAT_CODE_128,
                Barcode.FORMAT_CODE_39,
                Barcode.FORMAT_CODE_93,
                Barcode.FORMAT_EAN_13,
                Barcode.FORMAT_EAN_8,
                Barcode.FORMAT_UPC_A,
                Barcode.FORMAT_UPC_E,
                Barcode.FORMAT_QR_CODE
            )
            .build()
        BarcodeScanning.getClient(options)
    }

    override fun onBitmapPrepared(bitmap: Bitmap) {
        val inputImage: InputImage = InputImage.fromBitmap(bitmap, 0)
        val result = Tasks.await(scanner.process(inputImage))

        result.firstOrNull()?.let { barcode ->
            barcode.rawValue?.let { rawValue ->

                if (barcode.format == Barcode.FORMAT_CODE_39) {
                    val code32 = Utils.code39ToCode32(rawValue, prefix)
                    postResult(code32)
                } else {
                    postResult(rawValue)
                }
            }
        }
    }

    override fun close() {
        scanner.close()
    }
}

And the usage in the fragment:

@AndroidEntryPoint
class BarcodeReaderFragment : Fragment() {
...
private val scanner: BarcodeReaderViewModel by activityViewModels()
    private var camera: Camera? = null
    private var cameraProvider: ProcessCameraProvider? = null
    /** Blocking camera operations are performed using this executor */
    private lateinit var cameraExecutor: ExecutorService
    private var flashEnabled = false

    private val analyzer: BarcodeAnalyzer by lazy {
        BarcodeAnalyzer(binding.overlay, sharedPreferences.getString("code32Prefix", "A") ?: "A")
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        ...

        // Initialize our background executor
        cameraExecutor = Executors.newSingleThreadExecutor()
        lifecycleScope.launch { setupUIAndObservers() }

        setUp()
    }

    private fun setUp() {
        analyzer.liveData().observe(viewLifecycleOwner) { barcode ->
            beepManager.playBeepSoundAndVibrate()

            Log.d("DEBUG", barcode.toString())

            if (!args.singleDecode) {
                scanner.setBarcode(barcode)
            }
            if (args.singleDecode) {
                navigation.previousBackStackEntry?.savedStateHandle?.set("barcode", barcode)
                navigation.popBackStack()
            }
        }

        analyzer.errorLiveData().observe(viewLifecycleOwner) { e ->
            Log.e("Analysing failed", e.message.toString())
            Toast.makeText(requireContext(), "Scanner failed, reason: ${e.message}", Toast.LENGTH_SHORT).show()
        }
    }

    private fun bindPreview() {
        lifecycle.addObserver(analyzer)
        val cameraProvider = cameraProvider ?: throw IllegalStateException("Camera initialization failed.")

        val preview: Preview = Preview.Builder()
            .setTargetResolution(Size(TARGET_PREVIEW_WIDTH, TARGET_PREVIEW_HEIGHT))
            .build()

        val cameraSelector: CameraSelector = CameraSelector.Builder()
            .requireLensFacing(CameraSelector.LENS_FACING_BACK)
            .build()

        val imageAnalyzer = ImageAnalysis.Builder()
            .setTargetResolution(Size(TARGET_PREVIEW_WIDTH, TARGET_PREVIEW_HEIGHT))
            .setBackpressureStrategy(STRATEGY_KEEP_ONLY_LATEST)
            .build()
            .also {
                it.setAnalyzer(cameraExecutor, analyzer)
            }

        try {
            // Must unbind the use-cases before rebinding them
            cameraProvider.unbindAll()

            camera =
                cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageAnalyzer)

            preview.setSurfaceProvider(binding.cameraPreview.surfaceProvider)

            if (camera?.cameraInfo?.hasFlashUnit() == true) {
                lifecycleScope.launch {
                    binding.ivFlashControl.visibility = View.VISIBLE
                }

                binding.ivFlashControl.setOnClickListener {
                    camera?.cameraControl?.enableTorch(!flashEnabled)
                }

                camera?.cameraInfo?.torchState?.observe(viewLifecycleOwner) {
                    it?.let { torchState ->
                        if (torchState == TorchState.ON) {
                            flashEnabled = true
                            binding.ivFlashControl.isSelected = true
                        } else {
                            flashEnabled = false
                            binding.ivFlashControl.isSelected = false
                        }
                    }
                }
            }
        }catch (e: Exception) {
            e.printStackTrace()
        }

    }

    private fun observeScannerStatus() {
        scanner.isCodeReaderEnabled.observe(viewLifecycleOwner) { isEnabled ->
            if (isEnabled) {
                lifecycleScope.launch {
                    bindPreview()
                }
            } else {
                lifecycleScope.launch {
                    cameraProvider?.unbindAll()
                }
            }
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
        // Nullify camera when view is destroyed
        camera = null
        // Shut down our background executor
        cameraExecutor.shutdown()
        cameraProvider?.unbindAll()
    }

...
}

The scanner state is managed by the observeScannerStatus(), while in setUp() i get the value from the read barcode and here if the barcode is scanned, the value is got twice.

EDIT:

One of the solutions I've found is to create a public variable in BarcodeAnalyzer called isEnabled and to set it to false once the barcode is read, then in observeScannerStatus I'm setting it to true.

But it seems a bit messy as solution.

Upvotes: 0

Views: 562

Answers (1)

Joyful Wasp
Joyful Wasp

Reputation: 406

I don't know how to code it in Kotlin, but here is the solution I implemented in Java. Keep in mind that the variable barcodes is a list of Barcode!

public void analyze(@NonNull ImageProxy imageProxy) {

    Image mediaImage = imageProxy.getImage();
    int rotation = imageProxy.getImageInfo().getRotationDegrees();

    if (mediaImage != null) {
        InputImage image =
                InputImage.fromMediaImage(mediaImage, rotation);

        scanner.process(image)
                .addOnSuccessListener(barcodes -> {
                    if (!barcodes.isEmpty()) {
                        Barcode barcode = barcodes.get(0);
                        processBarcodeResult(barcode, imageProxy, barcodes.size());
                    }
                })
                .addOnCompleteListener(results -> imageProxy.close());
    }
}

Upvotes: 2

Related Questions