Navaron Bracke
Committed by GitHub

Merge pull request #785 from navaronbracke/increase_camera_quality_cherry_pick

fix: Increase camera quality (cherry-pick)
@@ -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;