3. 对焦搜索策略:全局搜索、爬山算法、Fibonacci搜索、黄金分割搜索、自适应步长

对焦搜索策略,说白了就是镜头怎么找到最清晰的那个位置。你想想看,镜头从近端跑到远端,中间有几十甚至上百个步进位置,总不能一个一个试过去吧?那得试到猴年马月去。

我刚开始做自动对焦的时候,觉得这事儿挺简单——从最近扫到最远,取清晰度最高的那个位置不就完了?后来发现,这里面门道多着呢。搜索策略选得好,对焦又快又准;选得不好,要么来回震荡,要么直接跑偏。

今天咱们就把五种主流策略掰开揉碎了讲。每种策略我都会结合项目里的实际案例,告诉你什么时候该用,什么时候千万别用。

3.1 全局搜索——最笨但最可靠的方法

全局搜索的思路特别直白:把整个对焦范围分成N个等间距的步进,每个位置算一次清晰度评价值,最后挑最大值对应的位置。

核心公式:

步进数 N = (远界 - 近界) / 步长

时间复杂度:O(N),N通常取 20~50

// 全局搜索伪代码
float globalSearch() {
    float bestPos = 0;
    float bestScore = 0;
    
    for (int i = 0; i < TOTAL_STEPS; i++) {
        moveLensTo(i * STEP_SIZE);
        float score = calculateSharpness();
        if (score > bestScore) {
            bestScore = score;
            bestPos = i * STEP_SIZE;
        }
    }
    return bestPos;
}

优点很明显:不会陷入局部极值,一定能找到全局最优。缺点也扎眼:慢。我做过一个项目,镜头行程有2000步,全局搜索每步都要采图算评价值,一次对焦要花3秒多,用户都疯了。

什么时候用全局搜索?

  • 场景变化剧烈,清晰度曲线可能有多个峰值
  • 对焦精度要求极高,不能容忍任何误判
  • 系统启动时的首次对焦(冷启动)
  • 步进数控制在20以内时,速度还能接受

3.2 爬山算法——又快又险的经典方案

爬山算法模拟的是人眼对焦的过程:先往一个方向走,如果清晰度在上升,就继续走;一旦开始下降,就回头找峰值。

我习惯把爬山算法分成两种:

类型 策略 适用场景
固定步长爬山 每次移动固定步长,直到清晰度下降 场景稳定,曲线平滑
变步长爬山 根据梯度大小动态调整步长 需要平衡速度与精度
// 固定步长爬山算法
float hillClimbing() {
    float currentPos = startPos;
    float currentScore = calculateSharpness();
    int direction = 1;  // 1: 正向, -1: 反向
    
    while (true) {
        float nextPos = currentPos + direction * STEP_SIZE;
        float nextScore = calculateSharpnessAt(nextPos);
        
        if (nextScore > currentScore) {
            currentPos = nextPos;
            currentScore = nextScore;
        } else {
            // 尝试反向
            direction *= -1;
            nextPos = currentPos + direction * STEP_SIZE;
            nextScore = calculateSharpnessAt(nextPos);
            
            if (nextScore <= currentScore) {
                break;  // 找到峰值
            }
        }
    }
    return currentPos;
}

我曾经踩过的坑:

有一次在户外强光下调试,爬山算法总是对不准。查了半天才发现,镜头在某个位置因为反光产生了虚假峰值,爬山算法直接卡在那儿不动了。后来我加了峰值有效性校验——如果峰值两侧的下降梯度不对称,就判定为虚假峰值,重新搜索。

3.3 Fibonacci搜索——数学之美

Fibonacci搜索利用斐波那契数列来缩小搜索区间。它的核心思想是:每次迭代只计算两个新位置,就能把搜索范围缩小到原来的约0.618倍。

为什么用Fibonacci数列?因为它在有限步数内能达到最优的区间压缩比。说白了,就是数学上证明了这是最省计算量的方法。

// Fibonacci搜索
float fibonacciSearch(float left, float right) {
    // 生成Fibonacci数列,直到覆盖搜索范围
    int fib[2] = {1, 1};
    while (fib[1] < (right - left) / PRECISION) {
        int next = fib[0] + fib[1];
        fib[0] = fib[1];
        fib[1] = next;
    }
    
    float x1 = left + (float)fib[0] / fib[1] * (right - left);
    float x2 = left + (float)fib[1] / fib[2] * (right - left);
    
    // 迭代缩小范围
    while (abs(x2 - x1) > PRECISION) {
        float s1 = calculateSharpnessAt(x1);
        float s2 = calculateSharpnessAt(x2);
        
        if (s1 < s2) {
            left = x1;
            x1 = x2;
            x2 = left + (float)fib[1] / fib[2] * (right - left);
        } else {
            right = x2;
            x2 = x1;
            x1 = left + (float)fib[0] / fib[1] * (right - left);
        }
    }
    return (x1 + x2) / 2;
}

Fibonacci搜索 vs 黄金分割搜索:

两者本质上是同一类算法。Fibonacci搜索在有限步数内最优,但需要预先生成数列;黄金分割搜索用固定比例0.618,实现更简单,适合无限迭代的场景。

3.4 黄金分割搜索——工程界的宠儿

黄金分割搜索是Fibonacci搜索的工程简化版。它固定使用0.618这个比例来分割区间,每次迭代只需要计算一个新位置。

我个人特别喜欢黄金分割搜索。为什么?因为它实现简单,性能稳定,而且不需要像Fibonacci那样预计算数列。在嵌入式系统上跑,代码量小,内存占用低。

// 黄金分割搜索
float goldenSectionSearch(float left, float right) {
    const float GOLDEN_RATIO = 0.6180339887f;
    
    float x1 = right - GOLDEN_RATIO * (right - left);
    float x2 = left + GOLDEN_RATIO * (right - left);
    
    float s1 = calculateSharpnessAt(x1);
    float s2 = calculateSharpnessAt(x2);
    
    while (abs(right - left) > PRECISION) {
        if (s1 < s2) {
            left = x1;
            x1 = x2;
            s1 = s2;
            x2 = left + GOLDEN_RATIO * (right - left);
            s2 = calculateSharpnessAt(x2);
        } else {
            right = x2;
            x2 = x1;
            s2 = s1;
            x1 = right - GOLDEN_RATIO * (right - left);
            s1 = calculateSharpnessAt(x1);
        }
    }
    return (left + right) / 2;
}

工程经验:

黄金分割搜索在单峰曲线上表现极好,但遇到多峰曲线就麻烦了。我建议先用粗步长做一次全局扫描,确定大致范围,再用黄金分割搜索精确定位。这样既保证了全局性,又利用了黄金分割的收敛速度。

3.5 自适应步长——动态调整的艺术

自适应步长策略的核心是:根据当前清晰度曲线的梯度大小,动态调整搜索步长。梯度大的地方(曲线陡峭),步长小一点,避免越过峰值;梯度小的地方(曲线平坦),步长大一点,加快搜索速度。

我常用的自适应步长公式:

// 自适应步长计算
float adaptiveStep(float currentGradient, float baseStep) {
    // 梯度归一化到 [0, 1]
    float normalizedGrad = min(1.0f, abs(currentGradient) / MAX_GRADIENT);
    
    // 步长 = 基础步长 * (1 - 梯度系数 * 归一化梯度)
    float step = baseStep * (1.0f - 0.8f * normalizedGrad);
    
    // 限制步长范围
    return clamp(step, MIN_STEP, MAX_STEP);
}

我曾经踩过的坑:

自适应步长在低光照环境下特别容易出问题。因为低光照下清晰度评价值的信噪比很低,梯度计算不稳定,步长会忽大忽小。后来我加了梯度平滑滤波——取最近3次梯度的加权平均,才把这个问题压下去。

3.6 五种策略对比

策略 收敛速度 抗干扰能力 实现复杂度 适用场景
全局搜索 慢 强 低 冷启动、多峰场景
爬山算法 中 弱 低 单峰、实时性要求高
Fibonacci搜索 快 中 中 有限步数内最优
黄金分割搜索 快 中 低 嵌入式、单峰精确定位
自适应步长 快 中 高 动态场景、光照变化大

3.7 知识体系总览

下面这张图把五种搜索策略的关系和适用场景梳理清楚了。你可以把它当作选型时的参考地图。

对焦搜索策略知识体系 对焦搜索策略 全局搜索 爬山算法 Fibonacci搜索 黄金分割搜索 自适应步长 特性 最可靠,但最慢 适合冷启动 步进数≤20时可用 特性 速度快,易实现 易陷入局部极值 需加防抖处理 特性 数学最优解 需预生成数列 实现稍复杂 特性 工程简化版 固定比例0.618 嵌入式首选 特性 动态调整步长 需梯度平滑滤波 低光照需特殊处理 选型建议 冷启动→全局搜索 | 实时对焦→爬山+自适应 | 精确定位→黄金分割

3.8 实战选型建议

说了这么多,到底该怎么选?我个人的经验是:

  • 冷启动或场景切换:先用全局搜索(步长设大一点,比如10步),找到大致位置,再切到黄金分割搜索精确定位。
  • 连续对焦(视频模式):用爬山算法+自适应步长。每次对焦从上一次位置开始,步长根据场景变化速度动态调整。
  • 低光照环境:优先用全局搜索或黄金分割搜索。爬山算法在低光照下容易误判方向。
  • 嵌入式系统(资源受限):黄金分割搜索是首选。代码量小,不需要额外存储空间。

核心原则:

没有万能的搜索策略。好的工程师会根据场景组合使用多种策略。我习惯把搜索过程分成两阶段:粗搜(全局搜索或大步长爬山)+ 精搜(黄金分割或小步长爬山)。这样既保证了全局性,又兼顾了速度。