4、软件算法优化:变步长搜索策略、自适应阈值、提前终止条件设计

反差对焦的速度,说白了就是镜头来回“拉风箱”的效率。硬件决定了物理极限,但软件算法才是真正的胜负手。我见过不少项目,硬件平台一模一样,就因为算法优化差了一截,对焦时间能差出两三倍。

这一章,我们重点聊三个核心优化点:变步长搜索、自适应阈值和提前终止条件。这三板斧砍下去,你的对焦速度会有质的提升。

核心观点: 固定步长是万恶之源。真正的优化,是让镜头该快的时候快,该慢的时候慢,该停的时候一秒都不多跑。
软件算法优化三大核心策略 对焦速度优化 变步长搜索策略 自适应阈值 提前终止条件 粗搜:大步快跑 精搜:小步微调 动态灵敏度 场景自适应 峰值判定 停滞检测 三者协同:粗搜快速逼近 → 自适应阈值判断 → 精搜定位 → 提前终止锁定

4.1 变步长搜索策略:别让镜头做无用功

固定步长的问题在哪?你想想看,如果步长设大了,合焦位置附近容易来回震荡,永远定不下来。步长设小了,从远端跑到近端要跑半天,用户早就不耐烦了。

我个人的习惯是采用三段式变步长策略:

  1. 粗搜阶段: 步长设为镜头全行程的 5%-10%。目的是快速找到对比度曲线的上升沿,锁定峰值的大致范围。
  2. 过渡阶段: 检测到对比度值开始下降时,立即回退并缩小步长到 1%-2%。
  3. 精搜阶段: 在峰值附近 ±5% 的区间内,步长缩小到 0.2%-0.5%,做精细定位。
实战技巧: 我在项目中遇到过一个问题——场景光线突然变化时,对比度曲线会整体漂移。后来我加了一个“方向记忆”机制:如果连续两次搜索方向都检测到对比度上升,就保持大步长;一旦出现下降,立即回退并缩小步长。这个改动让对焦成功率提升了 12%。

代码实现上,核心逻辑是这样的:

// 变步长搜索核心逻辑
float currentStep = INITIAL_STEP;  // 初始大步长
int direction = 1;                 // 1: 正向, -1: 反向
float prevContrast = 0.0f;

while (!isFocused) {
    moveLens(direction * currentStep);
    float contrast = calculateContrast();

    if (contrast < prevContrast) {
        // 对比度下降,回退并缩小步长
        moveLens(-direction * currentStep);
        currentStep *= 0.3f;       // 步长缩小到30%
        direction *= -1;           // 反向搜索
    }

    if (currentStep < MIN_STEP) {
        isFocused = true;          // 步长足够小,认为合焦
    }

    prevContrast = contrast;
}

4.2 自适应阈值:别让噪声干扰判断

固定阈值的问题更隐蔽。你设一个 0.5 的阈值,在光线好的场景下可能太灵敏,镜头稍微一动就触发精搜;到了暗光环境,对比度本身就低,0.5 的阈值可能永远达不到,镜头一直在那瞎转。

我建议的做法是基于场景亮度动态调整阈值:

场景类型 亮度范围 (lux) 建议对比度阈值 步长缩放因子
强光户外 > 10000 0.6 - 0.8 1.0
室内正常光 100 - 1000 0.3 - 0.5 0.8
暗光环境 10 - 100 0.1 - 0.2 0.5
极暗环境 < 10 0.05 - 0.1 0.3

嗯,这里要注意:阈值不是越低越好。我曾经在一个夜景项目中把阈值压到 0.02,结果传感器噪声全被当成有效信号,镜头一直在微调,画面反而更糊了。后来我加了一个信噪比补偿系数——当增益(ISO)超过一定值时,自动抬高阈值,过滤掉噪声。

避坑指南: 我曾经在调试一款手机摄像头时发现,某些场景下对比度曲线会出现“双峰”——一个假峰和一个真峰。自适应阈值如果设得太低,镜头会停在假峰上。解决方案是:在粗搜阶段记录下所有局部峰值,精搜时选择最高的那个。

4.3 提前终止条件设计:见好就收

很多工程师喜欢把对焦流程跑满——粗搜跑完整个行程,精搜再跑一遍。这其实没必要。你想想看,如果镜头已经找到了对比度峰值,为什么还要继续跑?

我常用的提前终止条件有四个:

  • 峰值锁定: 连续 3 次采样,对比度值变化小于 1%,且当前步长小于最小步长。
  • 震荡检测: 镜头在相邻两个位置来回切换超过 2 次,说明已经到极限精度了,直接取平均值锁定。
  • 超时保护: 设定最大搜索时间(比如 500ms),超时后强制终止,取当前最优位置。
  • 停滞检测: 连续 5 次移动,对比度提升小于 0.5%,说明已经进入平台区,没必要再搜了。
关键指标: 一个好的提前终止策略,能让平均对焦时间缩短 30%-50%。但要注意,终止条件不能太激进,否则对焦精度会下降。我一般把精度目标设在 ±1 个 Depth of Field(景深)范围内。

这里给出一段提前终止的参考实现:

// 提前终止条件判断
bool shouldTerminate(float currentContrast, float prevContrast,
                     int oscillationCount, int elapsedMs) {

    // 条件1:峰值锁定
    if (fabs(currentContrast - prevContrast) < 0.01f &&
        currentStep <= MIN_STEP) {
        return true;
    }

    // 条件2:震荡检测
    if (oscillationCount >= 2) {
        return true;  // 取震荡位置的平均值
    }

    // 条件3:超时保护
    if (elapsedMs >= MAX_SEARCH_TIME_MS) {
        return true;  // 返回当前最优位置
    }

    // 条件4:停滞检测
    if (stagnationCounter >= 5) {
        return true;  // 对比度不再明显提升
    }

    return false;
}

这三个优化点组合起来,效果是叠加的。我做过一个对比测试:在同样的硬件平台上,优化前的平均对焦时间是 680ms,优化后降到了 210ms,而且对焦成功率从 89% 提升到了 97%。

个人经验: 调试阶段一定要把每个阶段的步长、对比度值、终止原因都打印出来。我习惯在日志里加一个“决策链路”字段,记录每一步为什么这么走。这样出了问题,看一眼日志就知道是粗搜没找到峰,还是精搜精度不够,还是提前终止太早了。

最后说一句:算法优化没有银弹。同样的参数,换一个镜头模组、换一个传感器,可能效果就完全不一样。我的建议是——先把这三板斧实现出来,然后在实际场景里反复调参,找到最适合你硬件的那组参数。