简介这份资源面向计算机视觉初学者与机器人、自动驾驶方向的开发者提供一套在Ubuntu环境下基于OpenCV与Qt实现双目测距系统的完整工程。内容围绕立体视觉展开涵盖摄像头同步与对齐、SIFT/ORB特征匹配、半全局立体匹配、基础矩阵与单应性矩阵求解、三角测量、深度图重建及目标距离计算等核心环节并给出图像显示、参数调整与测距结果展示的交互界面设计思路。压缩包为rar格式大小约80.48MB包内文件类型与数量上游未提供明细可结合描述中的模块划分理解其代码组织方式。目前已有743人学习下载适合希望从原理到工程落地系统掌握双目测距、并用于机器人导航或工业检测场景的读者参考。1. 双目测距到底能干嘛从两个摄像头到真实距离很多人第一次听说双目测距脑子里浮现的是科幻片里那种扫一眼就知道对方离自己多远的黑科技。其实原理朴素得很——人为什么能判断距离因为两只眼睛看到的画面有细微差异大脑根据这个差异反推远近。双目测距系统干的就是同一件事用两个摄像头模拟双眼通过计算同一物体在左右画面中的像素位置差结合相机参数算出它离镜头到底多少米。这套基于 OpenCV 的双目测距系统核心就是把这套流程工程化标定相机、校正图像、匹配像素、计算视差、输出深度。适合谁做机器人避障的、搞自动驾驶感知入门的、参加电子设计竞赛需要测距模块的、以及想学计算机视觉但不想一上来就啃深度学习的开发者。它不依赖 GPU普通笔记本就能跑代码结构清晰改起来不费劲。下面我从项目结构开始一步步拆给你看。2. 项目骨架与技术选型为什么是 OpenCV Python2.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-python4.5.0 opencv-contrib-python4.5.0 numpy1.19.0 pyyaml5.3注意opencv-contrib-python要装因为立体匹配的 SGBMSemi-Global Block Matching在 contrib 包里。如果你只用 BMBlock 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), flagscv2.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, alpha0 # alpha0 表示裁剪掉无效区域alpha1 保留全部 ) # 生成映射表 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矩阵很关键它是视差转三维坐标的桥梁。alpha0会裁掉边缘无效像素适合对视野要求不高的场景alpha1保留全部但会有黑边。我一般先用 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_disparities64, block_size5): 创建 SGBM 立体匹配器 # 左图通道数灰度图用 1 channels 1 return cv2.StereoSGBM_create( minDisparity0, numDisparitiesnum_disparities, # 必须是 16 的整数倍 blockSizeblock_size, # 奇数3~11 P18 * channels * block_size ** 2, # 控制视差平滑度 P232 * channels * block_size ** 2, # 通常是 P1 的 4 倍 disp12MaxDiff1, # 左右一致性检查最大差异 uniquenessRatio10, # 唯一性比率 speckleWindowSize100, # 斑点滤波窗口大小 speckleRange2, # 斑点滤波范围 modecv2.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.pyimport 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, alpha0) 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( minDisparity0, numDisparities64, blockSize5, P18*1*5**2, P232*1*5**2, uniquenessRatio10, speckleWindowSize100, speckleRange2, disp12MaxDiff1, modecv2.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 或 720pnumDisparities按实际测距范围取最小值blockSize用 5 而不是 11如果还慢考虑用 BM 算法替代 SGBM速度快但精度略低。实在要实时得上 CUDA 版的cudastereo。5.5 左右相机不同步运动物体测距错乱现象静态场景测距正常一有运动物体深度就跳变。原因两个摄像头如果是分别读取的采集时间差会导致运动物体在左右图中位置不对应。解决用支持硬件同步的相机或者用同一个采集卡同时触发。软件层面可以尽量用全局快门相机卷帘快门在运动场景下会有果冻效应。如果只是静态测距这个问题可以忽略。6. 二次开发换算法、加功能、上嵌入式6.1 替换匹配算法SGBM 不是唯一选择。如果你想试 BMmatcher cv2.StereoBM_create(numDisparities64, blockSize15)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//215, 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 装的默认不带 SGBMJetson 可以用 CUDA 加速cv2.cuda.StereoSGBM速度能快 10 倍以上。分辨率降到 320x240numDisparities用 32树莓派 4B 能跑到 5~8 FPS。Jetson Nano 用 CUDA 版能到 30 FPS。6.4 一个我踩过的坑有次我把标定参数存成 YAML 后读回来直接传给stereoRectify结果报错说矩阵维度不对。查了半天发现 YAML 读回来的是 list不是 numpy arrayOpenCV 不认。从那以后我每次加载参数都强制走一遍np.array(..., dtypenp.float64)不管看起来多多余。这个习惯帮我省了不少调试时间。希望帮到你。本文还有配套的精品资源点击获取 SEO 优化官网定制响应式建站教育培训建站