简介基于Python与OpenCV的双目视觉测量项目源码面向计算机、通信、人工智能、自动化等相关专业的学生、教师及从业者用于测量被摄物体的尺寸。项目为个人毕业设计答辩评审分达98分代码经调试测试可直接运行适合课程设计、大作业或毕业设计参考也便于初学者进阶学习。资源共23个文件核心为1个Python源码文件与1份Markdown文档说明辅以21张双目图像样本左右视图及测量结果图压缩包约2.26MB。图片素材涵盖左右相机视图与测量结果图可配合源码对照理解双目匹配与尺寸测量流程文档说明则有助于梳理算法思路与工程实现细节。资源包结构简洁清晰按左右相机图像、处理结果和主程序分类组织便于对照运行效果逐步调试。已有201人学习使用。基础较好者可在此基础上调整相机参数或扩展测量对象实现更丰富的视觉测量应用。1. 双目视觉测量物体尺寸这个毕设题目到底在解决什么先问一个最直接的问题如果手里只有一个普通USB摄像头怎么知道眼前那个零件最长边是多少毫米常见做法是放一把尺子做参照或者在已知距离下用像素当量换算但这些方法换个距离、换个角度就得重来精度也经不起推敲。这个题目给了一条更稳的路用两个摄像头模仿人眼通过左右画面的视差算出每个点的深度再把像素坐标投影回真实的三维空间最后直接量两点之间的物理距离。这就是基于Python和OpenCV的双目视觉尺寸测量一个从标定到出数都能自己掌控的完整闭环。这个方案的价值在于不依赖任何深度传感器一台电脑加两个普通摄像头就能复现适合做毕业设计也适合做产线视觉检测的前期预研。标题里“源码文档说明”表明项目不只是算法Demo还有一套可交付的工程结构。接下来我从选型、标定、立体匹配、尺寸计算到踩坑按一条完整可复现的路径讲清楚。2. 先把硬件和标定做扎实双目系统的地基不能省2.1 硬件选型为什么两个摄像头必须同型号同焦距很多初学者第一反应是找两个旧手机摄像头或者手边任意两个USB摄像头拼在一起结果画面颜色、亮度、畸变都不一样后面校正和匹配全部跟着遭殃。双目视觉测量对硬件的基本要求是两个相机型号一致、分辨率一致、镜头焦距一致最好连感光芯片型号都相同。差异越小左右图像的色彩响应越接近立体匹配的误匹配率越低。我一般会建议用两个同型号的USB免驱摄像头固定在一根铝型材或者一块3D打印的支架上光轴尽量平行基线距离两个镜头光心之间的距离在5到15厘米之间。基线越大近距离深度分辨率越高但公共视野会变窄基线太小深度精度又不够。做毕业设计放在桌面上测量小物体10厘米左右的基线比较稳妥。还有一个容易被忽略的点两个摄像头必须同时触发采集。USB摄像头用OpenCV的VideoCapture分别打开两个设备时帧同步是做不到严格对齐的。常见做法是让两个摄像头对着同一块棋盘格拍摄视频流然后选时间戳最近的左右帧作为一对。如果被测物体是静止的这个问题不大如果物体在移动就要考虑用硬件触发或者接受一定的帧不同步误差。2.2 单目标定拿到内参和畸变系数在跑双目标定之前先把两个相机各自的单目标定做完。这一步的目的是拿到每个相机的内参矩阵、畸变系数和重投影误差。OpenCV里用cv2.calibrateCamera()来完成输入是棋盘格角点的世界坐标和像素坐标。单目标定的采集注意事项先讲清楚用A4纸打印一张10×7的棋盘格内部角点9×6贴在硬纸板上确保棋盘格绝对平整不要贴在软纸上。采集时让棋盘格在画面中占据不同位置每个位置旋转不同角度至少采集15到20张有效图像。图像模糊、高光反光、角点残缺的直接删掉留着只会拉高标定误差。import cv2 import numpy as np import glob # 棋盘格内部角点数 CHECKERBOARD (9, 6) criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) objp np.zeros((CHECKERBOARD[0] * CHECKERBOARD[1], 3), np.float32) objp[:, :2] np.mgrid[0:CHECKERBOARD[0], 0:CHECKERBOARD[1]].T.reshape(-1, 2) objpoints [] # 世界坐标系下的角点坐标 imgpoints_left [] # 左相机像素坐标 imgpoints_right [] # 右相机像素坐标 images_left sorted(glob.glob(calib_images/left/*.jpg)) images_right sorted(glob.glob(calib_images/right/*.jpg)) assert len(images_left) len(images_right), 左右图像数量不一致 for img_left_path, img_right_path in zip(images_left, images_right): img_left cv2.imread(img_left_path) img_right cv2.imread(img_right_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, CHECKERBOARD, None) ret_right, corners_right cv2.findChessboardCorners(gray_right, CHECKERBOARD, None) if ret_left and ret_right: corners_left cv2.cornerSubPix(gray_left, corners_left, (11, 11), (-1, -1), criteria) corners_right cv2.cornerSubPix(gray_right, corners_right, (11, 11), (-1, -1), criteria) objpoints.append(objp) imgpoints_left.append(corners_left) imgpoints_right.append(corners_right) # 单目标定左相机 ret_l, mtx_l, dist_l, rvecs_l, tvecs_l cv2.calibrateCamera( objpoints, imgpoints_left, gray_left.shape[::-1], None, None) # 单目标定右相机 ret_r, mtx_r, dist_r, rvecs_r, tvecs_r cv2.calibrateCamera( objpoints, imgpoints_right, gray_right.shape[::-1], None, None) print(左相机重投影误差:, ret_l) print(右相机重投影误差:, ret_r) print(左相机内参矩阵:\n, mtx_l) print(右相机内参矩阵:\n, mtx_r)这段代码的逻辑是先用findChessboardCorners找棋盘格角点粗位置再用cornerSubPix做亚像素细化最后把左右相机的角点分别送入calibrateCamera。返回的ret_l和ret_r是重投影误差单位是像素数值越小越好一般低于0.3像素就算合格。这里有个参数值得注意objp的尺度是任意的因为标定只需要角点的相对位置关系不需要真实的棋盘格尺寸。但如果后面要做双目标定的平移向量最好把objp改成用真实格边长乘以行列索引我一般会把棋盘格格边长设为30毫米这样标定出的平移向量直接就是毫米单位。2.3 双目标定把两个相机的位置关系锁死单目标定完成后立刻做双目标定。双目标定的目标是拿到左右相机之间的旋转矩阵和平移向量这决定了后续极线校正的质量。# 双目标定使用单目得到的初始值 flags cv2.CALIB_FIX_INTRINSIC criteria_stereo (cv2.TERM_CRITERIA_MAX_ITER cv2.TERM_CRITERIA_EPS, 100, 1e-5) ret_stereo, mtx_l, dist_l, mtx_r, dist_r, R, T, E, F cv2.stereoCalibrate( objpoints, imgpoints_left, imgpoints_right, mtx_l, dist_l, mtx_r, dist_r, gray_left.shape[::-1], criteriacriteria_stereo, flagsflags) print(立体标定重投影误差:, ret_stereo) print(旋转矩阵 R:\n, R) print(平移向量 T:\n, T) print(本征矩阵 E:\n, E) print(基础矩阵 F:\n, F)stereoCalibrate返回的R是右相机相对左相机的旋转矩阵T是平移向量。只要标定板被两边的相机同时完整看到这个函数就能把两相机的外参锁死。如果重投影误差超过1像素说明标定板在采集过程中弯了、图像模糊、或者某张图的角点检测结果错了需要把异常样本挑出来删除后重新标定。这里我没有用CALIB_FIX_INTRINSIC以外的flag是因为对毕设场景来说固定内参只优化外参已经足够稳定。如果你的镜头畸变特别大可以去掉这个flag让内参也参与优化但容易出现内参漂移导致结果不收敛的情况。先固定内参跑一遍如果误差太大再做一次全参数优化。3. 极线校正与SGBM立体匹配从双图到视差图3.1 极线校正让匹配变成一行的事双目匹配最核心的前提是左图中一个像素点在右图中的对应位置必须出现在同一行。这就是极线约束。现实中的两个相机很难做到光轴完全平行所以要用stereoRectify和initUndistortRectifyMap把左右图像变换到这个理想状态。# 图像尺寸 h, w gray_left.shape[:2] # 计算校正映射 R1, R2, P1, P2, Q, roi1, roi2 cv2.stereoRectify( mtx_l, dist_l, mtx_r, dist_r, (w, h), R, T, alpha0.0) # 初始化映射表 map1_l, map2_l cv2.initUndistortRectifyMap( mtx_l, dist_l, R1, P1, (w, h), cv2.CV_32FC1) map1_r, map2_r cv2.initUndistortRectifyMap( mtx_r, dist_r, R2, P2, (w, h), cv2.CV_32FC1) # 校正后的图像 rectified_left cv2.remap(img_left, map1_l, map2_l, cv2.INTER_LINEAR) rectified_right cv2.remap(img_right, map1_r, map2_r, cv2.INTER_LINEAR)stereoRectify返回的P1和P2是投影矩阵Q是视差到三维坐标的变换矩阵第四章会用到。alpha0.0表示对图像做最大程度的裁剪去掉校正后出现的黑边区域。如果不想丢失边缘像素可以把alpha设为0.5或1.0但这样有效像素区域变小匹配范围也受影响。代码写完后要做的验证是把校正后的左右图像并排显示在图像上用鼠标取几个特征点画水平线检查对应点是否在同一行。另一个更快的方法是直接看画面边缘的物体是否有明显的高度差。如果两侧画面里的同一物体不在同一水平位置说明标定有问题返回上一章检查。3.2 SGBM参数视差图质量的真正决定因素立体匹配的算法选择上OpenCV提供了StereoBM和StereoSGBM两种。BM速度快但精度低适合实时性要求高的场景SGBM是半全局块匹配精度高不少也是这个项目里最常用的选择。SGBM的核心思想是对每个像素计算多方向的匹配代价并做能量最小化效果明显优于局部块匹配。# 创建SGBM匹配器 stereo cv2.StereoSGBM_create( minDisparity0, numDisparities128, # 必须能被16整除数值越大可匹配的深度范围越大 blockSize11, # 匹配块大小奇数一般5~21之间 P18 * 3 * blockSize ** 2, P232 * 3 * blockSize ** 2, disp12MaxDiff1, # 左右一致性检查的最大容差 uniquenessRatio10, # 唯一性比例抑制低置信度匹配 speckleWindowSize100, # 去除斑点区域窗口大小 speckleRange32, # 斑点区域内视差最大变化 modecv2.STEREO_SGBM_MODE_SGBM ) disparity stereo.compute(rectified_left, rectified_right).astype(np.float32) / 16.0SGBM的几个关键参数需要展开细说。numDisparities决定视差搜索范围范围越大能测的深度范围越小近处物体但计算量也越大。blockSize是匹配窗口大小窗口越小细节保留越好但弱纹理区域误匹配越多窗口越大结果越平滑但边缘会被钝化。P1和P2是平滑惩罚项P2控制视差突变物体边缘的惩罚强度一般让P2是P1的4到10倍。调参有一个通用顺序先固定blockSize11numDisparities128把其他参数设成默认跑出一张视差图看整体效果再根据问题逐个调。如果视差图噪声很大加大speckleWindowSize和P2如果物体边缘发糊减小blockSize如果黑色区域大片空洞检查光照是否均匀而不是急着调参。注意compute返回的原始视差图是固定小数点格式必须除以16.0转换成浮点像素视差否则后面计算三维坐标全部不对。3.3 后处理滤波SGBM输出的视差图常常带着空洞和噪点尤其是弱纹理区域和物体边界。常见做法是用cv2.medianBlur做中值滤波或者用cv2.bilateralFilter做双边滤波。中值滤波速度快能有效去除椒盐噪点双边滤波保边效果好但参数要调好否则把边缘糊掉。下图效果不好时先检查后处理参数不要盲目加大滤波窗口。# 中值滤波去噪 disparity_filtered cv2.medianBlur(disparity, 5) # 可视化归一化到0-255并映射为彩色 disparity_visual cv2.normalize( disparity_filtered, None, alpha0, beta255, norm_typecv2.NORM_MINMAX, dtypecv2.CV_8U) disparity_color cv2.applyColorMap(disparity_visual, cv2.COLORMAP_JET)中值滤波的窗口大小不要超过7否则会严重破坏物体边缘的视差精度。如果你对测量精度要求比较高可以尝试用WLS滤波cv2.ximgproc.createRightMatchercv2.ximgproc.createDisparityWLSFilter它利用左右两张视差图做加权最小二乘优化边缘保持效果好很多但速度慢帧实时性就别想了。4. 从视差到三维坐标把像素距离换算成毫米尺寸4.1 深度图和点云Q矩阵的用法有了视差图下一步就是把每个像素的视差值转换成三维坐标。OpenCV已经在stereoRectify阶段返回了Q矩阵这个矩阵是4×4的投影矩阵能把像素坐标和视差直接映射到相机坐标系下的三维坐标。用cv2.reprojectImageTo3D一次性完成批量转换。# 将视差图转为三维点云 points_3d cv2.reprojectImageTo3D(disparity_filtered, Q) # points_3d的形状是(H, W, 3)每个像素对应(x, y, z)坐标 # 过滤无效深度视差为0或无穷大的点 valid_mask disparity_filtered 0 valid_points points_3d[valid_mask] # 点云中每个点的单位取决于标定时objp的单位 # 如果标定时用的是30mm的棋盘格这里直接就是毫米reprojectImageTo3D输出的三维坐标里x是水平方向y是垂直方向z是深度物体到相机平面的距离。这三个值都是在左相机坐标系下的度量结果。单位完全由标定时棋盘格的真实尺寸决定所以劝大家标定前先量好格子边长别随便填一个数。这个函数有个容易踩的坑输入视差图的类型必须是float32且数值是真实视差值不是归一化后的灰度值。很多人直接把disparity_visual丢进去出来的点云完全变形。正确做法是先把SGBM的输出除以16拿到浮点视差滤波后再送入reprojectImageTo3D。4.2 尺寸测量的实现思路拿到三维点云后尺寸测量就变成了一个点云几何问题。测量一个物体某个方向上的长度思路是先用鼠标框选或者用轮廓检测锁定感兴趣区域然后在该区域内找到三维坐标的最小值和最大值差值就是对应尺寸。def measure_distance(points_3d, point_a, point_b): 计算三维空间中两点之间的欧氏距离 point_a, point_b: (x, y) 像素坐标 x1, y1, z1 points_3d[point_a[1], point_a[0]] x2, y2, z2 points_3d[point_b[1], point_b[0]] distance np.sqrt((x1 - x2) ** 2 (y1 - y2) ** 2 (z1 - z2) ** 2) return distance def measure_bounding_box(points_3d, roi_mask): 测量ROI区域的包围盒尺寸 roi_mask: 二值掩码物体区域为255 object_points points_3d[roi_mask 0] if len(object_points) 0: return None min_coords object_points.min(axis0) max_coords object_points.max(axis0) length max_coords[0] - min_coords[0] width max_coords[1] - min_coords[1] height max_coords[2] - min_coords[2] return length, width, height这个方案的原理很直接ROI内所有三维点的坐标分布范围等同于物体的外接长方体尺寸。要注意的是包围盒方案测量的物体尺寸与物体姿态强相关。如果物体斜着放测量结果会偏大。所以要么想办法把物体摆正要么用主成分分析先算出物体的主轴方向再做旋转对齐。毕设阶段先把物体摆正测量把误差来源说清楚就已经能拿一个不错的分数了。还有个更实用的技巧如果只想测一条边在图像上手动点两个端点然后用measure_distance函数计算三维欧氏距离这样不用做物体分割适用面更广展示效果也好。4.3 像素当量法和三维点云法哪个更靠谱很多毕设里出现的单目方案其实是“像素当量法”就是先放一个已知尺寸的参照物算出每像素对应多少毫米然后乘上目标物体的像素尺寸。这个方法的优点是只需要一个相机但致命缺点是一旦被测物体不在参照物所在的深度平面上像素当量就变了误差随深度差线性放大。双目方案本质上绕开了这个问题每个像素的物理尺寸都通过深度计算出来不需要参照物深度变化也不怕。当然双目也不是万能。物体表面很平整、纹理很少时匹配算法找不到可靠的对应点深度图就会出现黑洞。一个常见补救是给物体表面打上结构光斑点或者在物体背后放一张纹理丰富的背景板对比之下能明显改善匹配质量。5. 避坑手册双目视觉测量最容易翻车的6个环节5.1 标定板不够平整重投影误差死活降不下去现象标定误差一直在0.5像素以上怎么增加图像数量都不改善。原因棋盘格打印在软纸上贴在硬纸板上时没有完全展平导致世界坐标建模错误。标定算法假设棋盘格是平面刚体一旦弯曲角点坐标与实际位置不符。解决把棋盘格用双面胶完全贴平在铝板或玻璃板上确认没有气泡和翘角。采集时不要把标定板拿在手里抖固定在一个支架上拍。重新采集后重投影误差一般能降到0.15到0.3像素。5.2 SGBM参数没调视差图出现大量横纹和空洞现象视差图上有明显的横条纹或者物体边缘附近出现大块黑色空洞。原因P1和P2设得太小平滑约束不够numDisparities小于场景实际的视差范围导致近处物体匹配不到blockSize太大把边缘糊掉。解决先按公式P18*3*blockSize**2和P232*3*blockSize**2设置基准值再根据画面效果微调。空洞多就检查光照避免物体表面大面积反光。横条纹明显时把uniquenessRatio调大到15左右强制匹配点唯一性。5.3 remap后图像出现黑边和拉伸变形现象校正后的图像边缘出现不规则黑边画面内容有明显变形。原因stereoRectify的alpha0.0会导致图像强烈裁剪alpha1.0则保留全部原始像素但引入严重的畸变拉伸。初学者经常一脸懵。解决对毕设项目设置alpha0.5到alpha1.0之间较好既保留有效区域又不过度变形。黑边区域用roi1和roi2裁剪掉不影响后续匹配。5.4 左右视图无法同时看到完整标定板现象双目标定时大量图像只有左相机看到棋盘格右相机只看到一半甚至看不到。原因两个摄像头的公共视野范围比想象中窄尤其是基线较大时近距离的标定板容易超出右侧画面。解决把基线缩短到8厘米以内标定板放远一点保证左右画面中棋盘格完整且占画面面积的四分之一到三分之一。同时让标定板在画面不同位置倾斜、旋转多角度采图。5.5 测量结果与真实尺寸偏差超过10%现象量一个50毫米的方块程序输出55毫米甚至偏差更大。原因第一标定棋盘格的实际边长和代码里设置不一致比如实际30毫米却写成26毫米第二视差图在边缘区域的误差大用包围盒测量时边缘的毛刺点把尺寸撑大了。解决先用游标卡尺精确测量棋盘格边长更新objp测量时对ROI内的三维点做百分位截断比如取2%到98%的分位区间剔除离群点后再计算尺寸。5.6 换了摄像头或者动过支架所有标定全部失效现象上次运行好好的过了一天重新插拔USB摄像头后视差图完全乱了。原因USB摄像头每次上电后的图像采集状态不一定完全一致而且手工固定支架一旦松动双目的外参就变了。标定参数和当前物理装配是对应的动一处就全废。解决把左右摄像头和支架做成一个整体用螺丝锁死不要用胶带粘。每次开机重新采集几帧确认画面稳定后再做一次双目标定。如果只是微调支架可以用cv2.solvePnP配合标定板算新的外参不必全部重跑。6. 精度验证方法用标准量块量化系统误差并做亚像素优化毕业设计能不能拿高分很大程度上取决于你有没有一套明确的精度验证流程。我自己的习惯是准备三块不同尺寸的标准量块比如20毫米、40毫米、60毫米的金属块分别放在距离相机30厘米、40厘米、50厘米的位置测量每个位置测10次记录平均值和标准差。测量完成后整理一张误差表列出每个量块在每个距离下的绝对误差和相对误差。这个表格直接写进论文结论里比任何文字描述都有说服力。正常情况下这套系统在40厘米距离内经过合理调参后能做到2%到3%的相对误差如果用了亚像素细化可以更好。亚像素细化的方向有两个。第一个方向是视差层面的亚像素插值用抛物线拟合寻找最小代价位置的亚像素偏移第二个方向是再对三维点云做平面拟合或边缘拟合替换掉单个像素的跳动。下面给出视差亚像素拟合的简单实现def subpixel_interp(disparity_map): 对视差图做亚像素精度的抛物线插值 rows, cols disparity_map.shape disparity_subpixel disparity_map.copy() for i in range(1, rows - 1): for j in range(1, cols - 1): d_minus disparity_map[i, j - 1] d_current disparity_map[i, j] d_plus disparity_map[i, j 1] denom (d_minus - 2 * d_current d_plus) if abs(denom) 1e-6: delta 0.5 * (d_minus - d_plus) / denom if abs(delta) 1: disparity_subpixel[i, j] d_current delta return disparity_subpixel这个实现的逻辑是对每个像素的视差值用左右相邻点的视差拟合一条抛物线抛物线的极小值点就是亚像素视差位置。实际使用时建议先对整张视差图做一次平滑避免噪声被插值放大。注意这个函数是纯Python循环实现的速度较慢对毕设场景够用如果要实时需要改成NumPy向量化或者用OpenCV的filter2D实现。做完亚像素细化后再跑一遍标准量块的测量流程对比细化前后的误差变化。如果误差没有明显下降问题很可能不在视差精度而是物体边缘的三维点本身就不稳定这时要回头检查光照和匹配参数。最后说一个我从这个项目里得到的血泪经验双目视觉测量最忌讳“一步到位”的心态。标定、校正、匹配、测量这几个环节是串联关系前一环的问题一定会被后一环放大。把每一步的中间结果可视化出来确认无误再往前走比一口气跑完全流程再回来找问题要节省大量时间。每次修改标定参数或者匹配参数后先用同一组标准量块记录数据参数好坏立刻能看到。希望这套方案和这些踩坑记录能帮你少走一段弯路把精度和数据都做得经得起推敲顺利拿下这个毕设。本文还有配套的精品资源点击获取