从‘双目交汇’到‘视差图’:用Python+OpenCV手把手复现一个简易的立体匹配流程

当我们用双眼观察世界时,大脑会自动计算左右眼图像的微小差异,从而感知深度——这就是 双目立体视觉 的核心原理。在计算机视觉领域,通过算法模拟这一过程,可以从两张不同视角拍摄的图像中恢复三维信息。本文将带你用Python和OpenCV一步步实现这个神奇的过程。

1. 环境准备与数据加载

1.1 安装必要的库

确保你的Python环境(建议3.8+)已安装以下库:

pip install opencv-python numpy matplotlib

1.2 获取测试图像

Middlebury数据集是立体匹配的标准测试集。我们使用经典的"Teddy"图像对:

import cv2
import numpy as np

# 加载左右视图
img_left = cv2.imread('teddy_left.png', cv2.IMREAD_GRAYSCALE)
img_right = cv2.imread('teddy_right.png', cv2.IMREAD_GRAYSCALE)

# 显示图像
import matplotlib.pyplot as plt
plt.figure(figsize=(10,4))
plt.subplot(121), plt.imshow(img_left, 'gray'), plt.title('左视图')
plt.subplot(122), plt.imshow(img_right, 'gray'), plt.title('右视图')
plt.show()

提示:如果使用KITTI数据集,需要注意图像已经过极线校正,可直接进行立体匹配

2. 极线校正原理与实现

2.1 为什么需要极线校正

原始双目图像中,对应点可能出现在任意位置。极线校正后:

  • 对应点仅出现在同一水平线上
  • 视差计算简化为水平方向搜索
  • 计算效率大幅提升

2.2 OpenCV实现

# 假设我们已经获得相机参数
stereo = cv2.StereoBM_create(numDisparities=64, blockSize=15)

# 计算视差图
disparity = stereo.compute(img_left, img_right)

# 可视化
plt.imshow(disparity, 'jet'), plt.colorbar()
plt.title('原始视差图'), plt.show()

校正前后的图像对比:

校正前 校正后
对应点位置任意 对应点在同一水平线
计算复杂度高 只需水平搜索

3. 代价计算与聚合

3.1 AD-Census混合代价

结合绝对差(AD)和Census变换的优点:

def ad_cost(left, right, d):
    return np.abs(left - np.roll(right, d))

def census_transform(img, window=3):
    center = window//2
    census = np.zeros_like(img)
    for i in range(center, img.shape[0]-center):
        for j in range(center, img.shape[1]-center):
            census[i,j] = (img[i-center:i+center+1, j-center:j+center+1] > img[i,j]).astype(np.uint8).sum()
    return census

def ad_census_cost(left, right, max_disp, alpha=0.5):
    cost_volume = np.zeros((left.shape[0], left.shape[1], max_disp))
    census_left = census_transform(left)
    census_right = census_transform(right)
    
    for d in range(max_disp):
        ad = ad_cost(left, right, d)
        census = np.abs(census_left - np.roll(census_right, d))
        cost_volume[:,:,d] = alpha*ad + (1-alpha)*census
    return cost_volume

3.2 代价聚合示例

简单的Box Filter实现:

def box_filter(cost_volume, radius=2):
    filtered = np.zeros_like(cost_volume)
    for d in range(cost_volume.shape[2]):
        filtered[:,:,d] = cv2.boxFilter(cost_volume[:,:,d], -1, (2*radius+1, 2*radius+1))
    return filtered

4. 视差计算与优化

4.1 WTA(赢者通吃)策略

def wta(cost_volume):
    return np.argmin(cost_volume, axis=2)

4.2 视差后处理

常见的优化步骤包括:

  1. 左右一致性检查
  2. 亚像素优化
  3. 中值滤波去噪
  4. 空洞填充

实现示例:

def post_process(disparity):
    # 中值滤波
    disparity = cv2.medianBlur(disparity.astype(np.float32), 3)
    
    # 空洞填充
    mask = disparity == 0
    disparity[mask] = np.median(disparity[~mask])
    
    return disparity

5. 完整流程与效果评估

5.1 整合完整流程

def stereo_matching(left, right, max_disp=64):
    # 代价计算
    cost = ad_census_cost(left, right, max_disp)
    
    # 代价聚合
    cost = box_filter(cost, radius=2)
    
    # WTA视差计算
    disp = wta(cost)
    
    # 后处理
    disp = post_process(disp)
    
    return disp

5.2 效果对比

将我们的结果与OpenCV内置算法比较:

方法 运行时间 主观质量
本文实现 中等 边缘清晰,有噪声
SGBM 较慢 平滑,细节保留好
BM 块状效应明显

在实际项目中,可以根据需求选择不同的代价计算和聚合方法。对于实时性要求高的场景,可以尝试CUDA加速或简化算法流程。

更多推荐