Python双目立体视觉测距实战:从标定到深度计算
简介本资源面向计算机、自动化等专业的本科毕业生及课程设计学习者提供一套基于双目立体视觉的图像匹配与测距完整项目源码与配套资料可直接用于毕业设计、期末大作业或课程设计场景。压缩包共164个文件约96.22MB包含38个Python源码文件、10个UI界面文件、49张bmp与23张jpg图像样本、13份caj文献、5份pdf与2份docx文档以及qrc、qss、xml等配置资源覆盖算法实现、界面搭建与测试数据。项目代码注释详尽新手也能理解双目校正、立体匹配与深度测距的核心流程部署后即可运行。已有69人学习关注适合希望快速获得可运行高分项目、节省选题与调试时间的同学参考借鉴。1. 双目立体视觉测距到底难在哪从一张毕业设计标题说起很多人第一次看到「Python 基于双目立体视觉的图像匹配与测距」这个题目第一反应是去搜现成源码下载下来跑一遍发现能出深度图就以为搞定了。真正动手做毕业设计或者工程落地时才会发现能跑通 demo 和能交付一套可复现、参数可调、误差可解释的测距系统中间隔着好几道坎。双目测距的核心链路其实就四步双目标定、立体校正、图像匹配、视差转深度。每一步都有大量参数和边界条件任何一步出问题最后的距离值就会离谱到没法用。这个方向适合两类人一类是正在做毕业设计、课程设计的学生需要一套完整可复现的 Python 方案另一类是想把双目测距用到机器人避障、体积测量、工业检测里的工程师需要知道参数怎么调、误差怎么控。本文不讲空泛概念直接从标定板怎么拍、匹配算法怎么选、视差图怎么过滤、测距公式怎么推导一路写到踩坑记录和精度验证方法。读完你至少能自己搭出一套能测 0.3 米到 5 米、误差可控的双目测距流程。2. 双目标定与立体校正把两个摄像头变成一把尺子2.1 为什么标定是测距精度的天花板双目测距的底层原理是三角测量同一个物点在左右两个相机里成像位置不同这个位置差叫视差视差越大物体越近。公式很简单Z f * B / d其中 f 是焦距像素单位B 是两个相机光心的基线距离d 是视差。问题在于这个公式成立的前提是两个相机光轴严格平行、成像平面严格共面。真实摄像头装上去一定有偏差镜头还有畸变所以必须通过标定把内参、外参、畸变系数全部算出来再用立体校正把左右图像重投影到同一个理想平面上。标定精度直接决定测距精度。我见过太多人用手机随便拍十几张棋盘格就标定结果重投影误差 1.5 像素以上测 1 米外的物体误差能到十几厘米。常见做法是棋盘格标定板打印在 A3 纸上贴在硬板上用双目相机同时采集 20 到 30 组不同角度、不同距离的图像覆盖画面四个角和中心区域。OpenCV 的 stereoCalibrate 会输出重投影误差这个值最好控制在 0.3 像素以内超过 0.5 像素就要重新采集。2.2 用 OpenCV 跑通双目标定的完整代码下面这段代码是标定流程的核心输入是左右相机各自拍的棋盘格图像文件夹输出是内参矩阵、畸变系数、旋转和平移矩阵。import cv2 import numpy as np import glob # 棋盘格内角点数量比如 9x6 的棋盘格就写 (9, 6) CHESSBOARD (9, 6) # 棋盘格方格实际物理尺寸单位毫米 SQUARE_SIZE 25.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(left/*.png)) right_images sorted(glob.glob(right/*.png)) for lpath, rpath in zip(left_images, right_images): img_l cv2.imread(lpath) img_r cv2.imread(rpath) 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_l, mtx_l, dist_l, _, _ cv2.calibrateCamera(objpoints, imgpoints_l, gray_l.shape[::-1], None, None) ret_r, mtx_r, dist_r, _, _ cv2.calibrateCamera(objpoints, imgpoints_r, gray_r.shape[::-1], None, None) # 双目标定 ret, mtx_l, dist_l, mtx_r, dist_r, R, T, E, F cv2.stereoCalibrate( objpoints, imgpoints_l, imgpoints_r, mtx_l, dist_l, mtx_r, dist_r, gray_l.shape[::-1], flagscv2.CALIB_FIX_INTRINSIC, criteria(cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 100, 1e-5) ) print(重投影误差:, ret) print(基线距离(mm):, np.linalg.norm(T)) np.savez(stereo_params.npz, mtx_lmtx_l, dist_ldist_l, mtx_rmtx_r, dist_rdist_r, RR, TT)这段代码的逻辑是先用单目标定拿到每个相机的初始内参再把这些内参作为初值传给 stereoCalibrate并用 CALIB_FIX_INTRINSIC 固定内参只优化外参。参数上CHESSBOARD 必须和实际棋盘格内角点数一致注意是内角点不是方格数SQUARE_SIZE 单位要和后续测距单位统一用毫米就全程毫米。cornerSubPix 的窗口 (11,11) 是常用值图像分辨率高可以适当加大。重投影误差 ret 如果超过 0.5优先检查棋盘格是否平整、图像是否有运动模糊、左右图是否严格同步采集。2.3 立体校正让左右图像行对齐标定完之后要做 stereoRectify 和 initUndistortRectifyMap把左右图像校正成行对齐。校正后同一个物点在左右图中应该在同一行这样后续匹配只需要在同一行搜索速度和准确率都会大幅提升。import cv2 import numpy as np data np.load(stereo_params.npz) mtx_l, dist_l data[mtx_l], data[dist_l] mtx_r, dist_r data[mtx_r], data[dist_r] R, T data[R], data[T] img_l cv2.imread(left/001.png) img_r cv2.imread(right/001.png) h, w img_l.shape[:2] # 立体校正alpha0 表示裁剪掉无效区域alpha1 保留全部像素 R1, R2, P1, P2, Q, roi1, roi2 cv2.stereoRectify( mtx_l, dist_l, mtx_r, dist_r, (w, h), R, T, alpha0 ) # 生成映射表 map1_l, map2_l cv2.initUndistortRectifyMap(mtx_l, dist_l, R1, P1, (w, h), cv2.CV_16SC2) map1_r, map2_r cv2.initUndistortRectifyMap(mtx_r, dist_r, R2, P2, (w, h), 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) cv2.imwrite(rect_left.png, rect_l) cv2.imwrite(rect_right.png, rect_r) np.save(Q.npy, Q)Q 矩阵是后面视差转三维坐标的关键必须保存。alpha 参数控制校正后图像是否裁剪做测距建议用 alpha0虽然会损失边缘像素但能保证所有像素都有效。校正完一定要肉眼检查在左右图上画几条水平线看同一个物体是否在同一行上如果偏差超过 1 到 2 像素说明标定有问题回去重新标。3. 图像匹配算法选型SGBM、BM 和深度学习怎么选3.1 传统匹配算法的参数逻辑OpenCV 里最常用的两种立体匹配算法是 StereoBM 和 StereoSGBM。BM 速度快但视差图噪声大SGBM 半全局匹配精度高很多是毕业设计和工程落地的默认选择。SGBM 的核心参数有这几个minDisparity 最小视差、numDisparities 视差搜索范围、blockSize 匹配块大小、P1 和 P2 平滑惩罚系数、uniquenessRatio 唯一性比率、speckleWindowSize 和 speckleRange 斑点过滤。numDisparities 必须能被 16 整除它决定了能测多近的物体。视差范围越大能测的最近距离越小但计算量也越大。blockSize 一般取 5 到 11 之间的奇数太小噪声多太大边缘模糊。P1 和 P2 控制视差平滑程度常见经验值是 P1 8 * channels * blockSize^2P2 32 * channels * blockSize^2。3.2 SGBM 完整实现与参数调优import cv2 import numpy as np rect_l cv2.imread(rect_left.png, cv2.IMREAD_GRAYSCALE) rect_r cv2.imread(rect_right.png, cv2.IMREAD_GRAYSCALE) # SGBM 参数配置 num_disparities 16 * 6 # 视差范围必须是 16 的倍数 block_size 5 # 匹配块大小奇数 stereo cv2.StereoSGBM_create( minDisparity0, numDisparitiesnum_disparities, blockSizeblock_size, P18 * 1 * block_size * block_size, P232 * 1 * block_size * block_size, disp12MaxDiff1, uniquenessRatio10, speckleWindowSize100, speckleRange2, preFilterCap63, modecv2.STEREO_SGBM_MODE_SGBM_3WAY ) disparity stereo.compute(rect_l, rect_r).astype(np.float32) / 16.0 # 归一化显示 disp_vis cv2.normalize(disparity, None, 0, 255, cv2.NORM_MINMAX) disp_vis np.uint8(disp_vis) cv2.imwrite(disparity.png, disp_vis) np.save(disparity.npy, disparity)compute 返回的是定点数除以 16 才是真实视差值。uniquenessRatio 设 10 表示最优匹配代价必须比次优好 10% 才接受能有效过滤误匹配。speckleWindowSize 设 100 表示小于 100 像素的连通区域会被当作噪声滤掉如果你的物体本身很小这个值要调小。disp12MaxDiff 设 1 表示左右一致性检查容差设 0 最严格但会产生更多空洞。3.3 深度学习匹配的适用边界近几年基于深度学习的立体匹配网络如 PSMNet、RAFT-Stereo在精度上确实超过传统方法尤其是弱纹理区域和反光表面。但毕业设计场景下我不建议一上来就上深度学习原因有三一是需要 GPU 和大量训练数据二是推理速度慢三是调参和部署复杂度高。如果你的场景是室内、纹理丰富、实时性要求不高SGBM 完全够用。只有在传统方法明显失效、且你有 GPU 资源的情况下才考虑用预训练模型做迁移。4. 视差转深度与测距实现从像素差到真实距离4.1 用 Q 矩阵把视差图变成三维点云立体校正时保存的 Q 矩阵可以直接把视差图重投影成三维点云。OpenCV 的 reprojectImageTo3D 就是干这个的输出每个像素对应的 (X, Y, Z) 坐标Z 就是深度值。import cv2 import numpy as np disparity np.load(disparity.npy) Q np.load(Q.npy) # 视差图中无效值处理小于等于 0 的视差设为 -1 disparity[disparity 0] -1 points_3d cv2.reprojectImageTo3D(disparity, Q) # 取图像中心区域的平均深度作为测距结果 h, w disparity.shape cx, cy w // 2, h // 2 roi points_3d[cy-20:cy20, cx-20:cx20, 2] valid roi[np.isfinite(roi) (roi 0)] if len(valid) 0: distance np.median(valid) print(中心区域距离(mm):, distance)这里用中位数而不是平均值是因为视差图边缘容易有异常值中位数更鲁棒。取中心 40x40 区域是为了避免边缘噪声影响。如果你的目标是测某个特定物体应该先用颜色或轮廓分割出目标区域再对该区域的深度值取中位数。4.2 测距公式的手动推导与验证不想用 Q 矩阵的话也可以手动算Z f * B / d。f 可以从 P1 矩阵里取 P1[0,0]B 是基线距离从 T 向量取模d 是该点的视差值。这两种方法结果应该一致如果不一致说明 Q 矩阵或 P1 有问题。# 手动计算验证 f P1[0, 0] # 校正后的焦距 B np.linalg.norm(T) # 基线距离 d disparity[cy, cx] # 中心点视差 if d 0: Z_manual f * B / d print(手动计算距离(mm):, Z_manual)验证方法把标定板放在已知距离比如 1000mm处拍一组图跑完整流程看输出距离和真实距离差多少。如果误差在 1% 到 3% 以内说明整套流程没问题。如果误差超过 5%优先检查标定重投影误差和基线距离是否准确。4.3 视差图后处理空洞填充与噪声过滤原始 SGBM 视差图一定会有空洞和噪声直接拿去测距会跳变。常见后处理有三步左右一致性检查、空洞填充、中值滤波。左右一致性检查是把右图当参考再算一次视差两次视差差异超过阈值的像素标记为无效。空洞填充可以用最近邻有效视差填充或者用形态学闭运算。中值滤波用 cv2.medianBlur窗口 5 或 7能去掉大部分椒盐噪声。# 中值滤波去噪 disp_filtered cv2.medianBlur(disparity.astype(np.float32), 5) # 空洞填充用形态学闭运算 kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) disp_filled cv2.morphologyEx(disp_filtered, cv2.MORPH_CLOSE, kernel)注意中值滤波窗口不能太大否则会把真实的深度边缘也模糊掉。如果你的场景里物体边缘很重要建议先做左右一致性检查再做小窗口中值滤波。5. 避坑与排查双目测距翻车实录5.1 重投影误差正常但测距就是不准现象标定重投影误差 0.2 像素看起来很好但测距误差超过 10%。原因通常是基线距离 T 的测量或计算有问题。T 是 stereoCalibrate 输出的平移向量它的模就是基线。如果标定时棋盘格没有同时出现在左右相机视野里或者左右图像没有严格同步T 就会偏。解决重新采集数据确保每组图像左右相机都能完整看到棋盘格且采集时相机和标定板都静止。5.2 视差图大面积黑色空洞现象SGBM 输出的视差图大部分是黑色只有零星几个点有值。原因一般是 numDisparities 设置太小或者图像纹理太弱。解决先增大 numDisparities 到 168 甚至 1610再看效果。如果还是不行检查校正后的图像是否真的行对齐可以在左右图上画水平线对比。弱纹理场景可以加投影纹理或者用深度学习匹配。5.3 测距值随距离跳变严重现象同一个物体静止不动连续测十次距离值在几十厘米范围内跳。原因是视差图噪声大中心区域取了太多无效值。解决先做左右一致性检查和空洞填充再取目标区域的中位数。如果还跳增大 blockSize 到 7 或 9提高 uniquenessRatio 到 15牺牲一些细节换取稳定性。5.4 近距离测不了远距离也不准现象0.5 米以内测不出来3 米以外误差巨大。原因是视差范围和基线距离的物理限制。numDisparities 决定了最小可测距离基线 B 决定了远距离精度。解决如果要测近增大 numDisparities如果要测远增大基线 B。但基线增大后近处盲区也会变大需要根据实际场景权衡。常见做法是基线取 60mm 到 120mm覆盖 0.3 米到 5 米范围。5.5 左右图像校正后行不对齐现象校正后左右图同一物体不在同一行偏差好几像素。原因是标定参数不准或者 stereoRectify 的 alpha 参数和 initUndistortRectifyMap 不匹配。解决重新标定确保重投影误差小于 0.3。检查 stereoRectify 和 initUndistortRectifyMap 是否用了同一组 R1、R2、P1、P2。校正后一定要画水平线肉眼验证不要只看代码跑通。6. 精度验证与进阶技巧把误差压到 1% 以内6.1 用已知距离标定板做端到端验证整套流程跑通后必须做端到端精度验证。我的习惯是把棋盘格放在 500mm、1000mm、1500mm、2000mm、3000mm 五个距离每个距离拍 10 组图跑完整流程记录测量值和真实值的偏差。下面是一个批量验证的脚本框架。import cv2 import numpy as np import glob def measure_distance(left_path, right_path, params_pathstereo_params.npz, Q_pathQ.npy): data np.load(params_path) Q np.load(Q_path) mtx_l, dist_l data[mtx_l], data[dist_l] mtx_r, dist_r data[mtx_r], data[dist_r] R, T data[R], data[T] img_l cv2.imread(left_path) img_r cv2.imread(right_path) h, w img_l.shape[:2] R1, R2, P1, P2, _, _, _ cv2.stereoRectify( mtx_l, dist_l, mtx_r, dist_r, (w, h), R, T, alpha0) map1_l, map2_l cv2.initUndistortRectifyMap(mtx_l, dist_l, R1, P1, (w, h), cv2.CV_16SC2) map1_r, map2_r cv2.initUndistortRectifyMap(mtx_r, dist_r, R2, P2, (w, h), 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) gray_l cv2.cvtColor(rect_l, cv2.COLOR_BGR2GRAY) gray_r cv2.cvtColor(rect_r, cv2.COLOR_BGR2GRAY) stereo cv2.StereoSGBM_create( minDisparity0, numDisparities16*6, blockSize5, P18*1*25, P232*1*25, disp12MaxDiff1, uniquenessRatio10, speckleWindowSize100, speckleRange2, preFilterCap63, modecv2.STEREO_SGBM_MODE_SGBM_3WAY) disp stereo.compute(gray_l, gray_r).astype(np.float32) / 16.0 disp[disp 0] -1 points_3d cv2.reprojectImageTo3D(disp, Q) cx, cy w // 2, h // 2 roi points_3d[cy-20:cy20, cx-20:cx20, 2] valid roi[np.isfinite(roi) (roi 0)] return np.median(valid) if len(valid) 0 else -1 # 批量验证 ground_truth {500: 500, 1000: 1000, 1500: 1500, 2000: 2000, 3000: 3000} for tag, gt in ground_truth.items(): errors [] for lpath in sorted(glob.glob(fval_{tag}/left/*.png)): rpath lpath.replace(left, right) d measure_distance(lpath, rpath) if d 0: errors.append(abs(d - gt) / gt * 100) if errors: print(f真实距离 {gt}mm平均误差 {np.mean(errors):.2f}%最大误差 {np.max(errors):.2f}%)这个脚本把测量函数封装好对每个距离批量跑输出平均误差和最大误差。如果某个距离误差特别大单独看那个距离的视差图大概率是视差范围不够或者纹理太弱。6.2 三个把误差压到 1% 以内的技巧第一个技巧是亚像素级视差优化。SGBM 输出的是整数视差精度有限。可以用 cv2.filterSpeckles 配合二次曲线拟合做亚像素插值把视差精度提到 0.1 像素级别测距精度能提升一个档次。第二个技巧是温度补偿。相机长时间工作会发热镜头和传感器热胀冷缩会导致基线微变远距离测量时这个误差不可忽略。我的习惯是开机预热 10 分钟再标定和测量。第三个技巧是分区域标定。如果测量范围跨度大可以在近处和远处分别标定一组参数测量时根据粗测距离切换参数组能显著降低全量程误差。6.3 从毕业设计到工程落地的最后一步毕业设计做到能出视差图、能测几个距离基本就够了。但如果要往工程落地走还要考虑实时性、鲁棒性和标定自动化。实时性方面SGBM 在 640x480 分辨率下能跑到 20 到 30 帧再大就要降采样或换 BM。鲁棒性方面强光、反光、弱纹理场景要有降级策略比如切换到主动双目或者加结构光。标定自动化方面可以做一个一键标定脚本自动采集、自动筛选、自动输出报告减少人工干预。我自己踩过最深的坑是标定的时候图省事用手机拍了几张棋盘格就标结果测距误差一直下不来查了两天才发现是标定板不平整导致角点检测有系统偏差。后来老老实实打印 A3 贴硬板重投影误差从 0.8 降到 0.2测距误差直接从 8% 降到 1.5%。所以如果你现在测距不准先回去看标定别急着调匹配参数。希望帮到你。本文还有配套的精品资源点击获取