Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/develop' into develop
Browse files Browse the repository at this point in the history
# Conflicts:
#	CHANGELOG.md
#	android/src/main/kotlin/dev/steenbakker/mobile_scanner/utils/YuvToRgbConverter.kt
  • Loading branch information
juliansteenbakker committed Jan 27, 2025
2 parents 1942ba7 + 05e9a8e commit 7179d3a
Show file tree
Hide file tree
Showing 9 changed files with 174 additions and 37 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ See the example app for detailed implementation information.
| analyzeImage (Gallery) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :x: |
| returnImage | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :x: |
| scanWindow | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :x: |
| autoZoom | :heavy_check_mark: | :x: | :x: | :x: |

## Platform Support

Expand Down
6 changes: 3 additions & 3 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ buildscript {
}

dependencies {
classpath 'com.android.tools.build:gradle:8.3.2'
classpath 'com.android.tools.build:gradle:8.8.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
Expand Down Expand Up @@ -77,8 +77,8 @@ dependencies {
// See: https://youtrack.jetbrains.com/issue/KT-55297/kotlin-stdlib-should-declare-constraints-on-kotlin-stdlib-jdk8-and-kotlin-stdlib-jdk7
implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.22"))

implementation 'androidx.camera:camera-lifecycle:1.3.4'
implementation 'androidx.camera:camera-camera2:1.3.4'
implementation 'androidx.camera:camera-camera2:1.4.1'
implementation 'androidx.camera:camera-lifecycle:1.4.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'

testImplementation 'org.jetbrains.kotlin:kotlin-test'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ package dev.steenbakker.mobile_scanner
import android.app.Activity
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.ColorMatrix
import android.graphics.ColorMatrixColorFilter
import android.graphics.Matrix
import android.graphics.Paint
import android.graphics.Rect
import android.hardware.display.DisplayManager
import android.net.Uri
Expand Down Expand Up @@ -63,6 +67,7 @@ class MobileScanner(

/// Configurable variables
var scanWindow: List<Float>? = null
private var invertImage: Boolean = false
private var detectionSpeed: DetectionSpeed = DetectionSpeed.NO_DUPLICATES
private var detectionTimeout: Long = 250
private var returnImage = false
Expand All @@ -83,7 +88,12 @@ class MobileScanner(
@ExperimentalGetImage
val captureOutput = ImageAnalysis.Analyzer { imageProxy -> // YUV_420_888 format
val mediaImage = imageProxy.image ?: return@Analyzer
val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)

val inputImage = if (invertImage) {
invertInputImage(imageProxy)
} else {
InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
}

if (detectionSpeed == DetectionSpeed.NORMAL && scannerTimeout) {
imageProxy.close()
Expand Down Expand Up @@ -134,8 +144,8 @@ class MobileScanner(
mobileScannerCallback(
barcodeMap,
null,
if (portrait) mediaImage.width else mediaImage.height,
if (portrait) mediaImage.height else mediaImage.width)
if (portrait) inputImage.width else inputImage.height,
if (portrait) inputImage.height else inputImage.width)
return@addOnSuccessListener
}

Expand Down Expand Up @@ -232,11 +242,13 @@ class MobileScanner(
mobileScannerStartedCallback: MobileScannerStartedCallback,
mobileScannerErrorCallback: (exception: Exception) -> Unit,
detectionTimeout: Long,
cameraResolutionWanted: Size?
cameraResolutionWanted: Size?,
invertImage: Boolean,
) {
this.detectionSpeed = detectionSpeed
this.detectionTimeout = detectionTimeout
this.returnImage = returnImage
this.invertImage = invertImage

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

Expand Down Expand Up @@ -429,14 +441,14 @@ class MobileScanner(
isPaused = true
}

private fun resumeCamera() {
// Resume camera by rebinding use cases
cameraProvider?.let { provider ->
val owner = activity as LifecycleOwner
cameraSelector?.let { provider.bindToLifecycle(owner, it, preview) }
}
isPaused = false
}
// private fun resumeCamera() {
// // Resume camera by rebinding use cases
// cameraProvider?.let { provider ->
// val owner = activity as LifecycleOwner
// cameraSelector?.let { provider.bindToLifecycle(owner, it, preview) }
// }
// isPaused = false
// }

private fun releaseCamera() {
if (displayListener != null) {
Expand Down Expand Up @@ -485,6 +497,50 @@ class MobileScanner(
}
}

/**
* Inverts the image colours respecting the alpha channel
*/
@ExperimentalGetImage
fun invertInputImage(imageProxy: ImageProxy): InputImage {
val image = imageProxy.image ?: throw IllegalArgumentException("Image is null")

// Convert YUV_420_888 image to RGB Bitmap
val bitmap = Bitmap.createBitmap(image.width, image.height, Bitmap.Config.ARGB_8888)
try {
val imageFormat = YuvToRgbConverter(activity.applicationContext)
imageFormat.yuvToRgb(image, bitmap)

// Create an inverted bitmap
val invertedBitmap = invertBitmapColors(bitmap)
imageFormat.release()

return InputImage.fromBitmap(invertedBitmap, imageProxy.imageInfo.rotationDegrees)
} finally {
// Release resources
bitmap.recycle() // Free up bitmap memory
imageProxy.close() // Close ImageProxy
}
}

// Efficiently invert bitmap colors using ColorMatrix
private fun invertBitmapColors(bitmap: Bitmap): Bitmap {
val colorMatrix = ColorMatrix().apply {
set(floatArrayOf(
-1f, 0f, 0f, 0f, 255f, // Red
0f, -1f, 0f, 0f, 255f, // Green
0f, 0f, -1f, 0f, 255f, // Blue
0f, 0f, 0f, 1f, 0f // Alpha
))
}
val paint = Paint().apply { colorFilter = ColorMatrixColorFilter(colorMatrix) }

val invertedBitmap = Bitmap.createBitmap(bitmap.width, bitmap.height, bitmap.config)
val canvas = Canvas(invertedBitmap)
canvas.drawBitmap(bitmap, 0f, 0f, paint)

return invertedBitmap
}

/**
* Analyze a single image.
*/
Expand Down Expand Up @@ -530,6 +586,11 @@ class MobileScanner(
camera?.cameraControl?.setLinearZoom(scale.toFloat())
}

fun setZoomRatio(zoomRatio: Double) {
if (camera == null) throw ZoomWhenStopped()
camera?.cameraControl?.setZoomRatio(zoomRatio.toFloat())
}

/**
* Reset the zoom rate of the camera.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package dev.steenbakker.mobile_scanner

import android.app.Activity
import android.content.Context
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraManager
import android.net.Uri
import android.os.Handler
import android.os.Looper
Expand All @@ -18,6 +21,7 @@ import io.flutter.plugin.common.PluginRegistry.RequestPermissionsResultListener
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.view.TextureRegistry
import java.io.File
import com.google.mlkit.vision.barcode.ZoomSuggestionOptions

class MobileScannerHandler(
private val activity: Activity,
Expand Down Expand Up @@ -138,13 +142,15 @@ class MobileScannerHandler(
val speed: Int = call.argument<Int>("speed") ?: 1
val timeout: Int = call.argument<Int>("timeout") ?: 250
val cameraResolutionValues: List<Int>? = call.argument<List<Int>>("cameraResolution")
val autoZoom: Boolean = call.argument<Boolean>("autoZoom") ?: false
val cameraResolution: Size? = if (cameraResolutionValues != null) {
Size(cameraResolutionValues[0], cameraResolutionValues[1])
} else {
null
}
val invertImage: Boolean = call.argument<Boolean>("invertImage") ?: false

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

val position =
if (facing == 0) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA
Expand Down Expand Up @@ -208,7 +214,8 @@ class MobileScannerHandler(
}
},
timeout.toLong(),
cameraResolution
cameraResolution,
invertImage,
)
}

Expand Down Expand Up @@ -241,7 +248,7 @@ class MobileScannerHandler(

mobileScanner!!.analyzeImage(
Uri.fromFile(File(filePath)),
buildBarcodeScannerOptions(formats),
buildBarcodeScannerOptions(formats, false),
analyzeImageSuccessCallback,
analyzeImageErrorCallback)
}
Expand All @@ -264,6 +271,14 @@ class MobileScannerHandler(
}
}

private fun setZoomRatio(scale: Float) : Boolean {
try {
mobileScanner!!.setZoomRatio(scale.toDouble())
return true
} catch (e: ZoomWhenStopped) { }
return false
}

private fun resetScale(result: MethodChannel.Result) {
try {
mobileScanner!!.resetScale()
Expand All @@ -280,25 +295,54 @@ class MobileScannerHandler(
result.success(null)
}

private fun buildBarcodeScannerOptions(formats: List<Int>?): BarcodeScannerOptions? {
private fun buildBarcodeScannerOptions(formats: List<Int>?, autoZoom: Boolean): BarcodeScannerOptions? {
val builder : BarcodeScannerOptions.Builder?
if (formats == null) {
return null
}
builder = BarcodeScannerOptions.Builder()
} else {
val formatsList: MutableList<Int> = mutableListOf()

val formatsList: MutableList<Int> = mutableListOf()
for (formatValue in formats) {
formatsList.add(BarcodeFormats.fromRawValue(formatValue).intValue)
}

for (formatValue in formats) {
formatsList.add(BarcodeFormats.fromRawValue(formatValue).intValue)
if (formatsList.size == 1) {
builder = BarcodeScannerOptions.Builder().setBarcodeFormats(formatsList.first())
} else {
builder = BarcodeScannerOptions.Builder().setBarcodeFormats(
formatsList.first(),
*formatsList.subList(1, formatsList.size).toIntArray()
)
}
}

if (formatsList.size == 1) {
return BarcodeScannerOptions.Builder().setBarcodeFormats(formatsList.first())
.build()
if (autoZoom) {
builder.setZoomSuggestionOptions(
ZoomSuggestionOptions.Builder {
setZoomRatio(it)
}.setMaxSupportedZoomRatio(getMaxZoomRatio())
.build())
}

return BarcodeScannerOptions.Builder().setBarcodeFormats(
formatsList.first(),
*formatsList.subList(1, formatsList.size).toIntArray()
).build()
return builder.build()
}

private fun getMaxZoomRatio(): Float {
val cameraManager = activity.getSystemService(Context.CAMERA_SERVICE) as CameraManager
var maxZoom = 1.0F

try {
for (cameraId in cameraManager.cameraIdList) {
val characteristics = cameraManager.getCameraCharacteristics(cameraId)

val maxZoomRatio = characteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM)
if (maxZoomRatio != null && maxZoomRatio > maxZoom) {
maxZoom = maxZoomRatio
}
}
} catch (e: Exception) {
e.printStackTrace()
}
return maxZoom
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ the conversion is done into these formats.
More about each format: https://www.fourcc.org/yuv.php
*/

@kotlin.annotation.Retention(AnnotationRetention.SOURCE)
@Retention(AnnotationRetention.SOURCE)
@IntDef(ImageFormat.NV21, ImageFormat.YUV_420_888)
annotation class YuvType

Expand Down Expand Up @@ -181,9 +181,7 @@ class YuvByteBuffer(image: Image, dstBuffer: ByteBuffer? = null) {
}
}

private class PlaneWrapper(width: Int, height: Int, plane: Image.Plane) {
val width = width
val height = height
private class PlaneWrapper(val width: Int, val height: Int, plane: Image.Plane) {
val buffer: ByteBuffer = plane.buffer
val rowStride = plane.rowStride
val pixelStride = plane.pixelStride
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@


package dev.steenbakker.mobile_scanner.utils

import android.content.Context
Expand Down Expand Up @@ -99,4 +101,4 @@ class YuvToRgbConverter(context: Context) {
scriptYuvToRgb.destroy()
rs.destroy()
}
}
}
4 changes: 3 additions & 1 deletion example/lib/barcode_scanner_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ class _BarcodeScannerWithControllerState
extends State<BarcodeScannerWithController> with WidgetsBindingObserver {
final MobileScannerController controller = MobileScannerController(
autoStart: false,
torchEnabled: true,
// torchEnabled: true,
autoZoom: true,
// invertImage: true,
);

@override
Expand Down
16 changes: 16 additions & 0 deletions lib/src/mobile_scanner_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
this.formats = const <BarcodeFormat>[],
this.returnImage = false,
this.torchEnabled = false,
this.invertImage = false,
this.autoZoom = false,
}) : detectionTimeoutMs =
detectionSpeed == DetectionSpeed.normal ? detectionTimeoutMs : 0,
assert(
Expand Down Expand Up @@ -84,11 +86,23 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
/// Defaults to false, and is only supported on iOS, MacOS and Android.
final bool returnImage;

/// Invert image colors for analyzer to support white-on-black barcodes, which are not supported by MLKit.
/// Usage of this parameter can incur a performance cost, as frames need to be altered during processing.
///
/// Defaults to false and is only supported on Android.
final bool invertImage;

/// Whether the flashlight should be turned on when the camera is started.
///
/// Defaults to false.
final bool torchEnabled;

/// Whether the camera should auto zoom if the detected code is to far from
/// the camera.
///
/// Only supported on Android.
final bool autoZoom;

/// The internal barcode controller, that listens for detected barcodes.
final StreamController<BarcodeCapture> _barcodesController =
StreamController.broadcast();
Expand Down Expand Up @@ -304,6 +318,8 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
formats: formats,
returnImage: returnImage,
torchEnabled: torchEnabled,
invertImage: invertImage,
autoZoom: autoZoom,
);

try {
Expand Down
Loading

0 comments on commit 7179d3a

Please sign in to comment.