简介
cameraX已经出来有一段时间了,现在已经从alpha版本到现在的beta3版本。其中内部的代码版本跨度特别大,而且资料相对来说只有官方的demo比较可以参考,所以最近完成了项目的开发之后,把经验分享一下提供给各位。
仓库地址
二维码扫描小优化
-
去除zxing额外支持的格式(有争议的点,其实并没有特别大的差距)
MultiFormatReader的decodeWithState()是使用方的入口方法,内部调用了decodeInternal(),输入是相机的一帧数据,如果抛了NotFoundException,则表示没找到二维码;如果返回了Result,则表示找到了二维码,并解析完成。
其中,readers变量是一个数组,数组的大小表示支持的条码格式个数,zxing原本因为支持很多格式,因此这个数组长度比较长。当拿到相机的一帧数据后,需要去检测是否是所有支持格式的某一个格式,每一种格式的检测都需要花费一些时间,因此这个遍历对于我们是不必要的。如果将zxing内部定制成只支持QR Code格式,那么就免去了额外的格式检测。 -
扫描区域放大到全局
去除项目中的扫描区域,将图像识别区域放大到整张区域,这样增加了二维码的边界情况,不需要特意的对准屏幕的扫描区域。
-
将相机升级到jetpack的CameraX
谷歌已经在官方提供了对于camera2的整合包,集成在CamreaX,而且CameraX内部有对于图片分析的接口,所以我们在这个接口中会对原来的二维码扫描进行一次转移,然后构建一个线程池专门去处理二维码扫描的分析器。
class CameraXModule(private val view: AutoZoomScanView) { private var lensFacing: Int = CameraSelector.LENS_FACING_BACK private var preview: Preview? = null private var imageAnalyzer: ImageAnalysis? = null private lateinit var cameraExecutor: ExecutorService private var camera: Camera? = null private lateinit var qrCodeAnalyzer: QRCodeAnalyzer private lateinit var mLifecycleOwner: LifecycleOwner fun bindWithCameraX(function: (Result) -> Unit, lifecycleOwner: LifecycleOwner) { mLifecycleOwner = lifecycleOwner val metrics = DisplayMetrics().also { view.display.getRealMetrics(it) } Log.d(TAG, "Screen metrics: ${metrics.widthPixels} x ${metrics.heightPixels}") val screenAspectRatio = aspectRatio(metrics.widthPixels, metrics.heightPixels) Log.i(TAG, "Preview aspect ratio: $screenAspectRatio") val cameraSelector = CameraSelector.Builder().requireLensFacing(lensFacing).build() val cameraProviderFuture = ProcessCameraProvider.getInstance(view.context) cameraProviderFuture.addListener( Runnable { val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get() // Preview val width = (view.measuredWidth * 1.5F).toInt() val height = (width * screenAspectRatio).toInt() preview = Preview.Builder() // We request aspect ratio but no resolution .setTargetResolution(Size(width, height)) // Set initial target rotation .build() preview?.setSurfaceProvider(view.preView.createSurfaceProvider(null)) cameraExecutor = Executors.newSingleThreadExecutor() qrCodeAnalyzer = QRCodeAnalyzer(this) { function(it) } // ImageAnalysis imageAnalyzer = ImageAnalysis.Builder() // We request aspect ratio but no resolution .setTargetResolution(Size(width, height)) // Set initial target rotation, we will have to call this again if rotation changes // during the lifecycle of this use case .build() // The analyzer can then be assigned to the instance .also { it.setAnalyzer(cameraExecutor, qrCodeAnalyzer) } // Must unbind the use-cases before rebinding them cameraProvider.unbindAll() try { // A variable number of use-cases can be passed here - // camera provides access to CameraControl & CameraInfo camera = cameraProvider.bindToLifecycle( mLifecycleOwner, cameraSelector, preview, imageAnalyzer ) qrCodeAnalyzer.camera = camera qrCodeAnalyzer.preview = preview setFocus(view.width.toFloat() / 2, view.height.toFloat() / 2) // camera?.cameraControl?.startFocusAndMetering(FocusMeteringAction.FLAG_AF) // Attach the viewfinder's surface provider to preview use case } catch (exc: Exception) { Log.e(TAG, "Use case binding failed", exc) } }, ContextCompat.getMainExecutor(view.context) ) } fun setFocus(x: Float, y: Float) { val factory: MeteringPointFactory = SurfaceOrientedMeteringPointFactory( view.width.toFloat(), view.height.toFloat() ) //create a point on the center of the view val autoFocusPoint = factory.createPoint(x, y) camera?.cameraControl?.startFocusAndMetering( FocusMeteringAction.Builder( autoFocusPoint, FocusMeteringAction.FLAG_AF ).apply { //auto-focus every 1 seconds setAutoCancelDuration(1, TimeUnit.SECONDS) }.build() ) } private fun aspectRatio(width: Int, height: Int): Double { val previewRatio = max(width, height).toDouble() / min(width, height) if (abs(previewRatio - RATIO_4_3_VALUE) <= abs(previewRatio - RATIO_16_9_VALUE)) { return RATIO_4_3_VALUE } return RATIO_16_9_VALUE } @SuppressLint("RestrictedApi") fun setZoomRatio(zoomRatio: Float) { if (zoomRatio > getMaxZoomRatio()) { return } val future: ListenableFuture<Void>? = camera?.cameraControl?.setZoomRatio( zoomRatio ) future?.apply { Futures.addCallback(future, object : FutureCallback<Void?> { override fun onSuccess(result: Void?) {} override fun onFailure(t: Throwable) {} }, CameraXExecutors.directExecutor()) } } fun getZoomRatio(): Float { return camera?.cameraInfo?.zoomState?.value?.zoomRatio ?: 0F } fun getMaxZoomRatio(): Float { return camera?.cameraInfo?.zoomState?.value?.maxZoomRatio ?: 0F } fun stopCamera() { // camera?.cameraControl?. } internal fun resetAnalyzer() { qrCodeAnalyzer.resetAnalyzer() } companion object { private const val TAG = "CameraXImp" private const val RATIO_4_3_VALUE = 4.0 / 3.0 private const val RATIO_16_9_VALUE = 16.0 / 9.0 } }上述代码基于的是CameraX内的CameraView,其中的构建的宽高必须基于4:3或者16:9的格式。
-
自动放大
当二维码很小很远时,自动放大能大大加快检测二维码的速度。QRCodeReader的decode()是二维码检测的主方法,分为两步:
(1)大致判断是否存在二维码;
val source = PlanarYUVLuminanceSource(data, width, height, 0, 0, width, height, false) val binarizer = HybridBinarizer(source) val bitmap = BinaryBitmap(binarizer) val detectorResult = Detector(bitmap.blackMatrix).detect(map)private fun calculateDistance(resultPoint: Array<ResultPoint>): Int { val point1X = resultPoint[0].x.toInt() val point1Y = resultPoint[0].y.toInt() val point2X = resultPoint[1].x.toInt() val point2Y = resultPoint[1].y.toInt() return sqrt( (point1X - point2X.toDouble()).pow(2.0) + (point1Y - point2Y.toDouble()).pow(2.0) ).toInt() }先要获取到当前区域内是否存在二维码,其次计算二维码的距离。
(2)所以我们需要做的就是先检测该图像区域内是否有一个二维码,同时计算二维码的大小,和图像比例进行一次大小换算,如果发现二维码过小的情况下,自动放大图片区域。
private fun zoomCamera(points: Array<ResultPoint>, image: BinaryBitmap): Boolean { val qrWidth = calculateDistance(points) * 2 val imageWidth = image.blackMatrix.width.toFloat() val zoomInfo = camera?.cameraInfo?.zoomState?.value zoomInfo?.apply { if (qrWidth < imageWidth / 8) { Log.i("BarcodeAnalyzer", "resolved!!! = $qrWidth imageWidth:${imageWidth}") val maxScale = zoomInfo.maxZoomRatio val curValue = zoomInfo.zoomRatio val gap = maxScale - curValue val upgradeRatio = if (gap / 4F * 3 > 3F) 3F else maxScale / 4F * 3 module.setZoomRatio(curValue + upgradeRatio) return true } } return false } -
双击放大
当前二维码扫描中没有调整焦距的功能,所以我们在这次调整中对其进行了一次双击放大的开发。
通过监控双击事件实现对应监听。
private val gestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() { override fun onDoubleTap(e: MotionEvent?): Boolean { cameraXModule.setZoomRatio(cameraXModule.getZoomRatio() + 1) return super.onDoubleTap(e) } override fun onSingleTapUp(e: MotionEvent?): Boolean { e?.apply { cameraXModule.setFocus(x, y) } return super.onSingleTapUp(e) } }) -
单击对焦
当前的对焦模式采取的是自动对焦,我们对对焦进行了一次增强,单击制定位置之后会对该区域进行一次对焦。
参考上面代码
简单使用
- 引入依赖
implementation 'com.github.leifzhang:QrCodeLibrary:0.0.1'
- 在布局xml中加入AutoZoomScanView
<com.kronos.camerax.qrcode.AutoZoomScanView
android:id="@+id/scanView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
- 先申请camera权限并绑定lifecycle
AndPermission.with(this)
.runtime()
.permission(Permission.Group.CAMERA)
.onGranted { permissions: List<String?>? ->
scanView.bindWithLifeCycle(this@MainActivity)
}
.onDenied { permissions: List<String?>? -> }
.start()
- 二维码结果回调,之后重新打开分析逻辑
scanView.setOnQrResultListener { view: View, s: String ->
Toast.makeText(
this@MainActivity, s,
Toast.LENGTH_LONG
).show()
scanView.reStart()
}