Skip to content

Commit d0cea31

Browse files
Merge branch 'refs/heads/develop' into saar
# Conflicts: # example/lib/barcode_scanner_controller.dart # lib/src/mobile_scanner_controller.dart # lib/src/objects/start_options.dart
2 parents 1b7d33d + 357c41c commit d0cea31

File tree

7 files changed

+154
-61
lines changed

7 files changed

+154
-61
lines changed

android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt

Lines changed: 69 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ package dev.steenbakker.mobile_scanner
33
import android.app.Activity
44
import android.content.Context
55
import android.graphics.Bitmap
6+
import android.graphics.Canvas
7+
import android.graphics.ColorMatrix
8+
import android.graphics.ColorMatrixColorFilter
69
import android.graphics.Matrix
10+
import android.graphics.Paint
711
import android.graphics.Rect
812
import android.hardware.display.DisplayManager
913
import android.net.Uri
@@ -59,6 +63,7 @@ class MobileScanner(
5963

6064
/// Configurable variables
6165
var scanWindow: List<Float>? = null
66+
private var invertImage: Boolean = false
6267
private var detectionSpeed: DetectionSpeed = DetectionSpeed.NO_DUPLICATES
6368
private var detectionTimeout: Long = 250
6469
private var returnImage = false
@@ -79,7 +84,12 @@ class MobileScanner(
7984
@ExperimentalGetImage
8085
val captureOutput = ImageAnalysis.Analyzer { imageProxy -> // YUV_420_888 format
8186
val mediaImage = imageProxy.image ?: return@Analyzer
82-
val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
87+
88+
val inputImage = if (invertImage) {
89+
invertInputImage(imageProxy)
90+
} else {
91+
InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
92+
}
8393

8494
if (detectionSpeed == DetectionSpeed.NORMAL && scannerTimeout) {
8595
imageProxy.close()
@@ -127,14 +137,13 @@ class MobileScanner(
127137
mobileScannerCallback(
128138
barcodeMap,
129139
null,
130-
if (portrait) mediaImage.width else mediaImage.height,
131-
if (portrait) mediaImage.height else mediaImage.width)
140+
if (portrait) inputImage.width else inputImage.height,
141+
if (portrait) inputImage.height else inputImage.width)
132142
return@addOnSuccessListener
133143
}
134144

135145
val bitmap = Bitmap.createBitmap(mediaImage.width, mediaImage.height, Bitmap.Config.ARGB_8888)
136146
val imageFormat = YuvToRgbConverter(activity.applicationContext)
137-
138147
imageFormat.yuvToRgb(mediaImage, bitmap)
139148

140149
val bmResult = rotateBitmap(bitmap, camera?.cameraInfo?.sensorRotationDegrees?.toFloat() ?: 90f)
@@ -145,6 +154,7 @@ class MobileScanner(
145154
val bmWidth = bmResult.width
146155
val bmHeight = bmResult.height
147156
bmResult.recycle()
157+
imageFormat.release()
148158

149159
mobileScannerCallback(
150160
barcodeMap,
@@ -219,11 +229,13 @@ class MobileScanner(
219229
mobileScannerStartedCallback: MobileScannerStartedCallback,
220230
mobileScannerErrorCallback: (exception: Exception) -> Unit,
221231
detectionTimeout: Long,
222-
cameraResolutionWanted: Size?
232+
cameraResolutionWanted: Size?,
233+
invertImage: Boolean,
223234
) {
224235
this.detectionSpeed = detectionSpeed
225236
this.detectionTimeout = detectionTimeout
226237
this.returnImage = returnImage
238+
this.invertImage = invertImage
227239

228240
if (camera?.cameraInfo != null && preview != null && textureEntry != null && !isPaused) {
229241

@@ -416,14 +428,14 @@ class MobileScanner(
416428
isPaused = true
417429
}
418430

419-
private fun resumeCamera() {
420-
// Resume camera by rebinding use cases
421-
cameraProvider?.let { provider ->
422-
val owner = activity as LifecycleOwner
423-
cameraSelector?.let { provider.bindToLifecycle(owner, it, preview) }
424-
}
425-
isPaused = false
426-
}
431+
// private fun resumeCamera() {
432+
// // Resume camera by rebinding use cases
433+
// cameraProvider?.let { provider ->
434+
// val owner = activity as LifecycleOwner
435+
// cameraSelector?.let { provider.bindToLifecycle(owner, it, preview) }
436+
// }
437+
// isPaused = false
438+
// }
427439

428440
private fun releaseCamera() {
429441
if (displayListener != null) {
@@ -472,6 +484,50 @@ class MobileScanner(
472484
}
473485
}
474486

487+
/**
488+
* Inverts the image colours respecting the alpha channel
489+
*/
490+
@ExperimentalGetImage
491+
fun invertInputImage(imageProxy: ImageProxy): InputImage {
492+
val image = imageProxy.image ?: throw IllegalArgumentException("Image is null")
493+
494+
// Convert YUV_420_888 image to RGB Bitmap
495+
val bitmap = Bitmap.createBitmap(image.width, image.height, Bitmap.Config.ARGB_8888)
496+
try {
497+
val imageFormat = YuvToRgbConverter(activity.applicationContext)
498+
imageFormat.yuvToRgb(image, bitmap)
499+
500+
// Create an inverted bitmap
501+
val invertedBitmap = invertBitmapColors(bitmap)
502+
imageFormat.release()
503+
504+
return InputImage.fromBitmap(invertedBitmap, imageProxy.imageInfo.rotationDegrees)
505+
} finally {
506+
// Release resources
507+
bitmap.recycle() // Free up bitmap memory
508+
imageProxy.close() // Close ImageProxy
509+
}
510+
}
511+
512+
// Efficiently invert bitmap colors using ColorMatrix
513+
private fun invertBitmapColors(bitmap: Bitmap): Bitmap {
514+
val colorMatrix = ColorMatrix().apply {
515+
set(floatArrayOf(
516+
-1f, 0f, 0f, 0f, 255f, // Red
517+
0f, -1f, 0f, 0f, 255f, // Green
518+
0f, 0f, -1f, 0f, 255f, // Blue
519+
0f, 0f, 0f, 1f, 0f // Alpha
520+
))
521+
}
522+
val paint = Paint().apply { colorFilter = ColorMatrixColorFilter(colorMatrix) }
523+
524+
val invertedBitmap = Bitmap.createBitmap(bitmap.width, bitmap.height, bitmap.config)
525+
val canvas = Canvas(invertedBitmap)
526+
canvas.drawBitmap(bitmap, 0f, 0f, paint)
527+
528+
return invertedBitmap
529+
}
530+
475531
/**
476532
* Analyze a single image.
477533
*/

android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScannerHandler.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ class MobileScannerHandler(
148148
} else {
149149
null
150150
}
151+
val invertImage: Boolean = call.argument<Boolean>("invertImage") ?: false
151152

152153
val barcodeScannerOptions: BarcodeScannerOptions? = buildBarcodeScannerOptions(formats, autoZoom)
153154

@@ -213,7 +214,8 @@ class MobileScannerHandler(
213214
}
214215
},
215216
timeout.toLong(),
216-
cameraResolution
217+
cameraResolution,
218+
invertImage,
217219
)
218220
}
219221

android/src/main/kotlin/dev/steenbakker/mobile_scanner/utils/Yuv.kt

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ the conversion is done into these formats.
4040
More about each format: https://www.fourcc.org/yuv.php
4141
*/
4242

43-
@kotlin.annotation.Retention(AnnotationRetention.SOURCE)
43+
@Retention(AnnotationRetention.SOURCE)
4444
@IntDef(ImageFormat.NV21, ImageFormat.YUV_420_888)
4545
annotation class YuvType
4646

@@ -181,9 +181,7 @@ class YuvByteBuffer(image: Image, dstBuffer: ByteBuffer? = null) {
181181
}
182182
}
183183

184-
private class PlaneWrapper(width: Int, height: Int, plane: Image.Plane) {
185-
val width = width
186-
val height = height
184+
private class PlaneWrapper(val width: Int, val height: Int, plane: Image.Plane) {
187185
val buffer: ByteBuffer = plane.buffer
188186
val rowStride = plane.rowStride
189187
val pixelStride = plane.pixelStride
Lines changed: 65 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,17 @@
1+
2+
13
package dev.steenbakker.mobile_scanner.utils
24

35
import android.content.Context
46
import android.graphics.Bitmap
5-
import android.graphics.ImageFormat
67
import android.media.Image
7-
import android.os.Build
88
import android.renderscript.Allocation
99
import android.renderscript.Element
1010
import android.renderscript.RenderScript
1111
import android.renderscript.ScriptIntrinsicYuvToRGB
1212
import android.renderscript.Type
13-
import androidx.annotation.RequiresApi
1413
import java.nio.ByteBuffer
1514

16-
1715
/**
1816
* Helper class used to efficiently convert a [Media.Image] object from
1917
* YUV_420_888 format to an RGB [Bitmap] object.
@@ -23,59 +21,84 @@ import java.nio.ByteBuffer
2321
* The [yuvToRgb] method is able to achieve the same FPS as the CameraX image
2422
* analysis use case at the default analyzer resolution, which is 30 FPS with
2523
* 640x480 on a Pixel 3 XL device.
26-
*/class YuvToRgbConverter(context: Context) {
24+
*/
25+
/// TODO: Upgrade to implementation without deprecated android.renderscript, but with same or better performance. See https://github.com/juliansteenbakker/mobile_scanner/issues/1142
26+
class YuvToRgbConverter(context: Context) {
27+
@Suppress("DEPRECATION")
2728
private val rs = RenderScript.create(context)
29+
@Suppress("DEPRECATION")
2830
private val scriptYuvToRgb =
2931
ScriptIntrinsicYuvToRGB.create(rs, Element.U8_4(rs))
3032

31-
// Do not add getters/setters functions to these private variables
32-
// because yuvToRgb() assume they won't be modified elsewhere
3333
private var yuvBits: ByteBuffer? = null
3434
private var bytes: ByteArray = ByteArray(0)
35+
@Suppress("DEPRECATION")
3536
private var inputAllocation: Allocation? = null
37+
@Suppress("DEPRECATION")
3638
private var outputAllocation: Allocation? = null
3739

3840
@Synchronized
3941
fun yuvToRgb(image: Image, output: Bitmap) {
40-
val yuvBuffer = YuvByteBuffer(image, yuvBits)
41-
yuvBits = yuvBuffer.buffer
42+
try {
43+
val yuvBuffer = YuvByteBuffer(image, yuvBits)
44+
yuvBits = yuvBuffer.buffer
4245

43-
if (needCreateAllocations(image, yuvBuffer)) {
44-
val yuvType = Type.Builder(rs, Element.U8(rs))
45-
.setX(image.width)
46-
.setY(image.height)
47-
.setYuvFormat(yuvBuffer.type)
48-
inputAllocation = Allocation.createTyped(
49-
rs,
50-
yuvType.create(),
51-
Allocation.USAGE_SCRIPT
52-
)
53-
bytes = ByteArray(yuvBuffer.buffer.capacity())
54-
val rgbaType = Type.Builder(rs, Element.RGBA_8888(rs))
55-
.setX(image.width)
56-
.setY(image.height)
57-
outputAllocation = Allocation.createTyped(
58-
rs,
59-
rgbaType.create(),
60-
Allocation.USAGE_SCRIPT
61-
)
62-
}
46+
if (needCreateAllocations(image, yuvBuffer)) {
47+
createAllocations(image, yuvBuffer)
48+
}
6349

64-
yuvBuffer.buffer.get(bytes)
65-
inputAllocation!!.copyFrom(bytes)
50+
yuvBuffer.buffer.get(bytes)
51+
@Suppress("DEPRECATION")
52+
inputAllocation!!.copyFrom(bytes)
6653

67-
// Convert NV21 or YUV_420_888 format to RGB
68-
inputAllocation!!.copyFrom(bytes)
69-
scriptYuvToRgb.setInput(inputAllocation)
70-
scriptYuvToRgb.forEach(outputAllocation)
71-
outputAllocation!!.copyTo(output)
54+
@Suppress("DEPRECATION")
55+
scriptYuvToRgb.setInput(inputAllocation)
56+
@Suppress("DEPRECATION")
57+
scriptYuvToRgb.forEach(outputAllocation)
58+
@Suppress("DEPRECATION")
59+
outputAllocation!!.copyTo(output)
60+
} catch (e: Exception) {
61+
throw IllegalStateException("Failed to convert YUV to RGB", e)
62+
}
7263
}
7364

7465
private fun needCreateAllocations(image: Image, yuvBuffer: YuvByteBuffer): Boolean {
75-
return (inputAllocation == null || // the very 1st call
76-
inputAllocation!!.type.x != image.width || // image size changed
77-
inputAllocation!!.type.y != image.height ||
78-
inputAllocation!!.type.yuv != yuvBuffer.type || // image format changed
79-
bytes.size == yuvBuffer.buffer.capacity())
66+
@Suppress("DEPRECATION")
67+
return inputAllocation?.type?.x != image.width ||
68+
inputAllocation?.type?.y != image.height ||
69+
inputAllocation?.type?.yuv != yuvBuffer.type
70+
}
71+
72+
private fun createAllocations(image: Image, yuvBuffer: YuvByteBuffer) {
73+
@Suppress("DEPRECATION")
74+
val yuvType = Type.Builder(rs, Element.U8(rs))
75+
.setX(image.width)
76+
.setY(image.height)
77+
.setYuvFormat(yuvBuffer.type)
78+
@Suppress("DEPRECATION")
79+
inputAllocation = Allocation.createTyped(
80+
rs,
81+
yuvType.create(),
82+
Allocation.USAGE_SCRIPT
83+
)
84+
bytes = ByteArray(yuvBuffer.buffer.capacity())
85+
@Suppress("DEPRECATION")
86+
val rgbaType = Type.Builder(rs, Element.RGBA_8888(rs))
87+
.setX(image.width)
88+
.setY(image.height)
89+
@Suppress("DEPRECATION")
90+
outputAllocation = Allocation.createTyped(
91+
rs,
92+
rgbaType.create(),
93+
Allocation.USAGE_SCRIPT
94+
)
95+
}
96+
97+
@Suppress("DEPRECATION")
98+
fun release() {
99+
inputAllocation?.destroy()
100+
outputAllocation?.destroy()
101+
scriptYuvToRgb.destroy()
102+
rs.destroy()
80103
}
81-
}
104+
}

example/lib/barcode_scanner_controller.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ class _BarcodeScannerWithControllerState
1818
extends State<BarcodeScannerWithController> with WidgetsBindingObserver {
1919
final MobileScannerController controller = MobileScannerController(
2020
autoStart: false,
21-
torchEnabled: true,
21+
// torchEnabled: true,
2222
enableAutoZoom: true,
23+
// invertImage: true,
2324
);
2425

2526
@override

lib/src/mobile_scanner_controller.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
2828
this.formats = const <BarcodeFormat>[],
2929
this.returnImage = false,
3030
this.torchEnabled = false,
31+
this.invertImage = false,
3132
this.autoZoom = false,
3233
}) : detectionTimeoutMs =
3334
detectionSpeed == DetectionSpeed.normal ? detectionTimeoutMs : 0,
@@ -85,6 +86,12 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
8586
/// Defaults to false, and is only supported on iOS, MacOS and Android.
8687
final bool returnImage;
8788

89+
/// Invert image colors for analyzer to support white-on-black barcodes, which are not supported by MLKit.
90+
/// Usage of this parameter can incur a performance cost, as frames need to be altered during processing.
91+
///
92+
/// Defaults to false and is only supported on Android.
93+
final bool invertImage;
94+
8895
/// Whether the flashlight should be turned on when the camera is started.
8996
///
9097
/// Defaults to false.
@@ -311,6 +318,7 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
311318
formats: formats,
312319
returnImage: returnImage,
313320
torchEnabled: torchEnabled,
321+
invertImage: invertImage,
314322
autoZoom: autoZoom,
315323
);
316324

lib/src/objects/start_options.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class StartOptions {
1515
required this.formats,
1616
required this.returnImage,
1717
required this.torchEnabled,
18+
required this.invertImage,
1819
required this.autoZoom,
1920
});
2021

@@ -24,6 +25,9 @@ class StartOptions {
2425
/// The desired camera resolution for the scanner.
2526
final Size? cameraResolution;
2627

28+
/// Invert image colors for analyzer to support white-on-black barcodes, which are not supported by MLKit.
29+
final bool invertImage;
30+
2731
/// The detection speed for the scanner.
2832
final DetectionSpeed detectionSpeed;
2933

@@ -60,6 +64,7 @@ class StartOptions {
6064
'speed': detectionSpeed.rawValue,
6165
'timeout': detectionTimeoutMs,
6266
'torch': torchEnabled,
67+
'invertImage': invertImage,
6368
'autoZoom': autoZoom,
6469
};
6570
}

0 commit comments

Comments
 (0)