如何使用 ML Kit 实现二维码扫描和人脸检测?

解读

在国内面试中,这道题考察的是候选人是否真正落地过“端侧 AI”能力,而非简单调用第三方库。面试官希望听到:

  1. 对 ML Kit 国内版(Firebase 中国版/华为 ML Kit)的选型差异与合规经验;
  2. 对 CameraX、ImageAnalysis 与 ML Kit Pipeline 的衔接细节;
  3. 对性能、隐私、权限、机型兼容性的闭环处理;
  4. 对“扫码”与“人脸”两条链路独立配置、互不抢占资源的架构设计。

一句话:既要“跑通”,又要“跑得稳”,还要“能上线”。

知识点

  1. 国内依赖坐标
    • Firebase ML Kit 国内需使用「com.google.firebase:firebase-ml-vision」+ 国内代理仓库,或切换为「com.huawei.hms:ml-computer-vision」。
    • 若出海双包,需 flavor 隔离,避免 GMS 与 HMS 同时打包导致体积膨胀。
  2. 权限与隐私
    • 扫码:仅 CAMERA 权限即可;人脸:需额外声明 android.permission.CAMERA 并在 6.0+ 动态申请,Android 13 后需 android.permission.POST_NOTIFICATIONS 提示后台分析。
    • 隐私政策必须单独列出「人脸信息」收集场景,否则华为应用市场会被驳回。
  3. 相机 Pipeline
    • CameraX ImageAnalysis.setBackpressureStrategy(STRATEGY_KEEP_ONLY_LATEST) + YUV_420_888,平均帧率 30 fps,但 ML Kit 官方建议 10-15 fps 以平衡功耗。
    • 扫码:推荐分辨率 1280×720,人脸:推荐 640×480,过高会触发 ML Kit 内部降采样,反而增加延迟。
  4. 生命周期绑定
    • 使用 androidx.camera.lifecycle.ProcessCameraProvider.bindToLifecycle(),在 ViewModel 中持有 analyzer,避免旋转重建重复绑定。
    • 人脸检测需在 onDestroy() 显式调用 detector.close(),否则高通 8 Gen 1 机型会出现 libc++ 层 FD 泄漏导致 crash。
  5. 扫码优化
    • 开启 BarcodeScannerOptions.Builder.setBarcodeFormats(FORMAT_QR_CODE) 减少 CPU 分支;对反光/弯曲场景,前置调用 ImageProxy.setCropRect() 裁掉边缘干扰。
    • 国内低端机(如红米 9A)扫码延迟 400 ms+,可降级到「华为统一扫码服务」,其内部使用 GPU 加速,延迟可压到 120 ms。
  6. 人脸优化
    • 使用 FaceDetectorOptions.Builder.setPerformanceMode(FAST).setLandmarkMode(ALL).setContourMode(ALL).setClassificationMode(ALL) 组合,在 720p 下帧耗时 25 ms;若开启 TRACKING 模式,可复用 ID,减少抖动。
    • 对戴口罩场景,ML Kit 国内版已集成口罩模型,但需手动打开 FaceDetectorOptions.Builder.setMinFaceSize(0.1f)。
  7. 结果回调线程
    • ML Kit 默认回调在后台线程,若直接操作 UI,需 postValue() 到主线程;扫码结果若跳转到支付 Activity,建议先放入 WorkManager 做本地缓存,防止跳页瞬间旋转重建丢失结果。
  8. 包体积与模型
    • 二维码模型已内置,不增量;人脸模型约 2.3 MB,使用动态加载 split=ml 后,aab 实际增加 0.9 MB。若 targetSdk 34,需声明 android:extractNativeLibs=true,否则 TFLite 模型解压失败。
  9. 合规与备案
    • 人脸信息属于「个人敏感信息」,需走「国家网信办安全评估」+「第三方机构认证」双通道;若仅本地处理、不上传云端,可豁免评估,但必须在隐私政策中明确「不会上传」。

答案

以 CameraX + ML Kit(Firebase 国内通道)为例,给出可直接落地的最小闭环:

  1. 依赖与选项
// 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"
  1. 权限
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera.any" />
  1. 扫码 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() }
    }
}
  1. 人脸 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() }
    }
}
  1. 生命周期绑定
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))
    }
}
  1. 释放资源
override fun onDestroy() {
    super.onDestroy()
    scanner.close()
    detector.close()
}
  1. 合规声明 在隐私政策中增加段落: 「二维码扫描与人脸检测功能均在本地完成,我们不会收集、上传或存储任何基于相机画面的人脸生物信息。」

拓展思考

  1. 双端一体化:若同时出海与国内上架,可在 build.gradle 中通过 flavor 隔离 GMS 与 HMS,扫码使用统一接口层 IBarcodeScanner,人脸使用 IFaceDetector,运行时动态绑定,保证同一套 UI 层代码零改动。
  2. 性能极限:在折叠屏内屏 2200×2480 分辨率下,全尺寸帧直接送入 ML Kit 会导致 GPU 占用 35% 以上,可先用 RenderScript 做 2×2 下采样,再送入 detector,帧耗时从 42 ms 降到 18 ms,但 RenderScript 已废弃,需迁移到 Vulkan 或 GPUImage。
  3. 安全加固:人脸比对场景(如刷脸登录)不能仅靠 ML Kit 的 boundingBox,需额外提取 128 维 embedding,与 TEE 内存储的模板做余弦相似度;ML Kit 不提供 embedding,需切换为 TensorFlow Lite 自定义模型,并通过 Android Keystore 绑定 BiometricPrompt 的 cryptoObject,实现「零信任」端到端。
  4. 无摄像头场景:在车载 Android 系统(AAOS)中,摄像头可能被 CarService 独占,此时可降级为「扫码」使用 NFC 标签触发「快速配对」二维码,人脸改用 UWB 测距+座椅压力传感器融合,完全绕过相机,仍保留 ML Kit 的解析逻辑,体现架构可扩展性。