Improve QRS scan

This commit is contained in:
世界
2026-01-02 22:33:31 +08:00
parent aaa3ef044a
commit 7a5c3640c6
10 changed files with 523 additions and 36 deletions

View File

@@ -8,12 +8,15 @@ import com.google.mlkit.vision.barcode.BarcodeScannerOptions
import com.google.mlkit.vision.barcode.BarcodeScanning
import com.google.mlkit.vision.barcode.common.Barcode
import com.google.mlkit.vision.common.InputImage
import io.nekohasekai.sfa.ui.profile.QRCodeCropArea
import io.nekohasekai.sfa.ui.profile.QRCodeSmartCrop
// kanged from: https://github.com/G00fY2/quickie/blob/main/quickie/src/main/kotlin/io/github/g00fy2/quickie/QRCodeAnalyzer.kt
class MLKitQRCodeAnalyzer(
private val onSuccess: ((String) -> Unit),
private val onFailure: ((Exception) -> Unit),
private val onCropArea: ((QRCodeCropArea?) -> Unit)? = null,
) : ImageAnalysis.Analyzer {
private val barcodeScanner =
BarcodeScanning.getClient(
@@ -26,6 +29,7 @@ class MLKitQRCodeAnalyzer(
private var failureOccurred = false
private var failureTimestamp = 0L
private var yDataBuffer: ByteArray? = null
private var pixelBuffer: IntArray? = null
@ExperimentalGetImage
@@ -38,6 +42,7 @@ class MLKitQRCodeAnalyzer(
val nowMills = System.currentTimeMillis()
if (failureOccurred && nowMills - failureTimestamp < 5000L) {
failureTimestamp = nowMills
onCropArea?.invoke(null)
image.close()
return
}
@@ -47,33 +52,75 @@ class MLKitQRCodeAnalyzer(
.addOnSuccessListener { codes ->
val rawValue = codes.firstOrNull()?.rawValue
if (rawValue != null) {
onCropArea?.invoke(null)
onSuccess(rawValue)
image.close()
} else {
tryInvertedScan(image)
val yData = image.toYData().copyOf()
val rotation = image.imageInfo.rotationDegrees
val cropArea = QRCodeSmartCrop.findCropArea(yData, image.width, image.height, rotation)
onCropArea?.invoke(cropArea)
if (cropArea == null) {
tryInvertedScan(yData, image.width, image.height, rotation) { image.close() }
} else {
val cropWidth = cropArea.right - cropArea.left
val cropHeight = cropArea.bottom - cropArea.top
val bitmap = toLumaBitmap(
yData,
image.width,
cropArea.left,
cropArea.top,
cropWidth,
cropHeight,
invert = false,
)
barcodeScanner.process(InputImage.fromBitmap(bitmap, rotation))
.addOnSuccessListener { cropCodes ->
val cropValue = cropCodes.firstOrNull()?.rawValue
if (cropValue != null) {
onSuccess(cropValue)
image.close()
} else {
tryInvertedScan(yData, image.width, image.height, rotation) { image.close() }
}
}
.addOnFailureListener {
tryInvertedScan(yData, image.width, image.height, rotation) { image.close() }
}
.addOnCompleteListener {
bitmap.recycle()
}
}
}
}
.addOnFailureListener {
failureOccurred = true
failureTimestamp = System.currentTimeMillis()
onCropArea?.invoke(null)
onFailure(it)
image.close()
}
}
private fun tryInvertedScan(image: ImageProxy) {
val inverted = image.toInvertedBitmap()
barcodeScanner.process(InputImage.fromBitmap(inverted, image.imageInfo.rotationDegrees))
private fun tryInvertedScan(
yData: ByteArray,
width: Int,
height: Int,
rotationDegrees: Int,
onComplete: () -> Unit,
) {
val inverted = toLumaBitmap(yData, width, 0, 0, width, height, invert = true)
barcodeScanner.process(InputImage.fromBitmap(inverted, rotationDegrees))
.addOnSuccessListener { codes ->
codes.firstOrNull()?.rawValue?.let { onSuccess(it) }
}
.addOnCompleteListener {
inverted.recycle()
image.close()
onComplete()
}
}
private fun ImageProxy.toInvertedBitmap(): Bitmap {
private fun ImageProxy.toYData(): ByteArray {
val yPlane = planes[0]
val yBuffer = yPlane.buffer.duplicate()
val rowStride = yPlane.rowStride
@@ -81,16 +128,41 @@ class MLKitQRCodeAnalyzer(
val height = height
val size = width * height
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val pixels = pixelBuffer?.takeIf { it.size >= size } ?: IntArray(size).also { pixelBuffer = it }
for (row in 0 until height) {
yBuffer.position(row * rowStride)
for (col in 0 until width) {
val y = 255 - (yBuffer.get().toInt() and 0xFF)
pixels[row * width + col] = (0xFF shl 24) or (y shl 16) or (y shl 8) or y
val yData = yDataBuffer?.takeIf { it.size >= size } ?: ByteArray(size).also { yDataBuffer = it }
if (rowStride == width) {
yBuffer.get(yData, 0, size)
} else {
for (row in 0 until height) {
yBuffer.position(row * rowStride)
yBuffer.get(yData, row * width, width)
}
}
return yData
}
private fun toLumaBitmap(
yData: ByteArray,
srcWidth: Int,
left: Int,
top: Int,
width: Int,
height: Int,
invert: Boolean,
): Bitmap {
val size = width * height
val pixels = pixelBuffer?.takeIf { it.size >= size } ?: IntArray(size).also { pixelBuffer = it }
var index = 0
for (row in 0 until height) {
val rowOffset = (top + row) * srcWidth + left
for (col in 0 until width) {
val y = yData[rowOffset + col].toInt() and 0xFF
val luma = if (invert) 255 - y else y
pixels[index++] = (0xFF shl 24) or (luma shl 16) or (luma shl 8) or luma
}
}
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
bitmap.setPixels(pixels, 0, width, 0, 0, width, height)
return bitmap
}

View File

@@ -12,6 +12,7 @@ import com.google.android.play.core.install.model.InstallStatus
import com.google.android.play.core.install.model.UpdateAvailability
import com.google.mlkit.common.MlKitException
import io.nekohasekai.sfa.R
import io.nekohasekai.sfa.ui.profile.QRCodeCropArea
import io.nekohasekai.sfa.update.UpdateInfo
import io.nekohasekai.sfa.update.UpdateState
@@ -82,9 +83,10 @@ object Vendor : VendorInterface {
override fun createQRCodeAnalyzer(
onSuccess: (String) -> Unit,
onFailure: (Exception) -> Unit,
onCropArea: ((QRCodeCropArea?) -> Unit)?,
): ImageAnalysis.Analyzer? {
try {
return MLKitQRCodeAnalyzer(onSuccess, onFailure)
return MLKitQRCodeAnalyzer(onSuccess, onFailure, onCropArea)
} catch (exception: Exception) {
if (exception !is MlKitException || exception.errorCode != MlKitException.UNAVAILABLE) {
Log.e(TAG, "failed to create MLKitQRCodeAnalyzer", exception)