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 = {  
161 - result.success(mapOf(  
162 - "textureId" to it.id,  
163 - "size" to mapOf("width" to it.width, "height" to it.height),  
164 - "torchable" to it.hasFlashUnit  
165 - ))  
166 - },  
167 - timeout.toLong())  
168 - 167 + mobileScanner!!.start(
  168 + barcodeScannerOptions,
  169 + returnImage,
  170 + position,
  171 + torch,
  172 + detectionSpeed,
  173 + torchStateCallback,
  174 + zoomScaleStateCallback,
  175 + mobileScannerStartedCallback = {
  176 + result.success(mapOf(
  177 + "textureId" to it.id,
  178 + "size" to mapOf("width" to it.width, "height" to it.height),
  179 + "torchable" to it.hasFlashUnit
  180 + ))
  181 + },
  182 + timeout.toLong(),
  183 + cameraResolution,
  184 + )
169 } catch (e: AlreadyStarted) { 185 } catch (e: AlreadyStarted) {
170 result.error( 186 result.error(
171 "MobileScanner", 187 "MobileScanner",
@@ -246,4 +262,4 @@ class MobileScannerHandler( @@ -246,4 +262,4 @@ class MobileScannerHandler(
246 private fun updateScanWindow(call: MethodCall) { 262 private fun updateScanWindow(call: MethodCall) {
247 mobileScanner!!.scanWindow = call.argument<List<Float>?>("rect") 263 mobileScanner!!.scanWindow = call.argument<List<Float>?>("rect")
248 } 264 }
249 -}  
  265 +}
@@ -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;