ZXing、Android、JetPack Compose - 在二维码周围绘制边界框

问题描述 投票:0回答:1

我正在 Android 中使用 Jetpack Compose 构建 QR 码扫描仪。扫描部分和读取 QR 码中的位值工作良好且符合预期。然而,作为我原型的一部分,我希望能够在二维码周围画一个红色框。

目前,我正在处理两个问题。红色框的高度似乎是正确的,但宽度太小,我不知道为什么。第二个问题是,当我移动手机时,边界框似乎到处都是,并且没有锁定实际的二维码。我不知道如何解决这个问题。

The bounding box doesn't seem to be the right size and it does not align with the QR code unless the angle is perfect.

这是我的

CameraView
可组合函数...

@Composable
fun CameraPreview() {
    val context = LocalContext.current
    val scanner = MultiFormatReader().apply {
        val hints: MutableMap<DecodeHintType, Any> = EnumMap(DecodeHintType::class.java)
        hints[DecodeHintType.POSSIBLE_FORMATS] = listOf(BarcodeFormat.QR_CODE)
        setHints(hints)
    }

    val qrCodeBounds = remember { mutableStateOf<Rect?>(null) }

    BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
        val screenWidth = maxWidth
        val scanBoxSize = screenWidth * 0.6f // adjust the size of the scanning area here

        AndroidView(
            factory = { ctx ->
                val previewView = PreviewView(ctx)
                val cameraProviderFuture = ProcessCameraProvider.getInstance(ctx)
                cameraProviderFuture.addListener({
                    val cameraProvider = cameraProviderFuture.get()
                    val preview = Preview.Builder().build().also {
                        it.setSurfaceProvider(previewView.surfaceProvider)
                    }
                    val imageAnalysis = ImageAnalysis.Builder()
                        .build()
                        .also {
                            it.setAnalyzer(ContextCompat.getMainExecutor(ctx)) { imageProxy ->
                                val result = scanQRCode(imageProxy, scanner)
                                imageProxy.close()
                                if (result != null) {
                                    println("QR Code found: ${result.text}")
                                    println("Image Proxy Width: ${imageProxy.width}, Height: ${imageProxy.height}")
                                    qrCodeBounds.value = getBoundingBox(result.resultPoints, imageProxy.width, imageProxy.height, previewView.width, previewView.height)
                                } else {
                                    qrCodeBounds.value = null
                                }
                            }
                        }

                    try {
                        cameraProvider.unbindAll()
                        cameraProvider.bindToLifecycle(
                            context as ComponentActivity,
                            CameraSelector.DEFAULT_BACK_CAMERA,
                            preview,
                            imageAnalysis
                        )
                    } catch (exc: Exception) {
                        // Handle exceptions
                    }
                }, ContextCompat.getMainExecutor(ctx))
                previewView
            },
            modifier = Modifier.fillMaxSize()
        )

        Box(modifier = Modifier
            .matchParentSize()
            .background(Color.Black.copy(alpha = 0.8f))
        )

        // Scanning area box with a clear cutout
        Box(
            modifier = Modifier
                .size(scanBoxSize)
                .align(Alignment.Center)
                .drawBehind {
                    // Draw a rounded clear rectangle to create a cutout effect
                    drawRoundRect(
                        color = Color.Transparent,
                        topLeft = Offset(0f, 0f),
                        size = Size(size.width, size.height),
                        cornerRadius = CornerRadius(x = 12.dp.toPx(), y = 12.dp.toPx()),
                        blendMode = BlendMode.Clear
                    )
                }
                .border(2.dp, MaterialTheme.colorScheme.onPrimary, RoundedCornerShape(12.dp))
        )

        qrCodeBounds.value?.let { bounds ->
            Canvas(modifier = Modifier.fillMaxSize()) {
                drawRect(
                    color = Color.Red,
                    topLeft = Offset(bounds.left.toFloat(), bounds.top.toFloat()),
                    size = Size(bounds.width().toFloat(), bounds.height().toFloat()),
                    style = Stroke(width = 3.dp.toPx())
                )
            }
        }
    }
}

这是我的

getBoundingBox
功能

private fun getBoundingBox(resultPoints: Array<ResultPoint>?, imageWidth: Int, imageHeight: Int, previewWidth: Int, previewHeight: Int): Rect? {
    if (resultPoints == null || resultPoints.size != 4) {
        return null
    }

    // Calculate scale factors
    val scaleX = previewWidth.toFloat() / imageWidth
    val scaleY = previewHeight.toFloat() / imageHeight

    // Apply scale factors to the coordinates
    val left = (resultPoints[0].x * scaleX).toInt()
    val top = (resultPoints[0].y * scaleY).toInt()
    val right = (resultPoints[2].x * scaleX).toInt()
    val bottom = (resultPoints[2].y * scaleY).toInt()

    return Rect(left, top, right, bottom)
}

这是我的

scanQRCode
功能

private fun scanQRCode(imageProxy: ImageProxy, scanner: MultiFormatReader): Result? {
    val data = imageProxy.planes[0].buffer.let { buffer ->
        val data = ByteArray(buffer.capacity())
        buffer.get(data)
        buffer.clear()
        data
    }
    val source = PlanarYUVLuminanceSource(
        data,
        imageProxy.width,
        imageProxy.height,
        0,
        0,
        imageProxy.width,
        imageProxy.height,
        false
    )
    val binaryBitmap = BinaryBitmap(HybridBinarizer(source))
    return try {
        scanner.decodeWithState(binaryBitmap)
    } catch (e: Exception) {
        null // QR Code not found
    }
}
android kotlin android-jetpack-compose zxing
1个回答
0
投票

我很高兴有人用这种方式制造扫描仪。很久以前我也遇到过同样的问题。我将在这里留下我的解决方案,以及二维码的理想位置。我希望它有帮助。

请将代码粘贴到单独的屏幕上并查看结果。

最主要的是CameraX不会在整个画布上显示整个相机预览。默认预览视图设置是:

scaleType = PreviewView.ScaleType.FILL_CENTER

这有助于填充整个屏幕并剪掉剩余部分,从而使坐标的计算变得复杂。对我来说:

scaleType = PreviewView.ScaleType.FIT_START

扫描库:Google ML Kit

对不起翻译者

使用扫描仪预览相机的完整代码:

@Composable
fun CameraView(
    modifier: Modifier = Modifier,
) {
    val density = LocalDensity.current
    val lifecycleOwner = LocalLifecycleOwner.current

    var barcodes by remember { mutableStateOf<Pair<Size, List<Barcode>>?>(null) }

    Box(Modifier.height(with(density) { 1920f.toDp() })) {
        AndroidView(
            modifier = modifier
                .align(Alignment.TopStart)
                .drawWithContent {
                    drawContent()
                    barcodes?.second?.forEachIndexed { barcodeIndex, barcodes ->
                        barcodes.cornerPoints?.let {
                            val path = Path()
                            it.forEachIndexed { index, point ->
                                if (index == 0)
                                    path.moveTo(
                                        point.x.toFloat() * (size.width / 720f),
                                        point.y.toFloat() * (size.height / 1280f)
                                    )
                                else
                                    path.lineTo(
                                        point.x.toFloat() * (size.width / 720f),
                                        point.y.toFloat() * (size.height / 1280f)
                                    )
                            }
                            path.close()
                            drawPath(
                                path,
                                if (barcodeIndex == 0) Color.Red else Color.Blue,
                                style = Stroke(20f)
                            )
                        }
                    }
                }
                .fillMaxSize(),
            factory = { context ->
                val cameraExecutor = Executors.newSingleThreadExecutor()
                val barcodeScanner = BarcodeScanning.getClient(
                    BarcodeScannerOptions.Builder()
                        .build()
                ).also {
                    lifecycleOwner.lifecycle.addObserver(it)
                }

                val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
                val previewView = PreviewView(context)
                    .apply {
                        scaleType = PreviewView.ScaleType.FIT_START
                    }

                val imageAnalyzer = ImageAnalysis.Builder()
                    .setTargetResolution(Size(720, 1280))
                    .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
                    .build()
                    .also {
                        it.setAnalyzer(cameraExecutor) { imageProxy ->
                            imageProxy.image?.let { image ->
                                barcodeScanner.process(
                                    InputImage.fromMediaImage(
                                        image,
                                        imageProxy.imageInfo.rotationDegrees
                                    )
                                ).addOnSuccessListener { results ->
                                    barcodes = Size(image.width, image.height) to results
                                    result = results.firstOrNull()
                                }.addOnCompleteListener {
                                    imageProxy.close()
                                }
                            }
                        }
                    }

                cameraProviderFuture.addListener(
                    {
                        // Used to bind the lifecycle of cameras to the lifecycle owner
                        val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

                        // Preview
                        val preview = Preview.Builder()
                            .setTargetResolution(Size(1080, 1920))
                            .build()
                            .also {
                                it.setSurfaceProvider(previewView.surfaceProvider)
                            }

                        // Select back camera as a default
                        val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

                        try {
                            // Unbind use cases before rebinding
                            cameraProvider.unbindAll()
                            // Bind use cases to camera
                            cameraProvider.bindToLifecycle(
                                lifecycleOwner, cameraSelector, preview, imageAnalyzer
                            )

                        } catch (exc: Exception) {
                            Log.e(TAG, "Use case binding failed", exc)
                        }
                    }, ContextCompat.getMainExecutor(context)
                )

                previewView
            }
        )
        Column(
            Modifier
                .fillMaxWidth()
                .background(MaterialTheme.colorScheme.secondary.copy(0.2f))
                .align(Alignment.TopCenter),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            barcodes?.second?.forEach {
                Text(it.displayValue ?: "")
            }
        }
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.