第四章 AF评价函数:清晰度评价与对比度计算
各位工程师朋友,咱们今天聊聊AF调优里最核心的东西——评价函数。
说白了,自动对焦就是让镜头找到一个位置,让画面最清晰。但“清晰”这个词,机器怎么理解?它得有个数学标准。这个标准,就是评价函数。
我刚开始做AF的时候,觉得这东西不就是算算梯度嘛,有啥难的?结果第一个项目就被现实狠狠教育了一顿。嗯,咱们今天就把这块彻底讲透。
4.1 为什么需要评价函数?
你想想看,相机传感器看到的只是一堆像素值。它不知道什么是“清晰”,什么是“模糊”。
我们需要一个数值,来量化图像的清晰程度。这个数值就叫清晰度评价值。对焦的过程,就是让这个值达到最大。
核心思想:清晰图像边缘锐利,像素间变化剧烈;模糊图像边缘平滑,像素间变化平缓。评价函数就是捕捉这种“变化剧烈程度”的数学工具。
常见的评价函数分三类:
- 基于梯度:Tenengrad、Laplacian、Brenner、Sobel
- 基于频域:FFT、DCT
- 基于统计:方差、熵
实际项目中,梯度类函数用得最多。原因很简单——计算快,效果稳。
4.2 Tenengrad函数
Tenengrad是我个人最常用的函数之一。它基于Sobel算子,计算图像在水平和垂直方向的梯度。
数学原理:
G(x,y) = sqrt(Gx² + Gy²)
其中:
Gx = Sobel_x * Image
Gy = Sobel_y * Image
Tenengrad值 = Σ G(x,y) (对所有像素求和)
代码实现:
// C语言实现,适用于嵌入式平台
int32_t Tenengrad_Evaluate(uint8_t *img, int32_t width, int32_t height) {
int32_t sum = 0;
int32_t gx, gy;
// Sobel核:水平方向
// [-1, 0, 1]
// [-2, 0, 2]
// [-1, 0, 1]
for (int y = 1; y < height - 1; y++) {
for (int x = 1; x < width - 1; x++) {
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)];
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 += abs(gx) + abs(gy); // 用值近似,省去开方
}
}
return sum;
}
我的经验:实际项目中,我一般用 abs(gx) + abs(gy) 代替 sqrt(gx² + gy²)。开方运算太慢了,对最终结果影响不大。这个优化在嵌入式平台上能省下30%的计算时间。
4.3 Laplacian函数
Laplacian算子是二阶微分算子。它不区分方向,直接检测图像中的边缘。
数学原理:
∇²f = ∂²f/∂x² + ∂²f/∂y²
离散形式(3x3核):
[0, 1, 0]
[1, -4, 1]
[0, 1, 0]
Laplacian值 = Σ |∇²f|
代码实现:
int32_t Laplacian_Evaluate(uint8_t *img, int32_t width, int32_t height) {
int32_t sum = 0;
int32_t lap;
for (int y = 1; y < height - 1; y++) {
for (int x = 1; x < width - 1; x++) {
lap = img[(y-1)*width + x]
+ img[y*width + (x-1)]
- 4*img[y*width + x]
+ img[y*width + (x+1)]
+ img[(y+1)*width + x];
sum += abs(lap);
}
}
return sum;
}
注意:Laplacian对噪声非常敏感。我曾经在一个低光照项目里用它,结果评价值乱跳,根本没法用。如果你发现Laplacian曲线抖动厉害,先检查一下图像噪声水平。
4.4 Brenner函数
Brenner函数是最简单的梯度函数之一。它只计算水平方向相邻像素的差异。
数学原理:
Brenner值 = Σ |f(x+2, y) - f(x, y)|²
注意:跳过一个像素,步长为2
代码实现:
int32_t Brenner_Evaluate(uint8_t *img, int32_t width, int32_t height) {
int32_t sum = 0;
int32_t diff;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width - 2; x++) {
diff = img[y*width + x + 2] - img[y*width + x];
sum += diff * diff; // 平方放大差异
}
}
return sum;
}
避坑指南:Brenner的步长为什么是2?因为步长为1时,对轻微模糊不敏感。步长太大又会丢失细节。我一般建议在1080p分辨率下用步长2,4K下可以考虑步长3或4。
4.5 三种函数的对比
| 函数 | 计算量 | 抗噪性 | 灵敏度 | 适用场景 |
|---|---|---|---|---|
| Tenengrad | 中 | 中 | 高 | 通用场景,推荐首选 |
| Laplacian | 低 | 低 | 中 | 低噪声、高对比度场景 |
| Brenner | 极低 | 高 | 低 | 资源受限的嵌入式平台 |
我个人习惯:先上Tenengrad。如果计算资源紧张,换Brenner。Laplacian我一般只用在实验室环境,或者作为辅助验证。
4.6 对比度计算
对比度是另一个重要的评价维度。它反映的是图像整体的明暗差异程度。
常用方法:
- RMS对比度:计算像素值的标准差
- Michelson对比度:(Imax - Imin) / (Imax + Imin)
- 局部对比度:分块计算,取平均值
代码实现(RMS对比度):
float RMS_Contrast(uint8_t *img, int32_t width, int32_t height) {
uint64_t sum = 0;
uint64_t sum_sq = 0;
int32_t total = width * height;
for (int i = 0; i < total; i++) {
sum += img[i];
sum_sq += img[i] * img[i];
}
float mean = (float)sum / total;
float variance = (float)sum_sq / total - mean * mean;
return sqrtf(variance); // 标准差即为RMS对比度
}
关键点:对比度不能单独作为对焦评价函数。为什么?因为一张全黑或全白的图,对比度是0,但它并不“模糊”。对比度通常和梯度函数配合使用,用来排除低对比度场景下的误判。
4.7 知识体系图
下面这张图,帮你理清本章的核心逻辑:
4.8 实战建议
最后,给各位几个实在的建议:
- 先跑仿真:在PC上用Python或Matlab把几种函数都跑一遍,看看曲线形状。我习惯用Tenengrad做基准,对比其他函数的表现。
- 关注曲线单峰性:好的评价函数,在从模糊到清晰再到模糊的过程中,应该只有一个明显的峰值。多峰曲线会导致对焦来回震荡。
- 注意计算耗时:嵌入式平台上,每帧的计算时间不能超过帧间隔的20%。否则会影响帧率。
- 组合使用:我最近一个项目里,用Brenner做粗对焦(快速找到大致位置),用Tenengrad做精对焦(精确到最佳位置)。效果比单用任何一种都好。
曾经踩过的坑:有一次我在一个户外监控项目里,直接用Laplacian做评价函数。白天效果很好,晚上画面噪声一大,评价值直接炸了。后来换成Tenengrad,配合一个简单的均值滤波预处理,问题才解决。记住:没有万能的评价函数,只有适合场景的评价函数。