本系列文章旨在系统性地阐述如何利用 Python 与 OpenCV 库,从零开始构建一个完整的双目立体视觉系统。
本项目github地址:https://github.com/present-cjn/stereo-vision-python.git
在上一篇文章中,我们完成了相机标定这一最关键的基础步骤,并得到了一份高精度的相机参数文件。现在,我们将利用这份参数文件,来执行双目视觉的核心任务——立体匹配 (Stereo Matching)。
我们的目标是,模仿人类大脑处理左右眼图像差异的方式,为左图中的每一个像素,在右图中找到它的对应点。通过计算它们之间的像素位置差异,即视差 (Disparity),我们就能得到一张灰色的、包含深度信息的图像——视差图 (Disparity Map)。这张图上的每一个像素点的强度值,都直接对应着真实世界的深度信息。
本文将详细讲解立体匹配的全过程,从准备工作“立体校正”,到核心算法“SGBM”,再到决定最终效果的“参数调优”。
1. 匹配前的准备:立体校正 (Rectification)
在原始的双目图像中,由于相机安装可能存在微小的角度偏差,左图中的一个点,其在右图中的对应点可能位于一条倾斜的直线上(我们称之为“对极线”)。在整张图上沿着成千上万条斜线去搜索匹配点,计算量巨大且效率低下。
立体校正 (Stereo Rectification) 的目的,就是通过数学变换,将这种复杂的二维搜索问题,简化为高效的一维搜索。
- 目标: 利用标定得到的旋转矩阵
R
和平移向量T
,对左右图像进行虚拟的旋转,使得两个相机的成像平面完全共面,并且像素行严格对齐。 - 效果: 校正后,对于左图上的任意一个像素点
(x, y)
,其在右图上的对应点必定位于同一水平线y
上。现在,算法只需要在这一行上进行一维搜索即可,效率和可靠性都大大提升。
原左右图像为
极线校正后左右图像为
代码实现 (utils/image_utils.py
)
我们的 rectify_stereo_pair
函数封装了整个校正过程。
# utils/image_utils.py
def rectify_stereo_pair(left_img, right_img, stereo_params, calib_image_size):
# 从标定参数中提取 K, D, R, T 矩阵
K1, D1, K2, D2, R, T = (stereo_params[k] for k in ('K1', 'D1', 'K2', 'D2', 'R', 'T'))
# 1. 计算校正变换和投影矩阵
# 这一步是核心,它计算出将相机坐标系旋转到理想平行状态所需的旋转矩阵 R1, R2
# 以及将3D点投影到校正后图像平面上的投影矩阵 P1, P2
# 最重要的是,它还输出了用于三维重建的 Q 矩阵
R1, R2, P1, P2, Q, _, _ = cv2.stereoRectify(
K1, D1, K2, D2, calib_image_size, R, T, alpha=0
)
# 2. 计算从原始图像到校正图像的像素映射表
# `initUndistortRectifyMap` 会为每个像素计算出它在校正后图像中的新位置
left_map1, left_map2 = cv2.initUndistortRectifyMap(K1, D1, R1, P1, left_img.shape[1::-1], cv2.CV_16SC2)
right_map1, right_map2 = cv2.initUndistortRectifyMap(K2, D2, R2, P2, right_img.shape[1::-1], cv2.CV_16SC2)
# 3. 应用映射表,执行校正
left_rectified = cv2.remap(left_img, left_map1, left_map2, cv2.INTER_LINEAR)
right_rectified = cv2.remap(right_img, right_map1, right_map2, cv2.INTER_LINEAR)
return left_rectified, right_rectified, Q
这个函数的输出,就是我们进行立体匹配所需要的、高质量的校正图像和Q矩阵。
2. 核心算法:SGBM (Semi-Global Block Matching)
现在我们有了对齐的左右图像,接下来就要计算视差了。本项目采用的是 SGBM (半全局块匹配) 算法,它是对传统 BM (块匹配) 算法的巨大改进。
- 基本思想: 它不仅像 BM 算法那样,通过比较小像素块的相似度(SAD)来寻找匹配,更引入了一个“全局”的视角。它会惩罚那些导致视差剧烈跳变的匹配,鼓励生成一条平滑、连续的视差路径。
- 优势: 相比 BM,SGBM 能更好地处理弱纹理区域(如白墙)和重复纹理区域,生成的视差图更完整、噪声更少。
代码实现 (processing/stereo_matcher.py
)
我们将 SGBM 的所有逻辑都封装在 StereoMatcher
类中。
# processing/stereo_matcher.py
import cv2
import config
class StereoMatcher:
def __init__(self):
# 在构造函数中,从 config.py 文件一次性加载所有 SGBM 参数
# 这使得调参变得非常方便
self.matcher = cv2.StereoSGBM_create(
minDisparity=config.SGBM_MIN_DISPARITY,
numDisparities=config.SGBM_NUM_DISPARITIES,
blockSize=config.SGBM_BLOCK_SIZE,
P1=config.SGBM_P1,
P2=config.SGBM_P2,
disp12MaxDiff=config.SGBM_DISP12_MAX_DIFF,
preFilterCap=config.SGBM_PRE_FILTER_CAP,
uniquenessRatio=config.SGBM_UNIQUENESS_RATIO,
speckleWindowSize=config.SGBM_SPECKLE_WINDOW_SIZE,
speckleRange=config.SGBM_SPECKLE_RANGE,
mode=config.SGBM_MODE
)
def compute_disparity(self, left_rectified_img, right_rectified_img):
# SGBM 算法要求输入单通道的灰度图
gray_left = cv2.cvtColor(left_rectified_img, cv2.COLOR_BGR2GRAY)
gray_right = cv2.cvtColor(right_rectified_img, cv2.COLOR_BGR2GRAY)
# 计算视差图
disparity_map = self.matcher.compute(gray_left, gray_right)
return disparity_map
4. 视差图的秘密:*16
的含义
当你拿到 compute
函数返回的 disparity_map
时,需要注意一个关键细节:它的数据类型是 CV_16S
(16位有符号整数),并且里面的值是真实视差的16倍。
这是 OpenCV 为了在内部用高效的整数运算来表示亚像素精度而做的设计。一个值为 800
的点,其真实的、以像素为单位的视差是 800 / 16.0 = 50.0
。
因此,当我们需要使用或显示真实的视差值时,必须记得将它除以16.0。
用项目的中的测试图片可以得到以下视差图
总结
通过立体校正的预处理和强大的 SGBM 算法,我们成功地从两张2D图像中提取出了包含深度信息的视差图。更重要的是,我们学会了如何通过交互式调参来优化匹配结果,这是所有立体视觉应用开发者的核心技能之一。
在下一篇文章中,我们将进入一个核心步骤:利用这张高质量的视差图和 Q
矩阵,计算出深度图并重建出真正的三维点云。