Reputation: 1872
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.
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
scan results randomly picking one of the following : R000000590, R00000590, V00000590, 40000590, 200000590 .. etc
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
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:
Put all the scanned barcodes into collection
When collection reaches size of X entries, filter this collection by "similarity" check
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
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