第三章 CDAF实现细节:搜索窗口、步长策略与速度精度权衡

反差对焦(CDAF)说起来原理不复杂——找图像对比度最大的位置嘛。但真要在嵌入式平台上跑起来,坑可不少。我最早做手机摄像头调试时,就吃过搜索窗口选得太小的亏,画面明明合焦了,拍出来却是糊的。这一章咱们就把CDAF落地的三个关键点掰开揉碎:搜索窗口怎么选、步长怎么定、速度和精度怎么平衡。

3.1 搜索窗口的选择

搜索窗口,说白了就是你在画面哪个区域算对比度。选错了窗口,对焦就偏了。

3.1.1 窗口位置

传统做法是把窗口放在画面中央,占画面面积的10%~20%。为什么?因为用户习惯把主体放中间。但现在的场景复杂多了:

  • 中央单窗口:最简单,适合人像、静物。我早期项目一直用这个,直到遇到拍集体照的场景——中间是背景墙,两边才是人,结果对焦对到墙上去了。
  • 多窗口加权:比如9宫格、13宫格,每个窗口给不同权重。人脸检测框内的权重最高,边缘区域权重低。我建议至少用3×3的网格,人脸区域权重设2.0,其他区域0.5~1.0。
  • 自适应窗口:根据场景动态调整。比如检测到人脸就缩小窗口到人脸框,检测到风景就放大到中央区域。
我的经验:窗口数量不是越多越好。超过25个窗口,计算量翻倍,但对焦精度提升微乎其微。我个人习惯用9宫格加权,性价比最高。

3.1.2 窗口大小

窗口大小直接影响对比度计算的灵敏度。太小了容易受噪声干扰,太大了会平滑掉细节。

窗口尺寸(像素) 适用场景 优缺点
32×32 ~ 64×64 低分辨率预览(640×480) 计算快,但精度低
128×128 ~ 256×256 高清预览(1080p) 平衡了速度和精度
512×512 以上 高精度对焦(4K/8K) 精度高,但计算量大

嗯,这里要注意:窗口大小要和镜头景深匹配。大光圈镜头景深浅,窗口可以小一点;小光圈镜头景深大,窗口要适当放大。我曾经在一个F1.8的镜头上用64×64的窗口,结果对焦总是来回震荡——窗口太小,对比度曲线太平坦了。

3.2 步长策略:粗搜 + 细搜

步长策略是CDAF的灵魂。你想想看,如果每一步都走得很细,对焦速度就慢;如果每一步都走得很粗,精度又不够。所以业界普遍采用两阶段策略:粗搜 + 细搜。

3.2.1 粗搜阶段

粗搜的目标是快速找到对比度峰值的大致位置。步长通常设为镜头全行程的1/10 ~ 1/20。

// 粗搜伪代码
void coarse_search() {
    int step = total_focus_range / 12;  // 步长:全行程的1/12
    int best_pos = 0;
    float max_contrast = 0;
    
    for (int pos = 0; pos <= total_focus_range; pos += step) {
        move_lens(pos);
        float contrast = calculate_contrast(window);
        if (contrast > max_contrast) {
            max_contrast = contrast;
            best_pos = pos;
        }
    }
    return best_pos;
}

粗搜的步长怎么定?我有个经验公式:步长 ≤ 景深对应的镜头位移量。比如景深是20个马达步,那步长就不能超过20步。否则可能直接跳过峰值区域。

避坑指南:我曾经在粗搜步长上吃过亏。一个项目用了1/8行程的步长,结果峰值正好落在两个采样点中间,细搜阶段怎么都找不到真正的峰值。后来改成1/12,问题就解决了。建议步长不要超过景深的一半。

3.2.2 细搜阶段

细搜在粗搜找到的峰值附近进行,步长通常是粗搜的1/4 ~ 1/8。细搜的搜索范围一般覆盖粗搜峰值前后各2~3个粗搜步长。

// 细搜伪代码
void fine_search(int coarse_peak) {
    int fine_step = coarse_step / 5;  // 细搜步长
    int search_start = coarse_peak - 2 * coarse_step;
    int search_end = coarse_peak + 2 * coarse_step;
    int best_pos = coarse_peak;
    float max_contrast = 0;
    
    for (int pos = search_start; pos <= search_end; pos += fine_step) {
        move_lens(pos);
        float contrast = calculate_contrast(window);
        if (contrast > max_contrast) {
            max_contrast = contrast;
            best_pos = pos;
        }
    }
    return best_pos;
}

细搜阶段有个关键点:要不要做插值?我个人建议做抛物线插值。用峰值点及其左右两个点的对比度值,拟合一条抛物线,抛物线的顶点就是更精确的合焦位置。这样可以用更少的采样点达到更高的精度。

3.3 对焦速度与精度的权衡

速度和精度,永远是CDAF的一对矛盾。你想想看,要快就得大步走,要准就得小步挪。怎么平衡?

3.3.1 影响速度的因素

  • 搜索步数:每走一步都要采集图像、计算对比度,这是最耗时的。
  • 图像采集延迟:从发出指令到拿到图像数据,通常有1~3帧的延迟。
  • 马达响应时间:镜头移动到目标位置需要时间,步进马达通常5~20ms。
  • 对比度计算时间:窗口越大、算法越复杂,耗时越长。

3.3.2 影响精度的因素

  • 步长大小:步长越小,能找到的峰值位置越精确。
  • 噪声水平:低光照下噪声大,对比度曲线会抖动,影响峰值判断。
  • 场景纹理:纹理丰富的场景对比度曲线尖锐,容易找到峰值;平坦场景曲线平缓,精度下降。
  • 窗口选择:窗口内如果有多个深度层次,对比度曲线可能出现多峰。

3.3.3 权衡策略

实际项目中,我通常采用动态策略:

  1. 场景识别:通过帧差法判断场景变化程度。变化大时用快速粗搜,变化小时用精细搜索。
  2. 自适应步长:根据当前对比度曲线的陡峭程度调整步长。曲线陡峭时步长可以大一些,曲线平缓时步长要小。
  3. 提前终止:如果连续3步的对比度都在下降,说明已经过了峰值,可以提前终止搜索。
  4. 历史位置缓存:记住上一次的合焦位置,下次搜索时优先从附近开始。
核心指标:我一般用"对焦时间"和"离焦量"两个指标来评估。对焦时间控制在200ms以内算及格,100ms以内算优秀。离焦量(实际合焦位置与理想位置的偏差)控制在1个景深以内。

3.4 知识结构图

下面这张图把CDAF实现的核心要素串起来了,方便你理解整体逻辑:

CDAF实现核心要素结构图 CDAF实现核心要素 搜索窗口选择 步长策略 速度 vs 精度 窗口位置 • 中央单窗口:简单但局限 • 多窗口加权:9宫格推荐 • 自适应窗口:动态调整 窗口大小 • 32×32:低分辨率快速 • 128×128:高清平衡 • 512×512:高精度慢速 粗搜阶段 • 步长:全行程1/10~1/20 • 目标:快速定位峰值区域 • 步长 ≤ 景深对应位移 细搜阶段 • 步长:粗搜步长1/4~1/8 • 范围:峰值前后2~3步 • 建议:抛物线插值提精度 速度影响因素 • 搜索步数:核心耗时 • 图像采集延迟:1~3帧 • 马达响应:5~20ms 精度影响因素 • 步长大小:越小越精确 • 噪声水平:低光抖动 • 场景纹理:纹理越丰富越好 权衡策略:场景识别 + 自适应步长 + 提前终止 + 历史缓存 目标:对焦时间 <200ms,离焦量<1景深

3.5 实战中的权衡案例

我去年做一个旗舰手机项目时,遇到了一个典型的速度精度矛盾。场景是这样的:用户快速切换拍摄对象,从近处的花(10cm)切换到远处的山(无穷远)。

如果按标准流程走:粗搜12步 + 细搜10步 = 22步。每步包括马达移动(10ms)+ 图像采集(33ms,30fps)+ 对比度计算(5ms),总共约1秒。用户等不了。

我的解决方案:

  • 第一步:检测到场景突变(帧差法判断),直接跳到上一次的无穷远位置。
  • 第二步:在无穷远附近做3步细搜,确认合焦。
  • 第三步:如果对比度不够,再回退到粗搜。

这样大部分场景下只需要3~5步,对焦时间控制在200ms以内。只有少数复杂场景才走完整流程。

小技巧:我习惯在驱动层维护一个"镜头位置-场景特征"映射表。每次合焦后,记录当前位置和对应的场景特征(亮度、对比度、直方图分布)。下次遇到相似场景,直接跳到历史位置附近,能省掉粗搜步骤。