一、Camera与AI:从“拍照”到“看懂世界”

各位同学,今天我们来聊聊Camera和AI的结合。说实话,这个话题我期待很久了。

你想想看,传统的相机系统,说白了就是个“光学+传感器+ISP”的流水线。它能把光信号变成数字图像,但图像里有什么?它不知道。一个人站在镜头前,它只知道像素值,不知道那是个人。

但AI加入后,情况完全变了。相机开始“看懂”画面了。它能认出人脸、检测物体、识别场景,甚至能帮你把照片修得更好看。这不是魔法,这是Camera2 + MLKit的功劳。

我在项目中遇到过不少团队,把AI和相机当成两个独立模块来开发。结果呢?延迟高、功耗大、体验差。其实,AI和相机应该是一体的——从Pipeline层面就融合在一起。

1.1 为什么是Camera2 + MLKit?

Android上做相机AI,有两个主流选择:CameraX和Camera2。我个人习惯用Camera2,为什么?

  • 更底层的控制:Camera2让你直接操作CaptureRequest、CaptureSession,能精确控制每一帧的流向
  • 更低的延迟:跳过CameraX的抽象层,直接把ImageReader的YUV_420_888数据喂给MLKit
  • 更灵活的Pipeline:你可以决定“先做检测再做美颜”,还是“先美颜再检测”

MLKit这边,Google已经封装好了各种模型:物体检测、人脸识别、文字识别、场景分类……你不需要自己训练模型,直接调用API就行。但要注意——MLKit的模型是跑在设备端的,不是云端。这意味着你的App可以离线工作,而且数据不会上传,隐私安全有保障。

核心思路:Camera2负责“取帧”,MLKit负责“理解帧”,两者通过ImageReader的OnImageAvailableListener桥接。

1.2 实时物体检测:让相机“看见”物体

实时物体检测,是AI相机最基础也最常用的功能。它的流程其实不复杂:

  1. Camera2每帧回调,拿到YUV_420_888格式的Image
  2. 把Image转换成MLKit的InputImage
  3. 调用MLKit的ObjectDetection API,得到检测结果
  4. 把结果(BoundingBox、标签、置信度)绘制到Preview上

嗯,这里要注意一个坑。我曾经在项目里直接在主线程调用MLKit的detect方法,结果卡得不行。后来才发现,MLKit的detect是同步的,必须在后台线程执行。正确的做法是:

// 错误示范:主线程调用
val result = detector.process(image).await()  // 卡死!

// 正确做法:在后台线程处理
imageReader.setOnImageAvailableListener({ reader ->
    val image = reader.acquireLatestImage() ?: return
    executor.execute {
        val inputImage = InputImage.fromMediaImage(image, rotation)
        detector.process(inputImage)
            .addOnSuccessListener { results ->
                // 在主线程更新UI
                runOnUiThread { drawResults(results) }
            }
            .addOnFailureListener { e -> Log.e(TAG, "检测失败", e) }
            .addOnCompleteListener { image.close() }
    }
}, backgroundHandler)

你看,关键就两点:异步处理及时释放Image。Image不释放,Camera2的Buffer池会耗尽,画面就会卡住。我见过不少新手踩这个坑。

1.3 人脸识别:不只是“框出人脸”

人脸识别比物体检测复杂一些。MLKit的人脸检测API不仅能检测人脸位置,还能提取人脸轮廓、眼睛位置、嘴巴状态,甚至能判断是否微笑、是否闭眼。

我在做一个考勤App时,需要检测用户是否“正视摄像头”。一开始我直接用FaceDetector的getHeadEulerAngleZ()来判断头部偏转角度,但发现角度阈值不好设——有的人天生歪头。后来我改用“双眼位置+鼻尖位置”的相对关系来判断,准确率提升了不少。

这里给个参考配置:

val options = FaceDetectorOptions.Builder()
    .setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_FAST)  // 实时场景用FAST
    .setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_ALL)         // 提取所有特征点
    .setClassificationMode(FaceDetectorOptions.CLASSIFICATION_MODE_ALL) // 开启表情分类
    .setMinFaceSize(0.15f)  // 最小人脸占比,太小了检测不准
    .build()

val detector = FaceDetection.getClient(options)

小技巧:实时人脸识别场景,建议把minFaceSize设到0.15~0.2之间。太小了容易误检,太大了远处的人脸检测不到。这个值我是在真机上反复调出来的。

1.4 场景识别:相机自动“看懂”你在拍什么

场景识别,说白了就是让相机知道你现在在拍什么——是风景、人像、食物,还是夜景?知道了场景,相机就能自动调整参数:拍风景时提高锐度,拍食物时增强饱和度,拍夜景时拉高ISO。

MLKit没有直接提供场景识别API,但我们可以用Image Labeling来实现。它会给图像打标签,比如“山”、“海滩”、“食物”、“室内”等。根据标签的置信度,我们可以推断场景类型。

举个例子:

val labeler = ImageLabeling.getClient(ImageLabelerOptions.DEFAULT_OPTIONS)

labeler.process(image)
    .addOnSuccessListener { labels ->
        for (label in labels) {
            when (label.text) {
                "Food" -> applyFoodMode()      // 食物模式:暖色调、高饱和度
                "Landscape" -> applyLandscapeMode() // 风景模式:高锐度、广角校正
                "Night" -> applyNightMode()    // 夜景模式:多帧合成、降噪
                "Portrait" -> applyPortraitMode() // 人像模式:背景虚化、肤色优化
            }
        }
    }

不过说实话,场景识别在工程上有个难点:场景切换的平滑性。你想想看,用户从拍风景切换到拍食物,如果相机参数突然大变,画面会闪一下,体验很差。我当时的做法是:用滑动窗口做场景判定,连续3帧都是“食物”才切换模式,切换时参数渐变,而不是突变。

1.5 AI美颜:从“磨皮”到“微整形”

AI美颜是用户感知最强的功能。传统的磨皮算法,就是高斯模糊+边缘保留,效果生硬。AI美颜不一样,它能识别出人脸的关键区域,然后针对性地处理:

  • 皮肤区域:只磨皮,不模糊眼睛、眉毛、嘴唇
  • 眼睛区域:增强瞳孔对比度,让眼睛更有神
  • 嘴唇区域:提升饱和度,让唇色更自然
  • 脸型轮廓:通过人脸关键点做液化,瘦脸、大眼

MLKit本身不提供美颜功能,但我们可以结合OpenCV或自研的Shader来实现。我常用的方案是:

  1. 用MLKit的人脸检测拿到106个关键点
  2. 根据关键点生成皮肤Mask(排除眼睛、眉毛、嘴唇)
  3. 在GPU上做双边滤波或引导滤波,只处理Mask区域
  4. 用关键点做三角剖分,实现瘦脸/大眼等液化效果

注意:美颜效果一定要可调节!我见过有些App把美颜开到最大,结果人脸像塑料一样。用户需要的是“自然美”,不是“假面”。建议提供0-100的滑条,让用户自己控制强度。

1.6 整体架构:一张图看懂

说了这么多,我们来画一张架构图,把Camera2 + MLKit的完整Pipeline展示出来:

Camera2 + MLKit 实时AI处理Pipeline Camera2 CaptureSession ImageReader YUV_420_888 MLKit InputImage 结果 回调 物体检测 ObjectDetection 人脸识别 FaceDetection 场景识别 ImageLabeling AI美颜 自定义 输出:绘制BoundingBox + 更新UI + 调整相机参数 SurfaceView / TextureView / Camera参数回调 ⚡ 性能关键:后台线程处理MLKit、及时释放Image、GPU加速渲染 避免在主线程做任何AI计算,否则掉帧严重

这张图把整个流程串起来了。你看,从Camera2取帧,到ImageReader拿到YUV数据,再到MLKit做各种检测,最后输出结果并更新UI——每一步都有明确的职责。

1.7 避坑指南:我踩过的那些坑

最后,分享几个我在实际项目中踩过的坑,希望能帮你少走弯路:

  • 坑一:Image不释放导致Camera卡死。我曾经调试了一整天,发现Camera预览突然停了,最后定位到是ImageReader的Image没有close()。记住:acquireLatestImage()拿到的Image,用完必须close(),否则Camera2的Buffer池会耗尽。
  • 坑二:MLKit检测结果和预览画面方向不一致。这是因为Camera2的Sensor方向和屏幕方向可能不同。解决办法:在创建InputImage时传入正确的rotation值,用InputImage.fromMediaImage(image, rotation)
  • 坑三:美颜效果在低端机上卡顿。AI美颜涉及人脸关键点检测+图像处理,计算量不小。我的建议是:在低端机上降低检测帧率(比如每3帧检测一次),或者降低美颜算法的精度(用快速双边滤波代替引导滤波)。

我的习惯:在开发阶段,我会在界面上显示当前帧率和MLKit处理耗时。这样能直观地看到性能瓶颈在哪里。推荐用Choreographer来统计帧率,用System.nanoTime()来统计MLKit耗时。

好了,这一章的内容就到这里。Camera2 + MLKit的组合,说白了就是让相机从“记录画面”升级到“理解画面”。下一章我们会深入Camera2的底层原理,看看它到底是怎么工作的。


公众号:蓝海资料掘金营,微信deep3321