简介:这份资源面向计算机视觉初学者与机器人、自动驾驶方向的开发者,提供一套在Ubuntu环境下基于OpenCV与Qt实现双目测距系统的完整工程。内容围绕立体视觉展开,涵盖摄像头同步与对齐、SIFT/ORB特征匹配、半全局立体匹配、基础矩阵与单应性矩阵求解、三角测量、深度图重建及目标距离计算等核心环节,并给出图像显示、参数调整与测距结果展示的交互界面设计思路。压缩包为rar格式,大小约80.48MB,包内文件类型与数量上游未提供明细,可结合描述中的模块划分理解其代码组织方式。目前已有743人学习下载,适合希望从原理到工程落地系统掌握双目测距、并用于机器人导航或工业检测场景的读者参考。
1. 双目测距到底能干嘛:从两个摄像头到真实距离
很多人第一次听说双目测距,脑子里浮现的是科幻片里那种扫一眼就知道对方离自己多远的黑科技。其实原理朴素得很——人为什么能判断距离?因为两只眼睛看到的画面有细微差异,大脑根据这个差异反推远近。双目测距系统干的就是同一件事:用两个摄像头模拟双眼,通过计算同一物体在左右画面中的像素位置差,结合相机参数,算出它离镜头到底多少米。
这套基于 OpenCV 的双目测距系统,核心就是把这套流程工程化:标定相机、校正图像、匹配像素、计算视差、输出深度。适合谁?做机器人避障的、搞自动驾驶感知入门的、参加电子设计竞赛需要测距模块的、以及想学计算机视觉但不想一上来就啃深度学习的开发者。它不依赖 GPU,普通笔记本就能跑,代码结构清晰,改起来不费劲。下面我从项目结构开始,一步步拆给你看。
2. 项目骨架与技术选型:为什么是 OpenCV + Python
2.1 目录结构一览
拿到源码包后,先别急着跑。花两分钟看清楚目录,后面改代码能省不少时间。这个项目的典型结构如下:
stereo_vision/ ├── config/ │ └── camera_params.yaml # 标定参数:内参、畸变、旋转平移矩阵 ├── data/ │ ├── left/ # 左摄像头采集的图片或视频帧 │ └── right/ # 右摄像头采集的图片或视频帧 ├── calibration/ │ ├── calibrate.py # 单目/双目标定脚本 │ └── chessboard/ # 棋盘格标定图存放目录 ├── core/ │ ├── stereo_matcher.py # 立体匹配核心:SGBM/BM 算法封装 │ ├── depth_calculator.py # 视差转深度计算 │ └── point_cloud.py # 可选:生成三维点云 ├── utils/ │ ├── image_utils.py # 图像读写、裁剪、绘制 │ └── config_loader.py # 读取 YAML 配置 ├── main.py # 主入口:单帧测距 / 视频流测距 └── requirements.txt这个结构的好处是职责分离:标定归标定,匹配归匹配,计算归计算。你想换匹配算法,只动core/stereo_matcher.py;想换输入源,只改main.py里的读取逻辑。常见做法是把配置全部外提到 YAML,避免硬编码参数散落在各个文件里。
2.2 技术栈与依赖
项目依赖很轻量,requirements.txt里通常就这几样:
opencv-python>=4.5.0 opencv-contrib-python>=4.5.0 numpy>=1.19.0 pyyaml>=5.3注意opencv-contrib-python要装,因为立体匹配的 SGBM(Semi-Global Block Matching)在 contrib 包里。如果你只用 BM(Block Matching),标准包也够,但 SGBM 效果通常更好,尤其纹理弱的区域。Python 版本建议 3.7 以上,3.10 也稳。
提示:不要同时装
opencv-python和opencv-python-headless,会冲突。用pip list | grep opencv检查一下。
2.3 为什么选 SGBM 而不是深度学习
双目测距的匹配算法分两派:传统方法和深度学习。深度学习(如 PSMNet、GANet)精度高,但需要 GPU、训练数据、模型文件,部署门槛高。SGBM 是 OpenCV 内置的传统算法,CPU 上就能跑,参数调好后在室内 1~10 米范围足够用。这个项目定位是教学和快速原型,选 SGBM 合理。如果你后面要上产品,再考虑换模型,接口留好了替换不难。
3. 标定与校正:让两个摄像头说同一种语言
3.1 双目标定到底在标什么
两个摄像头不可能完全平行,也不可能参数一模一样。标定的目的就是求出两组参数:内参(焦距、光心、畸变系数)和外参(右相机相对左相机的旋转矩阵 R 和平移向量 T)。有了这些,才能把左右图像校正到同一平面上,让极线对齐——说白了就是让同一个物体在左右图中只差水平方向的位置,垂直方向对齐,这样匹配时只需要在同一行找对应点,搜索范围从二维降到一维,速度快几个数量级。
标定用棋盘格,常见 9x6 或 11x8。采集 15~25 张不同角度的棋盘图,分别放在calibration/chessboard/left/和right/下。下面是一个标定脚本的核心片段:
import cv2 import numpy as np import glob # 棋盘格内角点数量,比如 9x6 CHESSBOARD = (9, 6) # 棋盘格方格实际物理尺寸,单位毫米 SQUARE_SIZE = 25.0 # 生成棋盘格三维坐标点 (0,0,0), (25,0,0), (50,0,0) ... objp = np.zeros((CHESSBOARD[0] * CHESSBOARD[1], 3), np.float32) objp[:, :2] = np.mgrid[0:CHESSBOARD[0], 0:CHESSBOARD[1]].T.reshape(-1, 2) objp *= SQUARE_SIZE objpoints = [] # 三维点 imgpoints_l = [] # 左图二维点 imgpoints_r = [] # 右图二维点 left_images = sorted(glob.glob('calibration/chessboard/left/*.jpg')) right_images = sorted(glob.glob('calibration/chessboard/right/*.jpg')) for fname_l, fname_r in zip(left_images, right_images): img_l = cv2.imread(fname_l) img_r = cv2.imread(fname_r) gray_l = cv2.cvtColor(img_l, cv2.COLOR_BGR2GRAY) gray_r = cv2.cvtColor(img_r, cv2.COLOR_BGR2GRAY) # 查找棋盘格角点 ret_l, corners_l = cv2.findChessboardCorners(gray_l, CHESSBOARD, None) ret_r, corners_r = cv2.findChessboardCorners(gray_r, CHESSBOARD, None) if ret_l and ret_r: # 亚像素级角点精化,提高标定精度 criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) corners_l = cv2.cornerSubPix(gray_l, corners_l, (11, 11), (-1, -1), criteria) corners_r = cv2.cornerSubPix(gray_r, corners_r, (11, 11), (-1, -1), criteria) objpoints.append(objp) imgpoints_l.append(corners_l) imgpoints_r.append(corners_r) # 双目标定 ret, K1, D1, K2, D2, R, T, E, F = cv2.stereoCalibrate( objpoints, imgpoints_l, imgpoints_r, None, None, None, None, gray_l.shape[::-1], criteria=(cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 100, 1e-5), flags=cv2.CALIB_FIX_INTRINSIC )逻辑说明:objpoints是棋盘格在真实世界中的坐标,imgpoints是它在图像中的像素坐标。stereoCalibrate同时优化两个相机的内参和外参。CALIB_FIX_INTRINSIC表示如果你已经单独标定过内参,可以固定不变,只算外参。参数SQUARE_SIZE必须和你打印出来的棋盘格实际尺寸一致,单位无所谓,但深度计算时要用同一单位。
3.2 立体校正:把左右图拉到同一平面
标定完得到 R 和 T,接下来用stereoRectify计算校正变换矩阵,再用initUndistortRectifyMap生成映射表。校正后的图像,极线是水平平行的。
# 计算校正变换 R1, R2, P1, P2, Q, roi1, roi2 = cv2.stereoRectify( K1, D1, K2, D2, gray_l.shape[::-1], R, T, alpha=0 # alpha=0 表示裁剪掉无效区域,alpha=1 保留全部 ) # 生成映射表 map1_l, map2_l = cv2.initUndistortRectifyMap(K1, D1, R1, P1, gray_l.shape[::-1], cv2.CV_16SC2) map1_r, map2_r = cv2.initUndistortRectifyMap(K2, D2, R2, P2, gray_r.shape[::-1], cv2.CV_16SC2) # 应用校正 rect_l = cv2.remap(img_l, map1_l, map2_l, cv2.INTER_LINEAR) rect_r = cv2.remap(img_r, map1_r, map2_r, cv2.INTER_LINEAR)Q矩阵很关键,它是视差转三维坐标的桥梁。alpha=0会裁掉边缘无效像素,适合对视野要求不高的场景;alpha=1保留全部但会有黑边。我一般先用 0,跑通了再根据实际视野调。
3.3 参数保存与加载
标定一次不容易,参数必须存下来。项目里用 YAML 存:
import yaml params = { 'K1': K1.tolist(), 'D1': D1.tolist(), 'K2': K2.tolist(), 'D2': D2.tolist(), 'R': R.tolist(), 'T': T.tolist(), 'Q': Q.tolist(), 'image_size': gray_l.shape[::-1] } with open('config/camera_params.yaml', 'w') as f: yaml.dump(params, f)加载时反向操作,np.array(params['K1'])转回来。注意 YAML 存 numpy 数组要转 list,读回来再转 numpy,不然 OpenCV 函数不认。
4. 立体匹配与深度计算:从像素差到米
4.1 SGBM 参数怎么调
立体匹配是双目测距最核心也最玄学的部分。SGBM 的参数直接影响视差图质量和测距精度。核心参数就几个:
| 参数 | 含义 | 典型值 | 影响 |
|---|---|---|---|
| minDisparity | 最小视差 | 0 | 物体太近时视差大,超出范围会匹配失败 |
| numDisparities | 视差搜索范围 | 16 的倍数,如 64/128 | 越大能测越近,但计算慢、噪声多 |
| blockSize | 匹配块大小 | 5/7/9 | 越大越平滑但边缘模糊 |
| uniquenessRatio | 唯一性比率 | 10~15 | 越大匹配越严格,弱纹理区域易空洞 |
| speckleWindowSize | 斑点滤波窗口 | 50~200 | 滤除小面积错误匹配 |
| speckleRange | 斑点范围 | 1~2 | 配合上者使用 |
import cv2 def create_sgbm_matcher(num_disparities=64, block_size=5): """创建 SGBM 立体匹配器""" # 左图通道数,灰度图用 1 channels = 1 return cv2.StereoSGBM_create( minDisparity=0, numDisparities=num_disparities, # 必须是 16 的整数倍 blockSize=block_size, # 奇数,3~11 P1=8 * channels * block_size ** 2, # 控制视差平滑度 P2=32 * channels * block_size ** 2, # 通常是 P1 的 4 倍 disp12MaxDiff=1, # 左右一致性检查最大差异 uniquenessRatio=10, # 唯一性比率 speckleWindowSize=100, # 斑点滤波窗口大小 speckleRange=2, # 斑点滤波范围 mode=cv2.STEREO_SGBM_MODE_SGBM_3WAY # 三路 SGBM,精度更高 )P1和P2是平滑惩罚项,控制视差图的连续性。纹理弱的区域容易产生噪点,适当增大P2可以平滑,但太大会把真实边缘也抹掉。disp12MaxDiff做左右一致性检查,能滤掉不少错误匹配,建议开启。
4.2 视差转深度:公式与代码
视差转深度的公式很简单:
depth = (f * B) / disparity其中f是校正后的焦距(从 P1 矩阵取),B是基线(两个相机光心距离,从 T 向量取模),disparity是视差。单位取决于标定时用的物理单位。
import numpy as np def disparity_to_depth(disparity, Q): """ 利用 Q 矩阵将视差图转为三维点云 disparity: SGBM 输出的视差图,需除以 16 转为真实视差 Q: stereoRectify 输出的 Q 矩阵 返回: 三维坐标图 (H, W, 3) """ # SGBM 输出的视差是 16 倍放大后的整数,需还原 disp_float = disparity.astype(np.float32) / 16.0 # 无效视差置零 disp_float[disp_float < 0] = 0 # reprojectImageTo3D 内部用 Q 矩阵做变换 points_3d = cv2.reprojectImageTo3D(disp_float, Q) return points_3d def get_depth_at_point(points_3d, x, y): """获取指定像素点的深度值(Z 坐标)""" return points_3d[y, x, 2]reprojectImageTo3D是 OpenCV 封装好的函数,内部就是矩阵乘法。如果你只想测某个点的距离,直接取points_3d[y, x, 2]就是该点到相机平面的垂直距离。注意视差为 0 或负数的点深度是无穷大或无效,实际使用时要过滤。
4.3 完整测距流程串起来
把前面的步骤串成一个可运行的main.py:
import cv2 import numpy as np import yaml # 加载标定参数 with open('config/camera_params.yaml', 'r') as f: params = yaml.safe_load(f) K1 = np.array(params['K1']) D1 = np.array(params['D1']) K2 = np.array(params['K2']) D2 = np.array(params['D2']) R = np.array(params['R']) T = np.array(params['T']) Q = np.array(params['Q']) img_size = tuple(params['image_size']) # 计算校正映射 R1, R2, P1, P2, Q, _, _ = cv2.stereoRectify(K1, D1, K2, D2, img_size, R, T, alpha=0) map1_l, map2_l = cv2.initUndistortRectifyMap(K1, D1, R1, P1, img_size, cv2.CV_16SC2) map1_r, map2_r = cv2.initUndistortRectifyMap(K2, D2, R2, P2, img_size, cv2.CV_16SC2) # 创建匹配器 matcher = cv2.StereoSGBM_create( minDisparity=0, numDisparities=64, blockSize=5, P1=8*1*5**2, P2=32*1*5**2, uniquenessRatio=10, speckleWindowSize=100, speckleRange=2, disp12MaxDiff=1, mode=cv2.STEREO_SGBM_MODE_SGBM_3WAY ) # 读取左右图 img_l = cv2.imread('data/left/frame.png') img_r = cv2.imread('data/right/frame.png') # 校正 rect_l = cv2.remap(img_l, map1_l, map2_l, cv2.INTER_LINEAR) rect_r = cv2.remap(img_r, map1_r, map2_r, cv2.INTER_LINEAR) # 转灰度 gray_l = cv2.cvtColor(rect_l, cv2.COLOR_BGR2GRAY) gray_r = cv2.cvtColor(rect_r, cv2.COLOR_BGR2GRAY) # 计算视差 disparity = matcher.compute(gray_l, gray_r) # 视差转深度 points_3d = disparity_to_depth(disparity, Q) # 取图像中心点的深度 h, w = gray_l.shape center_depth = points_3d[h//2, w//2, 2] print(f"中心点距离: {center_depth:.2f} mm")这段代码跑通,你就有了一个最基本的双目测距系统。实际项目中还会加滤波、加可视化、加多目标测距,但骨架就是这个。
5. 避坑与排查:那些让我熬夜的坑
5.1 标定误差大,测距飘得离谱
现象:标定完重投影误差 1.5 像素以上,测距结果和卷尺量出来差 20% 以上。
原因:棋盘格图片太少、角度不够丰富、棋盘格不平整、或者左右图没有严格同步采集。最常见的是棋盘格打印后贴在硬纸板上,纸板本身弯曲。
解决:采集 20 张以上,覆盖画面各个角落和不同倾斜角度。棋盘格贴在玻璃或亚克力板上。重投影误差控制在 0.5 像素以内再往下走。如果误差降不下来,检查SQUARE_SIZE是否和实际打印尺寸一致。
5.2 视差图大片黑色,匹配失败
现象:SGBM 输出的视差图大部分是黑的,只有零星亮点。
原因:numDisparities设小了,物体太近视差超出搜索范围;或者图像纹理太弱,白墙、纯色桌面匹配不出来;或者左右图没有校正对齐。
解决:先确认校正后的图极线对齐——在左图某行画一条水平线,右图同一行应该能找到对应点。然后增大numDisparities到 128 或 256。纹理弱的话,可以在镜头前加个散斑投射器,或者用cv2.createCLAHE增强对比度。
5.3 深度值出现负数或无穷大
现象:points_3d里很多点的 Z 值是负数或者极大值。
原因:视差为 0 或负数的点,深度公式分母为零或负,结果无意义。SGBM 对无匹配区域输出 0 或负视差。
解决:计算深度前先过滤disp_float > 0的像素,其余置零或设为 NaN。显示时用np.nan_to_num处理。不要直接拿原始视差图算深度。
5.4 帧率太低,视频流卡顿
现象:处理单帧要 200ms 以上,视频流根本跑不动。
原因:SGBM 在 CPU 上计算量大,尤其numDisparities大、blockSize大时。图像分辨率 1080p 比 720p 慢一倍以上。
解决:降分辨率到 640x480 或 720p;numDisparities按实际测距范围取最小值;blockSize用 5 而不是 11;如果还慢,考虑用 BM 算法替代 SGBM,速度快但精度略低。实在要实时,得上 CUDA 版的cudastereo。
5.5 左右相机不同步,运动物体测距错乱
现象:静态场景测距正常,一有运动物体深度就跳变。
原因:两个摄像头如果是分别读取的,采集时间差会导致运动物体在左右图中位置不对应。
解决:用支持硬件同步的相机,或者用同一个采集卡同时触发。软件层面可以尽量用全局快门相机,卷帘快门在运动场景下会有果冻效应。如果只是静态测距,这个问题可以忽略。
6. 二次开发:换算法、加功能、上嵌入式
6.1 替换匹配算法
SGBM 不是唯一选择。如果你想试 BM:
matcher = cv2.StereoBM_create(numDisparities=64, blockSize=15)BM 快但边缘差。想试深度学习,可以把core/stereo_matcher.py里的compute方法换成调用 ONNX 模型:
import onnxruntime as ort class DeepMatcher: def __init__(self, model_path): self.session = ort.InferenceSession(model_path) def compute(self, left, right): # 预处理:归一化、转 tensor inputs = { 'left': left[np.newaxis, np.newaxis, :, :].astype(np.float32), 'right': right[np.newaxis, np.newaxis, :, :].astype(np.float32) } disp = self.session.run(None, inputs)[0] return disp.squeeze()接口保持一致,main.py不用改。这是我觉得这个项目结构最舒服的地方——匹配算法可插拔。
6.2 加测距可视化
在图像上画深度热力图和距离标注:
# 视差归一化显示 disp_vis = cv2.normalize(disparity, None, 0, 255, cv2.NORM_MINMAX, cv2.CV_8U) disp_color = cv2.applyColorMap(disp_vis, cv2.COLORMAP_JET) # 在中心画十字并标注距离 cv2.drawMarker(rect_l, (w//2, h//2), (0, 0, 255), cv2.MARKER_CROSS, 20, 2) cv2.putText(rect_l, f"{center_depth/1000:.2f}m", (w//2+15, h//2-15), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) cv2.imshow('Left + Depth', np.hstack([rect_l, disp_color])) cv2.waitKey(0)6.3 移植到嵌入式平台
如果想跑在树莓派或 Jetson 上,几个注意点:树莓派上 OpenCV 要自己编译带 contrib 的版本,pip 装的默认不带 SGBM;Jetson 可以用 CUDA 加速,cv2.cuda.StereoSGBM速度能快 10 倍以上。分辨率降到 320x240,numDisparities用 32,树莓派 4B 能跑到 5~8 FPS。Jetson Nano 用 CUDA 版能到 30 FPS。
6.4 一个我踩过的坑
有次我把标定参数存成 YAML 后,读回来直接传给stereoRectify,结果报错说矩阵维度不对。查了半天发现 YAML 读回来的是 list,不是 numpy array,OpenCV 不认。从那以后我每次加载参数都强制走一遍np.array(..., dtype=np.float64),不管看起来多多余。这个习惯帮我省了不少调试时间。
希望帮到你。
本文还有配套的精品资源,点击获取