AF 算法核心:对焦评价函数与搜索策略

自动对焦这事儿,说白了就是让镜头自己找到最清晰的那个位置。我刚开始做摄像头驱动时,觉得 AF 不就是来回动一动嘛,后来才发现——评价函数选不对,搜索策略用不好,画面永远在“拉风箱”。

这一章我们聚焦两个核心问题:怎么判断画面清不清晰(评价函数),以及怎么快速找到最清晰的位置(搜索策略)。

对焦评价函数:清晰度的“尺子”

相机自己又没长眼睛,它怎么知道当前画面清不清晰?靠的是评价函数。评价函数输出一个数值,数值越大,代表图像高频信息越丰富——说白了就是边缘越锐利,画面越清晰。

我见过不少新手直接拿像素灰度方差当评价函数,结果在光照变化时频频误判。真正工程上常用的,是下面这三种基于梯度的算子。

1. Tenengrad 函数

Tenengrad 用的是 Sobel 算子,分别计算水平和垂直方向的梯度,然后求平方和。公式不复杂,但效果很稳。

// Tenengrad 评价函数实现
float tenengrad_eval(uint8_t *img, int w, int h) {
    float sum = 0.0f;
    int gx, gy;
    for (int y = 1; y < h-1; y++) {
        for (int x = 1; x < w-1; x++) {
            // Sobel 水平梯度
            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 垂直梯度
            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 sum / (w * h);  // 归一化,方便跨分辨率比较
}
我的经验:Tenengrad 在纹理丰富的场景表现最好。但如果你拍的是白墙或者天空,梯度值会非常低,这时候容易误判为“失焦”。我曾在安防摄像头项目里遇到过——晚上对着白墙,AF 死活找不到焦点。后来加了一个亮度自适应阈值,才解决。

2. Laplacian 函数

Laplacian 算子是二阶微分,对边缘的响应比 Sobel 更敏感。换句话说,它更容易“看到”微小的模糊变化。

// Laplacian 评价函数(简化版)
float laplacian_eval(uint8_t *img, int w, int h) {
    float sum = 0.0f;
    int lap;
    for (int y = 1; y < h-1; y++) {
        for (int x = 1; x < w-1; x++) {
            lap = 4*img[y*w + x]
                 - img[(y-1)*w + x] - img[(y+1)*w + x]
                 - img[y*w + (x-1)] - img[y*w + (x+1)];
            sum += abs(lap);  // 取值,避免正负抵消
        }
    }
    return sum / (w * h);
}
注意:Laplacian 对噪声非常敏感。传感器有暗电流噪声时,评价曲线会变得毛刺很多。我曾经在低光照下用 Laplacian,结果曲线抖动得像心电图——后来先做了一层高斯滤波,才把曲线磨平。

3. Sobel 函数(仅水平或垂直)

有时候我们只关心某个方向的清晰度。比如文字识别场景,水平方向的边缘更重要。这时候可以只算 Sobel 水平梯度。

评价函数 计算量 抗噪性 适用场景
Tenengrad 中 较好 通用场景,纹理丰富
Laplacian 低 较差 微距、高对比度场景
Sobel(单方向) 低 中等 条码、文字、特定纹理

搜索策略:怎么找到峰值

评价函数算出来了,接下来就是让镜头移动,找到评价函数的最大值。这就像爬山——你站在山脚,看不见山顶在哪,只能一步一步试探。

1. 爬山法(Hill Climbing)

爬山法是最常用的搜索策略。思路很简单:朝一个方向移动,如果评价函数值变大,就继续;如果变小,就回头。

// 爬山法搜索峰值(伪代码)
int hill_climbing_search() {
    int pos = current_position;
    float val = evaluate(pos);
    int step = STEP_SIZE;
    int direction = 1;  // 1: 正向, -1: 反向
    
    while (1) {
        int next_pos = pos + direction * step;
        float next_val = evaluate(next_pos);
        
        if (next_val > val) {
            // 还在上升,继续
            pos = next_pos;
            val = next_val;
        } else {
            // 下降或持平,换方向
            direction = -direction;
            step = step / 2;  // 步长减半,精细搜索
            if (step < MIN_STEP) break;
        }
    }
    return pos;
}
核心要点:爬山法最大的坑是“局部峰值”。如果评价曲线有多个波峰,爬山法可能停在半山腰。我遇到过最典型的情况——镜头前面有灰尘,灰尘边缘产生了一个小峰值,爬山法直接停在了那里,真正的焦点反而没找到。

2. 全局搜索(Global Search)

全局搜索就是暴力遍历整个对焦范围。虽然慢,但一定能找到全局最大值。

  • 优点:不会陷入局部峰值,结果可靠。
  • 缺点:速度慢。如果对焦步进有 1000 步,每一步都要算一次评价函数,耗时可能几百毫秒。
  • 适用场景:初始化对焦、场景切换后的首次对焦。

我一般在产品里这样搭配:首次对焦用全局搜索,找到大致位置;后续跟踪用爬山法,微调即可。这样既保证了准确性,又兼顾了速度。

评价曲线长什么样?

下面这张图展示了理想情况下的评价函数曲线。横轴是镜头位置,纵轴是清晰度评价值。

对焦评价函数曲线示意 镜头位置 评价值 全局峰值 局部峰值 理想曲线是单峰的,但实际中常出现多个局部峰值。 爬山法可能停在局部峰值,全局搜索则能找到最高点。

实际项目中的选择建议

你可能会问:那我到底该用哪个?我个人的习惯是这样的——

  1. 先看场景:如果是室内、光照稳定、纹理丰富,Tenengrad + 爬山法就够了。
  2. 再看硬件:如果镜头步进电机精度高、响应快,可以用更细的步长做全局搜索。
  3. 最后看功耗:爬山法计算量小,适合电池供电的设备;全局搜索费电,适合插电的设备。
避坑指南:我曾经在一个无人机项目里,直接用爬山法做连续对焦。结果无人机飞过一片水面时,评价曲线剧烈抖动,爬山法来回震荡,镜头一直在“呼吸”。后来我加了一个死区——评价值变化小于 5% 时不触发对焦,才稳住。

小结

这一章我们聊了两个核心模块:评价函数和搜索策略。评价函数是“眼睛”,搜索策略是“腿”。眼睛要准,腿要稳。Tenengrad、Laplacian、Sobel 各有优劣,爬山法和全局搜索各有适用场景。实际工程中,往往是组合使用——全局搜索找大方向,爬山法做精细调整。