清晰度评价函数:从时域到频域的全面解析
做自动对焦算法,说白了就是让镜头自己找到最清晰的那个位置。但有个核心问题——怎么定义“清晰”?
人眼一看就知道,但机器不行。机器需要一把“尺子”,能量化每张图的清晰程度。这把尺子,就是清晰度评价函数。
我入行那会儿,第一个项目就是做显微镜的自动对焦。当时天真地以为随便找个梯度算子就能搞定,结果被玻片上的噪声折腾得够呛。后来才明白——选对评价函数,项目就成功了一半。
今天咱们就把主流的六种方法掰开揉碎讲清楚:Tenengrad、Laplacian、Sobel、Brenner、Variance,还有FFT频域分析。
核心观点:没有最好的评价函数,只有最适合你场景的那一个。清晰度评价的本质,就是放大图像的高频信息,抑制低频干扰。
1. Tenengrad:最经典的梯度评价法
Tenengrad的核心思想很简单——图像越清晰,边缘越锐利,梯度值越大。它用Sobel算子分别计算水平和垂直方向的梯度,然后求平方和。
公式长这样:
// Tenengrad 清晰度值计算
// Gx = Sobel_x * I, Gy = Sobel_y * I
// F = Σ (Gx² + Gy²)
// 实际代码实现
float computeTenengrad(const cv::Mat& img) {
cv::Mat grad_x, grad_y;
cv::Sobel(img, grad_x, CV_32F, 1, 0, 3);
cv::Sobel(img, grad_y, CV_32F, 0, 1, 3);
cv::Mat mag;
cv::magnitude(grad_x, grad_y, mag);
return cv::sum(mag)[0];
}
我的经验:Tenengrad在纹理丰富的场景下表现极佳。但注意——它对噪声敏感。我曾经在低光照环境下用Tenengrad做对焦,结果噪声被当成“清晰度”给放大了。后来加了高斯预滤波才稳住。
2. Laplacian:二阶导数的魅力
Laplacian算子跟Sobel不一样,它用的是二阶导数。说白了,它检测的是灰度变化的“变化率”——边缘两侧的灰度突变越剧烈,Laplacian响应越大。
为什么用二阶导数?因为一阶导数只能检测到“有没有边缘”,而二阶导数能告诉你“边缘有多陡”。
// Laplacian 清晰度评价
float computeLaplacian(const cv::Mat& img) {
cv::Mat lap;
cv::Laplacian(img, lap, CV_32F, 3);
// 常用两种方式:方差或值求和
cv::Scalar mean, stddev;
cv::meanStdDev(lap, mean, stddev);
// 方差越大,清晰度越高
return stddev[0] * stddev[0];
}
注意:Laplacian对噪声的放大效果比Sobel更严重。如果图像信噪比低,建议先用高斯滤波平滑一下。我踩过这个坑——在工业相机抓拍运动物体时,直接用Laplacian结果全是噪点响应。
3. Sobel:方向敏感的边缘检测
Sobel算子是Tenengrad的基础,但也可以单独使用。它有两个方向——水平和垂直。你可以只用一个方向,也可以两个方向都算。
什么时候只用单方向?当你的场景有明显的方向性纹理时。
举个例子:我在做芯片引脚对焦时,引脚是水平排列的,垂直方向的梯度信息更有价值。这时候只算垂直Sobel,计算量减半,精度反而更高。
| 算子 | 核大小 | 方向性 | 计算量 | 推荐场景 |
|---|---|---|---|---|
| Sobel 3x3 | 3 | 水平/垂直 | 低 | 通用场景 |
| Sobel 5x5 | 5 | 水平/垂直 | 中 | 噪声较大时 |
| Scharr | 3 | 水平/垂直 | 中 | 需要更高精度 |
4. Brenner:最轻量级的梯度算子
Brenner可能是最简单的清晰度评价函数了。它只计算相隔两个像素的灰度差的平方。
公式:F = Σ (I(x+2, y) - I(x, y))²
为什么是相隔两个像素?因为相隔一个像素对噪声太敏感,相隔太远又丢失细节。两个像素是个不错的折中。
// Brenner 清晰度评价 - 极致轻量
float computeBrenner(const cv::Mat& img) {
float score = 0.0f;
int rows = img.rows, cols = img.cols;
for(int y = 0; y < rows; y++) {
const uchar* row = img.ptr<uchar>(y);
for(int x = 0; x < cols - 2; x++) {
int diff = row[x+2] - row[x];
score += diff * diff;
}
}
return score;
}
适用场景:嵌入式设备、手机摄像头、无人机——任何算力受限的地方。Brenner的运算量只有Sobel的1/3左右,但效果在大多数场景下够用。
5. Variance:从统计视角看清晰度
Variance(方差)的思路跟梯度完全不同。它不关心边缘,而是看整幅图像的灰度分布。
清晰图像通常有更宽的灰度分布——亮的更亮,暗的更暗。模糊图像则灰度分布集中,方差小。
你想想看,一张完全模糊的图像,所有像素灰度值都差不多,方差趋近于0。而一张清晰的图像,方差会大得多。
// 方差清晰度评价
float computeVariance(const cv::Mat& img) {
cv::Scalar mean, stddev;
cv::meanStdDev(img, mean, stddev);
// 方差 = 标准差²
float variance = stddev[0] * stddev[0];
return variance;
}
我的建议:方差适合作为辅助评价函数。它计算极快,但单独使用时精度不够。我通常把它跟Brenner或Tenengrad组合使用——先用方差快速排除明显离焦的位置,再用梯度法精确定位。
6. FFT频域分析:从频率角度看世界
前面五种方法都在空间域操作。FFT则把图像变换到频域——清晰图像的高频分量多,模糊图像的高频分量少。
为什么?因为边缘和细节对应高频信号。图像越模糊,高频成分衰减得越厉害。
具体做法:
- 对图像做二维FFT,得到频谱图
- 计算高频区域的能量占比
- 高频能量越高,图像越清晰
// FFT频域清晰度评价
float computeFFTSharpness(const cv::Mat& img) {
// 1. 扩展图像到最佳FFT尺寸
int m = cv::getOptimalDFTSize(img.rows);
int n = cv::getOptimalDFTSize(img.cols);
cv::Mat padded;
cv::copyMakeBorder(img, padded, 0, m - img.rows,
0, n - img.cols, cv::BORDER_CONSTANT,
cv::Scalar::all(0));
// 2. 转换为浮点并合并通道
cv::Mat planes[] = {cv::Mat_<float>(padded),
cv::Mat::zeros(padded.size(), CV_32F)};
cv::Mat complexI;
cv::merge(planes, 2, complexI);
// 3. 执行FFT
cv::dft(complexI, complexI);
// 4. 计算高频能量(中心区域为低频)
cv::split(complexI, planes);
cv::magnitude(planes[0], planes[1], planes[0]);
cv::Mat mag = planes[0];
// 5. 计算高频占比
int cx = mag.cols / 2;
int cy = mag.rows / 2;
// 低频区域(中心20%)
int lowR = mag.rows * 0.2;
int lowC = mag.cols * 0.2;
float totalEnergy = cv::sum(mag)[0];
float lowEnergy = cv::sum(mag(cv::Rect(cx - lowC/2, cy - lowR/2,
lowC, lowR)))[0];
// 高频能量占比越高越清晰
return (totalEnergy - lowEnergy) / totalEnergy;
}
性能警告:FFT的计算量远大于梯度方法。在嵌入式平台上,一次FFT可能需要几十毫秒。我一般只在调试阶段用FFT做分析,或者在对焦精度要求极高的静态场景下使用。
各方法对比总结
| 评价函数 | 计算量 | 抗噪性 | 精度 | 适用场景 |
|---|---|---|---|---|
| Tenengrad | 中 | 中 | 高 | 通用场景,纹理丰富 |
| Laplacian | 中 | 低 | 高 | 高信噪比场景 |
| Sobel | 低 | 中 | 中 | 方向性纹理 |
| Brenner | 极低 | 中 | 中 | 嵌入式/实时系统 |
| Variance | 极低 | 高 | 低 | 快速粗筛 |
| FFT频域 | 高 | 高 | 极高 | 调试分析/静态场景 |
我的选型原则:先看算力,再看场景。算力够用就上Tenengrad或Laplacian;算力紧张就用Brenner;需要全局分析时用FFT做辅助。没有银弹,只有权衡。