清晰度评价函数:从时域到频域的全面解析

做自动对焦算法,说白了就是让镜头自己找到最清晰的那个位置。但有个核心问题——怎么定义“清晰”?

人眼一看就知道,但机器不行。机器需要一把“尺子”,能量化每张图的清晰程度。这把尺子,就是清晰度评价函数。

我入行那会儿,第一个项目就是做显微镜的自动对焦。当时天真地以为随便找个梯度算子就能搞定,结果被玻片上的噪声折腾得够呛。后来才明白——选对评价函数,项目就成功了一半。

今天咱们就把主流的六种方法掰开揉碎讲清楚:Tenengrad、Laplacian、Sobel、Brenner、Variance,还有FFT频域分析。

核心观点:没有最好的评价函数,只有最适合你场景的那一个。清晰度评价的本质,就是放大图像的高频信息,抑制低频干扰。

清晰度评价函数分类体系 清晰度评价函数 时域(空间域)方法 频域方法 Tenengrad Laplacian Sobel Brenner Variance FFT频域分析 💡 选择建议: • 实时性要求高 → Brenner / Variance(计算量小) • 精度要求高 → Tenengrad / Laplacian(梯度信息丰富) • 需要全局分析 → 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则把图像变换到频域——清晰图像的高频分量多,模糊图像的高频分量少。

为什么?因为边缘和细节对应高频信号。图像越模糊,高频成分衰减得越厉害。

具体做法:

  1. 对图像做二维FFT,得到频谱图
  2. 计算高频区域的能量占比
  3. 高频能量越高,图像越清晰
// 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做辅助。没有银弹,只有权衡。