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 之间选。
- 步长太小:从起始位置到峰值可能要跑几百步,对焦速度慢得让人抓狂。
- 噪声干扰:清晰度值本身有波动,固定步长下很容易误判方向。
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)));
}
自适应步长还有一个好处——它对不同模组的兼容性更好。同一个算法,换一个镜头模组不用改参数,算法自己会适应。我在项目里用这套逻辑,把模组适配时间从原来的半天缩短到了 1 小时。
3.4 峰值检测与防抖动
爬山算法最大的敌人不是速度慢,而是抖动。你明明已经走到峰值了,但清晰度值因为噪声、光源频闪、传感器读出噪声等原因上下波动,算法以为还没到,继续来回跑。
我常用的防抖动手段有三个:
- 滞回比较器:设置一个阈值窗口,清晰度变化超过窗口才认为方向有效。
- 滑动窗口滤波:取最近 N 次清晰度值的平均或中值,再做方向判断。
- 峰值锁定机制:检测到峰值后,强制等待一段时间再确认。
// 带滞回和滤波的峰值检测
#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 整体流程与策略选择
下面这张图是我在实际项目中用的爬山算法优化流程,你可以参考一下:
这个流程我用了好几年,在几十个模组项目上验证过。核心就三点:大步粗搜找区域,小步精搜定位置,防抖动保稳定。
- 如果对焦速度要求高(比如 30fps 视频追焦),优先用变步长 + 滞回比较器,计算量小
- 如果精度要求高(比如 4800 万像素拍照),用自适应步长 + 滑动窗口滤波,但要注意滤波延迟
- 如果模组一致性差(不同模组曲线差异大),自适应步长是必选项
嗯,爬山算法优化就讲到这里。记住一点——没有万能的步长策略,只有适合你当前模组和场景的策略。多跑几组数据,看看清晰度曲线的形状,你自然就知道该用哪种了。