Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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 @@ -58,6 +60,8 @@ class MobileScanner(

/// Configurable variables
var scanWindow: List<Float>? = null
var intervalInvertImage: Boolean = false
private var invertImage: 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)

// Inversion
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
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 {
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
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
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? {
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
let uiImage : UIImage
if (intervalInvertImage) {
invertImage = !invertImage
}
if (invertImage) {
let tempImage = self.invertImage(image: latestBuffer.image)
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
if (invertImage) {
uiImage = self.invertImage(image: uiImage)
}
let visImage = VisionImage(image: uiImage)
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 {
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
Contributor 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? {
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…)
///
/// 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 intervalInvertImage;

/// 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).
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> setIntervalInvertImage(bool intervalInvertImage) {
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