Improve QRS scan
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user