3. 评价函数选型:梯度、统计与频域

自动对焦的核心,说白了就是找一个靠谱的“清晰度判据”。镜头推来推去,到底哪一帧最清楚?你得有个打分机制。这个打分机制,就是评价函数。

低光照下,信号本身就弱,噪声还特别多。选错了评价函数,AF 系统就像在黑夜里找东西——明明焦点就在那儿,它却死活不认。我这些年调过的低光 AF 项目,踩坑最多的就是这一步。

今天我把主流评价函数分成三类来讲:基于梯度、基于统计、基于频域。咱们一个一个看。

3.1 基于梯度的函数

这类函数的核心逻辑很简单:图像越清晰,边缘越锐利,梯度值越大。低光照下,边缘信息会被噪声淹没,所以梯度类函数对噪声特别敏感。

Sobel 算子

Sobel 算水平/垂直两个方向的梯度,然后求和。我习惯用 3×3 的核,计算量小,效果也还行。

// Sobel 梯度计算(简化版)
float sobel_score(uint8_t* img, int w, int h) {
    float sum = 0.0f;
    for (int y = 1; y < h-1; y++) {
        for (int x = 1; x < w-1; x++) {
            int 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)];
            int 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 += sqrtf(gx*gx + gy*gy);
        }
    }
    return sum / (w * h);
}
我的经验:低光照下,Sobel 的响应会变得很“平”。我曾经在一个暗光场景下调试,Sobel 曲线几乎是一条直线,根本找不到峰值。后来加了预处理降噪,才勉强能用。

Laplacian 算子

Laplacian 是二阶微分算子,对边缘的响应比 Sobel 更尖锐。但问题也很明显——它对噪声的放大更厉害。

你想想看,低光照图像里,噪声的梯度往往和真实边缘的梯度差不多大。Laplacian 一上去,噪声和边缘一起被放大,评价函数就乱跳了。

避坑:我曾经在一个夜视项目中直接用 Laplacian,结果 AF 在合焦点附近来回震荡,根本稳不住。后来换成 Sobel + 中值滤波,才把抖动压下去。

Tenengrad 函数

Tenengrad 其实是 Sobel 的变种。它用 Sobel 算梯度,然后只保留超过某个阈值的梯度值。这样做的好处是——可以滤掉一部分噪声引起的弱梯度。

公式长这样:

// Tenengrad 评分
float tenengrad_score(uint8_t* img, int w, int h, int threshold) {
    float sum = 0.0f;
    for (int y = 1; y < h-1; y++) {
        for (int x = 1; x < w-1; x++) {
            int gx = ...; // Sobel gx
            int gy = ...; // Sobel gy
            float mag = sqrtf(gx*gx + gy*gy);
            if (mag > threshold) {
                sum += mag * mag; // 平方放大差异
            }
        }
    }
    return sum;
}

阈值怎么选?我一般取图像平均梯度的 1.5~2 倍。太低滤不掉噪声,太高会把弱边缘也滤掉。

3.2 基于统计的函数

这类函数不直接看边缘,而是看图像的整体统计特性。低光照下,统计量往往比梯度更鲁棒。

Brenner 函数

Brenner 只计算相隔两个像素的灰度差。计算量极小,适合嵌入式实时处理。

// Brenner 评分
float brenner_score(uint8_t* img, int w, int h) {
    float sum = 0.0f;
    for (int y = 0; y < h; y++) {
        for (int x = 0; x < w-2; x++) {
            int diff = img[y*w + x] - img[y*w + x + 2];
            sum += diff * diff;
        }
    }
    return sum;
}
亮点:Brenner 在低光照下的表现出乎意料地好。因为它只关心相隔两个像素的差异,对单像素噪声有一定的抑制作用。我在一个电池供电的 IoT 摄像头项目里,最后就选了 Brenner——省电、稳定、够用。

Variance(方差)

方差衡量图像的对比度。图像越清晰,像素值分布越分散,方差越大。

但低光照下有个问题:整体偏暗,像素值集中在低灰度区,方差本身就小。再加上噪声会把方差“撑大”,导致评价函数在失焦时反而得分不低。

我建议:用方差做评价函数时,一定要配合自动增益控制(AGC)。否则暗场景下方差曲线太平,没法用。

Entropy(熵)

熵来自信息论,衡量图像的“信息量”。清晰图像纹理丰富,熵值高;模糊图像信息少,熵值低。

理论上熵很完美。但实际用起来,计算量太大——你要统计灰度直方图,还要算对数。在嵌入式平台上,跑一帧熵的时间,够跑三次 Sobel 了。

我的建议:熵适合做离线标定或辅助判断,不适合做实时 AF 的主评价函数。除非你的芯片有硬件加速。

3.3 基于频域的函数

这类函数把图像变换到频域,看高频分量有多少。清晰图像高频多,模糊图像高频少。这个逻辑在低光照下依然成立——但噪声也在高频区。

FFT(快速傅里叶变换)

FFT 能精确分析频谱。但计算量巨大,一帧 1080p 图像做 FFT,在普通嵌入式芯片上可能要几十毫秒。

我一般只取 FFT 结果中的高频环带(比如半径 0.4~0.8 归一化频率的区域)求和,作为清晰度评分。

// FFT 高频评分(伪代码)
float fft_highfreq_score(complex* fft_result, int w, int h) {
    float sum = 0.0f;
    float cx = w/2, cy = h/2;
    for (int y = 0; y < h; y++) {
        for (int x = 0; x < w; x++) {
            float r = sqrtf((x-cx)*(x-cx) + (y-cy)*(y-cy)) / (w/2);
            if (r > 0.4f && r < 0.8f) {
                sum += cabs(fft_result[y*w + x]);
            }
        }
    }
    return sum;
}
注意:FFT 对噪声非常敏感。低光照下,噪声的高频分量可能比真实纹理还多。我踩过这个坑——FFT 评分在纯噪声图像上反而很高,AF 直接拉到无穷远去了。

DCT(离散余弦变换)

DCT 是 JPEG 压缩的核心变换。它比 FFT 计算量小一些,而且能量集中——大部分信息集中在低频系数里。

我常用的做法:只取 DCT 的左上角 8×8 或 16×16 块的高频系数,求和作为评分。这样计算量可控,效果也还行。

3.4 三类函数的对比

类别 代表函数 低光鲁棒性 计算量 适用场景
梯度类 Sobel / Laplacian / Tenengrad 中(需降噪配合) 低~中 通用场景,低光需预处理
统计类 Brenner / Variance / Entropy 高(Brenner 尤佳) 低(熵除外) 低光、低功耗嵌入式
频域类 FFT / DCT 低(噪声干扰大) 高 离线标定、高算力平台

3.5 评价函数选型框架

下面这张图是我自己总结的选型流程。每次做新项目,我都会先走一遍这个框架。

评价函数选型框架 评价函数选型决策框架 输入:场景需求 算力是否充足?(> 100MIPS 可用) 否(低算力) 是(高算力) 统计类:Brenner 低光表现:良好 梯度类:Tenengrad 需配合降噪 噪声是否可控?(SNR > 15dB) 否(高噪声) 是(低噪声) 加降噪 + 统计类 梯度类 / 频域类 说明:实际项目中,建议先用 Brenner 快速验证,再根据效果切换到 Tenengrad 或 DCT

3.6 我的选型建议

做了这么多年 AF,我总结了几条铁律:

  1. 低光照下,优先选统计类函数。Brenner 是我的首选,计算量小,抗噪性好。
  2. 梯度类函数必须配降噪。我习惯用双边滤波或引导滤波做预处理,再用 Sobel 或 Tenengrad。
  3. 频域类函数慎用。除非你的平台算力足够,而且场景噪声可控。否则 FFT 和 DCT 在低光下就是灾难。
  4. 不要迷信单一函数。我现在的做法是:粗搜索用 Brenner,细搜索用 Tenengrad。两个函数互补,效果比单个好得多。
核心观点:没有最好的评价函数,只有最适合当前场景的评价函数。低光照下,稳定比精度更重要。选一个噪声鲁棒性强的函数,比选一个理论精度高的函数,实际效果要好得多。