2. 反差式对焦(CDAF)原理:基于图像对比度搜索峰值,爬山算法详解
反差式对焦,圈内人常叫它 CDAF(Contrast Detection Auto Focus)。说白了,它不关心你镜头里有没有相位检测点,也不管场景深度信息。它只做一件事——找图像最“锐”的那个位置。
怎么判断锐不锐?看对比度。图像越清晰,相邻像素之间的亮度差异就越大,对比度值就越高。反过来,画面一模糊,对比度就往下掉。CDAF 的核心逻辑就是:驱动镜头移动,同时实时计算图像对比度,直到找到对比度最大的那个点。
我早年做安防一体机的时候,第一次调 CDAF 算法,发现画面在某个位置来回震荡,死活定不下来。后来才明白,那是爬山算法的步长没设好。嗯,咱们先把原理讲透。
2.1 对比度评价函数:怎么量化“清晰度”?
要让机器判断画面清不清晰,得先有个数学指标。常用的对比度评价函数有好几种,我挑三个最典型的讲。
| 评价函数 | 计算方式 | 特点 | 我见过的坑 |
|---|---|---|---|
| Tenengrad 函数 | 基于 Sobel 算子,提取图像梯度 | 抗噪性好,单峰性强 | 计算量大,嵌入式平台要优化 |
| Laplacian 函数 | 二阶微分,对边缘敏感 | 响应快,峰值尖锐 | 对噪声极其敏感,容易误判 |
| 方差函数 | 计算像素灰度值的方差 | 实现简单,计算量小 | 平坦区域容易失效 |
我个人习惯用 Tenengrad 函数做主力。为什么?因为它在大多数场景下都能给出一个干净的单峰曲线。你想想看,如果评价函数本身就有多个峰值,爬山算法很容易爬到半山腰就以为自己到顶了。
关键认知:评价函数的选择直接决定了 CDAF 的成败。选错了函数,后面算法再精妙也白搭。
2.2 爬山算法:一步一步往“山顶”走
爬山算法(Hill Climbing)是 CDAF 最经典的搜索策略。它的思路特别直白:
- 先迈一步——驱动镜头向某个方向移动一小步。
- 算一下对比度——如果对比度变大了,说明方向对了,继续往这个方向走。
- 如果对比度变小了——说明走反了,掉头往回走。
- 重复以上步骤——直到对比度不再增加,甚至开始下降,就认为找到了峰值。
听起来很简单对吧?但实际工程里,问题全出在“一小步”上。
我曾经踩过的坑:步长设太大,镜头直接越过峰值,在峰值两侧来回振荡,永远定不下来。步长设太小,对焦过程慢得像蜗牛爬,用户早就没耐心了。
所以工业界通常用变步长策略:刚开始大步快跑,快速接近峰值区域;一旦检测到对比度变化趋势放缓,就切换成小步精细搜索。这就像你爬山,山脚可以大步跨,快到山顶就得一步一步挪。
2.3 爬山算法的核心流程
下面这张图是我自己画的 CDAF 爬山算法流程图,你看一眼就能明白整个逻辑。
这个流程里有一个关键判断:什么时候算“到顶了”?工程上常用的判据是:连续 N 次移动后,对比度变化量小于某个阈值。N 和阈值都需要根据具体镜头和场景来调。我一般先设 N=3,阈值取当前对比度值的 1%,然后根据实际效果微调。
2.4 爬山算法的三大痛点
做 CDAF 这么多年,我总结出三个绕不开的坑:
- 局部峰值陷阱——评价函数如果有多个波峰,算法可能停在次高峰,永远找不到真正的焦点。我遇到过最典型的情况是:镜头前面有根细铁丝,对比度在铁丝位置出现一个假峰,算法就傻乎乎地停在那了。
- 平坦区域失效——对着白墙或者天空,整个画面的对比度几乎不变,评价函数像一条平线。这时候爬山算法就像在沙漠里找方向,完全迷失。
- 速度与精度的博弈——步长大则速度快但精度差,步长小则精度高但速度慢。没有银弹,只能根据场景做权衡。
我的经验:对于局部峰值问题,可以在搜索前先做一次粗扫,记录对比度曲线的整体趋势,然后再用爬山算法做精细定位。这相当于先看地图再爬山,不容易走偏。
2.5 代码示例:一个简化的爬山算法实现
下面这个 Python 代码是我在原型验证阶段常用的框架。它不复杂,但把核心逻辑都体现出来了。
def hill_climbing_af(lens_driver, frame_grabber, eval_func, initial_step=50):
"""
爬山算法实现 CDAF 对焦
:param lens_driver: 镜头驱动接口
:param frame_grabber: 图像采集接口
:param eval_func: 对比度评价函数
:param initial_step: 初始步长(单位:镜头步进数)
"""
# 初始化
step = initial_step
direction = 1 # 1: 正向, -1: 反向
prev_contrast = eval_func(frame_grabber.get_frame())
# 记录连续下降次数,用于判断是否到顶
decline_count = 0
max_decline = 3
while step > 1: # 步长小于1时停止
# 移动镜头
lens_driver.move(direction * step)
current_contrast = eval_func(frame_grabber.get_frame())
# 对比度变化判断
if current_contrast > prev_contrast:
# 对比度上升,继续同方向
prev_contrast = current_contrast
decline_count = 0
else:
# 对比度下降,掉头并减小步长
direction *= -1
step = step // 2 # 步长减半
decline_count += 1
if decline_count >= max_decline:
# 连续下降多次,认为已到峰值附近
break
# 返回最终镜头位置
return lens_driver.get_position()
这段代码里,我用了 step // 2 来做步长衰减。实际项目中,步长衰减系数需要根据镜头行程和精度要求来调。有的镜头线性度好,可以用 step * 0.7;有的镜头有回程差,衰减系数就得设大一点。
2.6 工程落地建议
如果你正在做 CDAF 的工程实现,我给你三条建议:
- 先做评价函数选型测试——拿你的目标场景拍几组图,分别用 Tenengrad、Laplacian、方差算一遍,看哪个函数的单峰性最好。这一步花不了多少时间,但能省掉后面大量的调试痛苦。
- 步长策略要分层——粗搜阶段步长可以设到镜头全行程的 5%-10%,精搜阶段步长缩到 1% 以内。我习惯用三级步长:粗搜、中搜、精搜,每级步长差 3-5 倍。
- 加一个超时保护——万一算法陷入死循环或者场景太极端,不能让它无限跑下去。我一般设 2 秒超时,超时后直接返回当前最佳位置。
一句话总结:CDAF 的核心就是“评价函数 + 爬山搜索”。评价函数选对了,爬山算法调好了,剩下的就是工程细节的打磨。