4. CDAF实战:在OpenCV中实现一个简单的反差对焦算法

好,咱们进入实战环节。前面讲了那么多理论,什么对比度计算、爬山搜索,现在该动手了。我个人习惯是,学算法一定要亲手写一遍,哪怕只是跑通一个最简单的demo,也比看十遍公式管用。

这一章,我们就用OpenCV来实现一个最基础的反差对焦(CDAF)算法。我会把代码拆开,一行一行讲清楚。最后再聊聊那些坑——局部极值、平坦区域,我当年可没少在这上面栽跟头。

4.1 算法整体流程

先看整体逻辑。一个完整的CDAF闭环,其实就三步:

  1. 采集图像:从摄像头抓一帧画面
  2. 计算清晰度:用某种评价函数算出当前画面的“清晰度分数”
  3. 驱动马达:根据分数调整镜头位置,往更清晰的方向走

我们这里用OpenCV模拟第三步——通过读取本地图片序列来模拟不同焦点的画面。说白了,就是准备一组从模糊到清晰再到模糊的图片,让算法自己去找最清楚的那一张。

核心思路:CDAF的本质是一个一维搜索问题。搜索空间是镜头位置,目标函数是清晰度评价值。我们要找的是这个函数的峰值。

下面这张图,是我画的CDAF闭环流程。你看一眼,心里有个谱。

CDAF闭环流程 采集图像帧 计算清晰度评分 判断搜索方向 驱动镜头马达 模拟/真实摄像头 爬山策略决策

4.2 代码实现:逐行解析

好,上代码。我用的OpenCV版本是4.x,Python 3.8+。代码不长,但每个细节都有讲究。

import cv2
import numpy as np
import os

def compute_sharpness(image):
    """
    计算图像的清晰度评分
    使用拉普拉斯算子的方差作为评价指标
    """
    # 转为灰度图
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    # 拉普拉斯算子
    laplacian = cv2.Laplacian(gray, cv2.CV_64F)
    # 计算方差
    score = laplacian.var()
    return score

def cdaf_search(image_folder, start_pos=0, step=1):
    """
    反差对焦搜索主函数
    :param image_folder: 存放图片序列的文件夹
    :param start_pos: 起始位置索引
    :param step: 搜索步长
    :return: 最佳焦点位置
    """
    # 获取所有图片文件
    images = sorted([f for f in os.listdir(image_folder) 
                     if f.endswith(('.jpg', '.png'))])
    
    best_pos = start_pos
    best_score = -1
    current_pos = start_pos
    
    # 先往一个方向走,找到上升趋势
    direction = 1  # 1: 正向, -1: 反向
    
    while True:
        # 读取当前帧
        img_path = os.path.join(image_folder, images[current_pos])
        frame = cv2.imread(img_path)
        if frame is None:
            break
        
        # 计算清晰度
        score = compute_sharpness(frame)
        print(f"位置 {current_pos}: 清晰度评分 = {score:.2f}")
        
        # 更新最佳
        if score > best_score:
            best_score = score
            best_pos = current_pos
        
        # 判断是否到达峰值
        next_pos = current_pos + direction * step
        if next_pos < 0 or next_pos >= len(images):
            # 边界处理:反向搜索
            direction *= -1
            next_pos = current_pos + direction * step
        
        # 读取下一帧预判
        next_img_path = os.path.join(image_folder, images[next_pos])
        next_frame = cv2.imread(next_img_path)
        if next_frame is None:
            break
        
        next_score = compute_sharpness(next_frame)
        
        # 如果下一帧评分下降,说明已经过了峰值
        if next_score < score:
            print(f"检测到峰值,最佳位置: {best_pos}")
            break
        
        current_pos = next_pos
    
    return best_pos

if __name__ == "__main__":
    # 使用示例
    best = cdaf_search("focus_sequence", start_pos=0, step=1)
    print(f"最终对焦位置: {best}")

4.3 关键函数详解

4.3.1 compute_sharpness:清晰度评价

这个函数是整个算法的核心。我选的是拉普拉斯算子方差法,为什么?

  • 拉普拉斯算子:本质是一个高通滤波器,能提取图像的高频成分。对焦准确的图像,边缘锐利,高频分量多。
  • 方差:拉普拉斯响应图的方差越大,说明图像中边缘的强度差异越大,图像越清晰。

你想想看,一张模糊的图像,所有像素都差不多,拉普拉斯响应值也差不多,方差自然小。反过来,清晰图像边缘处响应值大,平坦区域响应值小,方差就大。

个人经验:我在项目中试过多种评价函数——Tenengrad、Brenner、SMD(灰度方差)。拉普拉斯方差法在大多数场景下表现最稳定,计算量也适中。但要注意,它对噪声比较敏感,如果图像本身噪声大,建议先做一下高斯模糊。

4.3.2 cdaf_search:搜索策略

这个函数实现了一个最简单的“爬山法”。逻辑是这样的:

  1. 从起始位置开始,朝一个方向走
  2. 每走一步,计算当前帧和下一帧的清晰度
  3. 如果下一帧更清晰,继续走
  4. 如果下一帧变模糊了,说明已经过了峰值,停止
  5. 如果走到边界,掉头

嗯,这里要注意。这个实现非常基础,实际产品中不会这么简单。但作为教学demo,它把核心思想讲清楚了。

4.4 常见问题与避坑指南

下面这几个坑,我当年做项目时都踩过。你提前知道,能省不少时间。

问题 现象 原因 解决方案
局部极值 算法停在某个位置,但画面并不清晰 评价函数有多个峰值,算法只找到第一个 使用全局搜索或粗精结合策略
平坦区域 清晰度评分几乎不变,算法无法判断方向 画面内容缺乏纹理,所有位置评分接近 增加辅助光源或使用相位对焦辅助
噪声干扰 评分曲线抖动剧烈,峰值不稳定 传感器噪声被拉普拉斯算子放大 先做降噪处理,或使用更鲁棒的评价函数
步长选择 步长太大容易跳过峰值,步长太小速度慢 没有根据场景自适应调整 采用变步长策略,粗搜大步,细搜小步

4.4.1 局部极值——我最头疼的问题

我曾经在一个项目中调试了整整三天,算法总是对焦到背景上,主体是模糊的。后来发现,背景里有高反光的金属纹理,清晰度评分比主体还高。这就是典型的局部极值问题。

怎么解决?我后来用了“粗精结合”的策略:

  • 粗搜阶段:大步长(比如步长=5)快速扫描整个范围,记录所有评分较高的位置
  • 精搜阶段:在候选位置附近用小步长(步长=1)精细搜索

这样虽然多花了一点时间,但大大降低了陷入局部极值的概率。

4.4.2 平坦区域——无解?其实有办法

你想想看,如果镜头对着白墙,所有位置的清晰度评分都差不多,算法根本没法判断往哪走。这种情况在纯CDAF系统里确实棘手。

我的建议是:

  1. 检测平坦区域:如果连续多个位置的评分变化小于某个阈值,判定为平坦区域
  2. 触发辅助机制:要么切换到相位对焦(如果有),要么驱动镜头做一个“扫描”,找到评分变化最大的区域
  3. 实在不行就保持当前位置:总比乱跑强

注意:平坦区域检测的阈值设置很关键。设得太小,容易误判;设得太大,又检测不到。我一般取整个搜索范围内评分标准差的10%作为阈值,你可以根据实际场景调整。

4.5 如何测试你的实现

代码写完了,怎么验证对不对?我建议你准备一组测试图片:

  1. 找一台相机或手机,固定在三脚架上
  2. 对着一个有纹理的物体(比如书脊、键盘),手动调整对焦环
  3. 从最模糊开始,每隔一小段拍一张,一直拍到最模糊
  4. 这样你就得到了一组“模糊→清晰→模糊”的序列

把图片按顺序命名(img_000.jpg, img_001.jpg, ...),放到一个文件夹里。然后跑上面的代码,看它能不能找到最清晰的那一张。

我第一次跑通这个demo的时候,看到算法准确找到了峰值位置,说实话还挺有成就感的。虽然它很简单,但这就是所有自动对焦系统的起点。