2. 反差对焦(CDAF)原理:图像对比度与爬山算法
反差对焦,圈里人常叫它 CDAF(Contrast Detection Auto Focus)。说白了,它不靠什么特殊传感器,就靠图像本身来判断焦清不清晰。你想想看,一张照片拍糊了,边缘是模糊的,像素之间的差异很小;拍清楚了,边缘锐利,相邻像素的亮度跳变就很大。CDAF 就是利用这个“跳变”的大小来找焦点。
我刚开始做相机算法时,觉得这玩意儿挺直观的。但真正上手调参才发现,坑不少。咱们今天就把它的核心——图像对比度、爬山算法、还有几个常用的评价函数——彻底讲透。
2.1 什么是图像对比度?
图像对比度,不是指整张图的明暗反差,而是指局部像素之间的灰度变化率。变化越剧烈,对比度越高,图像越清晰。
举个简单的例子:
- 一张纯色墙的照片,所有像素值都是 128,对比度 = 0(完全模糊)。
- 一张黑白条纹的照片,像素值在 0 和 255 之间来回跳,对比度很高(清晰)。
在自动对焦里,我们只关心对焦窗口(ROI)内的对比度。窗口外的背景再乱,也不参与计算。
对比度 = Σ (相邻像素差的值) 或 Σ (像素梯度的平方)
值越大 → 图像越清晰 → 对焦越准。
2.2 爬山算法:如何找到峰值?
有了对比度这个“评分标准”,接下来就是怎么找到评分最高的镜头位置。这就是爬山算法(Hill Climbing)干的事。
它的逻辑特别简单,像个瞎子爬山:
- 迈一步:镜头朝一个方向移动一小步。
- 算分:计算当前帧的对比度值。
- 比较:如果分数比上一帧高,说明方向对了,继续走。
- 回头:如果分数比上一帧低,说明走过头了,掉头往回走,步长减半。
- 收敛:重复直到步长小于某个阈值,或者分数变化极小,就认为找到了峰值。
听起来简单吧?但我曾经在一个项目中踩过坑:如果场景里有高频纹理(比如树叶),对比度曲线会有很多小毛刺。爬山算法容易卡在局部峰值上,而不是真正的焦点。后来我加了粗搜+细搜两步策略,先大步跨过整个行程,找到最高峰的大致区域,再小步精细搜索,才解决了问题。
我曾经在低光照环境下,因为噪声导致对比度曲线抖动剧烈,爬山算法反复震荡无法收敛。后来我引入了多帧平均和死区(Dead Zone)判断,才稳定下来。记住:噪声是 CDAF 的天敌。
2.3 评价函数详解
爬山算法需要一个“评分函数”来计算对比度。不同的函数,对纹理的敏感度、计算量、抗噪能力都不一样。下面是我最常用的三种:
2.3.1 Tenengrad 函数
Tenengrad 基于 Sobel 算子。它计算图像在水平和垂直方向的梯度,然后求平方和。说白了,就是检测边缘的强度。
// Tenengrad 评价函数(简化版)
float Tenengrad(uint8_t* img, int width, int height) {
float sum = 0.0f;
for (int y = 1; y < height - 1; y++) {
for (int x = 1; x < width - 1; x++) {
// Sobel 水平梯度 Gx
int gx = -img[(y-1)*width + (x-1)] + img[(y-1)*width + (x+1)]
-2*img[y*width + (x-1)] + 2*img[y*width + (x+1)]
-img[(y+1)*width + (x-1)] + img[(y+1)*width + (x+1)];
// Sobel 垂直梯度 Gy
int gy = -img[(y-1)*width + (x-1)] -2*img[(y-1)*width + x] - img[(y-1)*width + (x+1)]
+img[(y+1)*width + (x-1)] +2*img[(y+1)*width + x] + img[(y+1)*width + (x+1)];
sum += (float)(gx*gx + gy*gy);
}
}
return sum;
}
特点:
- 对边缘敏感,评价曲线陡峭,精度高。
- 计算量大(每个像素要做 6 次乘加)。
- 对噪声敏感,低光下表现一般。
2.3.2 Laplacian 函数
Laplacian 是二阶微分算子。它不关心边缘的方向,只关心像素值变化的剧烈程度。我个人的习惯是,在纹理丰富的场景下优先用 Laplacian,因为它对焦速度更快。
// Laplacian 评价函数(简化版)
float Laplacian(uint8_t* img, int width, int height) {
float sum = 0.0f;
for (int y = 1; y < height - 1; y++) {
for (int x = 1; x < width - 1; x++) {
int lap = 4*img[y*width + x]
- img[(y-1)*width + x]
- img[(y+1)*width + x]
- img[y*width + (x-1)]
- img[y*width + (x+1)];
sum += (float)(lap * lap);
}
}
return sum;
}
特点:
- 计算量比 Tenengrad 小(只有 5 次加减)。
- 对孤立噪声点非常敏感(二阶微分放大噪声)。
- 在平滑区域容易失效(梯度为 0)。
2.3.3 Brenner 函数
Brenner 是最简单的评价函数之一。它只计算水平方向相隔两个像素的灰度差的平方。我建议在资源受限的嵌入式平台上优先考虑它。
// Brenner 评价函数(简化版)
float Brenner(uint8_t* img, int width, int height) {
float sum = 0.0f;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width - 2; x++) {
int diff = img[y*width + x] - img[y*width + x + 2];
sum += (float)(diff * diff);
}
}
return sum;
}
特点:
- 计算量极小(一次减法、一次乘法)。
- 只考虑水平方向,对垂直纹理不敏感。
- 抗噪能力中等,适合快速粗搜。
2.4 三种评价函数对比
| 函数 | 计算量 | 精度 | 抗噪性 | 适用场景 |
|---|---|---|---|---|
| Tenengrad | 高 | 高 | 低 | 高光照、纹理丰富场景 |
| Laplacian | 中 | 中高 | 极低 | 纹理丰富、噪声可控场景 |
| Brenner | 低 | 中 | 中 | 嵌入式平台、粗搜阶段 |
在实际项目中,我很少只用一种函数。通常的做法是:粗搜用 Brenner(快),细搜用 Tenengrad(准)。如果场景噪声大,我会在 Laplacian 之前加一个高斯滤波,虽然多花点时间,但能避免误判。
2.5 核心流程:CDAF 对焦逻辑
下面这张 SVG 图展示了 CDAF 的完整工作流程。从图像采集到最终锁定焦点,每一步都离不开我们刚才讲的内容。
2.6 实际项目中的取舍
最后聊点实际的。你在做 CDAF 算法时,一定会面临三个选择:
- 精度 vs 速度:Tenengrad 精度高但慢,Brenner 快但精度一般。我的做法是动态切换——运动场景用 Brenner 快速跟焦,静止场景用 Tenengrad 精对焦。
- 窗口大小:窗口越大,计算越慢,但抗干扰能力越强。我一般取图像中心 1/4 区域,既保证速度,又覆盖主体。
- 步长策略:固定步长容易过冲或震荡。我习惯用指数衰减步长,前期大步快速逼近,后期小步精细调整。
我曾经在一个项目中,因为评价函数没有做归一化处理,导致不同光照下的对比度值差异巨大,爬山算法的阈值完全失效。后来我加了归一化(除以窗口内的平均亮度),才让算法在各种光照下都能稳定工作。这个坑,你大概率也会遇到。