AF算法核心:对焦窗口选择与爬山策略

这一章,我们聊聊AF算法里最核心的两个东西——对焦窗口(ROI)怎么选,以及爬山算法到底怎么爬。这两个点,说白了就是AF的“眼睛”和“腿”。眼睛看不准,腿再快也没用;腿不会走,眼睛再亮也白搭。

我在高通的几个项目里,见过太多因为ROI选错导致对焦失败的案例。嗯,咱们一个一个来拆。

一、对焦窗口(ROI)选择策略

ROI,Region of Interest,就是告诉ISP:“你只算这一块区域的清晰度,别的地方别管。” 为什么要有ROI?你想想看,如果整帧图像都参与计算,背景的纹理、天空的渐变都会干扰对焦。尤其是拍人像时,背景虚化,背景的对比度反而可能比人脸高,算法就会往背景跑——这就是典型的“拉风箱”。

核心原则:ROI必须覆盖主体,同时排除干扰区域。

1.1 固定中心ROI

最简单粗暴的方式。取画面中心一块矩形区域,比如宽高各占画面的30%。

  • 优点:实现简单,计算量小。
  • 缺点:主体不在中心时,直接翻车。

我记得早期功能机时代,基本都是固定中心ROI。用户得先把主体挪到中间,半按快门锁定,再构图。现在谁还这么干?

1.2 多区域加权ROI

把画面分成3x3或5x5的网格,每个格子给一个权重。中心区域权重高,边缘权重低。这样即使主体稍微偏离中心,也能被覆盖到。

网格位置权重系数说明
中心1格0.4主体最可能的位置
中间环8格0.4次重要区域
边缘16格0.2背景干扰,权重压低

这种策略在预览场景下够用,但遇到人脸在边缘、或者主体很小的情况,还是不够。

1.3 人脸/物体检测ROI

这才是现代AF的主流做法。通过Face Detection或Object Detection拿到主体坐标,然后动态生成ROI。

我的经验:人脸ROI不要直接用人脸框的完整尺寸。我习惯把人脸框缩小20%-30%,只取眼睛到鼻子的区域。为什么?因为额头和下巴的纹理往往不够丰富,反而会拉低对比度曲线的信噪比。

具体做法:

  1. 从3A统计模块获取人脸坐标 (x, y, w, h)。
  2. 计算ROI中心:center_x = x + w/2, center_y = y + h/2。
  3. 计算ROI尺寸:roi_w = w * 0.7, roi_h = h * 0.7。
  4. 将ROI坐标写入ISP的统计寄存器。
// 伪代码:人脸ROI生成
void generateFaceROI(FaceRect face, ROI *roi) {
    roi->center_x = face.x + face.width / 2;
    roi->center_y = face.y + face.height / 2;
    roi->width    = face.width * 0.7;   // 缩窄20%
    roi->height   = face.height * 0.7;  // 缩窄20%
    // 限制ROI不超出画面边界
    clampROI(roi, sensor_width, sensor_height);
}

1.4 触控对焦ROI

用户点哪里,ROI就放哪里。这个逻辑简单,但有个坑——用户点的位置可能刚好是纯色区域(比如白墙),对比度为零。这时候算法必须做fallback。

避坑:我曾经遇到一个case,用户点了一块纯白墙壁,AF直接卡死。后来加了“ROI有效性检测”:如果当前ROI的对比度统计值低于阈值,自动扩展ROI范围,或者回退到中心加权ROI。

二、爬山算法(Hill Climbing)详解

爬山算法,名字很形象。你想象自己站在一个山谷里,蒙着眼,想走到最高点。你只能靠脚底的感觉——哪边地势高,就往哪边迈一步。AF的爬山算法就是这个逻辑:通过移动镜头,找到对比度最大的位置。

2.1 基本原理

AF的“地势”就是对比度值(Focus Value, FV)。镜头在不同位置,FV不同。理想情况下,FV曲线是一个单峰函数,峰值对应最清晰的位置。

算法步骤:

  1. 初始化:镜头从当前位置开始,设定步长。
  2. 采样:移动镜头,采集当前FV。
  3. 比较:如果FV上升,继续同方向移动;如果FV下降,反转方向并减小步长。
  4. 终止:当步长小于阈值,或者FV变化小于阈值,认为到达峰值。
关键指标:爬山算法的成败取决于三个参数——步长、采样频率、终止条件。

2.2 步长策略

步长太大,容易越过峰值(overshoot);步长太小,对焦太慢。我一般用自适应步长:

  • 粗搜索阶段:大步长(比如每次移动镜头行程的10%),快速找到峰值附近区域。
  • 精搜索阶段:小步长(比如每次移动1%-2%),精确锁定峰值。

高通平台里,步长通常映射到DAC Code或者镜头位置。举个例子:

// 自适应步长示例
if (fv_delta > threshold_high) {
    step_size = coarse_step;   // 大步前进
} else if (fv_delta > threshold_low) {
    step_size = fine_step;     // 小步逼近
} else {
    // 到达峰值,停止
    stop_search();
}

2.3 全局搜索 vs 局部搜索

这是面试常问的问题,也是实际项目中必须做的选择。

对比项全局搜索局部搜索
搜索范围整个镜头行程(macro到infinity)当前焦点附近的小范围
耗时长,通常200-500ms短,通常50-100ms
适用场景开机首次对焦、场景突变连续AF、追踪AF
风险可能被局部峰值欺骗可能丢失主体(跑出范围)

我个人的习惯是:开机或场景切换时,做一次全局搜索,找到全局峰值。之后进入局部搜索模式,只在峰值附近小范围跟踪。如果连续几次局部搜索都找不到峰值(FV持续下降),就触发一次全局搜索重新定位。

一个技巧:全局搜索时,不要均匀采样。我习惯在macro端和infinity端多采几个点,因为这两个位置的FV曲线往往更陡峭。中间区域可以稀疏采样,节省时间。

三、SVG:AF对焦策略与搜索流程

下面这张图,我把ROI选择、全局搜索、局部搜索、爬山算法的关系画了出来。你看一眼就能明白整个AF的决策链路。

AF对焦策略与搜索流程 ROI选择策略 固定中心 多区域加权 人脸检测 触控对焦 搜索策略选择 全局搜索 局部搜索 爬山算法(Hill Climbing) 采样FV值 比较FV变化 调整步长方向 判断终止条件 输出:最佳对焦位置(峰值) 说明:全局搜索用于初始化 局部搜索用于连续跟踪

四、实战中的坑与对策

讲完理论,说几个我在项目中踩过的坑。

4.1 局部峰值陷阱

爬山算法最大的敌人就是局部峰值。比如你拍一个隔着纱窗的物体,纱窗的纹理可能形成一个局部峰值,算法停在那里,真正的主体反而虚了。

对策:我习惯在爬山过程中记录“历史最高FV”和对应的镜头位置。如果最终停下的位置FV低于历史最高值的90%,就强制跳转到历史最高位置重新确认。

4.2 低对比度场景

晚上拍纯色墙壁,FV曲线几乎是平的。爬山算法会随机游走,找不到峰值。

对策:检测FV的峰均比(Peak-to-Average Ratio)。如果比值小于1.2,说明场景对比度太低,直接切换到固定焦距(比如hyperfocal distance),或者启用激光/ToF辅助对焦。

4.3 运动主体

如果主体在动,爬山算法还没爬到峰值,主体已经跑了。

对策:结合运动检测(Motion Detection)。检测到运动时,增大步长,减少采样次数,用“粗定位+预测”代替精确爬山。高通平台里有专门的Motion Vector统计模块,可以拿来用。

我曾经犯过的错:在一个项目中,我把爬山算法的终止条件设得太严格(FV变化小于0.5%才停止)。结果在弱光下,噪声导致FV抖动,算法永远停不下来。后来改成“连续3次FV变化小于阈值”才终止,问题解决。

五、小结

ROI选择和爬山算法,是AF的两条腿。ROI选对了,爬山才有意义;爬山算法稳健了,ROI才能发挥价值。实际项目中,没有一套策略能打天下。你需要根据场景、硬件、用户行为动态调整。

嗯,这一章的内容就到这里。记住:多测、多调、多踩坑,AF的功力就是这么练出来的。