Fix QR scanning bug when using camerax.

This commit is contained in:
Cody Henthorne 2022-06-02 11:42:25 -04:00
parent 499cdd9f29
commit e83cb6fa8b
3 changed files with 97 additions and 17 deletions

View file

@ -0,0 +1,68 @@
package org.signal.qr
import android.graphics.ImageFormat
import androidx.camera.core.ImageProxy
import com.google.zxing.LuminanceSource
import java.nio.ByteBuffer
/**
* Luminance source that gets data via an [ImageProxy]. The main reason for this is because
* the Y-Plane provided by the camera framework can have a row stride (number of bytes that make up a row)
* that is different than the image width.
*
* An image width can be reported as 1080 but the row stride may be 1088. Thus when representing a row-major
* 2D array as a 1D array, the math can go sideways if width is used instead of row stride.
*/
class ImageProxyLuminanceSource(image: ImageProxy) : LuminanceSource(image.width, image.height) {
val yData: ByteArray
init {
require(image.format == ImageFormat.YUV_420_888) { "Invalid image format" }
yData = ByteArray(image.width * image.height)
val yBuffer: ByteBuffer = image.planes[0].buffer
yBuffer.position(0)
val yRowStride: Int = image.planes[0].rowStride
for (y in 0 until image.height) {
val yIndex: Int = y * yRowStride
yBuffer.position(yIndex)
yBuffer.get(yData, y * image.width, image.width)
}
}
override fun getRow(y: Int, row: ByteArray?): ByteArray {
require(y in 0 until height) { "Requested row is outside the image: $y" }
val toReturn: ByteArray = if (row == null || row.size < width) {
ByteArray(width)
} else {
row
}
val yIndex: Int = y * width
yData.copyInto(toReturn, 0, yIndex, yIndex + width)
return toReturn
}
override fun getMatrix(): ByteArray {
return yData
}
fun render(): IntArray {
val argbArray = IntArray(width * height)
var yValue: Int
yData.forEachIndexed { i, byte ->
yValue = (byte.toInt() and 0xff).coerceIn(0..255)
argbArray[i] = 255 shl 24 or (yValue and 255 shl 16) or (yValue and 255 shl 8) or (yValue and 255)
}
return argbArray
}
}

View file

@ -1,9 +1,11 @@
package org.signal.qr
import androidx.camera.core.ImageProxy
import com.google.zxing.BinaryBitmap
import com.google.zxing.ChecksumException
import com.google.zxing.DecodeHintType
import com.google.zxing.FormatException
import com.google.zxing.LuminanceSource
import com.google.zxing.NotFoundException
import com.google.zxing.PlanarYUVLuminanceSource
import com.google.zxing.Result
@ -21,19 +23,25 @@ class QrProcessor {
private var previousHeight = 0
private var previousWidth = 0
fun getScannedData(proxy: ImageProxy): String? {
return getScannedData(ImageProxyLuminanceSource(proxy))
}
fun getScannedData(
data: ByteArray,
width: Int,
height: Int
): String? {
try {
if (width != previousWidth || height != previousHeight) {
Log.i(TAG, "Processing $width x $height image, data: ${data.size}")
previousWidth = width
previousHeight = height
}
return getScannedData(PlanarYUVLuminanceSource(data, width, height, 0, 0, width, height, false))
}
val source = PlanarYUVLuminanceSource(data, width, height, 0, 0, width, height, false)
private fun getScannedData(source: LuminanceSource): String? {
try {
if (source.width != previousWidth || source.height != previousHeight) {
Log.i(TAG, "Processing ${source.width} x ${source.height} image")
previousWidth = source.width
previousHeight = source.height
}
val bitmap = BinaryBitmap(HybridBinarizer(source))
val result: Result? = reader.decode(bitmap, mapOf(DecodeHintType.TRY_HARDER to true, DecodeHintType.CHARACTER_SET to "ISO-8859-1"))

View file

@ -2,22 +2,30 @@ package org.signal.qr
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Bitmap
import android.graphics.ImageFormat
import android.util.Size
import android.widget.FrameLayout
import androidx.annotation.RequiresApi
import androidx.camera.core.AspectRatio
import androidx.camera.core.Camera
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.core.content.ContextCompat
import androidx.core.math.MathUtils.clamp
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import io.reactivex.rxjava3.subjects.PublishSubject
import org.signal.core.util.logging.Log
import org.signal.qr.kitkat.ScanListener
import java.nio.ByteBuffer
import java.util.concurrent.Executors
/**
* API21+ version of QR scanning view. Uses camerax APIs.
*/
@ -74,21 +82,17 @@ internal class ScannerView21 constructor(
val preview = Preview.Builder().build()
val imageAnalysis = ImageAnalysis.Builder()
.setTargetAspectRatio(AspectRatio.RATIO_4_3)
.setTargetResolution(Size(1920, 1080))
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
imageAnalysis.setAnalyzer(analyzerExecutor) { proxy ->
val buffer = proxy.planes[0].buffer.apply { rewind() }
val bytes = ByteArray(buffer.capacity())
buffer.get(bytes)
val data: String? = qrProcessor.getScannedData(bytes, proxy.width, proxy.height)
if (data != null) {
listener.onQrDataFound(data)
proxy.use {
val data: String? = qrProcessor.getScannedData(it)
if (data != null) {
listener.onQrDataFound(data)
}
}
proxy.close()
}
cameraProvider.unbindAll()