speedup + minor fixes

This commit is contained in:
Hadrian Burkhardt
2026-02-26 03:19:53 +01:00
parent 471270a396
commit 027d2391b7
5 changed files with 54 additions and 19 deletions
@@ -1,6 +1,7 @@
package com.clean.scanner.data.scanner package com.clean.scanner.data.scanner
import android.graphics.Rect import android.graphics.Rect
import android.os.SystemClock
import androidx.camera.core.ImageAnalysis import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy import androidx.camera.core.ImageProxy
import com.clean.scanner.domain.ScanResult import com.clean.scanner.domain.ScanResult
@@ -47,6 +48,8 @@ class MlKitBarcodeAnalyzer(
const val MATCH_DISTANCE_THRESHOLD = 0.18f const val MATCH_DISTANCE_THRESHOLD = 0.18f
const val BOX_SMOOTHING_ALPHA = 0.35f const val BOX_SMOOTHING_ALPHA = 0.35f
const val MAX_MISSED_FRAMES = 2 const val MAX_MISSED_FRAMES = 2
const val MIN_ANALYSIS_INTERVAL_MS = 45L
const val MIN_STATE_PUBLISH_INTERVAL_MS = 66L
} }
private val scanner = BarcodeScanning.getClient( private val scanner = BarcodeScanning.getClient(
@@ -58,6 +61,10 @@ class MlKitBarcodeAnalyzer(
@Volatile @Volatile
private var processing = false private var processing = false
@Volatile @Volatile
private var lastAnalysisStartMs = 0L
@Volatile
private var lastStatePublishMs = 0L
@Volatile
private var lastHasPotential = false private var lastHasPotential = false
@Volatile @Volatile
private var lastHasReadable = false private var lastHasReadable = false
@@ -86,6 +93,11 @@ class MlKitBarcodeAnalyzer(
imageProxy.close() imageProxy.close()
return return
} }
val nowMs = SystemClock.elapsedRealtime()
if (nowMs - lastAnalysisStartMs < MIN_ANALYSIS_INTERVAL_MS) {
imageProxy.close()
return
}
if (processing) { if (processing) {
imageProxy.close() imageProxy.close()
return return
@@ -100,6 +112,7 @@ class MlKitBarcodeAnalyzer(
val sourceHeight = if (rotationDegrees == 90 || rotationDegrees == 270) imageProxy.width else imageProxy.height val sourceHeight = if (rotationDegrees == 90 || rotationDegrees == 270) imageProxy.width else imageProxy.height
val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees) val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
processing = true processing = true
lastAnalysisStartMs = nowMs
scanner.process(image) scanner.process(image)
.addOnSuccessListener { barcodes -> .addOnSuccessListener { barcodes ->
@@ -210,6 +223,7 @@ class MlKitBarcodeAnalyzer(
sourceWidth: Int, sourceWidth: Int,
sourceHeight: Int sourceHeight: Int
) { ) {
val nowMs = SystemClock.elapsedRealtime()
if ( if (
lastHasPotential == hasPotential && lastHasPotential == hasPotential &&
lastHasReadable == hasReadable && lastHasReadable == hasReadable &&
@@ -217,11 +231,19 @@ class MlKitBarcodeAnalyzer(
lastSourceWidth == sourceWidth && lastSourceWidth == sourceWidth &&
lastSourceHeight == sourceHeight lastSourceHeight == sourceHeight
) return ) return
val isStateKindUnchanged = lastHasPotential == hasPotential &&
lastHasReadable == hasReadable &&
lastSourceWidth == sourceWidth &&
lastSourceHeight == sourceHeight
if (isStateKindUnchanged && nowMs - lastStatePublishMs < MIN_STATE_PUBLISH_INTERVAL_MS) {
return
}
lastHasPotential = hasPotential lastHasPotential = hasPotential
lastHasReadable = hasReadable lastHasReadable = hasReadable
lastBoxes = boxes lastBoxes = boxes
lastSourceWidth = sourceWidth lastSourceWidth = sourceWidth
lastSourceHeight = sourceHeight lastSourceHeight = sourceHeight
lastStatePublishMs = nowMs
onDetectionStateChanged(hasPotential, hasReadable, boxes, sourceWidth, sourceHeight) onDetectionStateChanged(hasPotential, hasReadable, boxes, sourceWidth, sourceHeight)
} }
@@ -1,6 +1,7 @@
package com.clean.scanner.ui.components package com.clean.scanner.ui.components
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.util.Size
import android.view.MotionEvent import android.view.MotionEvent
import android.view.ScaleGestureDetector import android.view.ScaleGestureDetector
import androidx.camera.core.CameraSelector import androidx.camera.core.CameraSelector
@@ -12,6 +13,7 @@ import androidx.camera.view.PreviewView
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.rememberUpdatedState
@@ -49,7 +51,7 @@ fun CameraPreview(
val mainExecutor = remember(context) { ContextCompat.getMainExecutor(context) } val mainExecutor = remember(context) { ContextCompat.getMainExecutor(context) }
val previewView = remember { PreviewView(context) } val previewView = remember { PreviewView(context) }
val cameraRef = remember { mutableStateOf<androidx.camera.core.Camera?>(null) } val cameraRef = remember { mutableStateOf<androidx.camera.core.Camera?>(null) }
val zoomRatio = remember { mutableStateOf(1f) } val zoomRatio = remember { mutableFloatStateOf(1f) }
val latestAnalysisEnabled = rememberUpdatedState(analysisEnabled) val latestAnalysisEnabled = rememberUpdatedState(analysisEnabled)
val latestOnScan = rememberUpdatedState(onScan) val latestOnScan = rememberUpdatedState(onScan)
val latestOnDetectionStateChanged = rememberUpdatedState(onDetectionStateChanged) val latestOnDetectionStateChanged = rememberUpdatedState(onDetectionStateChanged)
@@ -107,6 +109,7 @@ fun CameraPreview(
val imageAnalysis = ImageAnalysis.Builder() val imageAnalysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.setTargetResolution(Size(1280, 720))
.build().apply { .build().apply {
setAnalyzer(cameraExecutor, analyzer) setAnalyzer(cameraExecutor, analyzer)
} }
@@ -22,7 +22,6 @@ import androidx.compose.foundation.gestures.detectTransformGestures
import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@@ -55,6 +54,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
@@ -62,6 +63,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.asImageBitmap
@@ -85,6 +87,7 @@ import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.clean.scanner.R import com.clean.scanner.R
import com.clean.scanner.data.scanner.DetectionBox import com.clean.scanner.data.scanner.DetectionBox
import com.clean.scanner.data.scanner.DetectionPoint
import com.clean.scanner.domain.ScanResult import com.clean.scanner.domain.ScanResult
import com.clean.scanner.ui.BatchScanRecord import com.clean.scanner.ui.BatchScanRecord
import com.clean.scanner.ui.components.CameraPreview import com.clean.scanner.ui.components.CameraPreview
@@ -93,6 +96,7 @@ import com.clean.scanner.util.Intents
import com.clean.scanner.util.ScanContentParsers import com.clean.scanner.util.ScanContentParsers
import com.clean.scanner.util.UrlRiskScorer import com.clean.scanner.util.UrlRiskScorer
import com.google.mlkit.vision.barcode.BarcodeScanning import com.google.mlkit.vision.barcode.BarcodeScanning
import com.google.mlkit.vision.barcode.BarcodeScanner
import com.google.mlkit.vision.barcode.BarcodeScannerOptions import com.google.mlkit.vision.barcode.BarcodeScannerOptions
import com.google.mlkit.vision.barcode.common.Barcode import com.google.mlkit.vision.barcode.common.Barcode
import com.google.mlkit.vision.common.InputImage import com.google.mlkit.vision.common.InputImage
@@ -150,8 +154,8 @@ fun ScannerScreen(
var hasPotentialInView by remember { mutableStateOf(false) } var hasPotentialInView by remember { mutableStateOf(false) }
var hasReadableInView by remember { mutableStateOf(false) } var hasReadableInView by remember { mutableStateOf(false) }
var detectionBoxes by remember { mutableStateOf<List<DetectionBox>>(emptyList()) } var detectionBoxes by remember { mutableStateOf<List<DetectionBox>>(emptyList()) }
var detectionSourceWidth by remember { mutableStateOf(0) } var detectionSourceWidth by remember { mutableIntStateOf(0) }
var detectionSourceHeight by remember { mutableStateOf(0) } var detectionSourceHeight by remember { mutableIntStateOf(0) }
val activity = context as? Activity val activity = context as? Activity
val imageScanner = remember { val imageScanner = remember {
BarcodeScanning.getClient( BarcodeScanning.getClient(
@@ -193,7 +197,7 @@ fun ScannerScreen(
val raw = barcode.rawValue?.takeIf { it.isNotBlank() } ?: return@mapNotNull null val raw = barcode.rawValue?.takeIf { it.isNotBlank() } ?: return@mapNotNull null
val normalizedBox = barcode.boundingBox?.let { bounds -> val normalizedBox = barcode.boundingBox?.let { bounds ->
val corners = barcode.cornerPoints?.map { p -> val corners = barcode.cornerPoints?.map { p ->
com.clean.scanner.data.scanner.DetectionPoint( DetectionPoint(
x = (p.x / image.width.toFloat()).coerceIn(0f, 1f), x = (p.x / image.width.toFloat()).coerceIn(0f, 1f),
y = (p.y / image.height.toFloat()).coerceIn(0f, 1f) y = (p.y / image.height.toFloat()).coerceIn(0f, 1f)
) )
@@ -254,10 +258,16 @@ fun ScannerScreen(
} }
} }
BoxWithConstraints(modifier = Modifier.fillMaxSize()) { var containerSize by remember { mutableStateOf(IntSize.Zero) }
Box(
modifier = Modifier
.fillMaxSize()
.onSizeChanged { containerSize = it }
) {
val density = LocalDensity.current val density = LocalDensity.current
val viewW = with(density) { maxWidth.toPx() } val viewW = containerSize.width.toFloat()
val viewH = with(density) { maxHeight.toPx() } val viewH = containerSize.height.toFloat()
val galleryOpen = imageScanPreviewUri != null val galleryOpen = imageScanPreviewUri != null
val aimW = viewW * 0.62f val aimW = viewW * 0.62f
val aimH = with(density) { 200.dp.toPx() } val aimH = with(density) { 200.dp.toPx() }
@@ -324,7 +334,7 @@ fun ScannerScreen(
val right = offsetX + (box.right * sourceW * scale) val right = offsetX + (box.right * sourceW * scale)
val bottom = offsetY + (box.bottom * sourceH * scale) val bottom = offsetY + (box.bottom * sourceH * scale)
val mappedCorners = box.corners.map { p -> val mappedCorners = box.corners.map { p ->
androidx.compose.ui.geometry.Offset( Offset(
x = offsetX + (p.x * sourceW * scale), x = offsetX + (p.x * sourceW * scale),
y = offsetY + (p.y * sourceH * scale) y = offsetY + (p.y * sourceH * scale)
) )
@@ -343,8 +353,8 @@ fun ScannerScreen(
} else if (right > left && bottom > top) { } else if (right > left && bottom > top) {
drawRoundRect( drawRoundRect(
color = boxColor.copy(alpha = 0.95f), color = boxColor.copy(alpha = 0.95f),
topLeft = androidx.compose.ui.geometry.Offset(left, top), topLeft = Offset(left, top),
size = androidx.compose.ui.geometry.Size(right - left, bottom - top), size = Size(right - left, bottom - top),
cornerRadius = CornerRadius(14f, 14f), cornerRadius = CornerRadius(14f, 14f),
style = Stroke(width = 4f) style = Stroke(width = 4f)
) )
@@ -750,7 +760,7 @@ private fun loadBitmapFromUri(context: android.content.Context, uri: Uri): Bitma
} }
private suspend fun detectBarcodes( private suspend fun detectBarcodes(
scanner: com.google.mlkit.vision.barcode.BarcodeScanner, scanner: BarcodeScanner,
image: InputImage image: InputImage
): List<Barcode> = suspendCancellableCoroutine { cont -> ): List<Barcode> = suspendCancellableCoroutine { cont ->
scanner.process(image) scanner.process(image)
@@ -772,10 +782,10 @@ private fun GalleryScanPreviewDialog(
val context = LocalContext.current val context = LocalContext.current
val bitmap = remember(imageUri) { imageUri?.let { loadBitmapFromUri(context, it) } } val bitmap = remember(imageUri) { imageUri?.let { loadBitmapFromUri(context, it) } }
var liveCandidates by remember(imageUri, candidates) { mutableStateOf(candidates) } var liveCandidates by remember(imageUri, candidates) { mutableStateOf(candidates) }
var zoom by remember(imageUri) { mutableStateOf(1f) } var zoom by remember(imageUri) { mutableFloatStateOf(1f) }
var pan by remember(imageUri) { mutableStateOf(Offset.Zero) } var pan by remember(imageUri) { mutableStateOf(Offset.Zero) }
var viewportSize by remember { mutableStateOf(IntSize.Zero) } var viewportSize by remember { mutableStateOf(IntSize.Zero) }
var scanTick by remember { mutableStateOf(0) } var scanTick by remember { mutableIntStateOf(0) }
val markerPaint = remember { val markerPaint = remember {
Paint().apply { Paint().apply {
color = android.graphics.Color.WHITE color = android.graphics.Color.WHITE
@@ -850,7 +860,7 @@ private fun GalleryScanPreviewDialog(
val rightN = ((bounds.right + cropLeft) / imgW).coerceIn(0f, 1f) val rightN = ((bounds.right + cropLeft) / imgW).coerceIn(0f, 1f)
val bottomN = ((bounds.bottom + cropTop) / imgH).coerceIn(0f, 1f) val bottomN = ((bounds.bottom + cropTop) / imgH).coerceIn(0f, 1f)
val corners = barcode.cornerPoints?.map { p -> val corners = barcode.cornerPoints?.map { p ->
com.clean.scanner.data.scanner.DetectionPoint( DetectionPoint(
x = ((p.x + cropLeft) / imgW).coerceIn(0f, 1f), x = ((p.x + cropLeft) / imgW).coerceIn(0f, 1f),
y = ((p.y + cropTop) / imgH).coerceIn(0f, 1f) y = ((p.y + cropTop) / imgH).coerceIn(0f, 1f)
) )
@@ -952,8 +962,8 @@ private fun GalleryScanPreviewDialog(
if (right > left && bottom > top) { if (right > left && bottom > top) {
drawRoundRect( drawRoundRect(
color = color, color = color,
topLeft = androidx.compose.ui.geometry.Offset(left, top), topLeft = Offset(left, top),
size = androidx.compose.ui.geometry.Size(right - left, bottom - top), size = Size(right - left, bottom - top),
cornerRadius = CornerRadius(10f, 10f), cornerRadius = CornerRadius(10f, 10f),
style = Stroke(width = 4f) style = Stroke(width = 4f)
) )
+1 -1
View File
@@ -1,5 +1,5 @@
plugins { plugins {
id("com.android.application") version "9.0.0" apply false id("com.android.application") version "9.0.1" apply false
id("com.google.devtools.ksp") version "2.3.5" apply false id("com.google.devtools.ksp") version "2.3.5" apply false
id("org.jetbrains.kotlin.plugin.compose") version "2.3.10" apply false id("org.jetbrains.kotlin.plugin.compose") version "2.3.10" apply false
} }