speedup + minor fixes
This commit is contained in:
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user