Skip to content

Commit 0277f9b

Browse files
imp: improve scan speed for inverted images
1 parent 46850cd commit 0277f9b

File tree

3 files changed

+89
-71
lines changed

3 files changed

+89
-71
lines changed

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

Lines changed: 40 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import android.annotation.SuppressLint
44
import android.app.Activity
55
import android.content.Context
66
import android.graphics.Bitmap
7+
import android.graphics.Canvas
8+
import android.graphics.ColorMatrix
9+
import android.graphics.ColorMatrixColorFilter
710
import android.graphics.Matrix
11+
import android.graphics.Paint
812
import android.graphics.Rect
913
import android.hardware.display.DisplayManager
1014
import android.media.Image
@@ -141,14 +145,13 @@ class MobileScanner(
141145
mobileScannerCallback(
142146
barcodeMap,
143147
null,
144-
if (portrait) mediaImage.width else mediaImage.height,
145-
if (portrait) mediaImage.height else mediaImage.width)
148+
if (portrait) inputImage.width else inputImage.height,
149+
if (portrait) inputImage.height else inputImage.width)
146150
return@addOnSuccessListener
147151
}
148152

149153
val bitmap = Bitmap.createBitmap(mediaImage.width, mediaImage.height, Bitmap.Config.ARGB_8888)
150-
val imageFormat = YuvToRgbConverter(activity.applicationContext)
151-
154+
val imageFormat =YuvToRgbConverter(activity.applicationContext)
152155
imageFormat.yuvToRgb(mediaImage, bitmap)
153156

154157
val bmResult = rotateBitmap(bitmap, camera?.cameraInfo?.sensorRotationDegrees?.toFloat() ?: 90f)
@@ -159,6 +162,7 @@ class MobileScanner(
159162
val bmWidth = bmResult.width
160163
val bmHeight = bmResult.height
161164
bmResult.recycle()
165+
imageFormat.release()
162166

163167
mobileScannerCallback(
164168
barcodeMap,
@@ -233,8 +237,7 @@ class MobileScanner(
233237
mobileScannerStartedCallback: MobileScannerStartedCallback,
234238
mobileScannerErrorCallback: (exception: Exception) -> Unit,
235239
detectionTimeout: Long,
236-
cameraResolution: Size?,
237-
newCameraResolutionSelector: Boolean,
240+
cameraResolutionWanted: Size?,
238241
shouldConsiderInvertedImages: Boolean,
239242
) {
240243
this.detectionSpeed = detectionSpeed
@@ -492,40 +495,45 @@ class MobileScanner(
492495
/**
493496
* Inverts the image colours respecting the alpha channel
494497
*/
495-
@SuppressLint("UnsafeOptInUsageError")
498+
@ExperimentalGetImage
496499
fun invertInputImage(imageProxy: ImageProxy): InputImage {
497500
val image = imageProxy.image ?: throw IllegalArgumentException("Image is null")
498501

499-
// Convert YUV_420_888 image to NV21 format
500-
// based on our util helper
502+
// Convert YUV_420_888 image to RGB Bitmap
501503
val bitmap = Bitmap.createBitmap(image.width, image.height, Bitmap.Config.ARGB_8888)
502-
YuvToRgbConverter(activity).yuvToRgb(image, bitmap)
503-
504-
// Invert RGB values
505-
invertBitmapColors(bitmap)
506-
507-
return InputImage.fromBitmap(bitmap, imageProxy.imageInfo.rotationDegrees)
504+
try {
505+
val imageFormat =YuvToRgbConverter(activity.applicationContext);
506+
imageFormat.yuvToRgb(image, bitmap)
507+
508+
// Create an inverted bitmap
509+
val invertedBitmap = invertBitmapColors(bitmap)
510+
imageFormat.release()
511+
512+
return InputImage.fromBitmap(invertedBitmap, imageProxy.imageInfo.rotationDegrees)
513+
} finally {
514+
// Release resources
515+
bitmap.recycle() // Free up bitmap memory
516+
imageProxy.close() // Close ImageProxy
517+
}
508518
}
509519

510-
// Helper function to invert the colors of the bitmap
511-
private fun invertBitmapColors(bitmap: Bitmap) {
512-
val width = bitmap.width
513-
val height = bitmap.height
514-
for (x in 0 until width) {
515-
for (y in 0 until height) {
516-
val pixel = bitmap.getPixel(x, y)
517-
val invertedColor = invertColor(pixel)
518-
bitmap.setPixel(x, y, invertedColor)
519-
}
520+
// Efficiently invert bitmap colors using ColorMatrix
521+
private fun invertBitmapColors(bitmap: Bitmap): Bitmap {
522+
val colorMatrix = ColorMatrix().apply {
523+
set(floatArrayOf(
524+
-1f, 0f, 0f, 0f, 255f, // Red
525+
0f, -1f, 0f, 0f, 255f, // Green
526+
0f, 0f, -1f, 0f, 255f, // Blue
527+
0f, 0f, 0f, 1f, 0f // Alpha
528+
))
520529
}
521-
}
530+
val paint = Paint().apply { colorFilter = ColorMatrixColorFilter(colorMatrix) }
531+
532+
val invertedBitmap = Bitmap.createBitmap(bitmap.width, bitmap.height, bitmap.config)
533+
val canvas = Canvas(invertedBitmap)
534+
canvas.drawBitmap(bitmap, 0f, 0f, paint)
522535

523-
private fun invertColor(pixel: Int): Int {
524-
val alpha = pixel and 0xFF000000.toInt()
525-
val red = 255 - (pixel shr 16 and 0xFF)
526-
val green = 255 - (pixel shr 8 and 0xFF)
527-
val blue = 255 - (pixel and 0xFF)
528-
return alpha or (red shl 16) or (green shl 8) or blue
536+
return invertedBitmap
529537
}
530538

531539
/**

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,6 @@ class MobileScannerHandler(
211211
},
212212
timeout.toLong(),
213213
cameraResolution,
214-
useNewCameraSelector,
215214
shouldConsiderInvertedImages,
216215
)
217216
}

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

Lines changed: 49 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import android.graphics.ImageFormat
66
import android.media.Image
77
import android.os.Build
88
import android.renderscript.Allocation
9+
import android.renderscript.Allocation.createAllocations
910
import android.renderscript.Element
1011
import android.renderscript.RenderScript
1112
import android.renderscript.ScriptIntrinsicYuvToRGB
@@ -23,59 +24,69 @@ import java.nio.ByteBuffer
2324
* The [yuvToRgb] method is able to achieve the same FPS as the CameraX image
2425
* analysis use case at the default analyzer resolution, which is 30 FPS with
2526
* 640x480 on a Pixel 3 XL device.
26-
*/class YuvToRgbConverter(context: Context) {
27+
*/
28+
class YuvToRgbConverter(context: Context) {
2729
private val rs = RenderScript.create(context)
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)
3535
private var inputAllocation: Allocation? = null
3636
private var outputAllocation: Allocation? = null
3737

3838
@Synchronized
3939
fun yuvToRgb(image: Image, output: Bitmap) {
40-
val yuvBuffer = YuvByteBuffer(image, yuvBits)
41-
yuvBits = yuvBuffer.buffer
40+
try {
41+
val yuvBuffer = YuvByteBuffer(image, yuvBits)
42+
yuvBits = yuvBuffer.buffer
4243

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-
}
44+
if (needCreateAllocations(image, yuvBuffer)) {
45+
createAllocations(image, yuvBuffer)
46+
}
6347

64-
yuvBuffer.buffer.get(bytes)
65-
inputAllocation!!.copyFrom(bytes)
48+
yuvBuffer.buffer.get(bytes)
49+
inputAllocation!!.copyFrom(bytes)
6650

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)
51+
scriptYuvToRgb.setInput(inputAllocation)
52+
scriptYuvToRgb.forEach(outputAllocation)
53+
outputAllocation!!.copyTo(output)
54+
} catch (e: Exception) {
55+
throw IllegalStateException("Failed to convert YUV to RGB", e)
56+
}
7257
}
7358

7459
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())
60+
return inputAllocation?.type?.x != image.width ||
61+
inputAllocation?.type?.y != image.height ||
62+
inputAllocation?.type?.yuv != yuvBuffer.type
63+
}
64+
65+
private fun createAllocations(image: Image, yuvBuffer: YuvByteBuffer) {
66+
val yuvType = Type.Builder(rs, Element.U8(rs))
67+
.setX(image.width)
68+
.setY(image.height)
69+
.setYuvFormat(yuvBuffer.type)
70+
inputAllocation = Allocation.createTyped(
71+
rs,
72+
yuvType.create(),
73+
Allocation.USAGE_SCRIPT
74+
)
75+
bytes = ByteArray(yuvBuffer.buffer.capacity())
76+
val rgbaType = Type.Builder(rs, Element.RGBA_8888(rs))
77+
.setX(image.width)
78+
.setY(image.height)
79+
outputAllocation = Allocation.createTyped(
80+
rs,
81+
rgbaType.create(),
82+
Allocation.USAGE_SCRIPT
83+
)
84+
}
85+
86+
fun release() {
87+
inputAllocation?.destroy()
88+
outputAllocation?.destroy()
89+
scriptYuvToRgb.destroy()
90+
rs.destroy()
8091
}
81-
}
92+
}

0 commit comments

Comments
 (0)