2. 对焦评价函数(上):基于梯度的清晰度评价函数
好,咱们直接进入正题。对焦评价函数,说白了就是给镜头算命的——判断当前画面到底是清楚还是模糊。我做了这么多年相机系统,见过太多人一上来就调PID参数,结果评价函数本身就有问题,那后面全是白费功夫。
这一章我们先啃最经典的一类:基于梯度的清晰度评价函数。为什么先讲它们?因为梯度直接对应图像边缘的锐利程度,而边缘锐利程度恰恰就是人眼判断是否合焦的核心依据。
2.1 梯度与清晰度的关系
先想一个问题:一张合焦的照片和一张虚焦的照片,最直观的区别在哪?
答案是边缘。合焦时,物体边缘的亮度变化是陡峭的;虚焦时,边缘被模糊了,亮度变化变得平缓。梯度就是用来量化这个「变化陡峭程度」的数学工具。
核心思想:清晰图像 → 边缘锐利 → 梯度值大;模糊图像 → 边缘平滑 → 梯度值小。
我刚开始做自动对焦时,犯过一个低级错误:直接用原始像素值做评价。结果发现,画面亮度一变,评价曲线就乱跳。后来才明白,梯度算子本质上是一个高通滤波器,它滤掉了低频的亮度分量,只保留边缘信息。这样评价函数就对光照变化不那么敏感了。
下面这张图展示了三种经典梯度算子的计算逻辑:
2.2 Brenner 算子:最朴素的梯度评价
Brenner 算子是梯度评价里最「暴力」的一种。它只做一件事:计算相隔两个像素的亮度差,然后平方求和。
数学表达式很简单:
F = Σ_x Σ_y [I(x+2, y) - I(x, y)]²
为什么隔两个像素而不是一个?我个人的理解是:隔一个像素对噪声太敏感,隔两个像素相当于做了一个简单的平滑,反而更鲁棒。当然,如果你的传感器噪声很小,用步长1也行。
实战经验:Brenner 算子的计算量极低,非常适合嵌入式平台。我在一个低功耗摄像头项目里就用它,配合简单的爬山搜索,帧率能做到30fps不卡顿。
代码实现:
float brenner_eval(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 + 2] - img[y * width + x];
sum += (float)(diff * diff);
}
}
return sum / (width * height); // 归一化,方便不同分辨率对比
}
注意我做了归一化处理。如果不归一化,分辨率越大,F值越大,你就没法在不同分辨率之间做公平比较了。
2.3 Tenengrad 算子:引入 Sobel 的梯度幅值
Tenengrad 比 Brenner 讲究一些。它用 Sobel 算子计算水平和垂直两个方向的梯度,然后合成梯度幅值。
Sobel 算子有两个3x3的卷积核:
Gx = [[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1]]
Gy = [[-1, -2, -1],
[ 0, 0, 0],
[ 1, 2, 1]]
每个像素的梯度幅值:
G(x,y) = √(Gx² + Gy²)
整幅图的 Tenengrad 评价值就是所有像素梯度幅值的平方和:
F = Σ_x Σ_y [G(x,y)]²
注意:开平方运算在嵌入式平台上很慢。实际工程中,我经常直接用 Gx² + Gy² 代替 √(Gx² + Gy²),评价曲线的形状几乎不变,但速度能快30%以上。
代码实现:
float tenengrad_eval(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 / (width * height);
}
Tenengrad 是我个人用得最多的算子。它在抗噪性和灵敏度之间取得了不错的平衡。不过要注意,它对纹理方向敏感——如果场景里全是水平条纹,垂直方向的梯度贡献就很小。
2.4 Laplacian 算子:二阶导数的视角
Laplacian 走的是另一条路——它不关心梯度本身,而是关心梯度的变化率。说白了,它用二阶导数来检测边缘。
连续形式的 Laplacian:
∇²I = ∂²I/∂x² + ∂²I/∂y²
离散形式的常用卷积核(4邻域):
L = [[ 0, -1, 0],
[-1, 4, -1],
[ 0, -1, 0]]
或者8邻域版本:
L8 = [[-1, -1, -1],
[-1, 8, -1],
[-1, -1, -1]]
评价值就是所有像素 Laplacian 响应值的平方和:
F = Σ_x Σ_y [∇²I(x,y)]²
关键区别:Brenner 和 Tenengrad 是一阶梯度,响应在边缘处达到峰值;Laplacian 是二阶梯度,响应在边缘两侧符号相反,过零点才是边缘位置。所以 Laplacian 对孤立噪声点特别敏感——一个噪点就能产生巨大的二阶响应。
代码实现:
float laplacian_eval(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 / (width * height);
}
我曾经在一个项目里直接用 Laplacian 做评价,结果发现画面稍微有点噪点,评价曲线就抖得厉害。后来我加了一个高斯预滤波,曲线才平滑下来。所以我的建议是:用 Laplacian 之前,一定先做平滑处理。
2.5 三种算子的对比与选型建议
我把三种算子的关键特性整理成了一张表,方便你快速选型:
| 特性 | Brenner | Tenengrad | Laplacian |
|---|---|---|---|
| 数学阶数 | 一阶 | 一阶 | 二阶 |
| 计算量 | 极低 | 中等 | 低 |
| 抗噪性 | 中等 | 较好 | 差 |
| 方向敏感性 | 水平方向 | 各向同性(近似) | 各向同性 |
| 适用场景 | 低功耗、实时性要求高 | 通用场景,推荐首选 | 需配合平滑滤波使用 |
| 硬件实现难度 | 极简 | 简单 | 简单 |
我的选型建议:
- 如果你在做低功耗 IoT 摄像头,选 Brenner,省电又够用。
- 如果你在做手机或相机 AF,选 Tenengrad,综合表现最稳。
- 如果你在做显微镜或工业检测,场景可控且噪声低,Laplacian 的灵敏度更高。
2.6 一个容易被忽视的细节:ROI 区域选择
讲完算子本身,我想提一个工程上特别重要但教科书很少讲的问题——ROI(感兴趣区域)的选择。
你想想看,如果整幅图都参与评价,天空、墙壁这些平坦区域贡献的梯度值几乎为零,它们只会稀释有效信息。更糟糕的是,如果画面边缘有镜头渐晕(暗角),这些区域的梯度值还会引入偏差。
我一般会这么做:
- 只取画面中央 60%-80% 的区域做评价。
- 如果场景中有已知的纹理丰富区域(比如人脸检测框),只在该区域内计算。
- 对 ROI 内的像素做加权——中心权重高,边缘权重低。
代码里加一个 ROI 参数并不复杂:
float tenengrad_eval_roi(uint8_t* img, int width, int height,
int roi_x, int roi_y, int roi_w, int roi_h) {
float sum = 0.0f;
for (int y = roi_y + 1; y < roi_y + roi_h - 1; y++) {
for (int x = roi_x + 1; x < roi_x + roi_w - 1; x++) {
// 同样的 Sobel 计算,但只遍历 ROI 区域
// ...
}
}
return sum / (roi_w * roi_h);
}
避坑指南:我曾经在一个项目中,ROI 选得太小(只占画面的10%),结果镜头稍微抖动一下,目标物体跑出 ROI,评价函数直接崩了。ROI 的大小要留有余量,至少覆盖你期望的对焦区域加上一定的运动容差。
2.7 小结
这一章我们聊了三种基于梯度的清晰度评价函数。它们虽然简单,但却是自动对焦系统的基石。记住一个原则:评价函数的质量决定了对焦精度的上限,后面的搜索算法再优秀,也弥补不了评价函数的缺陷。