4、图像清晰度评价函数(上):基于梯度的评价函数与基于频域的评价函数

评价函数,说白了就是给图像打分的标准。你想想看,自动对焦的核心问题是什么?就是判断哪张图最清晰。那怎么判断?总得有个量化指标吧。

我刚开始做对焦算法时,犯过一个低级错误——直接用像素值方差来评价清晰度。结果呢?对着白墙对焦,算法直接懵了。后来才明白,清晰度评价函数的设计,本质上是在捕捉图像中的高频信息。图像越清晰,边缘越锐利,高频分量就越丰富。

这一章,我们重点讲两类最常用的评价函数:基于梯度的和基于频域的。它们各有各的脾气,选对了,对焦就稳了。

核心思想:清晰度评价函数 = 高频信息提取器。图像越清晰,边缘越锐利,高频分量越强。

图像清晰度评价函数分类体系 清晰度评价函数 基于梯度的评价函数 基于频域的评价函数 Brenner Tenengrad Laplacian FFT DCT 梯度法特点: • 计算简单,适合嵌入式实时处理 • 对噪声敏感,需配合滤波使用 • 单峰性好,适合爬山搜索 频域法特点: • 精度高,抗噪能力强 • 计算量大,实时性差 • 适合离线标定或高端相机

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评价函数去对焦低纹理物体(比如白墙)。结果评价曲线几乎是一条直线,对焦算法直接失效。后来我加了一个预处理步骤——先计算图像的局部方差,如果方差太小(说明纹理不足),就切换到红外辅助对焦。记住,评价函数不是万能的,场景适配才是关键。