16、Camera 人脸检测:FaceDetectionMode配置、Face矩形与分数回调、人脸跟踪与优先对焦、人脸检测性能优化

人脸检测,说白了就是让相机知道镜头前有个人,并且知道他的脸在哪。这个功能在现在的手机里几乎成了标配——你自拍时自动美颜、拍人时自动对焦到脸上,背后都是这套机制在干活。

我个人习惯把Camera的人脸检测分成两个层面来理解:硬件层面软件层面。硬件层面是ISP(图像信号处理器)直接输出的检测结果,速度快但功能有限;软件层面则是通过算法库(比如Google的ML Kit或者厂商自研的算法)跑出来的,功能丰富但更耗电。Android Camera2框架把这两者都封装好了,我们只需要配置对应的模式就行。

FaceDetectionMode配置

在Camera2中,人脸检测的开关是通过CaptureRequest.STATISTICS_FACE_DETECT_MODE来控制的。这个参数有三个可选值:

模式 说明
OFF 0 关闭人脸检测
SIMPLE 1 简单模式,只返回人脸矩形和置信度
FULL 2 完整模式,额外返回人脸 landmarks(眼睛、嘴巴等)

配置代码其实很简单,就一行:

captureRequestBuilder.set(
    CaptureRequest.STATISTICS_FACE_DETECT_MODE,
    CameraMetadata.STATISTICS_FACE_DETECT_MODE_SIMPLE
);

嗯,这里要注意:不是所有设备都支持FULL模式。我在项目中遇到过一台低端平板,只支持SIMPLE模式,结果我一开始配了FULL,回调里死活收不到人脸数据。后来查了characteristics.get(CameraCharacteristics.STATISTICS_INFO_AVAILABLE_FACE_DETECT_MODES)才发现,它只支持OFF和SIMPLE。所以建议你先检查一下设备支持哪些模式,再去做配置。

Face矩形与分数回调

人脸检测的结果是通过CaptureResult回调回来的。关键字段有两个:

  • CaptureResult.STATISTICS_FACES:返回一个Face[]数组
  • 每个Face对象包含:getBounds()(矩形)、getScore()(置信度)、getId()(跟踪ID)

回调的典型写法是这样的:

cameraCaptureSession.setRepeatingRequest(request, new CameraCaptureSession.CaptureCallback() {
    @Override
    public void onCaptureCompleted(CameraCaptureSession session,
                                   CaptureRequest request,
                                   TotalCaptureResult result) {
        Face[] faces = result.get(CaptureResult.STATISTICS_FACES);
        if (faces != null) {
            for (Face face : faces) {
                Rect bounds = face.getBounds();
                int score = face.getScore();
                // 这里就可以拿到人脸的位置和置信度了
            }
        }
    }
}, backgroundHandler);

这里有个坑——坐标系的转换。Face返回的矩形是相对于传感器输出尺寸的,不是屏幕坐标。如果你要在预览画面上画框,需要做一次映射。我曾经在这个问题上折腾了半天,最后发现是忘了考虑旋转角度。建议你拿到getBounds()后,先根据SensorOrientation做一次旋转校正,再映射到预览View的尺寸上。

关键点:Face的置信度分数范围是1到100。低于50的通常可以忽略,因为那很可能是误检。我一般设一个阈值,低于60的直接过滤掉。

人脸跟踪与优先对焦

人脸跟踪,说白了就是给每张脸分配一个ID,这样你就能知道上一帧的张三和这一帧的张三是不是同一个人。Android的Face对象里有个getId()方法,返回的就是这个跟踪ID。

有了跟踪ID,我们就可以做「优先对焦」了。思路是这样的:

  1. 检测到人脸后,取出人脸矩形
  2. 计算矩形中心点作为对焦区域
  3. 设置CaptureRequest.CONTROL_AF_REGIONS为这个人脸区域
  4. 触发一次自动对焦

代码实现大致如下:

// 假设我们检测到了第一张脸
if (faces.length > 0) {
    Face face = faces[0];
    Rect faceRect = face.getBounds();
    
    // 将人脸矩形映射到对焦坐标系([-1000, 1000]范围)
    int left = (faceRect.left * 2000 / sensorWidth) - 1000;
    int right = (faceRect.right * 2000 / sensorWidth) - 1000;
    int top = (faceRect.top * 2000 / sensorHeight) - 1000;
    int bottom = (faceRect.bottom * 2000 / sensorHeight) - 1000;
    
    MeteringRectangle focusArea = new MeteringRectangle(
        left, top, right - left, bottom - top, 
        MeteringRectangle.METERING_WEIGHT_MAX
    );
    
    captureRequestBuilder.set(
        CaptureRequest.CONTROL_AF_REGIONS,
        new MeteringRectangle[]{focusArea}
    );
    captureRequestBuilder.set(
        CaptureRequest.CONTROL_AF_MODE,
        CaptureRequest.CONTROL_AF_MODE_AUTO
    );
    captureRequestBuilder.set(
        CaptureRequest.CONTROL_AF_TRIGGER,
        CaptureRequest.CONTROL_AF_TRIGGER_START
    );
}

小技巧:不要每一帧都触发对焦。我一般设置一个冷却时间——比如500ms内只触发一次对焦。否则对焦马达会来回抽动,画面看起来忽明忽暗,用户体验很差。

人脸检测性能优化

人脸检测虽然好用,但也是有代价的。尤其是FULL模式,要计算眼睛、鼻子、嘴巴的位置,CPU开销不小。我总结了几条优化经验:

  • 按需选择模式:如果只是用来辅助对焦,SIMPLE模式就够了。只有需要做美颜或者表情识别时,才开FULL模式。
  • 降低检测频率:不需要每一帧都检测。你可以每3帧检测一次,中间2帧直接用上一帧的结果做跟踪。这样能省不少电。
  • 限制检测数量STATISTICS_INFO_MAX_FACE_COUNT这个参数告诉你设备最多能检测多少张脸。我建议不要超过这个值,否则检测结果会不稳定。
  • 避免在主线程处理:人脸回调的数据处理一定要放到后台线程。我见过有人直接在onCaptureCompleted里做UI更新,结果掉帧掉得厉害。

警告:有些设备在开启人脸检测后,预览帧率会明显下降。这是因为ISP要分一部分算力去做检测。如果你发现帧率掉得厉害,可以考虑关闭人脸检测,或者切换到SIMPLE模式。

说到性能,我记得有一次做视频通话应用,开了人脸检测后,画面延迟从30ms飙到了80ms。后来排查发现是FULL模式在作怪——它不仅要检测人脸,还要计算每个landmark的位置。换成SIMPLE模式后,延迟降到了40ms,完全可接受。

最后,我想强调一点:人脸检测不是万能的。侧脸、戴眼镜、光线暗的情况下,检测率会明显下降。你想想看,如果用户侧对着镜头,鼻子和眼睛的轮廓都看不清,算法自然就懵了。所以做产品时,一定要给用户一个「检测失败」的兜底方案,比如退回到中心区域对焦。

人脸检测核心流程 配置FaceDetectionMode Capture回调获取Face[] 解析Face数据 获取getBounds()矩形 获取getScore()置信度 获取getId()跟踪ID 设置AF_REGIONS优先对焦 性能优化:按需选模式 / 降频率 / 限数量

这张图把整个流程串起来了。从配置模式开始,到回调拿到数据,再到解析矩形、分数、跟踪ID,最后落到优先对焦和性能优化上。你照着这个流程走,基本不会出大问题。

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