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

Draft
wants to merge 20 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -58,6 +60,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 @@ -77,7 +81,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.
if (shouldConsiderInvertedImages) {
invertCurrentImage = !invertCurrentImage // so we jump from one normal to one inverted and viceversa
}

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

if (detectionSpeed == DetectionSpeed.NORMAL && scannerTimeout) {
imageProxy.close()
Expand Down Expand Up @@ -244,11 +258,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) {
mobileScannerErrorCallback(AlreadyStarted())
Expand Down Expand Up @@ -462,6 +478,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)

// 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 @@ -124,6 +124,7 @@ class MobileScannerHandler(
"setScale" -> setScale(call, result)
"resetScale" -> resetScale(result)
"updateScanWindow" -> updateScanWindow(call, result)
"setShouldConsiderInvertedImages" -> setShouldConsiderInvertedImages(call, result)
else -> result.notImplemented()
}
}
Expand All @@ -143,6 +144,7 @@ class MobileScannerHandler(
} else {
null
}
val shouldConsiderInvertedImages: Boolean = call.argument<Boolean>("shouldConsiderInvertedImages") ?: false

val barcodeScannerOptions: BarcodeScannerOptions? = buildBarcodeScannerOptions(formats)

Expand Down Expand Up @@ -209,10 +211,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 stop(result: MethodChannel.Result) {
try {
mobileScanner!!.stop()
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,
// 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 @@ -120,6 +126,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 @@ -136,10 +150,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 @@ -163,14 +186,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 @@ -355,6 +379,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 @@ -434,16 +462,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 @@ -117,6 +117,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
resetScale(call, result)
case "updateScanWindow":
updateScanWindow(call, result)
case "setShouldConsiderInvertedImages":
setShouldConsiderInvertedImages(call, result)
default:
result(FlutterMethodNotImplemented)
}
Expand All @@ -128,6 +130,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 @@ -139,7 +142,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 @@ -167,6 +170,19 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
}
}

/// Sets the zoomScale.
private func setShouldConsiderInvertedImages(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
let shouldConsiderInvertedImages = call.arguments as? Bool
if (shouldConsiderInvertedImages == nil) {
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 @@ -288,6 +288,14 @@ class MethodChannelMobileScanner extends MobileScannerPlatform {
);
}

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

@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.
///
/// 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.
final bool shouldConsiderInvertedImages;

/// Whether the flashlight should be turned on when the camera is started.
///
/// Defaults to false.
Expand Down Expand Up @@ -278,6 +290,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 @@ -111,6 +111,11 @@ abstract class MobileScannerPlatform extends PlatformInterface {
throw UnimplementedError('updateScanWindow() has not been implemented.');
}

/// Set inverting image colors in intervals (for negative Data Matrices).
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,
};
}
}