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: read inverted datamatrix (improve #1071) #1214

Closed
wants to merge 3 commits into from
Closed
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
3 changes: 3 additions & 0 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ dependencies {
implementation 'androidx.camera:camera-lifecycle:1.3.3'
implementation 'androidx.camera:camera-camera2:1.3.3'

// opencv
implementation 'com.quickbirdstudios:opencv:3.4.15'

testImplementation 'org.jetbrains.kotlin:kotlin-test'
testImplementation 'org.mockito:mockito-core:5.12.0'
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,12 @@ import dev.steenbakker.mobile_scanner.utils.YuvToRgbConverter
import io.flutter.view.TextureRegistry
import java.io.ByteArrayOutputStream
import kotlin.math.roundToInt

import com.google.mlkit.vision.common.internal.ImageConvertUtils
import org.opencv.core.Mat
import org.opencv.core.CvType
import org.opencv.core.Core
import org.opencv.android.Utils
import org.opencv.android.OpenCVLoader

class MobileScanner(
private val activity: Activity,
Expand All @@ -57,6 +62,8 @@ class MobileScanner(

/// Configurable variables
var scanWindow: List<Float>? = null
var intervalInvertImage: Boolean = false
var invertImage: Boolean = false
private var detectionSpeed: DetectionSpeed = DetectionSpeed.NO_DUPLICATES
private var detectionTimeout: Long = 250
private var returnImage = false
Expand All @@ -67,7 +74,16 @@ 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)
var inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)

// Invert
if (intervalInvertImage) {
invertImage = !invertImage
}

if (invertImage) {
inputImage = invertInputImage(inputImage)
}

if (detectionSpeed == DetectionSpeed.NORMAL && scannerTimeout) {
imageProxy.close()
Expand Down Expand Up @@ -218,6 +234,7 @@ class MobileScanner(
fun start(
barcodeScannerOptions: BarcodeScannerOptions?,
returnImage: Boolean,
intervalInvertImage: Boolean,
cameraPosition: CameraSelector,
torch: Boolean,
detectionSpeed: DetectionSpeed,
Expand All @@ -229,9 +246,13 @@ class MobileScanner(
cameraResolution: Size?,
newCameraResolutionSelector: Boolean
) {

OpenCVLoader.initDebug()

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 @@ -473,5 +494,32 @@ class MobileScanner(
if (camera == null) throw ZoomWhenStopped()
camera?.cameraControl?.setZoomRatio(1f)
}
/**
* Invert the input image.
*/
private fun invertInputImage(image: InputImage): InputImage {
val bitmap = ImageConvertUtils.getInstance().getUpRightBitmap(image)

val mat = Mat()
Utils.bitmapToMat(bitmap, mat)

val channels = ArrayList<Mat>(4)
Core.split(mat, channels)

// Invert only RGB channels
for (i in 0..2) {
Core.bitwise_not(channels[i], channels[i])
}

// Merge channels back
Core.merge(channels, mat)

Utils.matToBitmap(mat, bitmap)
mat.release()
for (channel in channels) {
channel.release()
}

return InputImage.fromBitmap(bitmap, 0)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ class MobileScannerHandler(
"setScale" -> setScale(call, result)
"resetScale" -> resetScale(result)
"updateScanWindow" -> updateScanWindow(call, result)
"setIntervalInvertImage" -> setIntervalInvertImage(call, result)
else -> result.notImplemented()
}
}
Expand All @@ -138,6 +139,7 @@ class MobileScannerHandler(
val facing: Int = call.argument<Int>("facing") ?: 0
val formats: List<Int>? = call.argument<List<Int>>("formats")
val returnImage: Boolean = call.argument<Boolean>("returnImage") ?: false
val intervalInvertImage: Boolean = call.argument<Boolean>("intervalInvertImage") ?: false
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")
Expand Down Expand Up @@ -174,6 +176,7 @@ class MobileScannerHandler(
mobileScanner!!.start(
barcodeScannerOptions,
returnImage,
intervalInvertImage,
position,
torch,
detectionSpeed,
Expand Down Expand Up @@ -275,4 +278,13 @@ class MobileScannerHandler(

result.success(null)
}

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

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

result.success(null)
}
}
64 changes: 57 additions & 7 deletions ios/Classes/MobileScanner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
/// Return image buffer with the Barcode event
var returnImage: Bool = false

/// Analyze inverted image (useful for scanning white barcodes on black background) and inverted at the same time
var intervalInvertImage: Bool = false

/// If [intervalInvertImage] is true, this will interval between true and false on each image so we can detect both images
var invertImage: Bool = false

/// Default position of camera
var videoPosition: AVCaptureDevice.Position = AVCaptureDevice.Position.back

Expand Down Expand Up @@ -124,6 +130,14 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
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) {
guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
Expand All @@ -141,9 +155,22 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
nextScanTime = currentTime + timeoutSeconds
imagesCurrentlyBeingProcessed = true

let ciImage = latestBuffer.image
let uiImage : UIImage
if (intervalInvertImage) {
invertImage = !invertImage
}

if (invertImage) {
let temp_image = self.invertImage(image: latestBuffer.image)
uiImage = temp_image
}
else
{
uiImage = latestBuffer.image
}

let image = VisionImage(image: ciImage)
var image = VisionImage(image: uiImage)

image.orientation = imageOrientation(
deviceOrientation: UIDevice.current.orientation,
defaultOrientation: .portrait,
Expand All @@ -165,19 +192,20 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
}
}

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

/// Start scanning for barcodes
func start(barcodeScannerOptions: BarcodeScannerOptions?, returnImage: Bool, cameraPosition: AVCaptureDevice.Position, torch: Bool, detectionSpeed: DetectionSpeed, completion: @escaping (MobileScannerStartParameters) -> ()) throws {
func start(barcodeScannerOptions: BarcodeScannerOptions?, returnImage: Bool, intervalInvertImage: Bool, cameraPosition: AVCaptureDevice.Position, torch: Bool, detectionSpeed: DetectionSpeed, completion: @escaping (MobileScannerStartParameters) -> ()) throws {
self.detectionSpeed = detectionSpeed
if (device != nil || captureSession != nil) {
throw MobileScannerError.alreadyStarted
}

barcodesString = nil
self.intervalInvertImage = intervalInvertImage
scanner = barcodeScannerOptions != nil ? BarcodeScanner.barcodeScanner(options: barcodeScannerOptions!) : BarcodeScanner.barcodeScanner()
captureSession = AVCaptureSession()
textureId = registry?.register(self)
Expand Down Expand Up @@ -390,6 +418,10 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
}
}

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

/// Set the zoom factor of the camera
func setScale(_ scale: CGFloat) throws {
if (device == nil) {
Expand Down Expand Up @@ -434,14 +466,32 @@ public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelega

/// Analyze a single image
func analyzeImage(image: UIImage, position: AVCaptureDevice.Position, callback: @escaping BarcodeScanningCallback) {
let image = VisionImage(image: image)
image.orientation = imageOrientation(
var uiimage = image

if (intervalInvertImage) {
invertImage = !invertImage
}

if (invertImage) {
uiimage = self.invertImage(image: uiimage)
}
let visImage = VisionImage(image: uiimage)
visImage.orientation = imageOrientation(
deviceOrientation: UIDevice.current.orientation,
defaultOrientation: .portrait,
position: position
)
scanner.process(visImage, completion: callback)
}

scanner.process(image, completion: callback)
func invertImage(image: UIImage) -> UIImage {
let ciImage = CIImage(image: image)
let 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
19 changes: 18 additions & 1 deletion ios/Classes/MobileScannerPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
toggleTorch(result)
case "analyzeImage":
analyzeImage(call, result)
case "setIntervalInvertImage":
setIntervalInvertImage(call, result)
case "setScale":
setScale(call, result)
case "resetScale":
Expand All @@ -101,6 +103,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 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 @@ -120,7 +123,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
let detectionSpeed: DetectionSpeed = DetectionSpeed(rawValue: speed)!

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


/// Sets interval inverting
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 invert (bool) when calling setIntervalInvertImage",
details: nil))
return
}
mobileScanner.setIntervalInvertImage(intervalInvertImage!)
result(nil)
}

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

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

@override
Future<void> dispose() async {
await stop();
Expand Down
9 changes: 9 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 @@ -88,6 +89,13 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
/// Defaults to false.
final bool torchEnabled;

/// Whether the image should be inverted on a 1 to 1 interval.
/// So images are normal - inverted - normal - inverted...
/// and we can read both kind of images.
///
/// Defaults to false.
final bool intervalInvertImage;

/// Use the new resolution selector.
///
/// This feature is experimental and not fully tested yet.
Expand Down Expand Up @@ -268,6 +276,7 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> {
formats: formats,
returnImage: returnImage,
torchEnabled: torchEnabled,
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 @@ -104,6 +104,11 @@ abstract class MobileScannerPlatform extends PlatformInterface {
throw UnimplementedError('updateScanWindow() has not been implemented.');
}

/// Set inverting image colors (for negative Data Matrixes) on intervals.
Future<void> setIntervalInvertImage(bool intervalInvertImage) {
throw UnimplementedError('setIntervalInvertImage() 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 @@ -14,6 +14,7 @@ class StartOptions {
required this.formats,
required this.returnImage,
required this.torchEnabled,
required this.intervalInvertImage,
});

/// The direction for the camera.
Expand All @@ -37,6 +38,9 @@ class StartOptions {
/// Whether the torch should be turned on when the scanner starts.
final bool torchEnabled;

/// Whether the image should be inverted on intervals.
final bool intervalInvertImage;

Map<String, Object?> toMap() {
return <String, Object?>{
if (cameraResolution != null)
Expand All @@ -51,6 +55,7 @@ class StartOptions {
'speed': detectionSpeed.rawValue,
'timeout': detectionTimeoutMs,
'torch': torchEnabled,
'intervalInvertImage': intervalInvertImage,
};
}
}