如何使用 ML Kit 实现二维码扫描和人脸检测?
解读
在国内面试中,这道题考察的是候选人是否真正落地过“端侧 AI”能力,而非简单调用第三方库。面试官希望听到:
- 对 ML Kit 国内版(Firebase 中国版/华为 ML Kit)的选型差异与合规经验;
- 对 CameraX、ImageAnalysis 与 ML Kit Pipeline 的衔接细节;
- 对性能、隐私、权限、机型兼容性的闭环处理;
- 对“扫码”与“人脸”两条链路独立配置、互不抢占资源的架构设计。
一句话:既要“跑通”,又要“跑得稳”,还要“能上线”。
知识点
- 国内依赖坐标
- Firebase ML Kit 国内需使用「com.google.firebase:firebase-ml-vision」+ 国内代理仓库,或切换为「com.huawei.hms:ml-computer-vision」。
- 若出海双包,需 flavor 隔离,避免 GMS 与 HMS 同时打包导致体积膨胀。
- 权限与隐私
- 扫码:仅 CAMERA 权限即可;人脸:需额外声明 android.permission.CAMERA 并在 6.0+ 动态申请,Android 13 后需 android.permission.POST_NOTIFICATIONS 提示后台分析。
- 隐私政策必须单独列出「人脸信息」收集场景,否则华为应用市场会被驳回。
- 相机 Pipeline
- CameraX ImageAnalysis.setBackpressureStrategy(STRATEGY_KEEP_ONLY_LATEST) + YUV_420_888,平均帧率 30 fps,但 ML Kit 官方建议 10-15 fps 以平衡功耗。
- 扫码:推荐分辨率 1280×720,人脸:推荐 640×480,过高会触发 ML Kit 内部降采样,反而增加延迟。
- 生命周期绑定
- 使用 androidx.camera.lifecycle.ProcessCameraProvider.bindToLifecycle(),在 ViewModel 中持有 analyzer,避免旋转重建重复绑定。
- 人脸检测需在 onDestroy() 显式调用 detector.close(),否则高通 8 Gen 1 机型会出现 libc++ 层 FD 泄漏导致 crash。
- 扫码优化
- 开启 BarcodeScannerOptions.Builder.setBarcodeFormats(FORMAT_QR_CODE) 减少 CPU 分支;对反光/弯曲场景,前置调用 ImageProxy.setCropRect() 裁掉边缘干扰。
- 国内低端机(如红米 9A)扫码延迟 400 ms+,可降级到「华为统一扫码服务」,其内部使用 GPU 加速,延迟可压到 120 ms。
- 人脸优化
- 使用 FaceDetectorOptions.Builder.setPerformanceMode(FAST).setLandmarkMode(ALL).setContourMode(ALL).setClassificationMode(ALL) 组合,在 720p 下帧耗时 25 ms;若开启 TRACKING 模式,可复用 ID,减少抖动。
- 对戴口罩场景,ML Kit 国内版已集成口罩模型,但需手动打开 FaceDetectorOptions.Builder.setMinFaceSize(0.1f)。
- 结果回调线程
- ML Kit 默认回调在后台线程,若直接操作 UI,需 postValue() 到主线程;扫码结果若跳转到支付 Activity,建议先放入 WorkManager 做本地缓存,防止跳页瞬间旋转重建丢失结果。
- 包体积与模型
- 二维码模型已内置,不增量;人脸模型约 2.3 MB,使用动态加载 split=ml 后,aab 实际增加 0.9 MB。若 targetSdk 34,需声明 android:extractNativeLibs=true,否则 TFLite 模型解压失败。
- 合规与备案
- 人脸信息属于「个人敏感信息」,需走「国家网信办安全评估」+「第三方机构认证」双通道;若仅本地处理、不上传云端,可豁免评估,但必须在隐私政策中明确「不会上传」。
答案
以 CameraX + ML Kit(Firebase 国内通道)为例,给出可直接落地的最小闭环:
- 依赖与选项
// root build.gradle
allprojects {
repositories {
google()
maven { url 'https://firebase-mirror.google.cn' } // 国内镜像
}
}
// app build.gradle
implementation 'com.google.mlkit:barcode-scanning:17.2.0'
implementation 'com.google.mlkit:face-detection:16.1.5'
implementation "androidx.camera:camera-camera2:1.3.0"
implementation "androidx.camera:camera-lifecycle:1.3.0"
implementation "androidx.camera:camera-view:1.3.0"
- 权限
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera.any" />
- 扫码 Analyzer
class QrAnalyzer(private val onQr: (String) -> Unit) : ImageAnalysis.Analyzer {
private val scanner = BarcodeScanning.getClient(
BarcodeScannerOptions.Builder()
.setBarcodeFormats(Barcode.FORMAT_QR_CODE)
.build()
)
@SuppressLint("UnsafeOptInUsageError")
override fun analyze(imageProxy: ImageProxy) {
val mediaImage = imageProxy.image ?: return
val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
scanner.process(image)
.addOnSuccessListener { barcodes ->
barcodes.firstOrNull()?.rawValue?.let { onQr(it) }
}
.addOnCompleteListener { imageProxy.close() }
}
}
- 人脸 Analyzer
class FaceAnalyzer(private val onFace: (List<Face>) -> Unit) : ImageAnalysis.Analyzer {
private val detector = FaceDetection.getClient(
FaceDetectorOptions.Builder()
.setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_FAST)
.setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_ALL)
.setContourMode(FaceDetectorOptions.CONTOUR_MODE_ALL)
.setClassificationMode(FaceDetectorOptions.CLASSIFICATION_MODE_ALL)
.setMinFaceSize(0.1f)
.build()
)
@SuppressLint("UnsafeOptInUsageError")
override fun analyze(imageProxy: ImageProxy) {
val mediaImage = imageProxy.image ?: return
val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
detector.process(image)
.addOnSuccessListener { faces -> onFace(faces) }
.addOnCompleteListener { imageProxy.close() }
}
}
- 生命周期绑定
class ScanViewModel : ViewModel() {
val qrResult = MutableLiveData<String>()
val faceResult = MutableLiveData<List<Face>>()
fun startCamera(lifecycleOwner: LifecycleOwner, previewView: PreviewView) {
val cameraProviderFuture = ProcessCameraProvider.getInstance(previewView.context)
cameraProviderFuture.addListener({
val provider = cameraProviderFuture.get()
val preview = Preview.Builder().build().also { it.setSurfaceProvider(previewView.surfaceProvider) }
val qrAnalysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.setTargetResolution(Size(1280, 720))
.build()
.also { it.setAnalyzer(ContextCompat.getMainExecutor(previewView.context), QrAnalyzer { qrResult.value = it }) }
val faceAnalysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.setTargetResolution(Size(640, 480))
.build()
.also { it.setAnalyzer(ContextCompat.getMainExecutor(previewView.context), FaceAnalyzer { faceResult.value = it }) }
provider.unbindAll()
provider.bindToLifecycle(
lifecycleOwner,
CameraSelector.DEFAULT_BACK_CAMERA,
preview,
qrAnalysis,
faceAnalysis
)
}, ContextCompat.getMainExecutor(previewView.context))
}
}
- 释放资源
override fun onDestroy() {
super.onDestroy()
scanner.close()
detector.close()
}
- 合规声明 在隐私政策中增加段落: 「二维码扫描与人脸检测功能均在本地完成,我们不会收集、上传或存储任何基于相机画面的人脸生物信息。」
拓展思考
- 双端一体化:若同时出海与国内上架,可在 build.gradle 中通过 flavor 隔离 GMS 与 HMS,扫码使用统一接口层 IBarcodeScanner,人脸使用 IFaceDetector,运行时动态绑定,保证同一套 UI 层代码零改动。
- 性能极限:在折叠屏内屏 2200×2480 分辨率下,全尺寸帧直接送入 ML Kit 会导致 GPU 占用 35% 以上,可先用 RenderScript 做 2×2 下采样,再送入 detector,帧耗时从 42 ms 降到 18 ms,但 RenderScript 已废弃,需迁移到 Vulkan 或 GPUImage。
- 安全加固:人脸比对场景(如刷脸登录)不能仅靠 ML Kit 的 boundingBox,需额外提取 128 维 embedding,与 TEE 内存储的模板做余弦相似度;ML Kit 不提供 embedding,需切换为 TensorFlow Lite 自定义模型,并通过 Android Keystore 绑定 BiometricPrompt 的 cryptoObject,实现「零信任」端到端。
- 无摄像头场景:在车载 Android 系统(AAOS)中,摄像头可能被 CarService 独占,此时可降级为「扫码」使用 NFC 标签触发「快速配对」二维码,人脸改用 UWB 测距+座椅压力传感器融合,完全绕过相机,仍保留 ML Kit 的解析逻辑,体现架构可扩展性。