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
import io.flutter.view.TextureRegistry
import java.io.ByteArrayOutputStream
import kotlin.math.roundToInt
import android.util.Size
import android.hardware.display.DisplayManager
import android.view.WindowManager
import android.content.Context
import android.os.Build
class MobileScanner(
... ... @@ -39,6 +44,7 @@ class MobileScanner(
private var scanner = BarcodeScanning.getClient()
private var lastScanned: List<String?>? = null
private var scannerTimeout = false
private var displayListener: DisplayManager.DisplayListener? = null
/// Configurable variables
var scanWindow: List<Float>? = null
... ... @@ -138,7 +144,7 @@ class MobileScanner(
}
}
fun rotateBitmap(bitmap: Bitmap, degrees: Float): Bitmap {
private fun rotateBitmap(bitmap: Bitmap, degrees: Float): Bitmap {
val matrix = Matrix()
matrix.postRotate(degrees)
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
... ... @@ -166,6 +172,34 @@ class MobileScanner(
return scaledScanWindow.contains(barcodeBoundingBox)
}
// Return the best resolution for the actual device orientation.
//
// By default the resolution is 480x640, which is too low for ML Kit.
// If the given resolution is not supported by the display,
// the closest available resolution is used.
//
// The resolution should be adjusted for the display rotation, to preserve the aspect ratio.
@Suppress("deprecation")
private fun getResolution(cameraResolution: Size): Size {
val rotation = if (Build.VERSION.SDK_INT >= 30) {
activity.applicationContext.display!!.rotation
} else {
val windowManager = activity.applicationContext.getSystemService(Context.WINDOW_SERVICE) as WindowManager
windowManager.defaultDisplay.rotation
}
val widthMaxRes = cameraResolution.width
val heightMaxRes = cameraResolution.height
val targetResolution = if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) {
Size(widthMaxRes, heightMaxRes) // Portrait mode
} else {
Size(heightMaxRes, widthMaxRes) // Landscape mode
}
return targetResolution
}
/**
* Start barcode scanning by initializing the camera and barcode scanner.
*/
... ... @@ -179,7 +213,8 @@ class MobileScanner(
torchStateCallback: TorchStateCallback,
zoomScaleStateCallback: ZoomScaleStateCallback,
mobileScannerStartedCallback: MobileScannerStartedCallback,
detectionTimeout: Long
detectionTimeout: Long,
cameraResolution: Size?
) {
this.detectionSpeed = detectionSpeed
this.detectionTimeout = detectionTimeout
... ... @@ -229,7 +264,30 @@ class MobileScanner(
// Build the analyzer to be passed on to MLKit
val analysisBuilder = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
// analysisBuilder.setTargetResolution(Size(1440, 1920))
val displayManager = activity.applicationContext.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
if (cameraResolution != null) {
// TODO: migrate to ResolutionSelector with ResolutionStrategy when upgrading to camera 1.3.0
// Override initial resolution
analysisBuilder.setTargetResolution(getResolution(cameraResolution))
if (displayListener == null) {
displayListener = object : DisplayManager.DisplayListener {
override fun onDisplayAdded(displayId: Int) {}
override fun onDisplayRemoved(displayId: Int) {}
override fun onDisplayChanged(displayId: Int) {
analysisBuilder.setTargetResolution(getResolution(cameraResolution))
}
}
displayManager.registerDisplayListener(
displayListener, null,
)
}
}
val analysis = analysisBuilder.build().apply { setAnalyzer(executor, captureOutput) }
camera = cameraProvider!!.bindToLifecycle(
... ... @@ -278,6 +336,13 @@ class MobileScanner(
throw AlreadyStopped()
}
if (displayListener != null) {
val displayManager = activity.applicationContext.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
displayManager.unregisterDisplayListener(displayListener)
displayListener = null
}
val owner = activity as LifecycleOwner
camera?.cameraInfo?.torchState?.removeObservers(owner)
cameraProvider?.unbindAll()
... ...
... ... @@ -2,6 +2,7 @@ package dev.steenbakker.mobile_scanner
import android.app.Activity
import android.net.Uri
import android.util.Size
import androidx.camera.core.CameraSelector
import androidx.camera.core.ExperimentalGetImage
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
... ... @@ -133,6 +134,12 @@ class MobileScannerHandler(
val returnImage: Boolean = call.argument<Boolean>("returnImage") ?: 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")
val cameraResolution: Size? = if (cameraResolutionValues != null) {
Size(cameraResolutionValues[0], cameraResolutionValues[1])
} else {
null
}
var barcodeScannerOptions: BarcodeScannerOptions? = null
if (formats != null) {
... ... @@ -157,15 +164,24 @@ class MobileScannerHandler(
val detectionSpeed: DetectionSpeed = DetectionSpeed.values().first { it.intValue == speed}
try {
mobileScanner!!.start(barcodeScannerOptions, returnImage, position, torch, detectionSpeed, torchStateCallback, zoomScaleStateCallback, mobileScannerStartedCallback = {
result.success(mapOf(
"textureId" to it.id,
"size" to mapOf("width" to it.width, "height" to it.height),
"torchable" to it.hasFlashUnit
))
},
timeout.toLong())
mobileScanner!!.start(
barcodeScannerOptions,
returnImage,
position,
torch,
detectionSpeed,
torchStateCallback,
zoomScaleStateCallback,
mobileScannerStartedCallback = {
result.success(mapOf(
"textureId" to it.id,
"size" to mapOf("width" to it.width, "height" to it.height),
"torchable" to it.hasFlashUnit
))
},
timeout.toLong(),
cameraResolution,
)
} catch (e: AlreadyStarted) {
result.error(
"MobileScanner",
... ... @@ -246,4 +262,4 @@ class MobileScannerHandler(
private fun updateScanWindow(call: MethodCall) {
mobileScanner!!.scanWindow = call.argument<List<Float>?>("rect")
}
}
\ No newline at end of file
}
... ...
... ... @@ -23,6 +23,7 @@ class MobileScannerController {
)
this.onPermissionSet,
this.autoStart = true,
this.cameraResolution,
});
/// Select which camera should be used.
... ... @@ -58,9 +59,24 @@ class MobileScannerController {
/// Automatically start the mobileScanner on initialization.
final bool autoStart;
/// The desired resolution for the camera.
///
/// When this value is provided, the camera will try to match this resolution,
/// or fallback to the closest available resolution.
/// When this is null, Android defaults to a resolution of 640x480.
///
/// Bear in mind that changing the resolution has an effect on the aspect ratio.
///
/// When the camera orientation changes,
/// the resolution will be flipped to match the new dimensions of the display.
///
/// Currently only supported on Android.
final Size? cameraResolution;
/// Sets the barcode stream
final StreamController<BarcodeCapture> _barcodesController =
StreamController.broadcast();
Stream<BarcodeCapture> get barcodes => _barcodesController.stream;
static const MethodChannel _methodChannel =
... ... @@ -133,6 +149,12 @@ class MobileScannerController {
arguments['formats'] = formats!.map((e) => e.rawValue).toList();
} else if (Platform.isAndroid) {
arguments['formats'] = formats!.map((e) => e.index).toList();
if (cameraResolution != null) {
arguments['cameraResolution'] = <int>[
cameraResolution!.width.toInt(),
cameraResolution!.height.toInt(),
];
}
}
}
arguments['returnImage'] = returnImage;
... ...