1、反差对焦基础:对比度检测原理、焦平面搜索算法、爬山算法详解

各位好,我是老周。做嵌入式影像系统调试有些年头了。今天咱们聊聊反差对焦,也就是CDAF(Contrast Detection Auto Focus)。

很多人觉得反差对焦就是“来回找最清晰的位置”,这话没错。但真要把它调快、调准,里面的门道可不少。我刚开始做这行时,也踩过不少坑。

1.1 对比度检测原理

说白了,反差对焦的核心就一句话:图像最清晰的时候,就是对比度最大的时候。

那怎么衡量对比度呢?我们一般用图像梯度来算。梯度越大,边缘越锐利,图像越清晰。

核心公式(简化版):

对比度值 = Σ( |I(x,y) - I(x+1,y)| + |I(x,y) - I(x,y+1)| )

说白了,就是计算每个像素和它右边、下边像素的差值,然后全部加起来。

我习惯把这个值叫做FV(Focus Value,对焦值)。FV越高,说明画面越清晰。

举个例子,一张纯白图片,所有像素值都是255,那FV就是0——完全没对比度。一张黑白相间的棋盘格,FV就很高。

避坑指南:

我曾经遇到过一个问题:在低光照环境下,图像噪声很大。噪声也会产生“伪梯度”,导致FV虚高。结果镜头停在了一个噪声大但实际模糊的位置。

后来我加了低通滤波,先降噪再算梯度,效果好了很多。

常用的对比度评价函数有几种,我列个表,大家对比着看:

评价函数 计算方式 特点 适用场景
Tenengrad Sobel算子求梯度 抗噪性好,计算量适中 通用场景,我最常用
Laplacian 二阶微分 对边缘敏感,易受噪声影响 高光照、纹理丰富的场景
Brenner 相邻像素差值的平方 计算量小,实现简单 资源受限的嵌入式平台
方差法 像素灰度值的方差 对整体模糊敏感 均匀场景,纹理较少时

你想想看,选哪个函数,其实取决于你的硬件算力和场景需求。我个人习惯在调试初期先用Tenengrad,稳定后再根据性能瓶颈做替换。

1.2 焦平面搜索算法

有了FV,下一步就是找到FV最大的那个镜头位置。这个过程,就是焦平面搜索。

搜索算法有很多种,但核心思路都一样:在镜头行程范围内,采样若干个位置,计算FV,然后找到峰值。

常见的搜索策略有这几种:

  1. 全局搜索:从近端到远端,步进式采样。简单粗暴,但慢。
  2. 二分搜索:先大步粗搜,再小步细搜。效率高一些。
  3. 自适应步长搜索:根据FV变化趋势动态调整步长。这是工程上最常用的。

注意:

全局搜索虽然慢,但在某些场景下不得不做。比如刚上电时,镜头位置未知,必须从头扫一遍。

我曾经在一个项目里,因为偷懒用了二分搜索,结果在微距场景下反复找不到焦点。后来发现是初始步长设太大了,跳过了峰值。

下面这张图,是我自己画的一个搜索流程示意,大家可以看看:

焦平面搜索算法流程图 焦平面搜索算法流程 开始搜索 大步粗搜(步长=大) 找到峰值区域? 是 小步细搜(步长=小) 锁定焦点 否 说明 大步粗搜:快速定位峰值大致范围 小步细搜:精确找到峰值位置 步长根据场景动态调整

1.3 爬山算法详解

爬山算法,是反差对焦里最经典的搜索策略。它模拟人爬山的过程:往FV增大的方向走,直到找不到更高的地方。

算法步骤很简单:

  1. 从当前位置出发,向一个方向移动一步。
  2. 计算新位置的FV。
  3. 如果FV变大,继续往这个方向走。
  4. 如果FV变小,说明爬过了山顶,往回退一步。
  5. 如果FV不变,可能是平台区,需要特殊处理。

嗯,这里要注意:爬山算法有个致命问题——容易陷入局部极值。比如画面里有多个纹理区域,FV曲线可能有多个峰。算法可能只爬到第一个小峰就停了,错过了真正的主峰。

我的经验:

我习惯在爬山算法前,先做一次粗扫。粗扫用大步长,快速找到全局峰值的大致范围。然后再用爬山算法做精细定位。

这样既保证了全局最优,又提高了速度。

下面是一个简单的爬山算法伪代码,大家可以参考:

// 爬山算法伪代码
int hill_climbing(int start_pos) {
    int current_pos = start_pos;
    float current_fv = calculate_fv(current_pos);
    int direction = 1;  // 1: 正向, -1: 反向
    int step = STEP_SIZE;
    
    while (1) {
        int next_pos = current_pos + direction * step;
        float next_fv = calculate_fv(next_pos);
        
        if (next_fv > current_fv) {
            // 继续往上爬
            current_pos = next_pos;
            current_fv = next_fv;
        } else if (next_fv < current_fv) {
            // 爬过了,回头
            direction = -direction;
            step = step / 2;  // 缩小步长
            if (step < MIN_STEP) {
                break;  // 精度够了,退出
            }
        } else {
            // FV不变,可能是平台区
            // 尝试继续走几步,确认不是噪声
            int confirm_count = 0;
            for (int i = 0; i < 3; i++) {
                next_pos += direction * step;
                next_fv = calculate_fv(next_pos);
                if (next_fv == current_fv) {
                    confirm_count++;
                }
            }
            if (confirm_count >= 2) {
                break;  // 确认是平台,退出
            }
        }
    }
    
    return current_pos;
}

避坑指南:

我曾经在一个项目中,爬山算法在弱纹理场景下反复震荡,就是找不到稳定的峰值。

后来发现是步长设置不合理。步长太大,容易跳过峰值;步长太小,搜索时间太长。

我的解决方案是:动态步长。在FV变化剧烈的地方用小步长,变化平缓的地方用大步长。这样既快又准。

最后,我总结一下爬山算法的优缺点:

优点 缺点
实现简单,计算量小 容易陷入局部极值
适合连续场景(如视频) 对初始位置敏感
收敛速度快(单峰时) 在多峰场景下表现不佳
硬件资源占用少 需要配合粗扫使用

好了,这一章的内容就到这里。对比度检测原理、焦平面搜索算法、爬山算法,这三个基础概念,是后面所有优化工作的基石。大家先把这些吃透,后面我们才能聊怎么提速、怎么调参。