-
-
Notifications
You must be signed in to change notification settings - Fork 532
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
base: develop
Are you sure you want to change the base?
Changes from 21 commits
dff1935
740b720
ebfe698
b1bb926
25e52d7
88b7b67
89d0f20
5ceb867
989f441
71d1e83
1e76615
c8f48b2
96becf0
d8512f4
62a728d
638c94b
b7080a6
47f8155
c1313ef
ebae80d
a386275
1debc4a
0b81241
46850cd
0277f9b
f1a60d1
a6cbfaf
6f45752
f420ab7
ab2021e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||||||
|
@@ -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 | ||||||
|
@@ -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. | ||||||
if (shouldConsiderInvertedImages) { | ||||||
invertCurrentImage = !invertCurrentImage // so we jump from one normal to one inverted and viceversa | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
} | ||||||
|
||||||
val inputImage = if (invertCurrentImage) { | ||||||
invertInputImage(imageProxy) | ||||||
} else { | ||||||
InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees) | ||||||
} | ||||||
|
||||||
if (detectionSpeed == DetectionSpeed.NORMAL && scannerTimeout) { | ||||||
imageProxy.close() | ||||||
|
@@ -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) { | ||||||
|
||||||
|
@@ -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) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||||||
*/ | ||||||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -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, | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this can be a one line comment.
Suggested change
|
||||||
// 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 | ||||||
|
@@ -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) { | ||||||
|
@@ -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, | ||||||
|
@@ -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 | ||||||
} | ||||||
|
@@ -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() { | ||||||
|
@@ -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?>? | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) | ||
} | ||
|
@@ -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) | ||
|
@@ -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, | ||
|
@@ -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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we guard |
||
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 { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -291,6 +291,14 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { | |
); | ||
} | ||
|
||
@override | ||
Future<void> setShouldConsiderInvertedImages(bool shouldConsiderInvertedImages) async { | ||
await methodChannel.invokeMethod<void>( | ||
'setShouldConsiderInvertedImages', | ||
{'shouldConsiderInvertedImages': shouldConsiderInvertedImages}, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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, | ||
|
@@ -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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The second |
||
final bool shouldConsiderInvertedImages; | ||
|
||
/// Whether the flashlight should be turned on when the camera is started. | ||
/// | ||
/// Defaults to false. | ||
|
@@ -309,6 +321,7 @@ class MobileScannerController extends ValueNotifier<MobileScannerState> { | |
returnImage: returnImage, | ||
torchEnabled: torchEnabled, | ||
useNewCameraSelector: useNewCameraSelector, | ||
shouldConsiderInvertedImages: shouldConsiderInvertedImages, | ||
); | ||
|
||
try { | ||
|
Original file line number | Diff line number | Diff line change | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -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). | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
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.'); | ||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.