1、反差对焦基础:对比度检测原理、焦平面搜索算法、爬山算法详解
各位好,我是老周。做嵌入式影像系统调试有些年头了。今天咱们聊聊反差对焦,也就是CDAF(Contrast Detection Auto Focus)。
很多人觉得反差对焦就是“来回找最清晰的位置”,这话没错。但真要把它调快、调准,里面的门道可不少。我刚开始做这行时,也踩过不少坑。
1.1 对比度检测原理
说白了,反差对焦的核心就一句话:图像最清晰的时候,就是对比度最大的时候。
那怎么衡量对比度呢?我们一般用图像梯度来算。梯度越大,边缘越锐利,图像越清晰。
核心公式(简化版):
对比度值 = Σ( |I(x,y) - I(x+1,y)| + |I(x,y) - I(x,y+1)| )
说白了,就是计算每个像素和它右边、下边像素的差值,然后全部加起来。
我习惯把这个值叫做FV(Focus Value,对焦值)。FV越高,说明画面越清晰。
举个例子,一张纯白图片,所有像素值都是255,那FV就是0——完全没对比度。一张黑白相间的棋盘格,FV就很高。
避坑指南:
我曾经遇到过一个问题:在低光照环境下,图像噪声很大。噪声也会产生“伪梯度”,导致FV虚高。结果镜头停在了一个噪声大但实际模糊的位置。
后来我加了低通滤波,先降噪再算梯度,效果好了很多。
常用的对比度评价函数有几种,我列个表,大家对比着看:
| 评价函数 | 计算方式 | 特点 | 适用场景 |
|---|---|---|---|
| Tenengrad | Sobel算子求梯度 | 抗噪性好,计算量适中 | 通用场景,我最常用 |
| Laplacian | 二阶微分 | 对边缘敏感,易受噪声影响 | 高光照、纹理丰富的场景 |
| Brenner | 相邻像素差值的平方 | 计算量小,实现简单 | 资源受限的嵌入式平台 |
| 方差法 | 像素灰度值的方差 | 对整体模糊敏感 | 均匀场景,纹理较少时 |
你想想看,选哪个函数,其实取决于你的硬件算力和场景需求。我个人习惯在调试初期先用Tenengrad,稳定后再根据性能瓶颈做替换。
1.2 焦平面搜索算法
有了FV,下一步就是找到FV最大的那个镜头位置。这个过程,就是焦平面搜索。
搜索算法有很多种,但核心思路都一样:在镜头行程范围内,采样若干个位置,计算FV,然后找到峰值。
常见的搜索策略有这几种:
- 全局搜索:从近端到远端,步进式采样。简单粗暴,但慢。
- 二分搜索:先大步粗搜,再小步细搜。效率高一些。
- 自适应步长搜索:根据FV变化趋势动态调整步长。这是工程上最常用的。
注意:
全局搜索虽然慢,但在某些场景下不得不做。比如刚上电时,镜头位置未知,必须从头扫一遍。
我曾经在一个项目里,因为偷懒用了二分搜索,结果在微距场景下反复找不到焦点。后来发现是初始步长设太大了,跳过了峰值。
下面这张图,是我自己画的一个搜索流程示意,大家可以看看:
1.3 爬山算法详解
爬山算法,是反差对焦里最经典的搜索策略。它模拟人爬山的过程:往FV增大的方向走,直到找不到更高的地方。
算法步骤很简单:
- 从当前位置出发,向一个方向移动一步。
- 计算新位置的FV。
- 如果FV变大,继续往这个方向走。
- 如果FV变小,说明爬过了山顶,往回退一步。
- 如果FV不变,可能是平台区,需要特殊处理。
嗯,这里要注意:爬山算法有个致命问题——容易陷入局部极值。比如画面里有多个纹理区域,FV曲线可能有多个峰。算法可能只爬到第一个小峰就停了,错过了真正的主峰。
我的经验:
我习惯在爬山算法前,先做一次粗扫。粗扫用大步长,快速找到全局峰值的大致范围。然后再用爬山算法做精细定位。
这样既保证了全局最优,又提高了速度。
下面是一个简单的爬山算法伪代码,大家可以参考:
// 爬山算法伪代码
int hill_climbing(int start_pos) {
int current_pos = start_pos;
float current_fv = calculate_fv(current_pos);
int direction = 1; // 1: 正向, -1: 反向
int step = STEP_SIZE;
while (1) {
int next_pos = current_pos + direction * step;
float next_fv = calculate_fv(next_pos);
if (next_fv > current_fv) {
// 继续往上爬
current_pos = next_pos;
current_fv = next_fv;
} else if (next_fv < current_fv) {
// 爬过了,回头
direction = -direction;
step = step / 2; // 缩小步长
if (step < MIN_STEP) {
break; // 精度够了,退出
}
} else {
// FV不变,可能是平台区
// 尝试继续走几步,确认不是噪声
int confirm_count = 0;
for (int i = 0; i < 3; i++) {
next_pos += direction * step;
next_fv = calculate_fv(next_pos);
if (next_fv == current_fv) {
confirm_count++;
}
}
if (confirm_count >= 2) {
break; // 确认是平台,退出
}
}
}
return current_pos;
}
避坑指南:
我曾经在一个项目中,爬山算法在弱纹理场景下反复震荡,就是找不到稳定的峰值。
后来发现是步长设置不合理。步长太大,容易跳过峰值;步长太小,搜索时间太长。
我的解决方案是:动态步长。在FV变化剧烈的地方用小步长,变化平缓的地方用大步长。这样既快又准。
最后,我总结一下爬山算法的优缺点:
| 优点 | 缺点 |
|---|---|
| 实现简单,计算量小 | 容易陷入局部极值 |
| 适合连续场景(如视频) | 对初始位置敏感 |
| 收敛速度快(单峰时) | 在多峰场景下表现不佳 |
| 硬件资源占用少 | 需要配合粗扫使用 |
好了,这一章的内容就到这里。对比度检测原理、焦平面搜索算法、爬山算法,这三个基础概念,是后面所有优化工作的基石。大家先把这些吃透,后面我们才能聊怎么提速、怎么调参。