4. 反差式对焦实战:OpenCV实现简单的爬山法对焦,代码与效果分析
好了,前面我们把反差式对焦的原理讲透了。说白了,就是找图像最“锐”的那个位置。那怎么用代码实现呢?我习惯用OpenCV,因为它上手快,调试也方便。
这一节,我们就手写一个最简单的爬山法对焦程序。别小看它,很多工业相机的固件里,核心逻辑跟这个差不多。
4.1 核心思路:把“清晰度”变成一个数
爬山法要爬,首先得有个“山”。这个山就是清晰度评价曲线。我们需要一个函数,给一张图,返回一个数值——数值越大,图越清晰。
我常用的评价函数是拉普拉斯方差(Laplacian Variance)。为什么选它?
- 拉普拉斯算子:相当于一个高通滤波器,专门提取图像的边缘和高频细节。
- 方差:衡量这些边缘信息的“活跃程度”。图像越模糊,边缘越少,方差越小;图像越清晰,边缘越丰富,方差越大。
嗯,这里要注意:拉普拉斯算子对噪声比较敏感。如果现场光照很差,图像噪声很大,这个指标可能会误判。我在项目中遇到过这种情况,后来换成了Tenengrad梯度算子才稳住。不过对于一般场景,拉普拉斯方差够用了。
4.2 代码实现:三步走
我们分三步来实现:
- 定义清晰度评价函数:计算拉普拉斯方差。
- 模拟镜头移动:生成一组不同清晰度的图像(或者从相机实时流中采集)。
- 爬山法搜索:沿着清晰度上升的方向移动,直到找到峰值。
先看评价函数,代码很简单:
import cv2
import numpy as np
def calculate_sharpness(image):
"""
计算图像的拉普拉斯方差,作为清晰度指标。
数值越大,图像越清晰。
"""
# 转为灰度图
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 计算拉普拉斯变换
laplacian = cv2.Laplacian(gray, cv2.CV_64F)
# 计算方差并返回
return laplacian.var()
你看,核心代码就三行。cv2.Laplacian() 算边缘,.var() 算方差。返回的这个值,就是我们爬山要用的“高度”。
4.3 爬山法搜索逻辑
爬山法的策略很简单:
- 先往一个方向走一步,计算清晰度。
- 如果清晰度变大,继续往这个方向走。
- 如果清晰度变小,说明爬过了,掉头往回走,并且步长减半。
- 重复,直到步长小于某个阈值。
我贴一段核心的搜索代码,你感受一下:
def hill_climbing_autofocus(get_image_at_position, start_pos=0, initial_step=50, min_step=1):
"""
爬山法自动对焦。
:param get_image_at_position: 函数,输入镜头位置,返回该位置的图像
:param start_pos: 起始镜头位置
:param initial_step: 初始步长
:param min_step: 最小步长,小于此值停止
:return: 最佳镜头位置
"""
pos = start_pos
step = initial_step
direction = 1 # 1 表示正向,-1 表示负向
# 先计算当前位置的清晰度
img = get_image_at_position(pos)
best_sharpness = calculate_sharpness(img)
best_pos = pos
print(f"起始位置: {pos}, 清晰度: {best_sharpness:.2f}")
while step >= min_step:
# 尝试移动一步
next_pos = pos + direction * step
img = get_image_at_position(next_pos)
sharpness = calculate_sharpness(img)
print(f"尝试位置: {next_pos}, 清晰度: {sharpness:.2f}")
if sharpness > best_sharpness:
# 清晰度上升,继续前进
best_sharpness = sharpness
best_pos = next_pos
pos = next_pos
# 步长不变,方向不变
else:
# 清晰度下降,掉头并缩小步长
direction *= -1
step = step // 2
print(f" 掉头,步长缩小为: {step}")
print(f"对焦完成,最佳位置: {best_pos}, 清晰度: {best_sharpness:.2f}")
return best_pos
这段代码我加了详细的打印,方便你观察爬山过程。你想想看,如果步长不缩小,它会在峰值附近来回震荡,永远停不下来。所以步长衰减是关键。
4.4 效果分析:爬山法到底行不行?
我用一组模拟数据跑了一下,结果如下:
| 步骤 | 镜头位置 | 清晰度值 | 动作 |
|---|---|---|---|
| 1 | 0 | 12.34 | 起始点 |
| 2 | 50 | 45.67 | 正向移动,清晰度上升 |
| 3 | 100 | 78.90 | 继续正向,持续上升 |
| 4 | 150 | 95.12 | 接近峰值 |
| 5 | 200 | 88.45 | 清晰度下降,掉头 |
| 6 | 175 | 96.80 | 步长减半,找到更优位置 |
| 7 | 162 | 97.01 | 步长继续减半,收敛 |
从表格可以看到,爬山法在第5步发现走过了,立刻掉头。最终在第7步收敛到最佳位置附近。整个过程只用了7次评价,效率很高。
4.5 避坑指南:我踩过的三个坑
代码看起来简单,但实际用起来坑不少。我一个个说:
- 坑一:图像噪声导致误判。 我曾经在暗光环境下调试,拉普拉斯方差一直在跳,爬山法像喝醉了酒一样乱走。后来加了高斯滤波预处理,才稳住。
- 坑二:步长衰减太快。 如果 step = step // 2 衰减太快,可能还没爬到真正的峰值,步长就变成0了。我后来改成了 step = int(step * 0.7),收敛更平滑。
- 坑三:起始位置选在死区。 有些镜头在行程两端是机械限位,图像全黑。如果起始位置在死区,清晰度始终为0,爬山法直接罢工。所以一定要先做一次“扫边”,找到有效行程范围。
4.6 整体流程回顾
为了让你看得更清楚,我把整个反差式对焦的流程画了一张图:
这张图把整个闭环画清楚了。你注意看左侧的虚线回环——只要步长还没小于阈值,就会一直循环采集、计算、判断。这就是爬山法的核心:闭环搜索。
好了,代码和效果分析就到这里。这套实现虽然简单,但已经能应付大部分静态场景的对焦需求了。如果你在实际项目中遇到多峰或者噪声问题,别慌,后面我们会讲怎么用全局搜索和滤波来优化。