
简介本资源为基于Python与双目立体视觉的图像匹配与测距毕业设计项目源码包面向计算机视觉方向的高校学生及需要完成毕设、期末大作业或课程设计的开发者。项目围绕双目相机标定、立体匹配与深度测距展开包含完整可运行代码与配套资料代码注释详尽新手也能快速理解算法流程与工程结构。压缩包共164个文件约96.22MB其中38个py源码文件承载核心算法与界面逻辑49个bmp与23个jpg图像用于匹配测试与效果展示另有10个ui界面文件、5个xml配置、5份pdf与13份caj参考文献以及dll依赖库和qss样式表资料体系较为完整。已有70人学习关注。项目经过严格调试部署后即可运行界面美观、操作简单可直接作为毕设或课程设计提交同时便于在此基础上扩展测距精度优化与匹配算法对比实验具有较高的参考与复用价值。1. 双目立体视觉测距从「看起来能用」到「真的能用」差在哪很多同学第一次跑通双目测距是在一张标准数据集图上左右图一放SGBM 一算视差图一出来鼠标点一下就能报出距离误差还挺小。于是信心满满地换成自己拍的图结果视差图糊成一片测出来的距离忽大忽小近处还行三米开外直接飘到离谱。问题不在代码在于标定、校正、匹配参数和坐标系换算这四件事里至少有一件没做对。这篇笔记围绕「Python 双目立体视觉的图像匹配与测距」这条线把一套能复现的流程拆开讲相机怎么标、极线怎么校正、SGBM 参数怎么调、视差怎么换成真实距离、误差怎么排查。适合正在做毕业设计、课程设计或者想把单目测距升级成双目测距的嵌入式与视觉方向同学。单目测距靠先验尺寸或深度学习猜深度双目靠几何三角关系直接算稳定性和可解释性都更好代价是标定和匹配要花功夫。2. 标定与校正测距精度的地基怎么打双目标定的本质是求出左右相机各自的内参以及两个相机之间的旋转和平移关系。内参决定像素和物理尺寸的换算外参决定两个相机光心的基线距离和相对姿态。测距公式里那个基线 B就是从外参里来的标定不准后面匹配再漂亮距离也是错的。2.1 棋盘格采集数量、角度和覆盖范围常见做法是用 9x6 或 11x8 的棋盘格方格实际边长用尺子量准比如 25mm。采集时左右相机同时拍保证同一时刻棋盘格在两幅图里都完整可见。张数一般 15 到 25 对太少外参不稳太多也没必要。采集时要注意三点棋盘格要覆盖画面四个角和中心不能只堆在中间要有不同角度和距离倾斜、旋转、远近都拍一些避免强反光和运动模糊模糊的角点检测会直接失败。我一般会拍 20 对左右图命名成 left01.jpg、right01.jpg 这种成对格式方便脚本批量读。# 目录结构建议 calib_data/ left01.jpg right01.jpg left02.jpg right02.jpg ...命名成对是为了后面按序号同时读入左右图避免左右错位。如果左右图数量不一致标定脚本会直接报错这也是一个常见的低级翻车点。2.2 用 OpenCV 跑双目标定完整脚本与参数下面这段是标准的双目标定流程核心是 findChessboardCorners 找角点、cornerSubPix 亚像素精化、stereoCalibrate 求内外参。import cv2 import numpy as np import glob # 棋盘格内角点数量列, 行注意是内角点不是方格数 CHESSBOARD (9, 6) # 方格实际边长单位 mm必须用尺子量 SQUARE_SIZE 25.0 # 生成棋盘格三维坐标Z0 平面 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_imgs sorted(glob.glob(calib_data/left*.jpg)) right_imgs sorted(glob.glob(calib_data/right*.jpg)) assert len(left_imgs) len(right_imgs), 左右图数量不一致 criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) for lpath, rpath in zip(left_imgs, right_imgs): 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: # 亚像素精化提升角点定位精度 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) # 双目标定固定内参只求外参更稳 flags cv2.CALIB_FIX_INTRINSIC 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], flagsflags ) 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 联合优化。flags 用 CALIB_FIX_INTRINSIC 表示固定单目标定得到的内参只优化外参这样在标定图不够多的时候更稳定。参数说明CHESSBOARD 是内角点数9x6 的棋盘格内角点是 9 列 6 行写错成方格数是最常见的错误SQUARE_SIZE 单位要和后面测距单位一致用 mm 就全程 mmcornerSubPix 的 (11,11) 是搜索窗口图像分辨率高可以适当加大。标定完一定要看重投影误差ret 这个值一般要小于 0.5 像素超过 1 像素说明采集质量有问题别急着往下做。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(calib_data/left01.jpg) img_r cv2.imread(calib_data/right01.jpg) h, w img_l.shape[:2] # 立体校正alpha0 表示裁剪掉无效区域 R1, R2, P1, P2, Q, _, _ 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 矩阵后面测距要用逻辑说明stereoRectify 输出 R1、R2 是左右相机的校正旋转矩阵P1、P2 是校正后的投影矩阵Q 是视差到三维坐标的转换矩阵。alpha0 会裁掉校正后出现的黑边保留有效像素如果不想裁用 alpha1。参数说明initUndistortRectifyMap 的最后一个参数 CV_16SC2 是映射表格式比 CV_32FC1 省内存适合嵌入式。校正完可以画几条水平线检查同一个物点在左右图里应该落在同一条线上对不齐说明标定有问题。3. 图像匹配SGBM 参数怎么调才不糊匹配是双目测距里最玄学的一环同一组图参数差一点视差图就从清晰变糊。SGBM半全局块匹配是 OpenCV 里性价比最高的选择比 BM 稳比深度学习方案轻适合毕业设计这种要在普通电脑上跑的场景。3.1 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) # 视差搜索范围必须是 16 的整数倍 num_disp 16 * 6 block_size 5 stereo cv2.StereoSGBM_create( minDisparity0, numDisparitiesnum_disp, 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 ) disp stereo.compute(rect_l, rect_r).astype(np.float32) / 16.0 disp_norm cv2.normalize(disp, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8) cv2.imwrite(disparity.png, disp_norm)逻辑说明SGBM 输出的视差是放大 16 倍的定点数所以要除以 16 才是真实视差。参数说明numDisparities 决定能测多近值越大能覆盖的视差范围越大但计算量也越大且必须是 16 的倍数blockSize 是匹配块大小奇数3 到 11 之间越大越平滑但边缘越糊P1、P2 是平滑惩罚项P2 一般是 P1 的 4 倍左右控制视差图的连续性uniquenessRatio 是唯一性检验值越大越严格误匹配越少但空洞越多speckleWindowSize 和 speckleRange 用来过滤小面积噪点。3.2 参数调节的实操顺序调参不要一次全改按下面顺序来每次只动一个第一步先定 numDisparities。看你的场景最近物体大概多远基线越大、距离越近视差越大。如果视差图左边大片黑色说明视差超出范围加大 numDisparities。第二步调 blockSize。纹理丰富的场景用 3 或 5纹理弱、噪点多的场景用 7 到 11。blockSize 太大物体边缘会糊成一团测距在边缘处会飘。第三步调 P1、P2。视差图有大量孤立噪点加大 P2视差图过度平滑、细节丢失减小 P2。第四步调 uniquenessRatio。误匹配多就加大到 15 甚至 20但会牺牲一些有效区域。第五步speckle 过滤。视差图里那些零散的小斑点靠 speckleWindowSize 和 speckleRange 清掉一般 100 和 2 就够用。提示调参时把视差图归一化后和原图并排看比盯着数字调快得多。纹理弱的白墙、纯色桌面是 SGBM 的天敌这种场景要么加纹理要么换方案。3.3 视差图后处理把空洞补上SGBM 出来的视差图在遮挡区和弱纹理区会有空洞直接拿去测距会得到无效值。常见做法是用左右一致性检验标记无效点再用中值滤波或空洞填充补一下。# 左右一致性检验过滤遮挡区误匹配 stereo_r cv2.StereoSGBM_create( minDisparity0, numDisparitiesnum_disp, 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 ) disp_r stereo_r.compute(rect_r, rect_l).astype(np.float32) / 16.0 # 一致性检验 h, w disp.shape valid np.zeros_like(disp, dtypenp.uint8) for y in range(h): for x in range(w): d int(disp[y, x]) if d 0 and x - d 0: if abs(disp[y, x] - disp_r[y, x - d]) 1.0: valid[y, x] 255 disp_filtered cv2.medianBlur(disp, 5) disp_filtered[valid 0] 0逻辑说明左右一致性检验的思路是左图某点的视差在右图对应位置反向查回来的视差应该一致不一致就判为遮挡或误匹配。参数说明一致性阈值 1.0 像素是经验值要求严格可以调到 0.5中值滤波窗口 5 能去掉椒盐噪点但也会轻微模糊边缘。这一步在 Python 里逐像素循环会慢实际项目里可以用 numpy 向量化或者直接上 cv2.filterSpeckles。4. 从视差到距离公式、代码和误差来源视差图有了接下来就是把它变成真实距离。核心公式只有一个Z f * B / d其中 Z 是深度f 是校正后的焦距B 是基线d 是视差。这个公式看着简单但每个量取错一点结果就差很多。4.1 用 Q 矩阵做重投影OpenCV 提供了更省事的方式用 stereoRectify 输出的 Q 矩阵直接 reprojectImageTo3D。import cv2 import numpy as np Q np.load(Q.npy) disp cv2.imread(disparity.png, cv2.IMREAD_GRAYSCALE).astype(np.float32) # 注意reprojectImageTo3D 要求视差是原始定点数或真实视差这里用真实视差 points_3d cv2.reprojectImageTo3D(disp, Q) # 取图像中心点的三维坐标 h, w disp.shape cx, cy w // 2, h // 2 x, y, z points_3d[cy, cx] print(f中心点距离: {z:.1f} mm)逻辑说明reprojectImageTo3D 把每个像素的视差通过 Q 矩阵映射成三维坐标Z 就是深度。参数说明Q 矩阵必须和生成视差图时用的是同一套校正参数换了标定就要重新生成 Q视差图里的 0 值会被映射成很大的无效值用之前要过滤掉。4.2 手动算距离验证 Q 矩阵结果想确认 Q 矩阵算得对不对可以手动用公式算一遍对比。# 从 P1 里取校正后的焦距 # P1 形如 [[fx, 0, cx, 0], [0, fy, cy, 0], [0, 0, 1, 0]] # 这里 fx 就是校正后的焦距 fx P1[0, 0] baseline np.linalg.norm(T) # 基线单位 mm # 取中心点视差 d disp[cy, cx] if d 0: Z_manual fx * baseline / d print(f手动计算距离: {Z_manual:.1f} mm)逻辑说明手动公式和 Q 矩阵结果应该基本一致差太多说明 Q 或者视差有问题。参数说明fx 从 P1 取不要用单目标定的 mtx_l因为校正后焦距可能变了baseline 用 T 的模单位要和标定时一致。4.3 误差来源逐个拆双目测距的误差不是均匀的近处准、远处飘是常态。误差主要来自这几块误差来源表现影响程度标定误差整体偏移近处也偏大影响全局视差量化误差远处距离跳变大随距离平方放大匹配误匹配局部距离突变中可过滤基线测量误差距离整体比例错大但可校准镜头畸变残留边缘区域偏中靠校正视差误差对距离的影响是平方级的公式求导可以看出来dZ/dd -f*B/d²。视差差 1 个像素距离越远误差越大。所以双目测距的有效范围是有限的基线 60mm、焦距 700 像素的配置三米开外误差就很可观了。想测更远要么加大基线要么提高分辨率。5. 避坑与排查那些让我重标了三次的坑这一章全是血泪经验每条都是实际踩过的。现象视差图整体全黑或全白。原因numDisparities 设太小或者左右图顺序反了。解决先确认左右图没搞反再把 numDisparities 加大到能覆盖最大视差可以先用 BM 算法粗看视差范围。现象标定重投影误差很小但测距就是不准。原因棋盘格方格尺寸量错或者标定时单位不统一。解决重新用尺子量方格边长确认标定和测距全程单位一致mm 就全 mm别中途换 cm。现象视差图中间清晰边缘一塌糊涂。原因镜头畸变没校正干净或者棋盘格没覆盖画面边缘。解决重新采集标定图让棋盘格覆盖四个角标定时打开畸变模型校正后再看边缘。现象同一物体距离读数一直跳。原因弱纹理区域匹配不稳定或者光照变化导致左右图亮度不一致。解决给场景加纹理或者用 preFilterCap 做预处理左右图曝光尽量一致能手动锁曝光就锁。现象近处准两米外误差突然变大。原因视差量化误差随距离放大这是双目的物理限制不是 bug。解决要么加大基线要么接受有效测距范围别硬吹能测十米。注意标定图别用手机随手拍左右相机不同步、有滚动快门标定出来外参是错的。要用能同步触发的双目相机或者至少用同一台设备固定好再拍。6. 把测距做稳的进阶技巧从能跑到能用跑通流程只是第一步要让测距真的能用还得在几个地方下功夫。第一标定结果要验证。标定完别急着测距先拿一个已知距离的物体放那看测出来差多少。我一般会在 500mm、1000mm、2000mm 各放一次记录误差心里有个数。如果 500mm 就差了 50mm说明标定或校正有问题别往下做。第二视差图要选点测距不要全图平均。全图平均会把背景和前景混在一起得到一个没意义的数。常见做法是框选一个区域取区域内有效视差的中位数比平均值抗噪。代码上就是先掩膜再统计。# 框选区域测距取中位数抗噪 roi disp[cy-20:cy20, cx-20:cx20] valid_disp roi[roi 0] if len(valid_disp) 0: d_median np.median(valid_disp) Z fx * baseline / d_median print(f区域中位距离: {Z:.1f} mm)逻辑说明中位数比平均值更抗离群点区域内有几个误匹配也不影响整体。参数说明区域大小根据目标物体尺寸定太小统计不稳太大混入背景。第三参数要存下来。标定参数、SGBM 参数、Q 矩阵全部存成文件测距脚本读文件别每次重新标定。换场景只调 SGBM 参数标定参数不动。第四知道什么时候该换方案。双目在弱纹理、重复纹理、远距离场景下就是不行这是原理决定的。如果场景是白墙、玻璃、大面积纯色考虑加结构光或者换 ToF别在 SGBM 上死磕。我自己的习惯是每换一个场景先拍一对图跑一遍看视差图质量再决定参数怎么调不盲目套上一套参数。双目测距没有万能参数只有针对场景调出来的参数。希望帮到你。本文还有配套的精品资源点击获取