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 知识体系总览
下面这张图把五种搜索策略的关系和适用场景梳理清楚了。你可以把它当作选型时的参考地图。
3.8 实战选型建议
说了这么多,到底该怎么选?我个人的经验是:
- 冷启动或场景切换:先用全局搜索(步长设大一点,比如10步),找到大致位置,再切到黄金分割搜索精确定位。
- 连续对焦(视频模式):用爬山算法+自适应步长。每次对焦从上一次位置开始,步长根据场景变化速度动态调整。
- 低光照环境:优先用全局搜索或黄金分割搜索。爬山算法在低光照下容易误判方向。
- 嵌入式系统(资源受限):黄金分割搜索是首选。代码量小,不需要额外存储空间。
核心原则:
没有万能的搜索策略。好的工程师会根据场景组合使用多种策略。我习惯把搜索过程分成两阶段:粗搜(全局搜索或大步长爬山)+ 精搜(黄金分割或小步长爬山)。这样既保证了全局性,又兼顾了速度。