3. 对比度对焦(CDAF):爬山算法原理、窗口选择策略、评价函数设计

好,我们进入自动对焦的第二个核心流派——对比度对焦(CDAF)。

相位对焦靠硬件算差值,快是快,但精度有时候差口气。对比度对焦呢?它不依赖额外传感器,直接用图像本身的清晰度来做判断。说白了,就是找画面里「边缘最锐利」的那个镜头位置。

我当年做第一颗手机摄像头驱动时,用的就是纯CDAF。那时候相位像素还没普及,大家全靠爬山算法一点点蹭。嗯,这里面的坑,我踩过不少。

3.1 爬山算法:核心逻辑与实现

爬山算法的名字很形象。你想象一下,一个盲人站在一座山上,他只能靠脚底的感觉判断是在上坡还是下坡。他要走到山顶,就得一直往「上」的方向走。

CDAF的爬山算法就是这个逻辑:

  1. 迈一步:驱动镜头移动一个步长。
  2. 算一下:在当前帧计算清晰度评价值(Focus Value, FV)。
  3. 比一比:如果FV比上一帧大,说明方向对了,继续走。
  4. 拐个弯:如果FV比上一帧小,说明走过了,掉头往回走,步长减半。
  5. 停下来:当步长小于某个阈值,或者FV变化极小时,认为找到了峰值。

核心要点:爬山算法本质上是一个单峰极值搜索问题。它假设FV曲线是理想的山峰形状。但现实世界嘛……从来没那么理想过。

我给你们看一段简化的伪代码,这是我早期项目里提炼出来的骨架:

// 简化版爬山算法
int current_pos = INFINITY_POS; // 从远端开始
int step = LARGE_STEP;
int prev_fv = 0;
int direction = -1; // 1: 向近端, -1: 向远端

while (step > MIN_STEP) {
    // 1. 移动镜头
    move_lens(current_pos + direction * step);
    wait_for_frame();

    // 2. 计算当前清晰度
    int curr_fv = calculate_fv(current_frame);

    // 3. 判断方向
    if (curr_fv > prev_fv) {
        // 方向正确,继续前进
        current_pos += direction * step;
        prev_fv = curr_fv;
        // 步长不变,加速前进
    } else {
        // 方向错误,掉头并缩小步长
        direction *= -1;
        step /= 2;
        // 注意:这里不更新current_pos,只改变方向
        prev_fv = curr_fv; // 记录这个拐点的值
    }
}
// 对焦完成
set_lens_position(current_pos);

我曾经踩过的坑: 上面这个伪代码有个致命问题——它假设每次移动都能立即得到正确的FV。实际项目中,帧率延迟和VCM(音圈马达)响应延迟会导致FV和镜头位置不同步。我曾经因为这个原因,让镜头在峰值附近来回震荡了十几帧,画面一直在「呼吸」。后来我加了一个位置-帧号映射表,才解决了这个问题。

3.2 窗口选择策略:别让背景毁了你的对焦

爬山算法算的是整张图的清晰度吗?当然不是。你想想看,如果画面主体在中间,背景是蓝天,你把整张图的边缘都算进去,蓝天部分全是低频,FV值会被严重稀释。

所以,窗口选择是CDAF的命门。

我个人习惯把窗口策略分为三个层级:

策略层级 方法 适用场景 风险点
固定中央窗 取画面中心 1/4 ~ 1/9 区域 常规拍照,主体居中 主体偏离中心时直接跑焦
多区域加权 将画面分成 5x5 或 7x7 网格,中心区域权重高 人像模式,人脸检测辅助 权重设计不合理,边缘主体被忽略
动态跟踪窗 结合人脸/物体检测,窗口跟随主体移动 视频追焦,运动物体 检测算法失效时,窗口会飞掉

我的经验: 在动态跟踪窗里,我习惯做一个「信任度衰减」机制。如果检测算法连续3帧没有更新窗口位置,就逐渐把窗口权重移回中央区域。这样即使检测丢了,也不会瞬间跑焦到背景上。

3.3 评价函数设计:如何量化「清晰」?

爬山算法需要一个「标尺」来衡量图像清晰度。这个标尺就是评价函数。说白了,就是如何用数学公式描述「边缘锐利」。

我整理了几种常用的评价函数,你们感受一下:

函数名称 公式(简化) 特点 我踩过的坑
Tenengrad 对图像做Sobel梯度,求和 经典,抗噪性一般 在弱光下噪声会被当成边缘,FV曲线毛刺多
Laplacian 二阶微分,对噪声敏感 响应快,但容易误判 纹理丰富的区域(如草地)FV值极高,导致对焦在错误位置
Brenner 计算相邻像素差值的平方和 计算量小,硬件友好 对低频场景(如纯色墙壁)几乎无效
方差/熵 统计像素分布的离散程度 全局评价,不受边缘方向影响 对焦峰值不够尖锐,容易陷入局部极值

实际项目中,我很少只用单一函数。我通常的做法是:

  • 粗搜阶段:用Brenner或方差,计算快,步长大,快速定位到峰值附近。
  • 精搜阶段:切换到Tenengrad或Laplacian,步长缩小,精确找到峰值。

这种多级评价函数切换的策略,能兼顾速度和精度。我曾在某个项目中,只用单一Laplacian,结果在拍摄毛绒玩具时,因为纹理过于复杂,FV曲线出现了多个小峰值,爬山算法直接卡在了半山腰。后来改成粗搜用方差,精搜用Tenengrad,问题就解决了。

3.4 知识体系:CDAF核心流程

下面这张图,是我自己总结的CDAF完整流程。你们可以把它当作一个检查清单:

CDAF 对比度对焦核心流程 CDAF 对比度对焦核心流程 ① 图像采集 ② 窗口选择 固定窗 / 加权窗 / 动态窗 ③ 评价函数计算 Tenengrad / Laplacian / Brenner ④ 爬山决策 比较FV值,决定方向与步长 ⑤ 驱动控制 VCM / 步进电机 反馈循环(下一帧) 关键:步长策略(粗搜→精搜)、窗口稳定性、评价函数抗噪性,三者缺一不可

3.5 避坑指南:我这些年遇到的CDAF问题

最后,我分享几个实战中容易忽略的细节:

  • 场景切换时的FV突变:当你从暗光切换到亮光,或者画面内容剧烈变化时,FV值会跳变。爬山算法会误以为找到了新峰值。我的做法是:在检测到场景切换时,重置爬山状态,重新开始粗搜。
  • 微距与无穷远的步长差异:在微距端,景深极浅,FV曲线非常陡峭,步长必须很小。在无穷远端,景深大,步长可以大一些。我习惯根据镜头位置动态调整步长,而不是用固定值。
  • 温度对VCM的影响:这个坑我印象最深。冬天室外,VCM的响应速度会变慢,同样的PWM值,镜头移动的距离会偏小。如果不做温度补偿,爬山算法在低温下会「爬不动」。后来我在驱动层加了一个温度-增益查找表,才解决了这个问题。

总结一下: CDAF的核心不在于算法有多花哨,而在于工程细节的打磨。窗口选对了,评价函数用对了,步长策略调好了,哪怕是最简单的爬山算法,也能跑出不错的对焦体验。反之,再好的算法,在噪声和延迟面前也是白搭。