Administrator
发布于 2026-08-25 / 39 阅读
0
0

系列文章《Android 工程师的 100 篇进阶笔记》第 80 篇:CameraX AI:摄像头与机器学习的深度集成

前言

ML Kit 那一篇讲了开箱即用的能力,这一篇聊更专业的:CameraX 与自训练视觉模型的集成。从一个"智能裁剪"案例出发,覆盖 CameraX 高级配置、ONNX Runtime / TFLite 推理、性能与功耗平衡。

一、CameraX vs Camera2:为什么选 CameraX

CameraX 把 Camera2 的复杂度藏到 Lifecycle 后面:

维度

Camera2

CameraX

API 复杂度

高(Surface、Session、CaptureRequest)

低(PreviewView + UseCase)

Lifecycle 集成

手写

自动

兼容性

厂商碎片化严重

抹平差异

厂商扩展

极少

CameraX Extensions(夜景、HDR)

性能开销

直接控制

轻微包装损失(< 5%)

工程上 95% 的场景选 CameraX 即可。

二、CameraX + 视觉模型的整体链路

Surface ──▶ PreviewView (预览)
              │
              ▼
ImageAnalysis ──▶ 抽帧 ──▶ 预处理 (resize/normalize) ──▶ 模型推理 ──▶ 后处理
              │                                                          │
              ▼                                                          ▼
          ImageCapture (拍照)                                     业务回调 / UI

其中"模型推理"可以用 MediaPipe、ML Kit、ONNX Runtime 或 TFLite。

三、一个完整示例:实时智能裁剪

需求:拍照时,根据画面主体(人脸或物体)自动裁剪成 1:1 头像。

3.1 添加依赖

// build.gradle.kts
implementation("androidx.camera:camera-camera2:1.3.4")
implementation("androidx.camera:camera-lifecycle:1.3.4")
implementation("androidx.camera:camera-view:1.3.4")
implementation("com.google.mlkit:face-detection:16.1.7")
implementation("ai.onnxruntime:onnxruntime-android:1.18.0")

3.2 CameraX 初始化

class SmartCameraActivity : AppCompatActivity() {
    private val analyzer by lazy { FaceAnalyzer() }
    private lateinit var previewView: PreviewView
    private lateinit var cropOverlay: CropOverlayView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_smart_camera)
        previewView = findViewById(R.id.preview)
        cropOverlay = findViewById(R.id.crop_overlay)

        val providerFuture = ProcessCameraProvider.getInstance(this)
        providerFuture.addListener({
            val provider = providerFuture.get()
            bindUseCases(provider)
        }, ContextCompat.getMainExecutor(this))
    }

    private fun bindUseCases(provider: ProcessCameraProvider) {
        val preview = Preview.Builder().build().also {
            it.setSurfaceProvider(previewView.surfaceProvider)
        }

        val analysis = ImageAnalysis.Builder()
            .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
            .setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888)
            .build().apply {
                setAnalyzer(Executors.newSingleThreadExecutor()) { proxy ->
                    analyzer.detect(proxy) { faces ->
                        runOnUiThread { cropOverlay.updateFaces(faces) }
                        proxy.close()
                    }
                }
            }

        provider.bindToLifecycle(this, CameraSelector.DEFAULT_FRONT_CAMERA, preview, analysis)
    }
}

3.3 人脸检测 Analyzer

class FaceAnalyzer {
    private val detector = FaceDetection.getClient(
        FaceDetectorOptions.Builder()
            .setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_FAST)
            .setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_NONE)
            .setContourMode(FaceDetectorOptions.CONTOUR_MODE_NONE)
            .build()
    )

    fun detect(imageProxy: ImageProxy, onResult: (List<RectF>) -> Unit) {
        val bitmap = imageProxy.toBitmap()
        val image = InputImage.fromBitmap(bitmap, imageProxy.imageInfo.rotationDegrees)
        detector.process(image)
            .addOnSuccessListener { faces ->
                val rects = faces.map {
                    val b = it.boundingBox
                    // 把像素 rect 映射到预览视图坐标
                    RectF(
                        b.exactCenterX() - b.width() / 2f,
                        b.exactCenterY() - b.height() / 2f,
                        b.exactCenterX() + b.width() / 2f,
                        b.exactCenterY() + b.height() / 2f
                    )
                }
                onResult(rects)
            }
            .addOnFailureListener { onResult(emptyList()) }
    }
}

3.4 智能裁剪的"主区域 + 留白"逻辑

fun calculateCropRect(faces: List<RectF>, previewSize: Size): RectF {
    if (faces.isEmpty()) {
        // 默认居中
        val cx = previewSize.width / 2f
        val cy = previewSize.height / 2f
        val half = min(previewSize.width, previewSize.height) / 2f
        return RectF(cx - half, cy - half, cx + half, cy + half)
    }

    // 找到最大人脸
    val main = faces.maxBy { it.width() * it.height() }
    val cx = main.centerX()
    val cy = main.centerY()
    val half = maxOf(main.width(), main.height()) * 1.4f

    // 限制在预览范围内
    val left = (cx - half).coerceAtLeast(0f)
    val right = (cx + half).coerceAtMost(previewSize.width.toFloat())
    val top = (cy - half).coerceAtLeast(0f)
    val bottom = (cy + half).coerceAtMost(previewSize.height.toFloat())

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

实际拍照时,按 previewView 的裁剪坐标 + ImageCapture 的 takePicture 回调用 Bitmap 替换。

四、低光场景:用 ONNX 自训练模型

通用 ML Kit 人脸检测在低光下会丢失。专业 App 会接入自训练模型。

4.1 模型转换(Python 端)

# export_yolov8_face.py
from ultralytics import YOLO
m = YOLO("yolov8n-face.pt")
m.export(format="onnx", imgsz=320, opset=12)

部署到 Android 的 .onnx 文件约 6MB。

4.2 Android 端推理

class FaceNetDetector(context: Context) {
    private val env = OrtEnvironment.getEnvironment()
    private val session = env.createSession(
        File(context.filesDir, "yolov8n-face.onnx").absolutePath,
        OrtSession.SessionOptions().apply {
            addCPU(OrtEpProvider.CPU)
            setIntraOpNumThreads(2)
        }
    )

    fun detect(bitmap: Bitmap): List<RectF> {
        val resized = Bitmap.createScaledBitmap(bitmap, 320, 320, false)
        val buf = ByteBuffer.allocateDirect(320 * 320 * 3 * 4)
            .order(ByteOrder.nativeOrder())
        val pixels = IntArray(320 * 320)
        resized.getPixels(pixels, 0, 320, 0, 0, 320, 320)
        pixels.forEach { px ->
            val r = ((px shr 16) and 0xFF) / 255f
            val g = ((px shr 8) and 0xFF) / 255f
            val b = (px and 0xFF) / 255f
            buf.putFloat(r)
            buf.putFloat(g)
            buf.putFloat(b)
        }
        buf.rewind()

        val shape = longArrayOf(1, 3, 320, 320)
        val tensor = OnnxTensor.createTensor(env, buf, shape)
        val output = session.run(mapOf("images" to tensor))
        // 后处理:解析 YOLO 输出,NMS
        return postProcess(output)
    }
}

4.3 性能调优

模型

大小

320x320 推理

备注

YOLOv8n-face

6MB

80-150ms

推荐

YOLOv8s-face

22MB

180-280ms

精度更高

BlazeFace

4MB

30-60ms

比 YOLO 快 3 倍

推理设备加速:

  • 高通 8 Gen 3 用 QNN EP(QNN Execution Provider)

  • 联发科用 Neuron EP

  • 苹果端用 Core ML EP(iOS)

各厂商 SDK 都在赶,2026 年下半年会看到更多端侧 NPU 跑 YOLO。

五、性能与功耗:踩过的坑

5.1 主线程爆炸

// 错误:直接传 ImageProxy 到模型
setAnalyzer(executor) { proxy ->
    val bitmap = proxy.toBitmap()  // CPU 解码
    detector.detect(bitmap)         // 模型推理
    // 上面的代码大概率卡在这
}

修法:限流 + 异步 + OutputImageFormat。

5.2 后台功耗

持续分析是耗电大头。建议:

class SmartCameraController {
    fun pauseAnalysis() {
        cameraProvider.unbind(imageAnalysis)
    }
    fun resumeAnalysis() {
        cameraProvider.bindToLifecycle(this, selector, preview, imageAnalysis)
    }
}

进入后台或不可见时停止分析。

5.3 内存上限

高分辨率分析需要更多 RAM,可以限制:

ImageAnalysis.Builder()
    .setTargetResolution(Size(720, 1280))
    .build()

六、商业落地的实际建议

  1. 离线 First:把模型打 APK 一起发布,放弃"动态下载"。商业 App 不希望 OTA 才能用

  2. 降级策略:模型推理失败或耗时过长,回退到 ML Kit 或默认策略

  3. 缓存命中:如果用户短时间内连续拍照,模型推理结果可以缓存 1-2 秒

  4. GPU/NPU 检查:在低端机型用 CPU 推理,旗舰机型用 NPU 加速

  5. 隐私:人脸数据高度敏感,建议明确告知用户

七、CameraX Extensions:超广角、夜景

CameraX Extensions 是 Google 给厂商留的扩展入口——夜景模式、HDR、超广角、人像虚化。

implementation("androidx.camera:camera-extensions:1.3.4")
val nightEffect = ImageCapture.Builder()
    .setCameraSelector(CameraSelector.DEFAULT_BACK_CAMERA)
    .setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
    .build()

val ext = ExtensionsManager.getExtensionsAsync(context).get()
if (ext.isExtensionAvailable(CameraSelector.DEFAULT_BACK_CAMERA, ExtensionCharacteristic.Night)) {
    val nightSelector = ext.getExtensionEnabledCameraSelector(
        CameraSelector.DEFAULT_BACK_CAMERA, ExtensionCharacteristic.Night
    )
    cameraProvider.bindToLifecycle(this, nightSelector, nightEffect)
}

配合 AI 推理,能做出"夜间拍照增强 + 智能补光"的效果。

小结

CameraX + AI 不是简单把模型堆进去,而是全链路调优:抽帧策略、预处理、模型选择、后处理、缓存、省电策略。哪个环节掉链子都会影响最终体验。建议先用一个简单版本(ML Kit 人脸检测)跑通,再升级到自训练模型。

下一篇聊 Android XR 与扩展现实——手机形态之外的新互动方式。

参考阅读

  • CameraX 官方文档

  • ONNX Runtime Mobile Android

  • YOLOv8 模型转换教程


评论