Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Ability to scan both normal codes and inverted codes #1215

Open
wants to merge 30 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
dff1935
Feature: Read inverted data matrix
RafaRuiz Oct 12, 2024
740b720
Feature: Read inverted data matrix
RafaRuiz Oct 12, 2024
ebfe698
Clean up
RafaRuiz Oct 12, 2024
b1bb926
Update lib/src/objects/start_options.dart
RafaRuiz Oct 14, 2024
25e52d7
Update lib/src/mobile_scanner_controller.dart
RafaRuiz Oct 14, 2024
88b7b67
Update android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileS…
RafaRuiz Oct 14, 2024
89d0f20
Update ios/Classes/MobileScanner.swift
RafaRuiz Oct 14, 2024
5ceb867
Rename feature
RafaRuiz Oct 18, 2024
989f441
Rename methods and variables
RafaRuiz Oct 18, 2024
71d1e83
Wording
RafaRuiz Oct 18, 2024
1e76615
Refactor the way to invert the image, applying the conversion to NV21…
RafaRuiz Oct 18, 2024
c8f48b2
Refactor convertCIImageToCGImage
RafaRuiz Oct 18, 2024
96becf0
redundant temp variable
RafaRuiz Oct 18, 2024
d8512f4
rename var
RafaRuiz Oct 18, 2024
62a728d
let
RafaRuiz Oct 18, 2024
638c94b
private
RafaRuiz Oct 18, 2024
b7080a6
CIFilter magic string
RafaRuiz Oct 18, 2024
47f8155
Merge branch 'master' into master
RafaRuiz Oct 18, 2024
c1313ef
let uiimage
RafaRuiz Oct 18, 2024
ebae80d
make CIFilter API available
RafaRuiz Oct 18, 2024
a386275
Merge branch 'master' into master-cv
juliansteenbakker Jan 16, 2025
1debc4a
Merge branch 'develop' into master-rafaruiz
juliansteenbakker Jan 17, 2025
0b81241
fix: remove merge conflicts
juliansteenbakker Jan 17, 2025
46850cd
style: format
juliansteenbakker Jan 17, 2025
0277f9b
imp: improve scan speed for inverted images
juliansteenbakker Jan 17, 2025
f1a60d1
imp: update parameter name, remove unused method, suppress deprecatio…
juliansteenbakker Jan 19, 2025
a6cbfaf
style: remove blank line
juliansteenbakker Jan 21, 2025
6f45752
style: rename invertImages to invertImage
juliansteenbakker Jan 21, 2025
f420ab7
style: inline deprecation
juliansteenbakker Jan 21, 2025
ab2021e
style: update docs
juliansteenbakker Jan 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package dev.steenbakker.mobile_scanner

import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Matrix
import android.graphics.Rect
import android.hardware.display.DisplayManager
import android.media.Image
import android.net.Uri
import android.os.Build
import android.os.Handler
Expand Down Expand Up @@ -59,6 +61,8 @@ class MobileScanner(

/// Configurable variables
var scanWindow: List<Float>? = null
var shouldConsiderInvertedImages: Boolean = false
private var invertCurrentImage: Boolean = false
private var detectionSpeed: DetectionSpeed = DetectionSpeed.NO_DUPLICATES
private var detectionTimeout: Long = 250
private var returnImage = false
Expand All @@ -79,7 +83,17 @@ 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)

// Invert every other frame.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Invert every other frame.
// Invert every other frame, by flipping the flag on every input frame.

if (shouldConsiderInvertedImages) {
invertCurrentImage = !invertCurrentImage // so we jump from one normal to one inverted and viceversa
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
invertCurrentImage = !invertCurrentImage // so we jump from one normal to one inverted and viceversa
invertCurrentImage = !invertCurrentImage

}

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

if (detectionSpeed == DetectionSpeed.NORMAL && scannerTimeout) {
imageProxy.close()
Expand Down Expand Up @@ -246,11 +260,13 @@ class MobileScanner(
mobileScannerErrorCallback: (exception: Exception) -> Unit,
detectionTimeout: Long,
cameraResolution: Size?,
newCameraResolutionSelector: Boolean
newCameraResolutionSelector: Boolean,
shouldConsiderInvertedImages: Boolean,
) {
this.detectionSpeed = detectionSpeed
this.detectionTimeout = detectionTimeout
this.returnImage = returnImage
this.shouldConsiderInvertedImages = shouldConsiderInvertedImages

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

Expand Down Expand Up @@ -510,6 +526,45 @@ class MobileScanner(
}
}

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

// Convert YUV_420_888 image to NV21 format
// based on our util helper
val bitmap = Bitmap.createBitmap(image.width, image.height, Bitmap.Config.ARGB_8888)
YuvToRgbConverter(activity).yuvToRgb(image, bitmap)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adds another usage of the deprecated RenderScript API's. Can we avoid this?

See #1142


// Invert RGB values
invertBitmapColors(bitmap)

return InputImage.fromBitmap(bitmap, imageProxy.imageInfo.rotationDegrees)
}

// Helper function to invert the colors of the bitmap
private fun invertBitmapColors(bitmap: Bitmap) {
val width = bitmap.width
val height = bitmap.height
for (x in 0 until width) {
for (y in 0 until height) {
val pixel = bitmap.getPixel(x, y)
val invertedColor = invertColor(pixel)
bitmap.setPixel(x, y, invertedColor)
}
}
}

private fun invertColor(pixel: Int): Int {
val alpha = pixel and 0xFF000000.toInt()
val red = 255 - (pixel shr 16 and 0xFF)
val green = 255 - (pixel shr 8 and 0xFF)
val blue = 255 - (pixel and 0xFF)
return alpha or (red shl 16) or (green shl 8) or blue
}

/**
* Analyze a single image.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ class MobileScannerHandler(
"setScale" -> setScale(call, result)
"resetScale" -> resetScale(result)
"updateScanWindow" -> updateScanWindow(call, result)
"setShouldConsiderInvertedImages" -> setShouldConsiderInvertedImages(call, result)
else -> result.notImplemented()
}
}
Expand All @@ -144,6 +145,7 @@ class MobileScannerHandler(
} else {
null
}
val shouldConsiderInvertedImages: Boolean = call.argument<Boolean>("shouldConsiderInvertedImages") ?: false

val barcodeScannerOptions: BarcodeScannerOptions? = buildBarcodeScannerOptions(formats)

Expand Down Expand Up @@ -210,10 +212,20 @@ class MobileScannerHandler(
},
timeout.toLong(),
cameraResolution,
useNewCameraSelector
useNewCameraSelector,
shouldConsiderInvertedImages,
)
}

private fun setShouldConsiderInvertedImages(call: MethodCall, result: MethodChannel.Result) {
val shouldConsiderInvertedImages = call.argument<Boolean?>("shouldConsiderInvertedImages")

if (shouldConsiderInvertedImages != null)
mobileScanner?.shouldConsiderInvertedImages = shouldConsiderInvertedImages

result.success(null)
}

private fun pause(result: MethodChannel.Result) {
try {
mobileScanner!!.pause()
Expand Down
69 changes: 61 additions & 8 deletions ios/Classes/MobileScanner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega

var detectionSpeed: DetectionSpeed = DetectionSpeed.noDuplicates

var shouldConsiderInvertedImages: Bool = false
// local variable to invert this image only this time,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can be a one line comment.

Suggested change
// local variable to invert this image only this time,
// This flag is used to determine if the current frame should be color-inverted.

// it changes based on [shouldConsiderInvertedImages] and
// it defaults as false
private var invertCurrentImage: Bool = false

private let backgroundQueue = DispatchQueue(label: "camera-handling")

var standardZoomFactor: CGFloat = 1
Expand Down Expand Up @@ -128,6 +134,14 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
func requestPermission(_ result: @escaping FlutterResult) {
AVCaptureDevice.requestAccess(for: .video, completionHandler: { result($0) })
}

private func convertCIImageToCGImage(inputImage: CIImage) -> CGImage? {
let context = CIContext(options: nil)
if let cgImage = context.createCGImage(inputImage, from: inputImage.extent) {
return cgImage
}
return nil
}

/// Gets called when a new image is added to the buffer
public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
Expand All @@ -145,10 +159,19 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega

nextScanTime = currentTime + timeoutSeconds
imagesCurrentlyBeingProcessed = true

let ciImage = latestBuffer.image

let image = VisionImage(image: ciImage)
// Invert every other frame.
let uiImage : UIImage
if (shouldConsiderInvertedImages) {
invertCurrentImage = !invertCurrentImage
}
if (invertCurrentImage) {
uiImage = self.invertInputImage(image: latestBuffer.image)
} else {
uiImage = latestBuffer.image
}

let image = VisionImage(image: uiImage)
image.orientation = imageOrientation(
deviceOrientation: UIDevice.current.orientation,
defaultOrientation: .portrait,
Expand All @@ -172,14 +195,15 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
}
}

mobileScannerCallback(barcodes, error, ciImage)
mobileScannerCallback(barcodes, error, uiImage)
}
}
}

/// Start scanning for barcodes
func start(barcodeScannerOptions: BarcodeScannerOptions?, cameraPosition: AVCaptureDevice.Position, torch: Bool, detectionSpeed: DetectionSpeed, completion: @escaping (MobileScannerStartParameters) -> ()) throws {
func start(barcodeScannerOptions: BarcodeScannerOptions?, cameraPosition: AVCaptureDevice.Position, shouldConsiderInvertedImages: Bool, torch: Bool, detectionSpeed: DetectionSpeed, completion: @escaping (MobileScannerStartParameters) -> ()) throws {
self.detectionSpeed = detectionSpeed
self.shouldConsiderInvertedImages = shouldConsiderInvertedImages
if (device != nil || captureSession != nil) {
throw MobileScannerError.alreadyStarted
}
Expand Down Expand Up @@ -386,6 +410,10 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
device.unlockForConfiguration()
} catch(_) {}
}

func setShouldConsiderInvertedImages(_ shouldConsiderInvertedImages: Bool) {
self.shouldConsiderInvertedImages = shouldConsiderInvertedImages
}

/// Turn the torch on.
private func turnTorchOn() {
Expand Down Expand Up @@ -465,16 +493,41 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
/// Analyze a single image
func analyzeImage(image: UIImage, position: AVCaptureDevice.Position,
barcodeScannerOptions: BarcodeScannerOptions?, callback: @escaping BarcodeScanningCallback) {
let image = VisionImage(image: image)
image.orientation = imageOrientation(
let uiImage: UIImage
if (invertCurrentImage) {
uiImage = self.invertInputImage(image: uiImage)
} else {
uiImage = image
}
let visionImage = VisionImage(image: uiImage)
visionImage.orientation = imageOrientation(
deviceOrientation: UIDevice.current.orientation,
defaultOrientation: .portrait,
position: position
)

let scanner: BarcodeScanner = barcodeScannerOptions != nil ? BarcodeScanner.barcodeScanner(options: barcodeScannerOptions!) : BarcodeScanner.barcodeScanner()

scanner.process(image, completion: callback)
scanner.process(visionImage, completion: callback)
}

private func invertInputImage(image: UIImage) -> UIImage {
let ciImage = CIImage(image: image)

let filter: CIFilter?

if #available(iOS 13.0, *) {
filter = CIFilter.colorInvert()
filter?.setValue(ciImage, forKey: kCIInputImageKey)
} else {
filter = CIFilter(name: "CIColorInvert")
filter?.setValue(ciImage, forKey: kCIInputImageKey)
}

let outputImage = filter?.outputImage
let cgImage = convertCIImageToCGImage(inputImage: outputImage!)

return UIImage(cgImage: cgImage!, scale: image.scale, orientation: image.imageOrientation)
}

var barcodesString: Array<String?>?
Expand Down
18 changes: 17 additions & 1 deletion ios/Classes/MobileScannerPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
resetScale(call, result)
case "updateScanWindow":
updateScanWindow(call, result)
case "setShouldConsiderInvertedImages":
setShouldConsiderInvertedImages(call, result)
default:
result(FlutterMethodNotImplemented)
}
Expand All @@ -130,6 +132,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
let facing: Int = (call.arguments as! Dictionary<String, Any?>)["facing"] as? Int ?? 1
let formats: Array<Int> = (call.arguments as! Dictionary<String, Any?>)["formats"] as? Array ?? []
let returnImage: Bool = (call.arguments as! Dictionary<String, Any?>)["returnImage"] as? Bool ?? false
let shouldConsiderInvertedImages: Bool = (call.arguments as! Dictionary<String, Any?>)["shouldConsiderInvertedImages"] as? Bool ?? false
let speed: Int = (call.arguments as! Dictionary<String, Any?>)["speed"] as? Int ?? 0
let timeoutMs: Int = (call.arguments as! Dictionary<String, Any?>)["timeout"] as? Int ?? 0
self.mobileScanner.timeoutSeconds = Double(timeoutMs) / Double(1000)
Expand All @@ -141,7 +144,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
let detectionSpeed: DetectionSpeed = DetectionSpeed(rawValue: speed)!

do {
try mobileScanner.start(barcodeScannerOptions: barcodeOptions, cameraPosition: position, torch: torch, detectionSpeed: detectionSpeed) { parameters in
try mobileScanner.start(barcodeScannerOptions: barcodeOptions, cameraPosition: position, shouldConsiderInvertedImages: shouldConsiderInvertedImages, torch: torch, detectionSpeed: detectionSpeed) { parameters in
DispatchQueue.main.async {
result([
"textureId": parameters.textureId,
Expand Down Expand Up @@ -177,6 +180,19 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
result(nil)
}

/// Sets the zoomScale.
private func setShouldConsiderInvertedImages(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
let shouldConsiderInvertedImages = call.arguments as? Bool
if (shouldConsiderInvertedImages == nil) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we guard mobileScanner.setShouldConsiderInvertedImages(shouldConsiderInvertedImages!) to not do anything if the argument is null ? Otherwise this does not match Android.

result(FlutterError(code: "MobileScanner",
message: "You must provide a shouldConsiderInvertedImages (bool) when calling setShouldConsiderInvertedImages",
details: nil))
return
}
mobileScanner.setShouldConsiderInvertedImages(shouldConsiderInvertedImages!)
result(nil)
}

/// Stops the mobileScanner and closes the texture.
private func stop(_ result: @escaping FlutterResult) {
do {
Expand Down
8 changes: 8 additions & 0 deletions lib/src/method_channel/mobile_scanner_method_channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,14 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
);
}

@override
Future<void> setShouldConsiderInvertedImages(bool shouldConsiderInvertedImages) async {
await methodChannel.invokeMethod<void>(
'setShouldConsiderInvertedImages',
{'shouldConsiderInvertedImages': shouldConsiderInvertedImages},
Copy link
Collaborator

@navaronbracke navaronbracke Jan 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we just pass in a boolean flag instead of a Map here? The native implementations will need to be updated accordingly.

);
}

@override
Future<void> stop() async {
if (_textureId == null) {
Expand Down
13 changes: 13 additions & 0 deletions lib/src/mobile_scanner_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
this.formats = const <BarcodeFormat>[],
this.returnImage = false,
this.torchEnabled = false,
this.shouldConsiderInvertedImages = false,
this.useNewCameraSelector = false,
}) : detectionTimeoutMs =
detectionSpeed == DetectionSpeed.normal ? detectionTimeoutMs : 0,
Expand Down Expand Up @@ -82,6 +83,17 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
/// Defaults to false, and is only supported on iOS, MacOS and Android.
final bool returnImage;

/// Whether the scanner should try to detect color-inverted barcodes in every other frame.
juliansteenbakker marked this conversation as resolved.
Show resolved Hide resolved
///
/// When this option is enabled, every odd frame from the camera preview has its colors inverted before processing.
/// This is useful if barcodes can be both black-on-white (the most common) and white-on-black (less common).
/// Usage of this parameter can incur a performance cost, as some frames need to be altered further during processing.
///
/// Defaults to false and is only supported on Android and iOS.
///
/// Defaults to false.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The second Defaults to false. can be removed.

final bool shouldConsiderInvertedImages;

/// Whether the flashlight should be turned on when the camera is started.
///
/// Defaults to false.
Expand Down Expand Up @@ -309,6 +321,7 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
returnImage: returnImage,
torchEnabled: torchEnabled,
useNewCameraSelector: useNewCameraSelector,
shouldConsiderInvertedImages: shouldConsiderInvertedImages,
);

try {
Expand Down
5 changes: 5 additions & 0 deletions lib/src/mobile_scanner_platform_interface.dart
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ abstract class MobileScannerPlatform extends PlatformInterface {
throw UnimplementedError('updateScanWindow() has not been implemented.');
}

/// Set inverting image colors in intervals (for negative Data Matrices).
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Set inverting image colors in intervals (for negative Data Matrices).
/// Enable or disable the inverting of image colors.
///
/// This is useful when working with negative-color Data Matrices.
/// See also: https://en.wikipedia.org/wiki/Negative_(photography)

Future<void> setShouldConsiderInvertedImages(bool shouldConsiderInvertedImages) {
throw UnimplementedError('setInvertImage() has not been implemented.');
}

/// Dispose of this [MobileScannerPlatform] instance.
Future<void> dispose() {
throw UnimplementedError('dispose() has not been implemented.');
Expand Down
5 changes: 5 additions & 0 deletions lib/src/objects/start_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class StartOptions {
required this.returnImage,
required this.torchEnabled,
required this.useNewCameraSelector,
required this.shouldConsiderInvertedImages,
});

/// The direction for the camera.
Expand All @@ -23,6 +24,9 @@ class StartOptions {
/// The desired camera resolution for the scanner.
final Size? cameraResolution;

/// Whether the scanner should try to detect color-inverted barcodes in every other frame.
final bool shouldConsiderInvertedImages;

/// The detection speed for the scanner.
final DetectionSpeed detectionSpeed;

Expand Down Expand Up @@ -58,6 +62,7 @@ class StartOptions {
'timeout': detectionTimeoutMs,
'torch': torchEnabled,
'useNewCameraSelector': useNewCameraSelector,
'shouldConsiderInvertedImages': shouldConsiderInvertedImages,
};
}
}
Loading