3. 图像清晰度评价函数:基于梯度、基于频域、基于统计的方法

好,咱们进入第三个核心话题。自动对焦这事儿,说白了就是让镜头自己找到那个「最清楚」的位置。那问题来了——怎么判断一张图到底清不清楚?

嗯,这就是图像清晰度评价函数要干的事。我做了这么多年嵌入式视觉,试过的评价函数少说也有几十种。今天我把它们分成三大类来讲:基于梯度的、基于频域的、基于统计的。每一类都有自己的脾气,咱们一个一个看。

3.1 基于梯度的评价函数

这类方法最直观。你想想看,一张清晰的图,边缘肯定是锐利的,像素之间的变化很剧烈。模糊的图呢?边缘就像被抹了一层油,过渡平缓。所以,我们只要计算图像中像素值的变化率——也就是梯度——就能知道清晰度。

我个人习惯用 Tenengrad 函数。它用 Sobel 算子算梯度,然后求和。代码长这样:

// Tenengrad 清晰度评价
float tenengrad(uint8_t* img, int w, int h) {
    int gx, gy, sum = 0;
    for (int y = 1; y < h-1; y++) {
        for (int x = 1; x < w-1; x++) {
            // Sobel X
            gx = -img[(y-1)*w + (x-1)] + img[(y-1)*w + (x+1)]
                 -2*img[y*w + (x-1)] + 2*img[y*w + (x+1)]
                 -img[(y+1)*w + (x-1)] + img[(y+1)*w + (x+1)];
            // Sobel Y
            gy = -img[(y-1)*w + (x-1)] -2*img[(y-1)*w + x] - img[(y-1)*w + (x+1)]
                 + img[(y+1)*w + (x-1)] +2*img[(y+1)*w + x] + img[(y+1)*w + (x+1)];
            sum += gx*gx + gy*gy;
        }
    }
    return (float)sum / (w*h);
}

还有 Laplacian 函数,它用二阶导数。边缘处一阶导数的变化率最大,所以 Laplacian 响应也最强。我建议在纹理丰富的场景用 Laplacian,它比 Sobel 更敏感。

我的经验: 在嵌入式平台上,Tenengrad 的性价比最高。计算量不大,效果也稳定。我曾经在一个低功耗的 MCU 上跑过,每帧只多花 2ms,完全能接受。

3.2 基于频域的评价函数

这类方法换个角度看问题。清晰图像的高频分量多——边缘、细节都是高频信号。模糊图像呢?高频被衰减了,能量集中在低频。

最经典的是 快速傅里叶变换(FFT)。计算频谱后,统计高频分量的能量占比。占比越高,图像越清晰。

但说实话,在嵌入式系统里直接做 FFT 有点奢侈。我一般用 离散余弦变换(DCT),它计算量小一些。JPEG 压缩用的就是 DCT,所以很多摄像头 ISP 里已经有硬件加速模块。

还有一种更取巧的方法——高频带通滤波。设计一个带通滤波器,只让中高频通过,然后统计输出图像的方差。我在项目中遇到过光照变化剧烈的场景,梯度法会误判,但频域法稳如老狗。

注意: 频域法对噪声很敏感。我曾经在一个工业检测项目里,环境光有 50Hz 的频闪,FFT 结果全是噪声峰值。后来我加了一个陷波滤波器才搞定。

3.3 基于统计的评价函数

这类方法不直接看边缘,而是看图像的统计特性。清晰图像的像素值分布更「散」,模糊图像的分布更「集中」。

方差法是最简单的。计算整幅图像的灰度方差。方差大,说明对比度高,图像清晰。代码就几行:

float variance(uint8_t* img, int w, int h) {
    float mean = 0, var = 0;
    int total = w * h;
    for (int i = 0; i < total; i++) mean += img[i];
    mean /= total;
    for (int i = 0; i < total; i++) var += (img[i] - mean) * (img[i] - mean);
    return var / total;
}

还有 熵(Entropy)。信息论里的概念,熵越大,信息量越大,图像越清晰。计算灰度直方图,然后套公式:

float entropy(uint8_t* img, int w, int h) {
    int hist[256] = {0};
    int total = w * h;
    for (int i = 0; i < total; i++) hist[img[i]]++;
    float ent = 0;
    for (int i = 0; i < 256; i++) {
        if (hist[i] == 0) continue;
        float p = (float)hist[i] / total;
        ent -= p * log2f(p);
    }
    return ent;
}

嗯,这里要注意。熵在纹理丰富的场景表现很好,但在纯色背景(比如白墙)上会失灵。因为纯色图像的熵很低,但你不能说它「不清晰」——它本来就是平的。

3.4 三种方法的对比

我把它们放在一起比一比,方便你选型:

方法 计算量 抗噪性 适用场景 我的推荐
Tenengrad 中等 通用场景 ⭐⭐⭐⭐⭐
Laplacian 较差 纹理丰富 ⭐⭐⭐⭐
FFT/DCT 光照变化大 ⭐⭐⭐
方差 极低 中等 对比度明显 ⭐⭐⭐
纹理丰富 ⭐⭐⭐⭐
避坑指南: 我曾经在一个显微对焦项目里,只用 Tenengrad 做评价。结果遇到低对比度的生物样本,评价曲线全是平的,根本找不到峰值。后来我改成「梯度 + 熵」的加权组合,才把问题解决。所以,别迷信单一方法,组合拳往往更有效。

3.5 嵌入式平台上的选型建议

在嵌入式系统里,资源是有限的。我一般按这个优先级来选:

  1. 首选:Tenengrad 或 Laplacian。计算量小,效果稳定。大部分场景够用。
  2. 次选:方差或熵。如果梯度法效果不好,试试统计法。代码改动小。
  3. 最后:频域法。只有在前两种都失效时,我才上 FFT。而且我会用定点数优化,避免浮点运算。

你想想看,一个自动对焦系统,每帧图像的处理时间可能只有 10ms。如果评价函数就占了 5ms,那留给电机控制和图像处理的时间就太紧了。所以,效率是第一位的

好了,这一章就到这里。下一章咱们聊聊「对焦搜索策略」——怎么用最少的步数找到那个最清晰的位置。到时候我会分享一个我踩过的坑,保证让你少走弯路。