4、图像清晰度评价函数(上):基于梯度的评价函数与基于频域的评价函数
评价函数,说白了就是给图像打分的标准。你想想看,自动对焦的核心问题是什么?就是判断哪张图最清晰。那怎么判断?总得有个量化指标吧。
我刚开始做对焦算法时,犯过一个低级错误——直接用像素值方差来评价清晰度。结果呢?对着白墙对焦,算法直接懵了。后来才明白,清晰度评价函数的设计,本质上是在捕捉图像中的高频信息。图像越清晰,边缘越锐利,高频分量就越丰富。
这一章,我们重点讲两类最常用的评价函数:基于梯度的和基于频域的。它们各有各的脾气,选对了,对焦就稳了。
核心思想:清晰度评价函数 = 高频信息提取器。图像越清晰,边缘越锐利,高频分量越强。
4.1 基于梯度的评价函数
梯度,说白了就是像素值的变化率。图像边缘处,像素值跳变剧烈,梯度值就大。清晰图像边缘锐利,梯度值自然高;模糊图像边缘平滑,梯度值就小。
这类函数最大的优点是计算简单,非常适合嵌入式平台。我做过一个项目,用STM32F4跑Brenner函数,一帧640×480的图像,计算时间不到1毫秒。这在实时对焦场景下,优势非常明显。
4.1.1 Brenner 函数
Brenner函数是最简单的梯度评价函数之一。它只计算水平方向相隔两个像素的差分值之和。
// Brenner 清晰度评价函数实现
float brenner_evaluate(uint8_t *img, int width, int height) {
float score = 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];
score += diff * diff; // 平方放大差异
}
}
return score;
}
经验之谈:Brenner函数为什么跳两个像素?我一开始也不理解。后来调试时发现,如果只跳一个像素,对轻微模糊的区分度不够。跳两个像素相当于放大了边缘的梯度变化,对焦曲线的陡峭度更好。但要注意,图像分辨率太低时(比如<100px宽),跳两个像素会丢失细节。
4.1.2 Tenengrad 函数
Tenengrad函数用Sobel算子计算梯度。它同时考虑水平和垂直两个方向,比Brenner更精确。
// Tenengrad 清晰度评价函数实现
float tenengrad_evaluate(uint8_t *img, int width, int height) {
float score = 0.0f;
// Sobel 水平核
int sobel_x[3][3] = {{-1, 0, 1}, {-2, 0, 2}, {-1, 0, 1}};
// Sobel 垂直核
int sobel_y[3][3] = {{-1, -2, -1}, {0, 0, 0}, {1, 2, 1}};
for (int y = 1; y < height - 1; y++) {
for (int x = 1; x < width - 1; x++) {
int gx = 0, gy = 0;
for (int ky = -1; ky <= 1; ky++) {
for (int kx = -1; kx <= 1; kx++) {
int pixel = img[(y + ky) * width + (x + kx)];
gx += pixel * sobel_x[ky + 1][kx + 1];
gy += pixel * sobel_y[ky + 1][kx + 1];
}
}
score += sqrtf(gx * gx + gy * gy); // 梯度幅值
}
}
return score;
}
注意:Tenengrad函数计算量是Brenner的10倍以上。在嵌入式平台上,如果每帧都要全图计算,帧率会掉得很厉害。我建议的做法是:粗搜索阶段用Brenner快速定位,精搜索阶段再用Tenengrad提高精度。
4.1.3 Laplacian 函数
Laplacian算子是一个二阶微分算子。它不像Sobel那样只检测一阶梯度,而是检测梯度的变化率。说白了,它对边缘的响应更尖锐。
// Laplacian 清晰度评价函数实现
float laplacian_evaluate(uint8_t *img, int width, int height) {
float score = 0.0f;
// 标准 Laplacian 核 (4邻域)
int laplacian_kernel[3][3] = {{0, -1, 0}, {-1, 4, -1}, {0, -1, 0}};
for (int y = 1; y < height - 1; y++) {
for (int x = 1; x < width - 1; x++) {
int lap = 0;
for (int ky = -1; ky <= 1; ky++) {
for (int kx = -1; kx <= 1; kx++) {
int pixel = img[(y + ky) * width + (x + kx)];
lap += pixel * laplacian_kernel[ky + 1][kx + 1];
}
}
score += abs(lap); // 值求和
}
}
return score;
}
三种梯度函数的对比,我整理了一张表,方便你选型时参考:
| 评价函数 | 计算量 | 抗噪性 | 单峰性 | 适用场景 |
|---|---|---|---|---|
| Brenner | 极低 | 差 | 好 | 低功耗嵌入式、粗搜索 |
| Tenengrad | 中等 | 中等 | 好 | 通用场景、精搜索 |
| Laplacian | 中等 | 较差 | 极好 | 高对比度场景、显微镜 |
我的建议:如果你刚开始做自动对焦,先从Brenner入手。它简单、可靠、容易调试。等把对焦搜索策略跑通了,再换成Tenengrad或Laplacian提升精度。一口吃不成胖子,对焦算法也是。
4.2 基于频域的评价函数
梯度法在时域操作,频域法换了个思路——把图像变换到频率空间。清晰图像的边缘对应高频分量,模糊图像的高频分量被衰减。所以,高频分量的能量大小,直接反映了图像的清晰度。
频域法的优点是精度高、抗噪能力强。但代价是计算量大。我记得有一次在ARM Cortex-A7上跑FFT,一帧1080p的图像,算完要200多毫秒。这显然不适合实时对焦。
4.2.1 基于FFT的评价函数
FFT(快速傅里叶变换)把图像从空间域变换到频率域。变换后,低频分量集中在中心,高频分量分布在四周。我们只需要统计高频区域的能量总和,就能评价清晰度。
// FFT 清晰度评价函数(伪代码)
float fft_evaluate(uint8_t *img, int width, int height) {
// 1. 对图像进行2D FFT变换
Complex *fft_result = fft2d(img, width, height);
// 2. 计算频谱幅度
float *magnitude = (float *)malloc(width * height * sizeof(float));
for (int i = 0; i < width * height; i++) {
magnitude[i] = sqrt(fft_result[i].real * fft_result[i].real +
fft_result[i].imag * fft_result[i].imag);
}
// 3. 定义高频区域(例如:半径 > 图像尺寸的1/4)
int cx = width / 2, cy = height / 2;
float high_freq_energy = 0.0f;
int threshold = min(width, height) / 4;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int dx = x - cx, dy = y - cy;
float dist = sqrt(dx * dx + dy * dy);
if (dist > threshold) {
high_freq_energy += magnitude[y * width + x];
}
}
}
free(magnitude);
free(fft_result);
return high_freq_energy;
}
性能警告:FFT的计算复杂度是O(N log N),对于嵌入式平台来说,这个N就是图像像素数。640×480的图像,FFT需要约30万次复数运算。我建议只在以下场景使用:离线标定、高端相机、或者对焦速度要求不高的场景(如显微镜)。
4.2.2 基于DCT的评价函数
DCT(离散余弦变换)和FFT类似,但只使用实数运算,计算量更小。JPEG压缩用的就是DCT,所以很多图像处理芯片都有DCT硬件加速单元。
// DCT 清晰度评价函数(伪代码)
float dct_evaluate(uint8_t *img, int width, int height) {
// 1. 对图像进行2D DCT变换
float *dct_coeff = dct2d(img, width, height);
// 2. 提取AC系数能量(排除DC分量)
float ac_energy = 0.0f;
for (int u = 0; u < height; u++) {
for (int v = 0; v < width; v++) {
if (u == 0 && v == 0) continue; // 跳过DC分量
ac_energy += dct_coeff[u * width + v] * dct_coeff[u * width + v];
}
}
// 3. 也可以只取中高频系数
// 例如:只取 u+v > 阈值 的系数
float high_ac_energy = 0.0f;
int threshold = (width + height) / 4;
for (int u = 0; u < height; u++) {
for (int v = 0; v < width; v++) {
if (u + v > threshold) {
high_ac_energy += dct_coeff[u * width + v] * dct_coeff[u * width + v];
}
}
}
free(dct_coeff);
return high_ac_energy; // 或返回 ac_energy
}
FFT和DCT的对比,我总结了一下:
| 特性 | FFT | DCT |
|---|---|---|
| 计算类型 | 复数运算 | 实数运算 |
| 计算量 | 大(约4倍DCT) | 小 |
| 频率分辨率 | 高 | 中等 |
| 硬件加速 | 少见 | 常见(JPEG/视频编码) |
| 抗噪性 | 好 | 好 |
| 嵌入式适用性 | 差 | 中等(有硬件加速时好) |
实用技巧:如果你用的芯片有JPEG编码器(比如海思、安霸的IPC芯片),可以直接从编码器内部拿到DCT系数,不需要自己算。这样几乎零开销就能得到频域评价结果。我曾经在一个项目中这么干过,对焦速度直接提升了3倍。
4.3 如何选择评价函数
说了这么多,到底该用哪个?我个人的经验是,没有最好的,只有最合适的。给你几个选择标准:
- 计算资源紧张? 选Brenner。它简单到可以在中断服务函数里跑。
- 需要高精度? 选Tenengrad或Laplacian。它们对轻微模糊的区分度更好。
- 图像噪声大? 选频域法。FFT和DCT天然有噪声抑制能力。
- 有硬件加速? 选DCT。利用芯片的JPEG/视频编码单元,几乎零开销。
- 做科研/标定? 选FFT。频率分辨率最高,分析结果最全面。
避坑指南:我曾经在一个项目中,直接用Laplacian评价函数去对焦低纹理物体(比如白墙)。结果评价曲线几乎是一条直线,对焦算法直接失效。后来我加了一个预处理步骤——先计算图像的局部方差,如果方差太小(说明纹理不足),就切换到红外辅助对焦。记住,评价函数不是万能的,场景适配才是关键。