3、爬山算法优化:变步长爬山、自适应步长策略、峰值检测与防抖动

爬山算法,说白了就是「摸着石头过河」。你站在当前清晰度位置,往左走一步看看,往右走一步看看,哪边更清晰就往哪边挪。听起来简单,但实际做起来坑不少。

我记得刚入行那会儿,第一次在模组上跑爬山算法,镜头就在对焦位置附近来回震荡,死活停不下来。后来才明白——固定步长爬山,在真实硬件上就是个「半成品」。

3.1 固定步长爬山的痛点

先看一个最基础的版本。每次移动固定步长,比如 10 个 DAC 码值。

// 固定步长爬山伪代码
int current_pos = 200;
int step = 10;
int prev_sharpness = get_sharpness(current_pos - step);
int curr_sharpness = get_sharpness(current_pos);

while (1) {
    int next_sharpness = get_sharpness(current_pos + step);
    if (next_sharpness > curr_sharpness) {
        current_pos += step;  // 往右走
        prev_sharpness = curr_sharpness;
        curr_sharpness = next_sharpness;
    } else if (prev_sharpness > curr_sharpness) {
        current_pos -= step;  // 往左走
        curr_sharpness = prev_sharpness;
        // 需要重新计算 prev_sharpness
    } else {
        break;  // 找到峰值
    }
}

这个代码看着逻辑没问题,但实际跑起来你会遇到三个麻烦:

  • 步长太大:容易跳过真正的峰值点,精度不够。比如真实峰值在 205,步长 10 的话,你只能在 200 或 210 之间选。
  • 步长太小:从起始位置到峰值可能要跑几百步,对焦速度慢得让人抓狂。
  • 噪声干扰:清晰度值本身有波动,固定步长下很容易误判方向。
我曾经踩过的坑: 有一次在户外强光下调试,固定步长设了 8 个码值。结果镜头在峰值附近来回跳了 20 多次才停下,用户按快门时画面还在「呼吸」。后来我养成了一个习惯——任何爬山算法都必须加步长自适应逻辑。

3.2 变步长爬山:粗调 + 细调

变步长的思路很直观:先大步跑,再小步挪。就像你找停车位,先开到附近区域,再慢慢调整位置。

我个人习惯把变步长分成三个阶段:

阶段 步长范围 触发条件 目的
粗搜 20~50 DAC 起始阶段 快速接近峰值区域
中搜 8~15 DAC 清晰度变化率 < 30% 缩小搜索范围
精搜 2~5 DAC 连续两次方向反转 锁定最终峰值
// 变步长爬山示例
int step_table[] = {30, 12, 4};
int phase = 0;  // 0:粗搜, 1:中搜, 2:精搜
int step = step_table[phase];
int direction = 0;  // -1:左, 0:未定, 1:右
int reverse_count = 0;

while (phase < 3) {
    int next_val = get_sharpness(current_pos + step * direction);
    if (next_val > curr_sharpness) {
        // 继续同方向
        current_pos += step * direction;
        curr_sharpness = next_val;
        reverse_count = 0;
    } else {
        // 方向反转
        direction *= -1;
        reverse_count++;
        if (reverse_count >= 2) {
            phase++;  // 进入下一阶段
            step = step_table[phase];
            reverse_count = 0;
        }
    }
}

这个策略的好处是:粗搜阶段能快速跨越平坦区域,精搜阶段保证最终精度。我在一个 500 万像素的模组上测试过,变步长比固定步长快了大约 40%,而且峰值定位误差从 ±8 DAC 降到了 ±2 DAC。

3.3 自适应步长策略

变步长虽然好,但步长切换阈值是写死的。不同模组、不同光照下,清晰度曲线的「陡峭程度」差别很大。这时候就需要自适应步长——让算法自己决定走多大步。

核心思想很简单:根据清晰度变化率动态调整步长。

  • 变化率大(曲线陡峭)→ 步长减小,防止过冲
  • 变化率小(曲线平坦)→ 步长增大,加速搜索
// 自适应步长核心逻辑
float k_step = 0.5f;  // 步长调整系数
int min_step = 2;
int max_step = 40;

int delta = abs(curr_sharpness - prev_sharpness);
float ratio = (float)delta / max(curr_sharpness, 1);

// 根据变化率调整步长
if (ratio > 0.3f) {
    // 陡峭区域,缩小步长
    step = max(min_step, (int)(step * (1.0f - k_step)));
} else if (ratio < 0.1f) {
    // 平坦区域,放大步长
    step = min(max_step, (int)(step * (1.0f + k_step)));
}
我的经验: k_step 值建议设在 0.3~0.6 之间。太小了步长调整跟不上地形变化,太大了容易震荡。我一般先用 0.5 试跑,再根据实际对焦曲线微调。

自适应步长还有一个好处——它对不同模组的兼容性更好。同一个算法,换一个镜头模组不用改参数,算法自己会适应。我在项目里用这套逻辑,把模组适配时间从原来的半天缩短到了 1 小时。

3.4 峰值检测与防抖动

爬山算法最大的敌人不是速度慢,而是抖动。你明明已经走到峰值了,但清晰度值因为噪声、光源频闪、传感器读出噪声等原因上下波动,算法以为还没到,继续来回跑。

我常用的防抖动手段有三个:

  1. 滞回比较器:设置一个阈值窗口,清晰度变化超过窗口才认为方向有效。
  2. 滑动窗口滤波:取最近 N 次清晰度值的平均或中值,再做方向判断。
  3. 峰值锁定机制:检测到峰值后,强制等待一段时间再确认。
// 带滞回和滤波的峰值检测
#define HYSTERESIS 5      // 滞回阈值
#define WINDOW_SIZE 3     // 滑动窗口大小

int history[WINDOW_SIZE];
int hist_idx = 0;

int filtered_sharpness(int raw) {
    history[hist_idx % WINDOW_SIZE] = raw;
    hist_idx++;
    // 取中值滤波
    int sorted[WINDOW_SIZE];
    memcpy(sorted, history, sizeof(history));
    // 简单排序(实际用快速选择)
    for (int i = 0; i < WINDOW_SIZE; i++)
        for (int j = i+1; j < WINDOW_SIZE; j++)
            if (sorted[i] > sorted[j]) swap(&sorted[i], &sorted[j]);
    return sorted[WINDOW_SIZE / 2];
}

int peak_detect(int curr, int prev) {
    int diff = curr - prev;
    if (abs(diff) < HYSTERESIS) {
        return 0;  // 变化太小,忽略
    }
    return (diff > 0) ? 1 : -1;
}

峰值检测的核心原则:

  • 不要相信单次采样值,至少看连续 3 次的变化趋势
  • 滞回窗口大小根据噪声水平设定,一般取清晰度满量程的 1%~3%
  • 锁定峰值后,可以回退半步验证——如果回退后清晰度下降,说明峰值是真的

为什么会抖动?说白了就是信噪比不够。我遇到过最极端的情况——在暗光环境下(10 lux 以下),清晰度值的噪声幅度达到了信号幅度的 20%。那时候什么滤波算法都不好使,最后我加了硬件上的 LED 补光同步,才把噪声压下去。

3.5 整体流程与策略选择

下面这张图是我在实际项目中用的爬山算法优化流程,你可以参考一下:

爬山算法优化流程 爬山算法优化流程 起始位置 粗搜阶段:大步长 20~50 DAC 快速接近峰值区域 变化率 >30%? 是 中搜阶段:步长 8~15 DAC 缩小搜索范围 否 继续粗搜 调整方向继续大步搜索 方向反转 >=2次? 是 精搜阶段:步长 2~5 DAC 锁定最终峰值 否 峰值检测 + 防抖动 防抖动手段: ① 滞回比较器 ② 滑动窗口滤波

这个流程我用了好几年,在几十个模组项目上验证过。核心就三点:大步粗搜找区域,小步精搜定位置,防抖动保稳定。

选型建议:
  • 如果对焦速度要求高(比如 30fps 视频追焦),优先用变步长 + 滞回比较器,计算量小
  • 如果精度要求高(比如 4800 万像素拍照),用自适应步长 + 滑动窗口滤波,但要注意滤波延迟
  • 如果模组一致性差(不同模组曲线差异大),自适应步长是必选项

嗯,爬山算法优化就讲到这里。记住一点——没有万能的步长策略,只有适合你当前模组和场景的策略。多跑几组数据,看看清晰度曲线的形状,你自然就知道该用哪种了。