简介:本资源是一套基于Python实现的双目立体视觉测距系统源码,面向计算机视觉初学者、机器学习实践者及嵌入式视觉应用开发者,聚焦解决真实场景下双目测距精度受光照变化、纹理缺失与基线限制等关键问题。压缩包共6个文件(79KB),含核心算法脚本stereo_vision.py、标定用棋盘格图像(9x6_1-8cm_chessboard.png)、项目说明文档README.md、开源许可证LICENSE及备份文件,结构精简,便于快速部署与调试。已有39人学习下载,适合在工业检测、教学实验等光照稳定、纹理可控环境中开展原理验证与参数调优。读者可直接运行主程序完成相机标定、视差图生成与距离计算全流程,并结合文档理解光照补偿思路、纹理增强必要性及基线长度对测距范围的影响机制,为后续引入红外结构光或自适应匹配策略提供可扩展基础。
1. 项目概述:从“看见”到“测距”的跨越
在计算机视觉领域,让机器像人一样感知三维空间的距离,一直是个既基础又充满挑战的任务。我们人类天生就拥有双目立体视觉,通过左右眼图像的微小差异(视差)来估算深度。而用Python实现一套双目立体视觉测距系统,就是把这一生物本能转化为一行行可执行代码的过程。这不仅仅是调用几个OpenCV函数那么简单,它涉及从摄像头标定、图像校正、立体匹配到三维坐标计算的全链路工程实践。我花了相当长的时间,从理论推导到代码调试,踩过不少坑,也积累了一些心得。这套源码的核心价值在于,它提供了一个完整、可复现的框架,无论是用于机器人导航、自动驾驶的辅助感知、工业检测中的尺寸测量,还是制作一个有趣的体感交互装置,你都能从中找到清晰的实现路径。接下来,我将拆解这个系统的每一个关键环节,分享其中的技术细节和实操要点。
2. 系统核心原理与设计思路拆解
2.1 双目测距的几何基础:对极几何与三角测量
双目测距的物理基础是三角测量法,而其数学表述则依赖于对极几何。想象一下,你用两只眼睛看面前的一个点,这个点在左右眼视网膜上的成像位置是不同的。这个位置差就是“视差”。物体越近,视差越大;物体越远,视差越小,直至消失。在数学上,我们通过两个相机的投影中心(相当于双眼)和空间点,构成了一个三角形。已知两个相机的相对位置(基线距离)和焦距,通过测量视差,就能解算出这个空间点的深度(Z坐标)。
这里的关键在于“已知两个相机的相对位置”。在理想情况下,我们希望两个相机是完全平行放置的。这样,匹配点只在水平方向(x轴)有差异,计算会变得非常简单。但现实中,相机安装不可能绝对平行,镜头本身也存在畸变。因此,整个系统的第一步,也是最关键的一步,就是相机标定和立体校正。标定是为了获取相机的内参(焦距、主点)和畸变系数,校正则是为了将实际非共面行对准的双目图像,变换成理想平行对准的形态。这个过程就像是给两台相机做一次“眼科手术”和“正骨”,让它们严格按照我们设定的几何模型来“看”世界。
2.2 系统架构与模块化设计
一个健壮的双目测距系统不能是“一锅粥”式的脚本,而应该模块清晰、职责分明。我的源码采用了典型的流水线设计,主要分为以下几个模块:
- 标定模块:负责采集棋盘格标定板图像,计算单目和双目标定参数,并保存结果。这是后续所有步骤的基石。
- 校正模块:加载标定参数,对实时视频流或图像对进行去畸变和立体校正,输出共面行对准的“理想”图像对。
- 匹配模块:在校正后的图像对上,为左图的每一个像素,在右图上寻找其对应点(即计算视差图)。这是算法核心,计算量大,精度要求高。
- 测距模块:利用标定得到的相机参数(焦距f、基线距离B)和计算出的视差d,通过公式
Z = f * B / d将视差图转换为深度图(距离图)。 - 应用与可视化模块:从深度图中提取指定点的距离,或者将深度信息进行三维重建、伪彩色可视化,用于结果展示和调试。
这种设计的好处是,每个模块都可以独立测试和优化。例如,你可以尝试不同的立体匹配算法,而不影响标定和校正流程;也可以更换不同的相机,只需重新标定即可。
3. 核心细节解析与实操要点
3.1 相机标定:精度决定一切
标定的精度直接决定了整个系统测距的准确性。这里有两个层面:单目标定和双目标定。
单目标定是为了获取每个相机自身的内参矩阵和畸变系数。内参矩阵描述了相机如何将三维点投影到二维图像上,包含焦距和主点坐标。畸变系数则用于纠正镜头引入的径向和切向畸变。OpenCV提供了cv2.calibrateCamera函数,我们需要准备一个已知物理尺寸的棋盘格(比如每个格子30mm x 30mm),从不同角度、不同位置拍摄至少10-15张图片。拍摄时,要确保棋盘格覆盖图像的各个角落和中心,且有一定倾斜角度,这样标定结果才更鲁棒。
注意:棋盘格必须平整。我曾因为使用一张轻微弯曲的纸质棋盘格,导致标定误差巨大,后续测距飘忽不定。建议使用亚克力或铝板制作的硬质标定板。
双目标定则是为了获取两个相机之间的相对位置关系,即旋转矩阵R和平移向量T。其中平移向量T的模长就是两个相机光心之间的距离,即基线距离B,这是测距公式中的关键参数。使用cv2.stereoCalibrate函数,输入左右相机对同一标定板拍摄的图片对,即可得到R和T。
实操心得:
- 标定板图片质量:确保标定板图像清晰、对比度高。模糊的图像会引入噪声。
- 角点检测:OpenCV的
findChessboardCorners函数有时会失败,特别是在图像边缘或光照不均时。可以手动调整图片的对比度和亮度,或使用cornerSubPix进行亚像素级精化,能有效提升标定精度。 - 参数保存:将标定结果(相机矩阵、畸变系数、R、T)用
np.save或pickle保存下来,避免每次运行都重新标定。
3.2 立体校正:让匹配变简单
拿到标定参数后,下一步是立体校正。目标是让左右图像的极线变为水平线,这样对应点就只在同一行上寻找,将二维搜索问题降为一维,极大减少了计算量和匹配歧义。
OpenCV提供了两种主要的校正函数:
cv2.stereoRectify:计算校正变换所需的映射矩阵(旋转矩阵R1, R2)和投影矩阵(P1, P2)。cv2.initUndistortRectifyMap:结合相机内参、畸变系数和上一步得到的校正参数,生成用于重映射的映射表(mapx, mapy)。cv2.remap:利用生成的映射表,对原始图像进行变换,得到校正后的图像。
校正后的图像应该满足“共面行对准”。一个简单的检查方法是:在左右校正图上画几条水平线,观察同一场景特征(如桌角、书本边缘)是否严格位于左右图像的同一行上。
3.3 立体匹配:算法的核心战场
这是双目视觉中最复杂、最影响效果和性能的环节。目标是为左图的每个像素(x, y),在右图的同一行上找到一个水平坐标x',使得d = x - x'即为该点的视差。匹配算法主要分为两类:
局部匹配算法:例如块匹配(Block Matching, BM)和半全局块匹配(Semi-Global Block Matching, SGBM)。BM算法简单粗暴,对每个像素,在其周围取一个固定大小的窗口(如5x5),在右图同行的一定搜索范围内滑动,计算窗口内像素的相似度(常用SAD、SSD或归一化互相关NCC),取相似度最高的位置作为匹配点。BM速度快,但在纹理稀疏、重复的区域容易误匹配。
SGBM算法是BM的极大增强版。它不仅仅考虑局部窗口的代价,还通过动态规划,在多个路径(通常8或16个方向)上对匹配代价进行聚合,强制执行视差图平滑的约束,从而得到更连贯、噪声更少的视差图。OpenCV中的cv2.StereoSGBM_create函数提供了丰富的参数进行调优。
全局匹配算法:如图割(Graph Cut)、**置信传播(Belief Propagation)**等。它们通过构建一个全局能量函数(包含数据项和平滑项)并优化它来求解视差图,精度通常更高,但计算速度非常慢,不适合实时应用。
在我的源码中,我主要实现了SGBM算法,因为它是在精度和速度之间一个非常好的折中。
SGBM关键参数调优心得:
minDisparity和numDisparities:numDisparities定义了搜索的视差范围,必须是16的整数倍。例如设置为128,则算法会为每个像素计算0到127共128个可能的视差值。这个值越大,能检测的深度范围越广,但计算量也越大。需要根据你的基线距离和最近/最远测距目标来设定。minDisparity通常设为0。blockSize:匹配窗口的大小。奇数,通常在3到11之间。较小的窗口对细节更敏感,但噪声更大;较大的窗口更平滑,但会损失边缘精度。对于室内场景,5或7是不错的起点。P1,P2:控制视差平滑度的惩罚参数。P1是相邻像素视差变化为1时的惩罚,P2是变化大于1时的惩罚,且P2>>P1。P2越大,视差图越平滑。一个经验法则是:P1 = 8*通道数*blockSize*blockSize,P2 = 32*通道数*blockSize*blockSize。uniquenessRatio:唯一性检验参数。通常设为5-15。值越大,对匹配唯一性的要求越严格,可以过滤掉一些模糊的匹配。speckleWindowSize和speckleRange:用于后处理,过滤视差图中的小斑点噪声。speckleWindowSize是斑点区域的最大像素尺寸,speckleRange是视差变化阈值。例如设置speckleWindowSize=100,speckleRange=32,可以很好地去除小的孤立噪声块。
调参是一个反复迭代的过程,需要对着你的特定场景(室内/室外、纹理丰富/稀疏)和相机硬件,一边看生成的视差图效果,一边调整。
4. 实操过程与核心环节实现
4.1 环境搭建与依赖安装
首先,你需要一个Python环境(3.7及以上版本推荐)和必要的库。核心依赖是OpenCV,它提供了从标定到匹配的全套工具。
# 使用pip安装,强烈建议使用OpenCV的完整版(包含contrib模块) pip install opencv-contrib-python pip install numpy matplotlib # 用于数值计算和可视化硬件方面,你需要两个型号、参数一致的USB摄像头,并固定在一个刚性支架上,尽量保持两者光轴平行,间距(基线)根据你的测距范围设定。近距离测距(0.5m-3m)基线5-10cm即可,远距离则需要更长的基线。
4.2 标定流程代码详解
以下是标定模块的核心代码片段及解释:
import cv2 import numpy as np import glob # 1. 准备标定板参数 chessboard_size = (9, 6) # 棋盘格内角点数量(格子数-1) square_size = 0.03 # 每个格子的实际物理尺寸,单位:米 # 为世界坐标系中的角点准备坐标,例如 (0,0,0), (1,0,0), (2,0,0) ....,(8,5,0) objp = np.zeros((chessboard_size[0]*chessboard_size[1], 3), np.float32) objp[:, :2] = np.mgrid[0:chessboard_size[0], 0:chessboard_size[1]].T.reshape(-1, 2) objp *= square_size # 用于存储所有图像的对象点和图像点 objpoints = [] # 真实世界中的3D点 imgpoints_left = [] # 左图像中的2D点 imgpoints_right = [] # 右图像中的2D点 # 读取左右相机拍摄的标定板图像对 left_images = sorted(glob.glob('calib/left*.jpg')) right_images = sorted(glob.glob('calib/right*.jpg')) for left_img_path, right_img_path in zip(left_images, right_images): img_left = cv2.imread(left_img_path) img_right = cv2.imread(right_img_path) gray_left = cv2.cvtColor(img_left, cv2.COLOR_BGR2GRAY) gray_right = cv2.cvtColor(img_right, cv2.COLOR_BGR2GRAY) # 查找角点 ret_left, corners_left = cv2.findChessboardCorners(gray_left, chessboard_size, None) ret_right, corners_right = cv2.findChessboardCorners(gray_right, chessboard_size, None) if ret_left and ret_right: objpoints.append(objp) # 亚像素级角点精化 criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) corners_left_refined = cv2.cornerSubPix(gray_left, corners_left, (11,11), (-1,-1), criteria) corners_right_refined = cv2.cornerSubPix(gray_right, corners_right, (11,11), (-1,-1), criteria) imgpoints_left.append(corners_left_refined) imgpoints_right.append(corners_right_refined) # 2. 单目标定 ret_left, mtx_left, dist_left, rvecs_left, tvecs_left = cv2.calibrateCamera( objpoints, imgpoints_left, gray_left.shape[::-1], None, None) ret_right, mtx_right, dist_right, rvecs_left, tvecs_left = cv2.calibrateCamera( objpoints, imgpoints_right, gray_right.shape[::-1], None, None) # 3. 双目标定 flags = cv2.CALIB_FIX_INTRINSIC # 使用单目标定的内参,只优化外参 criteria_stereo = (cv2.TERM_CRITERIA_MAX_ITER + cv2.TERM_CRITERIA_EPS, 100, 1e-5) ret_stereo, mtx_left, dist_left, mtx_right, dist_right, R, T, E, F = cv2.stereoCalibrate( objpoints, imgpoints_left, imgpoints_right, mtx_left, dist_left, mtx_right, dist_right, gray_left.shape[::-1], criteria=criteria_stereo, flags=flags) print(f"基线距离 B = {np.linalg.norm(T)} 米") # 平移向量的模长就是基线距离 np.savez('stereo_calib_params.npz', mtx_left=mtx_left, dist_left=dist_left, mtx_right=mtx_right, dist_right=dist_right, R=R, T=T)4.3 实时校正与测距实现
标定完成后,就可以进行实时视频流的处理和测距了。
# 加载标定参数 calib_data = np.load('stereo_calib_params.npz') mtx_left = calib_data['mtx_left'] dist_left = calib_data['dist_left'] mtx_right = calib_data['mtx_right'] dist_right = calib_data['dist_right'] R = calib_data['R'] T = calib_data['T'] # 计算立体校正映射 image_size = (640, 480) # 你的相机分辨率 R1, R2, P1, P2, Q, roi1, roi2 = cv2.stereoRectify( mtx_left, dist_left, mtx_right, dist_right, image_size, R, T, alpha=0) map_left_x, map_left_y = cv2.initUndistortRectifyMap( mtx_left, dist_left, R1, P1, image_size, cv2.CV_32FC1) map_right_x, map_right_y = cv2.initUndistortRectifyMap( mtx_right, dist_right, R2, P2, image_size, cv2.CV_32FC1) # 初始化SGBM匹配器 window_size = 5 min_disp = 0 num_disp = 128 - min_disp # 视差搜索范围 stereo = cv2.StereoSGBM_create( minDisparity=min_disp, numDisparities=num_disp, blockSize=window_size, P1=8 * 3 * window_size ** 2, P2=32 * 3 * window_size ** 2, disp12MaxDiff=1, uniquenessRatio=10, speckleWindowSize=100, speckleRange=32 ) # 打开左右摄像头 cap_left = cv2.VideoCapture(0) # 左摄像头索引 cap_right = cv2.VideoCapture(1) # 右摄像头索引 while True: ret_left, frame_left = cap_left.read() ret_right, frame_right = cap_right.read() if not (ret_left and ret_right): break # 1. 立体校正 frame_left_rectified = cv2.remap(frame_left, map_left_x, map_left_y, cv2.INTER_LINEAR) frame_right_rectified = cv2.remap(frame_right, map_right_x, map_right_y, cv2.INTER_LINEAR) # 2. 转换为灰度图(SGBM处理灰度图) gray_left = cv2.cvtColor(frame_left_rectified, cv2.COLOR_BGR2GRAY) gray_right = cv2.cvtColor(frame_right_rectified, cv2.COLOR_BGR2GRAY) # 3. 计算视差图 disparity = stereo.compute(gray_left, gray_right).astype(np.float32) / 16.0 # OpenCV SGBM返回的视差值是16倍整数 # 4. 将视差图转换为深度图 # Q是stereoRectify得到的重投影矩阵,其中包含基线距离和焦距信息 depth_map = cv2.reprojectImageTo3D(disparity, Q) # depth_map是一个3通道图像,其中Z通道就是深度(距离) # 也可以手动计算:Z = f * B / d,其中d是视差 # f = mtx_left[0,0] (fx), B = np.linalg.norm(T) # 注意:视差为0的点(无法匹配)会导致除零错误,需要过滤 depth_map = np.where(disparity > min_disp, (mtx_left[0,0] * np.linalg.norm(T)) / disparity, 0) # 5. 可视化 # 将视差图归一化到0-255以便显示 disp_vis = cv2.normalize(disparity, None, alpha=0, beta=255, norm_type=cv2.NORM_MINMAX, dtype=cv2.CV_8U) disp_vis = cv2.applyColorMap(disp_vis, cv2.COLORMAP_JET) # 在左图校正图上点击,显示该点距离 def mouse_callback(event, x, y, flags, param): if event == cv2.EVENT_LBUTTONDOWN: depth = depth_map[y, x] if depth > 0: print(f"点击位置 ({x}, {y}) 的距离为: {depth:.3f} 米") else: print(f"点击位置 ({x}, {y}) 无法计算距离(视差无效)") cv2.imshow('Left Rectified', frame_left_rectified) cv2.imshow('Disparity', disp_vis) cv2.setMouseCallback('Left Rectified', mouse_callback) if cv2.waitKey(1) & 0xFF == ord('q'): break cap_left.release() cap_right.release() cv2.destroyAllWindows()5. 常见问题与排查技巧实录
在实际搭建和调试过程中,你几乎一定会遇到下面这些问题。我把它们和我的解决方案整理成了下表。
| 问题现象 | 可能原因 | 排查与解决思路 |
|---|---|---|
| 标定重投影误差很大 | 1. 标定板图像不清晰或角点检测不准。 2. 标定板移动模式单一(如只在同一平面移动)。 3. 棋盘格物理尺寸输入错误。 | 1. 确保标定板图像清晰,使用cornerSubPix精化角点。2. 从不同角度、不同距离、倾斜地拍摄标定板,覆盖整个视野。 3. 仔细测量并确认 square_size的单位和数值正确。 |
| 立体校正后图像严重扭曲或错位 | 1. 标定参数不准确,特别是旋转矩阵R和平移向量T。 2. 左右相机图像顺序搞反了。 3. stereoRectify中图像尺寸参数错误。 | 1. 重新进行高精度双目标定,检查标定误差。 2. 确认加载和传入 stereoRectify的左右相机参数顺序正确。3. 确保 image_size参数与实际视频流分辨率一致。 |
| 视差图全是噪声,没有连续区域 | 1. 相机对未正确校正,极线未水平对齐。 2. SGBM参数(如 P1,P2)设置不当,平滑约束太弱。3. 场景纹理过于单一(如白墙)。 | 1. 显示校正后的图像,画水平线检查特征点是否同行对齐。 2. 增大 P2值,增强平滑约束;调整uniquenessRatio。3. 增加场景纹理,或尝试使用更复杂的匹配算法(代价更高)。 |
| 视差图在物体边缘出现“拖影”或“膨胀” | 这是SGBM等局部算法的固有问题,由于匹配窗口跨越了前景和背景。 | 1. 减小blockSize,但会增加噪声。2. 尝试使用 cv2.ximgproc.createDisparityWLSFilter进行视差图后处理滤波,能有效改善边缘效果。3. 考虑换用全局算法或在应用层对结果进行形态学处理。 |
| 测距值不稳定,跳动剧烈 | 1. 视差计算本身有噪声,尤其在弱纹理区。 2. 基线距离B或焦距f测量/计算不准确。 3. 相机在物理上有轻微晃动。 | 1. 对视差图或深度图进行中值滤波(cv2.medianBlur)。2. 重新检查标定流程,确认B和f的值。双目标定的T向量模长就是B。 3. 确保相机支架稳固。对于动态场景,可以考虑使用更快的匹配算法或硬件加速。 |
| 远处物体测距不准或无法测距 | 1. 基线距离B太短,导致远处物体视差过小,小于算法的最小可检测视差。 2. numDisparities设置太小,未覆盖足够的视差范围。3. 相机分辨率不足,远处物体在图像中像素太少。 | 1. 根据测距范围调整基线。测远距离需要更长的基线。 2. 增大 numDisparities,但会显著增加计算量。3. 使用更高分辨率的相机,或从算法上尝试亚像素级别的视差优化。 |
| 程序运行速度很慢,无法实时 | SGBM计算复杂度高,尤其是在高分辨率和大视差范围下。 | 1. 降低图像处理分辨率(如从1280x720降到640x480)。 2. 减小 numDisparities和blockSize。3. 使用OpenCV的CUDA版本(如 cv2.cuda.StereoSGM)进行GPU加速。4. 考虑在嵌入式平台(如Jetson Nano)上使用硬件优化的视觉库。 |
独家避坑技巧:
- 标定阶段多用可视化:标定后,立即用
cv2.projectPoints将角点重投影回图像,直观查看误差。用cv2.stereoRectify的alpha参数可以控制校正后图像的有效区域(alpha=0会裁剪掉所有无效像素,alpha=1会保留所有原始像素但会有黑边)。 - 视差图后处理是必修课:原始的视差图几乎不可直接用。除了中值滤波,左右一致性检查(Left-Right Consistency Check, LRC)是过滤错误匹配的利器。原理是同时计算左图到右图的视差图(D_left)和右图到左图的视差图(D_right),对于左图中的点p,其视差值为d,那么在右图中对应点p-d的视差值应该大致为-d。如果两者差异过大,则认为p点是无效匹配。OpenCV的
cv2.ximgproc模块提供了相关函数。 - 焦距的获取:测距公式
Z = f * B / d中的f,通常使用内参矩阵中的fx(因为像素可能是矩形)。更严谨的做法是使用校正后的投影矩阵P1或P2中的焦距值,或者直接使用Q矩阵进行重投影。 - 从深度图到点云:如果你需要三维点云进行进一步分析,
cv2.reprojectImageTo3D函数配合Q矩阵可以直接得到每个像素对应的三维坐标(点云)。这为机器人避障、三维建模等应用打开了大门。
本文还有配套的精品资源,点击获取