Fix QR scanning bug when using camerax.
This commit is contained in:
parent
499cdd9f29
commit
e83cb6fa8b
3 changed files with 97 additions and 17 deletions
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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"))
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Add table
Reference in a new issue