Committed by
GitHub
Merge pull request #785 from navaronbracke/increase_camera_quality_cherry_pick
fix: Increase camera quality (cherry-pick)
Showing
3 changed files
with
109 additions
and
6 deletions
| @@ -22,6 +22,11 @@ import dev.steenbakker.mobile_scanner.utils.YuvToRgbConverter | @@ -22,6 +22,11 @@ import dev.steenbakker.mobile_scanner.utils.YuvToRgbConverter | ||
| 22 | import io.flutter.view.TextureRegistry | 22 | import io.flutter.view.TextureRegistry |
| 23 | import java.io.ByteArrayOutputStream | 23 | import java.io.ByteArrayOutputStream |
| 24 | import kotlin.math.roundToInt | 24 | import kotlin.math.roundToInt |
| 25 | +import android.util.Size | ||
| 26 | +import android.hardware.display.DisplayManager | ||
| 27 | +import android.view.WindowManager | ||
| 28 | +import android.content.Context | ||
| 29 | +import android.os.Build | ||
| 25 | 30 | ||
| 26 | 31 | ||
| 27 | class MobileScanner( | 32 | class MobileScanner( |
| @@ -39,6 +44,7 @@ class MobileScanner( | @@ -39,6 +44,7 @@ class MobileScanner( | ||
| 39 | private var scanner = BarcodeScanning.getClient() | 44 | private var scanner = BarcodeScanning.getClient() |
| 40 | private var lastScanned: List<String?>? = null | 45 | private var lastScanned: List<String?>? = null |
| 41 | private var scannerTimeout = false | 46 | private var scannerTimeout = false |
| 47 | + private var displayListener: DisplayManager.DisplayListener? = null | ||
| 42 | 48 | ||
| 43 | /// Configurable variables | 49 | /// Configurable variables |
| 44 | var scanWindow: List<Float>? = null | 50 | var scanWindow: List<Float>? = null |
| @@ -138,7 +144,7 @@ class MobileScanner( | @@ -138,7 +144,7 @@ class MobileScanner( | ||
| 138 | } | 144 | } |
| 139 | } | 145 | } |
| 140 | 146 | ||
| 141 | - fun rotateBitmap(bitmap: Bitmap, degrees: Float): Bitmap { | 147 | + private fun rotateBitmap(bitmap: Bitmap, degrees: Float): Bitmap { |
| 142 | val matrix = Matrix() | 148 | val matrix = Matrix() |
| 143 | matrix.postRotate(degrees) | 149 | matrix.postRotate(degrees) |
| 144 | return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true) | 150 | return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true) |
| @@ -166,6 +172,34 @@ class MobileScanner( | @@ -166,6 +172,34 @@ class MobileScanner( | ||
| 166 | return scaledScanWindow.contains(barcodeBoundingBox) | 172 | return scaledScanWindow.contains(barcodeBoundingBox) |
| 167 | } | 173 | } |
| 168 | 174 | ||
| 175 | + // Return the best resolution for the actual device orientation. | ||
| 176 | + // | ||
| 177 | + // By default the resolution is 480x640, which is too low for ML Kit. | ||
| 178 | + // If the given resolution is not supported by the display, | ||
| 179 | + // the closest available resolution is used. | ||
| 180 | + // | ||
| 181 | + // The resolution should be adjusted for the display rotation, to preserve the aspect ratio. | ||
| 182 | + @Suppress("deprecation") | ||
| 183 | + private fun getResolution(cameraResolution: Size): Size { | ||
| 184 | + val rotation = if (Build.VERSION.SDK_INT >= 30) { | ||
| 185 | + activity.applicationContext.display!!.rotation | ||
| 186 | + } else { | ||
| 187 | + val windowManager = activity.applicationContext.getSystemService(Context.WINDOW_SERVICE) as WindowManager | ||
| 188 | + | ||
| 189 | + windowManager.defaultDisplay.rotation | ||
| 190 | + } | ||
| 191 | + | ||
| 192 | + val widthMaxRes = cameraResolution.width | ||
| 193 | + val heightMaxRes = cameraResolution.height | ||
| 194 | + | ||
| 195 | + val targetResolution = if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) { | ||
| 196 | + Size(widthMaxRes, heightMaxRes) // Portrait mode | ||
| 197 | + } else { | ||
| 198 | + Size(heightMaxRes, widthMaxRes) // Landscape mode | ||
| 199 | + } | ||
| 200 | + return targetResolution | ||
| 201 | + } | ||
| 202 | + | ||
| 169 | /** | 203 | /** |
| 170 | * Start barcode scanning by initializing the camera and barcode scanner. | 204 | * Start barcode scanning by initializing the camera and barcode scanner. |
| 171 | */ | 205 | */ |
| @@ -179,7 +213,8 @@ class MobileScanner( | @@ -179,7 +213,8 @@ class MobileScanner( | ||
| 179 | torchStateCallback: TorchStateCallback, | 213 | torchStateCallback: TorchStateCallback, |
| 180 | zoomScaleStateCallback: ZoomScaleStateCallback, | 214 | zoomScaleStateCallback: ZoomScaleStateCallback, |
| 181 | mobileScannerStartedCallback: MobileScannerStartedCallback, | 215 | mobileScannerStartedCallback: MobileScannerStartedCallback, |
| 182 | - detectionTimeout: Long | 216 | + detectionTimeout: Long, |
| 217 | + cameraResolution: Size? | ||
| 183 | ) { | 218 | ) { |
| 184 | this.detectionSpeed = detectionSpeed | 219 | this.detectionSpeed = detectionSpeed |
| 185 | this.detectionTimeout = detectionTimeout | 220 | this.detectionTimeout = detectionTimeout |
| @@ -229,7 +264,30 @@ class MobileScanner( | @@ -229,7 +264,30 @@ class MobileScanner( | ||
| 229 | // Build the analyzer to be passed on to MLKit | 264 | // Build the analyzer to be passed on to MLKit |
| 230 | val analysisBuilder = ImageAnalysis.Builder() | 265 | val analysisBuilder = ImageAnalysis.Builder() |
| 231 | .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) | 266 | .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) |
| 232 | -// analysisBuilder.setTargetResolution(Size(1440, 1920)) | 267 | + val displayManager = activity.applicationContext.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager |
| 268 | + | ||
| 269 | + if (cameraResolution != null) { | ||
| 270 | + // TODO: migrate to ResolutionSelector with ResolutionStrategy when upgrading to camera 1.3.0 | ||
| 271 | + // Override initial resolution | ||
| 272 | + analysisBuilder.setTargetResolution(getResolution(cameraResolution)) | ||
| 273 | + | ||
| 274 | + if (displayListener == null) { | ||
| 275 | + displayListener = object : DisplayManager.DisplayListener { | ||
| 276 | + override fun onDisplayAdded(displayId: Int) {} | ||
| 277 | + | ||
| 278 | + override fun onDisplayRemoved(displayId: Int) {} | ||
| 279 | + | ||
| 280 | + override fun onDisplayChanged(displayId: Int) { | ||
| 281 | + analysisBuilder.setTargetResolution(getResolution(cameraResolution)) | ||
| 282 | + } | ||
| 283 | + } | ||
| 284 | + | ||
| 285 | + displayManager.registerDisplayListener( | ||
| 286 | + displayListener, null, | ||
| 287 | + ) | ||
| 288 | + } | ||
| 289 | + } | ||
| 290 | + | ||
| 233 | val analysis = analysisBuilder.build().apply { setAnalyzer(executor, captureOutput) } | 291 | val analysis = analysisBuilder.build().apply { setAnalyzer(executor, captureOutput) } |
| 234 | 292 | ||
| 235 | camera = cameraProvider!!.bindToLifecycle( | 293 | camera = cameraProvider!!.bindToLifecycle( |
| @@ -278,6 +336,13 @@ class MobileScanner( | @@ -278,6 +336,13 @@ class MobileScanner( | ||
| 278 | throw AlreadyStopped() | 336 | throw AlreadyStopped() |
| 279 | } | 337 | } |
| 280 | 338 | ||
| 339 | + if (displayListener != null) { | ||
| 340 | + val displayManager = activity.applicationContext.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager | ||
| 341 | + | ||
| 342 | + displayManager.unregisterDisplayListener(displayListener) | ||
| 343 | + displayListener = null | ||
| 344 | + } | ||
| 345 | + | ||
| 281 | val owner = activity as LifecycleOwner | 346 | val owner = activity as LifecycleOwner |
| 282 | camera?.cameraInfo?.torchState?.removeObservers(owner) | 347 | camera?.cameraInfo?.torchState?.removeObservers(owner) |
| 283 | cameraProvider?.unbindAll() | 348 | cameraProvider?.unbindAll() |
| @@ -2,6 +2,7 @@ package dev.steenbakker.mobile_scanner | @@ -2,6 +2,7 @@ package dev.steenbakker.mobile_scanner | ||
| 2 | 2 | ||
| 3 | import android.app.Activity | 3 | import android.app.Activity |
| 4 | import android.net.Uri | 4 | import android.net.Uri |
| 5 | +import android.util.Size | ||
| 5 | import androidx.camera.core.CameraSelector | 6 | import androidx.camera.core.CameraSelector |
| 6 | import androidx.camera.core.ExperimentalGetImage | 7 | import androidx.camera.core.ExperimentalGetImage |
| 7 | import com.google.mlkit.vision.barcode.BarcodeScannerOptions | 8 | import com.google.mlkit.vision.barcode.BarcodeScannerOptions |
| @@ -133,6 +134,12 @@ class MobileScannerHandler( | @@ -133,6 +134,12 @@ class MobileScannerHandler( | ||
| 133 | val returnImage: Boolean = call.argument<Boolean>("returnImage") ?: false | 134 | val returnImage: Boolean = call.argument<Boolean>("returnImage") ?: false |
| 134 | val speed: Int = call.argument<Int>("speed") ?: 1 | 135 | val speed: Int = call.argument<Int>("speed") ?: 1 |
| 135 | val timeout: Int = call.argument<Int>("timeout") ?: 250 | 136 | val timeout: Int = call.argument<Int>("timeout") ?: 250 |
| 137 | + val cameraResolutionValues: List<Int>? = call.argument<List<Int>>("cameraResolution") | ||
| 138 | + val cameraResolution: Size? = if (cameraResolutionValues != null) { | ||
| 139 | + Size(cameraResolutionValues[0], cameraResolutionValues[1]) | ||
| 140 | + } else { | ||
| 141 | + null | ||
| 142 | + } | ||
| 136 | 143 | ||
| 137 | var barcodeScannerOptions: BarcodeScannerOptions? = null | 144 | var barcodeScannerOptions: BarcodeScannerOptions? = null |
| 138 | if (formats != null) { | 145 | if (formats != null) { |
| @@ -157,15 +164,24 @@ class MobileScannerHandler( | @@ -157,15 +164,24 @@ class MobileScannerHandler( | ||
| 157 | val detectionSpeed: DetectionSpeed = DetectionSpeed.values().first { it.intValue == speed} | 164 | val detectionSpeed: DetectionSpeed = DetectionSpeed.values().first { it.intValue == speed} |
| 158 | 165 | ||
| 159 | try { | 166 | try { |
| 160 | - mobileScanner!!.start(barcodeScannerOptions, returnImage, position, torch, detectionSpeed, torchStateCallback, zoomScaleStateCallback, mobileScannerStartedCallback = { | 167 | + mobileScanner!!.start( |
| 168 | + barcodeScannerOptions, | ||
| 169 | + returnImage, | ||
| 170 | + position, | ||
| 171 | + torch, | ||
| 172 | + detectionSpeed, | ||
| 173 | + torchStateCallback, | ||
| 174 | + zoomScaleStateCallback, | ||
| 175 | + mobileScannerStartedCallback = { | ||
| 161 | result.success(mapOf( | 176 | result.success(mapOf( |
| 162 | "textureId" to it.id, | 177 | "textureId" to it.id, |
| 163 | "size" to mapOf("width" to it.width, "height" to it.height), | 178 | "size" to mapOf("width" to it.width, "height" to it.height), |
| 164 | "torchable" to it.hasFlashUnit | 179 | "torchable" to it.hasFlashUnit |
| 165 | )) | 180 | )) |
| 166 | }, | 181 | }, |
| 167 | - timeout.toLong()) | ||
| 168 | - | 182 | + timeout.toLong(), |
| 183 | + cameraResolution, | ||
| 184 | + ) | ||
| 169 | } catch (e: AlreadyStarted) { | 185 | } catch (e: AlreadyStarted) { |
| 170 | result.error( | 186 | result.error( |
| 171 | "MobileScanner", | 187 | "MobileScanner", |
| @@ -23,6 +23,7 @@ class MobileScannerController { | @@ -23,6 +23,7 @@ class MobileScannerController { | ||
| 23 | ) | 23 | ) |
| 24 | this.onPermissionSet, | 24 | this.onPermissionSet, |
| 25 | this.autoStart = true, | 25 | this.autoStart = true, |
| 26 | + this.cameraResolution, | ||
| 26 | }); | 27 | }); |
| 27 | 28 | ||
| 28 | /// Select which camera should be used. | 29 | /// Select which camera should be used. |
| @@ -58,9 +59,24 @@ class MobileScannerController { | @@ -58,9 +59,24 @@ class MobileScannerController { | ||
| 58 | /// Automatically start the mobileScanner on initialization. | 59 | /// Automatically start the mobileScanner on initialization. |
| 59 | final bool autoStart; | 60 | final bool autoStart; |
| 60 | 61 | ||
| 62 | + /// The desired resolution for the camera. | ||
| 63 | + /// | ||
| 64 | + /// When this value is provided, the camera will try to match this resolution, | ||
| 65 | + /// or fallback to the closest available resolution. | ||
| 66 | + /// When this is null, Android defaults to a resolution of 640x480. | ||
| 67 | + /// | ||
| 68 | + /// Bear in mind that changing the resolution has an effect on the aspect ratio. | ||
| 69 | + /// | ||
| 70 | + /// When the camera orientation changes, | ||
| 71 | + /// the resolution will be flipped to match the new dimensions of the display. | ||
| 72 | + /// | ||
| 73 | + /// Currently only supported on Android. | ||
| 74 | + final Size? cameraResolution; | ||
| 75 | + | ||
| 61 | /// Sets the barcode stream | 76 | /// Sets the barcode stream |
| 62 | final StreamController<BarcodeCapture> _barcodesController = | 77 | final StreamController<BarcodeCapture> _barcodesController = |
| 63 | StreamController.broadcast(); | 78 | StreamController.broadcast(); |
| 79 | + | ||
| 64 | Stream<BarcodeCapture> get barcodes => _barcodesController.stream; | 80 | Stream<BarcodeCapture> get barcodes => _barcodesController.stream; |
| 65 | 81 | ||
| 66 | static const MethodChannel _methodChannel = | 82 | static const MethodChannel _methodChannel = |
| @@ -133,6 +149,12 @@ class MobileScannerController { | @@ -133,6 +149,12 @@ class MobileScannerController { | ||
| 133 | arguments['formats'] = formats!.map((e) => e.rawValue).toList(); | 149 | arguments['formats'] = formats!.map((e) => e.rawValue).toList(); |
| 134 | } else if (Platform.isAndroid) { | 150 | } else if (Platform.isAndroid) { |
| 135 | arguments['formats'] = formats!.map((e) => e.index).toList(); | 151 | arguments['formats'] = formats!.map((e) => e.index).toList(); |
| 152 | + if (cameraResolution != null) { | ||
| 153 | + arguments['cameraResolution'] = <int>[ | ||
| 154 | + cameraResolution!.width.toInt(), | ||
| 155 | + cameraResolution!.height.toInt(), | ||
| 156 | + ]; | ||
| 157 | + } | ||
| 136 | } | 158 | } |
| 137 | } | 159 | } |
| 138 | arguments['returnImage'] = returnImage; | 160 | arguments['returnImage'] = returnImage; |
-
Please register or login to post a comment