简介:这是一套面向计算机、通信、人工智能及自动化等专业学生与从业者的双目视觉测量项目资料,以Python与OpenCV为核心工具,实现通过双目相机标定、立体匹配与视差计算来测量被摄物体实际尺寸的完整流程,可作为毕业设计、课程大作业或期末项目的参考方案,也适合具备一定基础后在此基础上修改扩展功能。资源包共23个文件,以21张jpg图像、1个py源码文件与1个md说明文档为主,图像涵盖左右相机采集的标定与测试样本,源码承载核心测量逻辑,文档则交代项目结构与使用方式,压缩包整体约2.26MB,体积轻便便于本地部署调试。目前已有200人学习关注。项目代码经过调试测试,答辩评审分达到98分,读者可据此掌握双目视觉从标定到尺寸解算的关键环节,理解视差与深度映射关系,并借助配套说明快速复现实验、排查常见问题,具备较高的学习借鉴价值。
1. 从一张照片到真实毫米:双目视觉尺寸测量到底在解决什么
用单目相机拍一个零件,你能拿到像素坐标,却拿不到真实尺寸——除非你在画面里放一把已知长度的尺子当参照。双目视觉的思路完全不同:两个相机从不同位置同时拍同一物体,靠视差反推深度,再结合相机内参把像素换算成毫米。这套方案在工业质检、零件分拣、毕业设计里都很常见,核心链路是「标定 → 校正 → 匹配 → 三维重建 → 尺寸计算」。
这个标题对应的项目,本质是用 Python 和 OpenCV 把上述链路串成一个可运行的尺寸测量工具:输入左右两张图,输出物体的长宽或某个特征的真实尺寸。它适合两类人——一类是计算机、电子信息、机械设计制造及其自动化专业做毕业设计的学生,需要一套能跑通、能讲清楚原理、能写进论文的完整代码;另一类是想快速验证双目测量可行性的工程师,先跑通最小闭环再决定要不要上结构光或激光。下面按「先立住原理、再动手复现、最后避坑」的顺序拆开讲。
2. 双目测距的几何原理与 OpenCV 标定链路
2.1 视差为什么能换出深度
双目视觉的底层公式只有一个,但它是整条链路的根。两个相机光心之间的距离叫基线 B,左右图像中同一个物点成像位置的横坐标差叫视差 d,焦距为 f,则深度 Z = f·B / d。视差越大,物体越近;视差越小,物体越远。这个反比关系决定了双目系统的精度分布——近处精度高,远处误差迅速放大。
理解这一点,就能明白为什么标定质量直接决定测量成败。f 和 B 的误差会线性传递到 Z,而视差 d 的误差在远处会被放大。所以做尺寸测量时,被测物体应尽量放在工作距离的中近段,别指望一套基线 60mm 的系统在 3 米外还能测到毫米级。
OpenCV 里对应的函数是stereoRectify和reprojectImageTo3D,前者把两个相机的成像面校正到共面行对齐,后者把视差图转成三维点云。校正之后,同一个物点在左右图中处于同一行,匹配搜索从二维降到一维,这是后续StereoBM或StereoSGBM能实时跑的前提。
2.2 用棋盘格做双目标定:采集与标定代码
标定的输入是一组左右同步拍摄的棋盘格图像,通常 15 到 25 对,棋盘格覆盖画面各个区域和角度。采集时棋盘格要平整,别用打印后卷边的纸。下面这段代码完成单目标定加双目标定,输出内参、畸变系数、旋转平移矩阵。
import cv2 import numpy as np import glob # 棋盘格内角点数量,比如 9x6 的格子,内角点是 8x5 CHESSBOARD = (8, 5) # 棋盘格单个方格的真实边长,单位 mm,必须用尺子量准 SQUARE_SIZE = 25.0 # 生成棋盘格的三维世界坐标,Z=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 def calibrate_single(image_paths): objpoints, imgpoints = [], [] gray_shape = None for fname in image_paths: img = cv2.imread(fname) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) gray_shape = gray.shape[::-1] ret, corners = cv2.findChessboardCorners(gray, CHESSBOARD, None) if ret: # 亚像素精化,标定精度靠这一步 corners = cv2.cornerSubPix( gray, corners, (11, 11), (-1, -1), (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)) objpoints.append(objp) imgpoints.append(corners) ret, mtx, dist, _, _ = cv2.calibrateCamera( objpoints, imgpoints, gray_shape, None, None) return ret, mtx, dist, objpoints, imgpoints, gray_shape # 左右相机分别标定 retL, mtxL, distL, objL, imgL, shape = calibrate_single( sorted(glob.glob('calib/left/*.jpg'))) retR, mtxR, distR, objR, imgR, _ = calibrate_single( sorted(glob.glob('calib/right/*.jpg'))) # 双目标定,固定内参只优化外参 flags = cv2.CALIB_FIX_INTRINSIC retS, mtxL, distL, mtxR, distR, R, T, E, F = cv2.stereoCalibrate( objL, imgL, imgR, mtxL, distL, mtxR, distR, shape, criteria=(cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 100, 1e-5), flags=flags) print('基线 B =', np.linalg.norm(T), 'mm') np.savez('stereo_params.npz', mtxL=mtxL, distL=distL, mtxR=mtxR, distR=distR, R=R, T=T)逻辑说明:findChessboardCorners找角点,cornerSubPix把角点精度从像素级提到亚像素级,这一步不做,标定重投影误差会明显偏大。calibrateCamera分别求左右内参和畸变,stereoCalibrate在固定内参的前提下求两个相机之间的旋转 R 和平移 T,T 的模长就是基线。参数方面,CHESSBOARD必须和实际棋盘内角点数一致,写错一位角点全找不到;SQUARE_SIZE用卡尺量,别信打印标注的尺寸,打印机缩放会带来百分之几的系统误差。
2.3 立体校正与视差图生成
标定完拿到参数,下一步是校正。校正的目的是让左右图像行对齐,之后匹配只需在同一行找对应点。
import cv2 import numpy as np data = np.load('stereo_params.npz') mtxL, distL = data['mtxL'], data['distL'] mtxR, distR = data['mtxR'], data['distR'] R, T = data['R'], data['T'] h, w = 480, 640 # 计算校正变换 R1, R2, P1, P2, Q, _, _ = cv2.stereoRectify( mtxL, distL, mtxR, distR, (w, h), R, T, alpha=0) # 生成映射表,避免每帧重复计算 mapLx, mapLy = cv2.initUndistortRectifyMap( mtxL, distL, R1, P1, (w, h), cv2.CV_32FC1) mapRx, mapRy = cv2.initUndistortRectifyMap( mtxR, distR, R2, P2, (w, h), cv2.CV_32FC1) imgL = cv2.imread('scene/left.jpg') imgR = cv2.imread('scene/right.jpg') rectL = cv2.remap(imgL, mapLx, mapLy, cv2.INTER_LINEAR) rectR = cv2.remap(imgR, mapRx, mapRy, cv2.INTER_LINEAR) # SGBM 视差计算,参数直接影响稠密度和噪声 grayL = cv2.cvtColor(rectL, cv2.COLOR_BGR2GRAY) grayR = cv2.cvtColor(rectR, cv2.COLOR_BGR2GRAY) stereo = cv2.StereoSGBM_create( minDisparity=0, numDisparities=16 * 6, # 必须是 16 的整数倍 blockSize=5, P1=8 * 3 * 5 ** 2, P2=32 * 3 * 5 ** 2, disp12MaxDiff=1, uniquenessRatio=10, speckleWindowSize=100, speckleRange=2, mode=cv2.STEREO_SGBM_MODE_SGBM_3WAY) disp = stereo.compute(grayL, grayR).astype(np.float32) / 16.0 # 视差转三维 points3D = cv2.reprojectImageTo3D(disp, Q)逻辑说明:stereoRectify的alpha=0表示校正后只保留有效像素,画面会裁掉一部分边缘,但不会引入黑色无效区;如果想让画面完整,改成alpha=1,代价是边缘出现无效像素。initUndistortRectifyMap生成映射表后可以缓存,视频流场景下不要每帧重算。StereoSGBM_create里numDisparities决定搜索范围,物体越近需要的视差范围越大,设小了近处物体直接丢失;blockSize越大视差图越平滑但边缘越糊,5 是常用起点;uniquenessRatio用来剔除误匹配,10 到 15 之间比较稳。
2.4 从三维点云到真实尺寸
拿到points3D后,每个像素对应一个 (X, Y, Z) 毫米坐标。测量物体尺寸的常见做法是:先分割出目标区域,再在点云里取该区域的包围盒或特征点距离。
import numpy as np import cv2 # 用颜色阈值或轮廓分割出目标,得到掩码 mask = cv2.inRange(rectL, (0, 0, 80), (80, 80, 255)) mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, np.ones((5, 5), np.uint8)) # 只保留有效视差区域 valid = (disp > disp.min()) & (disp < disp.max()) & (mask > 0) pts = points3D[valid] # 去掉离群点,Z 方向 3 倍标准差以外剔除 z = pts[:, 2] keep = np.abs(z - np.median(z)) < 3 * np.std(z) pts = pts[keep] # 包围盒尺寸,单位 mm xmin, ymin, zmin = pts.min(axis=0) xmax, ymax, zmax = pts.max(axis=0) print('宽 %.2f mm, 高 %.2f mm, 厚 %.2f mm' % (xmax - xmin, ymax - ymin, zmax - zmin))逻辑说明:掩码用来限定测量区域,避免背景点云干扰包围盒。离群点剔除很关键,视差图在物体边缘和纹理缺失区会产生飞点,不剔除的话包围盒会被拉大好几毫米。参数上,形态学核大小根据物体尺寸调,小物体用 3x3,大物体可以到 7x7。这里输出的是轴对齐包围盒,如果物体在画面里是斜的,需要先做主成分分析找主轴再算尺寸,否则测出来偏大。
3. 把测量精度从厘米级压到毫米级:参数调优与验证
3.1 影响精度的四个关键参数
双目测量的误差来源不止一个,按影响从大到小排:标定重投影误差、基线长度、视差算法参数、被测距离。下面这张表是我在实际调试中总结的参数对照,可以直接拿去当调参起点。
| 参数 | 典型值 | 调大后果 | 调小后果 |
|---|---|---|---|
| 基线 B | 60~120mm | 远处精度提升,近处盲区变大 | 近处好,远处误差放大 |
| numDisparities | 96~192 | 计算变慢,远处可测 | 近处物体视差溢出丢失 |
| blockSize | 5~11 | 视差平滑,边缘模糊 | 边缘锐利,噪声增多 |
| uniquenessRatio | 10~15 | 误匹配减少,空洞增多 | 稠密但飞点多 |
标定重投影误差要盯住,stereoCalibrate返回的 ret 值如果大于 0.5 像素,说明采集的棋盘图质量不够或者角点检测有偏差,这时候后面怎么调视差参数都救不回来。我一般要求单目重投影误差在 0.2 像素以内,双目在 0.3 以内才继续往下做。
3.2 用已知尺寸物体做精度验证
调完参数必须验证,不能只看视差图好看就下结论。找一个已知尺寸的标准件,比如 50mm 的量块或者打印后卡尺量过的方块,放在工作距离上测,对比测量值和真值。
import numpy as np def measure_error(true_size, measured_size): abs_err = abs(measured_size - true_size) rel_err = abs_err / true_size * 100 return abs_err, rel_err # 在多个距离上重复测量,画误差曲线 distances = [300, 500, 800, 1200] # mm for d in distances: # 每个距离采集一组左右图,跑完整链路得到 measured measured = run_pipeline(f'scene/{d}mm/left.jpg', f'scene/{d}mm/right.jpg') ae, re = measure_error(50.0, measured) print('距离 %dmm: 绝对误差 %.2fmm, 相对误差 %.2f%%' % (d, ae, re))逻辑说明:验证要在多个工作距离上做,因为双目误差随距离增长。如果 300mm 处误差 0.5mm、1200mm 处误差 5mm,说明系统适合近距测量,论文里就要如实写工作范围。参数上,true_size必须用更高精度的工具测,用卡尺量方块比用直尺靠谱。这一步的数据直接可以放进毕业设计的结果分析章节。
3.3 纹理缺失表面的处理办法
被测物体表面如果是纯色、反光或者透明,SGBM 会大面积匹配失败,视差图出现空洞。常见做法是投影散斑或者贴随机纹理贴纸。如果条件不允许,可以退而求其次用边缘特征做稀疏匹配,精度下降但至少能出结果。
# 对弱纹理区域,先用 CLAHE 增强局部对比度再算视差 clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8, 8)) grayL_eq = clahe.apply(grayL) grayR_eq = clahe.apply(grayR) disp_eq = stereo.compute(grayL_eq, grayR_eq).astype(np.float32) / 16.0逻辑说明:CLAHE 把局部对比度拉高,让弱纹理区域产生可匹配的梯度。clipLimit控制增强强度,3.0 是温和档,再高会放大噪声。这个方法对金属拉丝面、塑料白件有一定效果,但对镜面反射无能为力,那种情况只能改硬件方案。
4. 双目尺寸测量的避坑与排查清单
4.1 标定重投影误差偏大
现象:stereoCalibrate返回的误差超过 0.5 像素,校正后左右图行不对齐。原因通常是棋盘格图像太少、角度覆盖不够,或者棋盘格不平整导致角点检测偏移。解决:补拍到 20 对以上,棋盘格覆盖画面四角和中心,打印后贴在硬板上,采集时避免运动模糊。
4.2 视差图大面积全黑
现象:disp几乎全是负值或零,点云为空。原因多半是numDisparities设小了,近处物体视差超出搜索范围;或者左右图顺序搞反,compute的第一个参数必须是左图。解决:把numDisparities加到 192 试一次,确认左右图没有互换,再检查校正后的图是否行对齐。
4.3 测量值随距离线性偏大
现象:物体放远一点,测出来的尺寸就大一圈。原因是标定得到的基线或焦距有系统误差,误差随 Z 放大。解决:重新标定,重点检查SQUARE_SIZE是否量准;如果基线是手工输入的,用np.linalg.norm(T)从标定结果里取,别自己填。
4.4 边缘飞点拉大包围盒
现象:包围盒尺寸比实际大 3 到 5 毫米,点云在物体边缘有散乱点。原因是视差在深度不连续处匹配错误。解决:加speckleWindowSize滤波,或者在计算包围盒前做统计离群点剔除,前面代码里的 3 倍标准差方法就能压下去大部分。
4.5 运行报 ModuleNotFoundError: No module named 'opencv'
现象:import cv2直接报错。原因是只装了 Python 没装 OpenCV,或者装到了另一个 Python 环境。解决:确认python -m pip install opencv-python opencv-contrib-python装在了当前解释器下,VSCode 里检查右下角选的解释器路径和 pip 装的是同一个。opencv-contrib-python包含 SGBM 等扩展模块,别只装基础包。
5. 让测量结果可复现:把标定参数固化成配置文件
做到这里,链路已经能跑通,但每次换场景都要重新标定、重新调参,效率很低。我的习惯是把标定结果和算法参数写进一个 YAML 配置文件,主程序只读配置,这样换相机只需换配置,代码不动。
import yaml import numpy as np def save_config(path, mtxL, distL, mtxR, distR, R, T, algo): cfg = { 'left': {'mtx': mtxL.tolist(), 'dist': distL.tolist()}, 'right': {'mtx': mtxR.tolist(), 'dist': distR.tolist()}, 'extrinsic': {'R': R.tolist(), 'T': T.tolist()}, 'sgbm': algo } with open(path, 'w') as f: yaml.safe_dump(cfg, f) def load_config(path): with open(path) as f: cfg = yaml.safe_load(f) return (np.array(cfg['left']['mtx']), np.array(cfg['left']['dist']), np.array(cfg['right']['mtx']), np.array(cfg['right']['dist']), np.array(cfg['extrinsic']['R']), np.array(cfg['extrinsic']['T']), cfg['sgbm'])逻辑说明:YAML 可读性好,参数改了肉眼能看出来,比 pickle 适合毕业设计答辩时展示。algo字典里放numDisparities、blockSize这些,调参时只改配置不改代码。加载后转回 numpy 数组,后续流程不变。
再进一步,把测量结果和当时的配置一起存成 CSV,每次实验都有记录,论文里的数据表直接从这里出。我吃过亏——早期调参时改了代码没记参数,回头复现不出最好那组结果,只能重标重调,浪费两天。后来养成习惯:任何一次有效测量,配置、图像路径、测量值、真值四列一起落盘。这个习惯比任何算法技巧都值钱。
验证方法上,除了前面说的多距离对比,还可以做一个重复性测试:同一个物体、同一个位置,连续采集 20 次,看测量值的标准差。标准差反映系统稳定性,如果标准差超过 0.5mm,说明视差图抖动大,要回去查光照或者uniquenessRatio。这套流程走下来,一个能写进毕业设计、也能拿去和工业方案对比的双目尺寸测量系统就成型了。希望帮到你。
本文还有配套的精品资源,点击获取