11package org.ncgroup.kscan
22
3+ import androidx.annotation.OptIn
34import androidx.camera.core.Camera
45import androidx.camera.core.ExperimentalGetImage
56import androidx.camera.core.ImageAnalysis
67import androidx.camera.core.ImageProxy
78import com.google.mlkit.vision.barcode.BarcodeScannerOptions
89import com.google.mlkit.vision.barcode.BarcodeScanning
910import com.google.mlkit.vision.barcode.ZoomSuggestionOptions
10- import com.google.mlkit.vision.barcode.common.Barcode.FORMAT_ALL_FORMATS
11- import com.google.mlkit.vision.barcode.common.Barcode.FORMAT_AZTEC
12- import com.google.mlkit.vision.barcode.common.Barcode.FORMAT_CODABAR
13- import com.google.mlkit.vision.barcode.common.Barcode.FORMAT_CODE_128
14- import com.google.mlkit.vision.barcode.common.Barcode.FORMAT_CODE_39
15- import com.google.mlkit.vision.barcode.common.Barcode.FORMAT_CODE_93
16- import com.google.mlkit.vision.barcode.common.Barcode.FORMAT_DATA_MATRIX
17- import com.google.mlkit.vision.barcode.common.Barcode.FORMAT_EAN_13
18- import com.google.mlkit.vision.barcode.common.Barcode.FORMAT_EAN_8
19- import com.google.mlkit.vision.barcode.common.Barcode.FORMAT_ITF
20- import com.google.mlkit.vision.barcode.common.Barcode.FORMAT_PDF417
21- import com.google.mlkit.vision.barcode.common.Barcode.FORMAT_QR_CODE
22- import com.google.mlkit.vision.barcode.common.Barcode.FORMAT_UNKNOWN
23- import com.google.mlkit.vision.barcode.common.Barcode.FORMAT_UPC_A
24- import com.google.mlkit.vision.barcode.common.Barcode.FORMAT_UPC_E
2511import com.google.mlkit.vision.common.InputImage
2612
2713/* *
28- * Analyzes images for barcodes using ML Kit.
14+ * Analyzes camera frames for barcodes using ML Kit.
2915 *
30- * This class implements [ImageAnalysis.Analyzer] to process camera frames.
31- * It uses ML Kit's Barcode Scanning API to detect and decode barcodes.
32- *
33- * It features:
34- * - **Configurable Barcode Types**: Scans for specific barcode formats defined by `codeTypes`.
35- * - **Zoom Suggestion**: Utilizes ML Kit's zoom suggestion feature to prompt the user to zoom if a barcode is detected but is too small. The zoom is handled automatically if the camera supports it.
36- * - **Duplicate Filtering**: To ensure accuracy and prevent multiple triggers for the same barcode, a barcode must be detected twice in quick succession before it's considered successfully processed.
37- * - **Single Success Processing**: Once a barcode is successfully processed (detected twice), further analysis is stopped to prevent redundant callbacks.
38- * - **Callbacks**:
39- * - `onSuccess`: Called when a barcode is successfully detected and meets the criteria.
40- * - `onFailed`: Called if an error occurs during the barcode scanning process.
41- * - `onCanceled`: Called if the barcode scanning task is canceled.
42- *
43- * The analyzer maps ML Kit's barcode formats to a custom `BarcodeFormat` enum for application-specific use.
44- *
45- * @property camera The [Camera] instance, used for zoom control. Can be null if zoom control is not needed or available.
46- * @property codeTypes A list of [BarcodeFormat] enums specifying which barcode types to scan for. If empty or contains `BarcodeFormat.FORMAT_ALL_FORMATS`, all supported formats are scanned.
47- * @property onSuccess A callback function that is invoked when a barcode is successfully detected and validated. It receives a list containing the single detected [Barcode].
48- * @property onFailed A callback function that is invoked when an error occurs during the image analysis or barcode scanning process. It receives the [Exception] that occurred.
49- * @property onCanceled A callback function that is invoked if the barcode scanning task is canceled.
16+ * Features duplicate filtering (barcode must be detected twice) and auto-zoom suggestions.
5017 */
5118class BarcodeAnalyzer (
52- private val camera : Camera ? ,
19+ private val getCamera : () -> Camera ? ,
5320 private val codeTypes : List <BarcodeFormat >,
5421 private val onSuccess : (List <Barcode >) -> Unit ,
5522 private val onFailed : (Exception ) -> Unit ,
@@ -59,10 +26,11 @@ class BarcodeAnalyzer(
5926 private val scannerOptions =
6027 BarcodeScannerOptions
6128 .Builder ()
62- .setBarcodeFormats(getMLKitBarcodeFormats (codeTypes))
29+ .setBarcodeFormats(BarcodeFormatMapper .toMlKitFormats (codeTypes))
6330 .setZoomSuggestionOptions(
6431 ZoomSuggestionOptions
6532 .Builder { zoomRatio ->
33+ val camera = getCamera()
6634 val maxZoomRatio =
6735 (
6836 camera
@@ -83,9 +51,9 @@ class BarcodeAnalyzer(
8351
8452 private val scanner = BarcodeScanning .getClient(scannerOptions)
8553 private val barcodesDetected = mutableMapOf<String , Int >()
86- private var hasSuccessfullyProcessedBarcode = false //
54+ private var hasSuccessfullyProcessedBarcode = false
8755
88- @androidx.annotation. OptIn (ExperimentalGetImage ::class )
56+ @OptIn(ExperimentalGetImage ::class )
8957 override fun analyze (imageProxy : ImageProxy ) {
9058 if (hasSuccessfullyProcessedBarcode) {
9159 imageProxy.close()
@@ -106,18 +74,87 @@ class BarcodeAnalyzer(
10674 val relevantBarcodes = barcodes.filter { isRequestedFormat(it) }
10775 if (relevantBarcodes.isNotEmpty()) {
10876 processFoundBarcodes(relevantBarcodes)
77+ imageProxy.close()
78+ } else {
79+ // If no barcodes found, try scanning the inverted image
80+ scanInverted(imageProxy)
10981 }
11082 }.addOnFailureListener {
11183 onFailed(it)
11284 imageProxy.close()
11385 }.addOnCanceledListener {
11486 onCanceled()
11587 imageProxy.close()
88+ }
89+ }
90+
91+ private fun scanInverted (imageProxy : ImageProxy ) {
92+ val invertedImage =
93+ try {
94+ createInvertedInputImage(imageProxy)
95+ } catch (e: Exception ) {
96+ // Conversion failed, clean up and exit
97+ imageProxy.close()
98+ return
99+ }
100+
101+ scanner
102+ .process(invertedImage)
103+ .addOnSuccessListener { barcodes ->
104+ val relevantBarcodes = barcodes.filter { isRequestedFormat(it) }
105+ if (relevantBarcodes.isNotEmpty()) {
106+ processFoundBarcodes(relevantBarcodes)
107+ }
108+ }.addOnFailureListener {
109+ onFailed(it)
110+ }.addOnCanceledListener {
111+ onCanceled()
116112 }.addOnCompleteListener {
113+ // CRITICAL: Always close the proxy after the final attempt
117114 imageProxy.close()
118115 }
119116 }
120117
118+ @OptIn(ExperimentalGetImage ::class )
119+ private fun createInvertedInputImage (imageProxy : ImageProxy ): InputImage {
120+ val mediaImage = imageProxy.image ? : throw IllegalArgumentException (" Image is null" )
121+ require(mediaImage.planes.isNotEmpty()) { " Image has no planes" }
122+
123+ val width = mediaImage.width
124+ val height = mediaImage.height
125+ val yPixelCount = width * height
126+ val nv21Bytes = ByteArray (yPixelCount * 3 / 2 )
127+
128+ val yPlane = mediaImage.planes[0 ]
129+ val rowStride = yPlane.rowStride
130+ require(rowStride >= width) { " Invalid Y rowStride: $rowStride , width: $width " }
131+
132+ val yBuffer = yPlane.buffer.duplicate()
133+ val rowBytes = ByteArray (width)
134+
135+ // Bulk-read one row at a time, then invert into output (fewer ByteBuffer.get() calls)
136+ for (row in 0 until height) {
137+ yBuffer.position(row * rowStride)
138+ yBuffer.get(rowBytes, 0 , width)
139+
140+ val outBase = row * width
141+ for (col in 0 until width) {
142+ nv21Bytes[outBase + col] = (rowBytes[col].toInt() xor 0xFF ).toByte()
143+ }
144+ }
145+
146+ // Neutral chroma for grayscale in NV21 (VU interleaved)
147+ java.util.Arrays .fill(nv21Bytes, yPixelCount, nv21Bytes.size, 128 .toByte())
148+
149+ return InputImage .fromByteArray(
150+ nv21Bytes,
151+ width,
152+ height,
153+ imageProxy.imageInfo.rotationDegrees,
154+ InputImage .IMAGE_FORMAT_NV21 ,
155+ )
156+ }
157+
121158 private fun processFoundBarcodes (mlKitBarcodes : List <com.google.mlkit.vision.barcode.common.Barcode >) {
122159 if (hasSuccessfullyProcessedBarcode) return
123160
@@ -127,7 +164,7 @@ class BarcodeAnalyzer(
127164
128165 barcodesDetected[displayValue] = (barcodesDetected[displayValue] ? : 0 ) + 1
129166 if ((barcodesDetected[displayValue] ? : 0 ) >= 2 ) {
130- val appSpecificFormat = mlKitFormatToAppFormat (mlKitBarcode.format)
167+ val appSpecificFormat = BarcodeFormatMapper .toAppFormat (mlKitBarcode.format)
131168 val detectedAppBarcode =
132169 Barcode (
133170 data = displayValue,
@@ -147,47 +184,9 @@ class BarcodeAnalyzer(
147184
148185 private fun isRequestedFormat (mlKitBarcode : com.google.mlkit.vision.barcode.common.Barcode ): Boolean {
149186 if (codeTypes.contains(BarcodeFormat .FORMAT_ALL_FORMATS )) {
150- return MLKIT_TO_APP_FORMAT_MAP .containsKey (mlKitBarcode.format)
187+ return BarcodeFormatMapper .isKnownFormat (mlKitBarcode.format)
151188 }
152- val appFormat = mlKitFormatToAppFormat (mlKitBarcode.format)
189+ val appFormat = BarcodeFormatMapper .toAppFormat (mlKitBarcode.format)
153190 return codeTypes.contains(appFormat)
154191 }
155-
156- companion object {
157- private val APP_TO_MLKIT_FORMAT_MAP : Map <BarcodeFormat , Int > =
158- mapOf (
159- BarcodeFormat .FORMAT_QR_CODE to FORMAT_QR_CODE ,
160- BarcodeFormat .FORMAT_CODE_128 to FORMAT_CODE_128 ,
161- BarcodeFormat .FORMAT_CODE_39 to FORMAT_CODE_39 ,
162- BarcodeFormat .FORMAT_CODE_93 to FORMAT_CODE_93 ,
163- BarcodeFormat .FORMAT_CODABAR to FORMAT_CODABAR ,
164- BarcodeFormat .FORMAT_DATA_MATRIX to FORMAT_DATA_MATRIX ,
165- BarcodeFormat .FORMAT_EAN_13 to FORMAT_EAN_13 ,
166- BarcodeFormat .FORMAT_EAN_8 to FORMAT_EAN_8 ,
167- BarcodeFormat .FORMAT_ITF to FORMAT_ITF ,
168- BarcodeFormat .FORMAT_UPC_A to FORMAT_UPC_A ,
169- BarcodeFormat .FORMAT_UPC_E to FORMAT_UPC_E ,
170- BarcodeFormat .FORMAT_PDF417 to FORMAT_PDF417 ,
171- BarcodeFormat .FORMAT_AZTEC to FORMAT_AZTEC ,
172- )
173-
174- private val MLKIT_TO_APP_FORMAT_MAP : Map <Int , BarcodeFormat > =
175- APP_TO_MLKIT_FORMAT_MAP .entries
176- .associateBy({ it.value }) { it.key }
177- .plus(FORMAT_UNKNOWN to BarcodeFormat .TYPE_UNKNOWN )
178-
179- fun getMLKitBarcodeFormats (appFormats : List <BarcodeFormat >): Int {
180- if (appFormats.isEmpty() || appFormats.contains(BarcodeFormat .FORMAT_ALL_FORMATS )) {
181- return FORMAT_ALL_FORMATS
182- }
183-
184- return appFormats
185- .mapNotNull { APP_TO_MLKIT_FORMAT_MAP [it] }
186- .distinct()
187- .fold(0 ) { acc, formatInt -> acc or formatInt }
188- .let { if (it == 0 ) FORMAT_ALL_FORMATS else it }
189- }
190-
191- fun mlKitFormatToAppFormat (mlKitFormat : Int ): BarcodeFormat = MLKIT_TO_APP_FORMAT_MAP [mlKitFormat] ? : BarcodeFormat .TYPE_UNKNOWN
192- }
193192}
0 commit comments