3. 对比度对焦(CDAF):爬山算法原理、窗口选择策略、评价函数设计
好,我们进入自动对焦的第二个核心流派——对比度对焦(CDAF)。
相位对焦靠硬件算差值,快是快,但精度有时候差口气。对比度对焦呢?它不依赖额外传感器,直接用图像本身的清晰度来做判断。说白了,就是找画面里「边缘最锐利」的那个镜头位置。
我当年做第一颗手机摄像头驱动时,用的就是纯CDAF。那时候相位像素还没普及,大家全靠爬山算法一点点蹭。嗯,这里面的坑,我踩过不少。
3.1 爬山算法:核心逻辑与实现
爬山算法的名字很形象。你想象一下,一个盲人站在一座山上,他只能靠脚底的感觉判断是在上坡还是下坡。他要走到山顶,就得一直往「上」的方向走。
CDAF的爬山算法就是这个逻辑:
- 迈一步:驱动镜头移动一个步长。
- 算一下:在当前帧计算清晰度评价值(Focus Value, FV)。
- 比一比:如果FV比上一帧大,说明方向对了,继续走。
- 拐个弯:如果FV比上一帧小,说明走过了,掉头往回走,步长减半。
- 停下来:当步长小于某个阈值,或者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完整流程。你们可以把它当作一个检查清单:
3.5 避坑指南:我这些年遇到的CDAF问题
最后,我分享几个实战中容易忽略的细节:
- 场景切换时的FV突变:当你从暗光切换到亮光,或者画面内容剧烈变化时,FV值会跳变。爬山算法会误以为找到了新峰值。我的做法是:在检测到场景切换时,重置爬山状态,重新开始粗搜。
- 微距与无穷远的步长差异:在微距端,景深极浅,FV曲线非常陡峭,步长必须很小。在无穷远端,景深大,步长可以大一些。我习惯根据镜头位置动态调整步长,而不是用固定值。
- 温度对VCM的影响:这个坑我印象最深。冬天室外,VCM的响应速度会变慢,同样的PWM值,镜头移动的距离会偏小。如果不做温度补偿,爬山算法在低温下会「爬不动」。后来我在驱动层加了一个温度-增益查找表,才解决了这个问题。
总结一下: CDAF的核心不在于算法有多花哨,而在于工程细节的打磨。窗口选对了,评价函数用对了,步长策略调好了,哪怕是最简单的爬山算法,也能跑出不错的对焦体验。反之,再好的算法,在噪声和延迟面前也是白搭。