4. 对焦算法基础:爬山算法详解、全局搜索与局部搜索、步长自适应策略、对焦窗口选择

各位好,我是老张。今天咱们聊聊对焦算法里最核心的几个概念。说实话,我入行那会儿,对焦算法还没现在这么智能,很多手机还在用最原始的爬山法。但你别小看它,直到今天,几乎所有自动对焦系统里,爬山算法依然是灵魂。

为什么这么说?你想想看,相机怎么知道当前画面清不清晰?它得有个评价标准。这个标准就是对比度——画面边缘越锐利,对比度越高,图像越清晰。爬山算法,说白了就是让镜头来回移动,找到那个对比度最高的位置。

4.1 爬山算法详解

爬山算法的思路很直观。我习惯把它比作「盲人爬山」——你站在一个山坡上,眼睛看不见,只能用脚探路。哪边高就往哪边挪一步,直到发现两边都变低了,那你就站在山顶了。

在自动对焦里,这个「山」就是对比度曲线。横轴是镜头位置,纵轴是图像清晰度评分。算法的工作就是找到曲线的最高点。

核心流程:

  1. 确定一个初始方向(比如向近端移动)
  2. 移动镜头,计算当前帧的对比度值
  3. 如果对比度上升,继续同方向移动
  4. 如果对比度下降,说明翻过了山顶,掉头往回走
  5. 反复逼近,直到找到峰值

嗯,这里要注意一个坑。我曾经在项目里遇到过,爬山算法在低光照环境下特别容易「迷路」。因为噪声会干扰对比度计算,明明没对上焦,评分却虚高。后来我加了一个噪声抑制的预处理,情况才好转。

下面是一个简化的爬山算法伪代码,你可以感受一下:

// 爬山算法伪代码
int currentPos = 0;
int currentScore = getSharpness(currentPos);
int direction = +1;  // 初始方向

while (true) {
    int nextPos = currentPos + direction * stepSize;
    int nextScore = getSharpness(nextPos);
    
    if (nextScore > currentScore) {
        // 还在上升,继续走
        currentPos = nextPos;
        currentScore = nextScore;
    } else {
        // 下降了,掉头并缩小步长
        direction = -direction;
        stepSize = stepSize / 2;
        if (stepSize < MIN_STEP) break;
    }
}

个人经验:爬山算法最怕「假峰」。比如拍摄纯色墙壁,对比度曲线几乎是平的,算法会随机乱跑。我建议在启动爬山前,先做一个简单的场景判断——如果对比度方差太小,直接跳到预设位置。

4.2 全局搜索与局部搜索

爬山算法有个致命弱点——它只能找到局部最优,不是全局最优。你想想看,如果对比度曲线有多个波峰,爬山算法爬到第一个峰顶就停了,根本不知道后面还有更高的山。

这就引出了全局搜索和局部搜索的配合问题。

搜索方式 特点 适用场景
全局搜索 遍历整个对焦范围,找到最高峰 开机首次对焦、场景突变
局部搜索 在当前位置附近精细搜索 连续对焦、微距切换

全局搜索说白了就是「地毯式排查」。镜头从最近端跑到最远端,每走一步算一次对比度,最后挑出最大值。这个方法可靠,但慢。我记得有一次做4K视频对焦,全局搜索一次要花1.2秒,用户都骂翻了。

局部搜索就快多了。它假设目标没有大幅移动,只在当前位置前后一小段范围内搜索。但问题来了——如果目标突然从近处跳到远处,局部搜索就抓瞎了。

我个人的做法是:用全局搜索做「粗定位」,用局部搜索做「精调」。就像狙击手先通过望远镜找到目标大致方位,再用瞄准镜微调。

避坑指南:我曾经在一个项目中,全局搜索的步长设得太大了。结果呢?明明有一个很锐利的峰,因为采样点太少,直接跳过去了。后来我规定:全局搜索的步长不能超过景深的一半。这个经验值你可以记一下。

4.3 步长自适应策略

步长怎么定?这是个老生常谈的问题。步长太大,容易越过峰值;步长太小,对焦慢得像蜗牛。

我的思路是:步长应该动态调整。说白了,就是「大路大步走,小路小步挪」。

具体怎么做?我分享三种常用策略:

  • 基于对比度变化率:如果对比度上升很快,说明离峰值还远,用大步;如果变化趋缓,说明快到山顶了,换小步。
  • 基于镜头位置:在微距端和无穷远端,景深变化剧烈,步长要相应调整。
  • 基于历史数据:记录上一次对焦成功的步长模式,下次优先使用。

下面是我在项目中用过的一个自适应步长公式:

// 自适应步长计算
float adaptiveStep(float currentScore, float prevScore, float baseStep) {
    float delta = abs(currentScore - prevScore);
    float ratio = delta / max(currentScore, prevScore);
    
    if (ratio > 0.1) {
        // 变化剧烈,用大步
        return baseStep * 2.0;
    } else if (ratio > 0.02) {
        // 中等变化,保持步长
        return baseStep;
    } else {
        // 变化平缓,用小步精细搜索
        return baseStep * 0.5;
    }
}

一个小技巧:我习惯在步长调整时加一个「惯性因子」。就是说,不要一下子从大步切到小步,而是平滑过渡。这样能避免镜头来回震荡,对焦过程更顺滑。

4.4 对焦窗口选择

最后一个话题,对焦窗口。你想想看,整个画面那么大,你不可能全图计算对比度吧?一来计算量大,二来背景干扰多。所以我们需要选择一个合适的区域作为对焦窗口。

对焦窗口的选择,直接决定了算法的鲁棒性。我见过最蠢的做法——固定取画面中心的一个小方块。结果呢?用户把主体放在画面边缘,相机对着背景对焦,拍出来全是糊的。

现在主流的方法有这么几种:

  • 中心权重窗口:以画面中心为主,边缘权重递减。适合大部分场景。
  • 人脸/物体跟踪窗口:检测到人脸后,窗口跟着人脸走。这个在手机人像模式里很常见。
  • 多窗口投票:在画面中设置多个候选窗口,每个窗口独立计算对比度,最后投票选出最佳对焦位置。

我个人比较推崇多窗口投票。为什么?因为它容错率高。就算某个窗口被遮挡了,其他窗口还能正常工作。

下面是一个多窗口投票的示意图:

窗口1 (中心) 窗口2 窗口3 窗口4 窗口5 画面边界 多窗口对焦区域示意图

注意:窗口不是越多越好。窗口太多,计算量翻倍,而且容易引入噪声。我一般控制在3-5个窗口。另外,窗口大小也要注意——太小了容易受噪声影响,太大了又失去局部性。我习惯用画面面积的5%-10%作为单个窗口的大小。

好了,关于对焦算法的基础就聊到这儿。爬山算法、全局与局部搜索、步长自适应、窗口选择,这四个点环环相扣。你想想看,任何一个环节没做好,最终拍出来的照片就是糊的。做影像系统就是这样,细节决定成败。

专注资料整理