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 3 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 intervalInvertImage: Boolean = false
private var invertImage: Boolean = false
RafaRuiz marked this conversation as resolved.
Show resolved Hide resolved
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)

// Inversion
RafaRuiz marked this conversation as resolved.
Show resolved Hide resolved
if (intervalInvertImage) {
invertImage = !invertImage // so we jump from one normal to one inverted and viceversa
}

val inputImage = if (invertImage) {
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,
intervalInvertImage: Boolean,
) {
this.detectionSpeed = detectionSpeed
this.detectionTimeout = detectionTimeout
this.returnImage = returnImage
this.intervalInvertImage = intervalInvertImage

if (camera?.cameraInfo != null && preview != null && textureEntry != null) {
mobileScannerErrorCallback(AlreadyStarted())
Expand Down Expand Up @@ -462,6 +478,54 @@ class MobileScanner(
}
}

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

// Convert YUV_420_888 image to NV21 format
val imageByteArray = yuv420888toNV21(image)

// Invert the cropped image
RafaRuiz marked this conversation as resolved.
Show resolved Hide resolved
val invertedBytes = inverse(imageByteArray)

// Create a new InputImage from the inverted byte array
return InputImage.fromByteArray(
invertedBytes,
image.width,
image.height,
imageProxy.imageInfo.rotationDegrees,
InputImage.IMAGE_FORMAT_NV21
)
}

// Helper function to convert YUV_420_888 to NV21
private fun yuv420888toNV21(image: Image): ByteArray {
RafaRuiz marked this conversation as resolved.
Show resolved Hide resolved
val yBuffer = image.planes[0].buffer
val uBuffer = image.planes[1].buffer
val vBuffer = image.planes[2].buffer

val ySize = yBuffer.remaining()
val uSize = uBuffer.remaining()
val vSize = vBuffer.remaining()

val nv21 = ByteArray(ySize + uSize + vSize)

yBuffer.get(nv21, 0, ySize)
vBuffer.get(nv21, ySize, vSize)
uBuffer.get(nv21, ySize + vSize, uSize)

return nv21
}

// Helper function to invert image data
RafaRuiz marked this conversation as resolved.
Show resolved Hide resolved
private fun inverse(bytes: ByteArray): ByteArray {
return ByteArray(bytes.size) { i -> (bytes[i].toInt() xor 0xFF).toByte() }
}

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

val barcodeScannerOptions: BarcodeScannerOptions? = buildBarcodeScannerOptions(formats)

Expand Down Expand Up @@ -209,10 +211,20 @@ class MobileScannerHandler(
},
timeout.toLong(),
cameraResolution,
useNewCameraSelector
useNewCameraSelector,
intervalInvertImage,
)
}

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

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

result.success(null)
}

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

var detectionSpeed: DetectionSpeed = DetectionSpeed.noDuplicates

/// Analyze inverted image intervally to include both inverted and normal images
RafaRuiz marked this conversation as resolved.
Show resolved Hide resolved
var intervalInvertImage: Bool = false
private var invertImage: Bool = false // local to invert intervally

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

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

func convertCIImageToCGImage(inputImage: CIImage) -> CGImage? {
RafaRuiz marked this conversation as resolved.
Show resolved Hide resolved
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 +148,20 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega

nextScanTime = currentTime + timeoutSeconds
imagesCurrentlyBeingProcessed = true

let ciImage = latestBuffer.image

let image = VisionImage(image: ciImage)
// Inversion
RafaRuiz marked this conversation as resolved.
Show resolved Hide resolved
let uiImage : UIImage
if (intervalInvertImage) {
invertImage = !invertImage
}
if (invertImage) {
let tempImage = self.invertImage(image: latestBuffer.image)
RafaRuiz marked this conversation as resolved.
Show resolved Hide resolved
uiImage = tempImage
} else {
uiImage = latestBuffer.image
}

let image = VisionImage(image: uiImage)
image.orientation = imageOrientation(
deviceOrientation: UIDevice.current.orientation,
defaultOrientation: .portrait,
Expand All @@ -163,14 +185,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, intervalInvertImage: Bool, torch: Bool, detectionSpeed: DetectionSpeed, completion: @escaping (MobileScannerStartParameters) -> ()) throws {
self.detectionSpeed = detectionSpeed
self.intervalInvertImage = intervalInvertImage
if (device != nil || captureSession != nil) {
throw MobileScannerError.alreadyStarted
}
Expand Down Expand Up @@ -355,6 +378,10 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
device.unlockForConfiguration()
} catch(_) {}
}

func setIntervalInvertImage(_ intervalInvertImage: Bool) {
self.intervalInvertImage = intervalInvertImage
}

/// Turn the torch on.
private func turnTorchOn() {
Expand Down Expand Up @@ -434,16 +461,30 @@ 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(
var uiImage = image
RafaRuiz marked this conversation as resolved.
Show resolved Hide resolved
if (invertImage) {
uiImage = self.invertImage(image: uiImage)
}
let visImage = VisionImage(image: uiImage)
RafaRuiz marked this conversation as resolved.
Show resolved Hide resolved
visImage.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(visImage, completion: callback)
}

func invertImage(image: UIImage) -> UIImage {
RafaRuiz marked this conversation as resolved.
Show resolved Hide resolved
let ciImage = CIImage(image: image)
let filter = CIFilter(name: "CIColorInvert")
Copy link
Collaborator

Choose a reason for hiding this comment

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

I would not use a magic string here, you should be using https://developer.apple.com/documentation/coreimage/cifilter/3228292-colorinvert

Copy link
Author

Choose a reason for hiding this comment

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

@navaronbracke can you give me a hand here?

I'm trying to make it API available, as I've seen there are a few statements for iOS 13 somewhere in the project, but despite setting my project deployment target to 15, I keep seeing this:

image

I've done just a little Swift in my life, it's been more Android

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
26 changes: 25 additions & 1 deletion ios/Classes/MobileScannerPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -117,17 +117,28 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
resetScale(call, result)
case "updateScanWindow":
updateScanWindow(call, result)
case "setIntervalInvertImage":
setIntervalInvertImage(call, result)
default:
result(FlutterMethodNotImplemented)
}
}

func convertCIImageToCGImage(inputImage: CIImage) -> CGImage? {
RafaRuiz marked this conversation as resolved.
Show resolved Hide resolved
let context = CIContext(options: nil)
if let cgImage = context.createCGImage(inputImage, from: inputImage.extent) {
return cgImage
}
return nil
}

/// Start the mobileScanner.
private func start(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
let torch: Bool = (call.arguments as! Dictionary<String, Any?>)["torch"] as? Bool ?? false
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 intervalInvertImage: Bool = (call.arguments as! Dictionary<String, Any?>)["intervalInvertImage"] 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 +150,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, intervalInvertImage: intervalInvertImage, torch: torch, detectionSpeed: detectionSpeed) { parameters in
DispatchQueue.main.async {
result([
"textureId": parameters.textureId,
Expand Down Expand Up @@ -167,6 +178,19 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
}
}

/// Sets the zoomScale.
private func setIntervalInvertImage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
let intervalInvertImage = call.arguments as? Bool
if (intervalInvertImage == nil) {
result(FlutterError(code: "MobileScanner",
message: "You must provide a intervalInvertImage (bool) when calling setIntervalInvertImage",
details: nil))
return
}
mobileScanner.setIntervalInvertImage(intervalInvertImage!)
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> setIntervalInvertImage(bool intervalInvertImage) async {
await methodChannel.invokeMethod<void>(
'setIntervalInvertImage',
{'intervalInvertImage': intervalInvertImage},
);
}

@override
Future<void> stop() async {
if (_textureId == null) {
Expand Down
7 changes: 7 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.intervalInvertImage = false,
this.useNewCameraSelector = false,
}) : detectionTimeoutMs =
detectionSpeed == DetectionSpeed.normal ? detectionTimeoutMs : 0,
Expand Down Expand Up @@ -82,6 +83,11 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
/// Defaults to false, and is only supported on iOS, MacOS and Android.
final bool returnImage;

/// Whether the image should be inverted in intervals (original - inverted - original…)
RafaRuiz marked this conversation as resolved.
Show resolved Hide resolved
///
/// Defaults to false.
final bool intervalInvertImage;
RafaRuiz marked this conversation as resolved.
Show resolved Hide resolved

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

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> setIntervalInvertImage(bool intervalInvertImage) {
RafaRuiz marked this conversation as resolved.
Show resolved Hide resolved
throw UnimplementedError('setInvertImage() has not been implemented.');
}

/// Dispose of this [MobileScannerPlatform] instance.
Future<void> dispose() {
throw UnimplementedError('dispose() has not been implemented.');
Expand Down
Loading
Loading