简介:基于Python与OpenCV的双目视觉尺寸测量毕业设计项目,利用左右相机图像计算被摄物体的实际尺寸,属于典型的非接触式视觉测量方案,主要面向计算机、人工智能、自动化等专业的学生和从业者,可用于课程设计、大作业或毕业设计参考。资源共23个文件,包含Python主程序、README说明文档以及21张左右视图、测试样图与结果图,压缩包仅2.26MB,结构简洁清爽,便于快速定位代码与图像资料。项目源自答辩评审98分的个人毕设,代码经过调试测试,可稳定运行,目前已获得200人学习下载。通过主程序与成对左右视图的配合,可以完整走通相机标定、图像校正、特征匹配、视差计算到尺寸换算的流程,帮助初学者理解双目测距与测量的核心原理;配套说明文档还提供了运行环境、操作步骤与排错要点,具有很强的借鉴与二次开发价值。
1. 双目视觉测量物体尺寸,难点不在深度图,而在“怎么把像素换算成毫米”
有人把双目视觉毕业设计做成“出几帧彩色视差图就收工”,答辩时却被一句“怎么证明你输出的尺寸是毫米?”问住。原因是 SGBM 只产出以像素为单位的视差,真正的尺寸测量要求把视差映射到物理坐标,这一步完全由双目标定参数决定。本文按毕业设计最常见的链路来组织:python + opencv 完成双目标定、极线校正、SGBM 视差、三维点云计算和物体长宽提取,整条链路在普通笔记本上就能复现。适合刚接触双目视觉尺寸测量的在校生,也适合想确认 OpenCV 测量精度边界的工程师。读完你能回答三个问题:标定做到什么程度算合格、SGBM 每个参数在改什么、最终测量误差到底控制在什么水平。
2. 双目标定与极线校正:先把“两幅图”对齐成“一台虚拟相机”
2.1 为什么标定误差会直接变成测量误差
双目测距的经典公式是Z = f * B / d,其中Z是深度,f是焦距(像素单位),B是左右相机光心间距即基线,d是视差。对这个公式做误差传播,可以得到近似的深度误差关系:
ΔZ ≈ Z² / (f * B) * Δd
也就是说,深度误差与测量距离的平方成正比,与焦距和基线成反比。这个公式解释了很多同学“标定完了但数字不对”的困惑:不是视差算法不行,而是装置本身的几何条件限制了精度上限。比如在f = 1200、B = 60mm、Z = 300mm的条件下,1 个像素的视差误差就会造成约1.25mm的深度误差;把基线加大到120mm,同样条件下误差降到约0.63mm。
| 基线 B(mm) | 焦距 f(px) | Z=300mm 时 1px 视差误差对应的深度误差 |
|---|---|---|
| 60 | 800 | 1.875 mm |
| 60 | 1200 | 1.25 mm |
| 120 | 1200 | 0.63 mm |
| 120 | 2400 | 0.31 mm |
所以设计测量装置时,基线不是随意选的。一般我会建议把两块摄像头固定在一条刚性横杆上,尽量拉开到 80mm 以上,同时保证被测物体在左右画面中都完整可见。基线越大,深度精度越高,但两个相机的公共视野会变小,物体离得太近时还可能出画。这套权衡是毕业设计开题阶段就要定的,不是后期调参能补回来的。
2.2 棋盘格标定的两处关键:角点数与样本姿态
双目标定通常先分别标定左右相机,再联合标定。标定板用棋盘格,核心函数是findChessboardCorners和calibrateCamera。采集样本时有两个最容易犯的错:一是把棋盘格内角点数写错,二是所有照片上的棋盘都正面朝前,姿态变化太少。先看代码:
import numpy as np import cv2 CHESS_SIZE = (9, 6) # 内角点列数、行数,不是格子总数 SQUARE_SIZE = 25.0 # 棋盘方格边长,单位 mm IMG_SIZE = (1280, 720) # 采集分辨率,后续测量必须保持一致 objp = np.zeros((CHESS_SIZE[0] * CHESS_SIZE[1], 3), np.float32) objp[:, :2] = np.mgrid[0:CHESS_SIZE[0], 0:CHESS_SIZE[1]].T.reshape(-1, 2) objp *= SQUARE_SIZE obj_points, img_points_l, img_points_r = [], [], [] for path_l, path_r in zip(list_left, list_right): img_l = cv2.imread(path_l) img_r = cv2.imread(path_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, CHESS_SIZE, None) ret_r, corners_r = cv2.findChessboardCorners(gray_r, CHESS_SIZE, None) if not (ret_l and ret_r): continue criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 1e-3) 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) obj_points.append(objp) img_points_l.append(corners_l) img_points_r.append(corners_r)代码逻辑不复杂,关键在于两个参数:CHESS_SIZE填的是内角点数。如果打印的棋盘格是 10×7 的格子,那么内角点就是 9×6,填反的话检测函数会一直返回 False。SQUARE_SIZE决定物体坐标系的比例尺,单位用毫米,后面所有三维坐标都继承这个单位。
采集标定样本时,棋盘要放在测量距离附近,比如设计指标是测 200~400mm 距离的物体,就把棋盘放在 200~400mm 区间内,变换位姿拍摄 15~20 对图像。姿态变化要包含绕 X、Y 轴的倾斜,不能只是平移旋转。cornerSubPix会把角点精度从整数像素提升到亚像素,迭代条件里30表示最大迭代次数,1e-3表示角点移动阈值,两者满足其一就停止。
2.3 stereoCalibrate 与 stereoRectify 的组合使用
有了左右角点序列,就可以做联合标定。一个比较稳的做法是先分别用calibrateCamera拿到左右内参,再传给stereoCalibrate,避免两个相机参数一起迭代时发散:
ret, mtx_l, dist_l, mtx_r, dist_r, R, T, E, F = cv2.stereoCalibrate( obj_points, img_points_l, img_points_r, mtx_l, dist_l, mtx_r, dist_r, IMG_SIZE, flags=cv2.CALIB_FIX_INTRINSIC ) print(f"stereo RMS = {ret:.3f} px") R1, R2, P1, P2, Q, _, _ = cv2.stereoRectify( mtx_l, dist_l, mtx_r, dist_r, IMG_SIZE, R, T, alpha=0, newImageSize=IMG_SIZE ) map_l1, map_l2 = cv2.initUndistortRectifyMap( mtx_l, dist_l, R1, P1, IMG_SIZE, cv2.CV_32FC1) map_r1, map_r2 = cv2.initUndistortRectifyMap( mtx_r, dist_r, R2, P2, IMG_SIZE, cv2.CV_32FC1)stereoCalibrate返回的ret是重投影 RMS 误差,单位是像素。一般认为小于 0.5 像素很好,1.0 像素以内也能用。如果 RMS 超过 1 像素,不要急着调算法,先检查标定样本里有没有模糊帧、棋盘是否超出画面边缘、左右角点是否误检。CALIB_FIX_INTRINSIC表示只用单目标定的内参作为固定值,只优化旋转和平移,这种做法在样本数量不多时更稳定。
stereoRectify输出四个关键量:R1、R2是左右相机的校正旋转矩阵,P1、P2是校正后的投影矩阵,Q是 4×4 重投影矩阵,后续用reprojectImageTo3D把视差转成三维坐标就靠它。alpha=0会把校正后图像边缘的黑边裁剪掉,节省计算量,但也会损失一部分视野;如果被测物容易出现在画面边缘,可以改成alpha=0.5保留更多像素。
校正效果可以用一条经验方法验证:在左右校正图上画同一条水平线,如果左右图中同一个点的 y 坐标一致,说明极线校正基本到位。差几个像素也能接受,SGBM 本身有一定容错能力。
3. SGBM 视差计算:参数对了,深度图才能真正用来测尺寸
3.1 为什么是 SGBM 而不是 BM 或深度学习
OpenCV 里可选的立体匹配算法很多,毕业设计最常用的是 BM 和 SGBM。BM 是块匹配,速度最快,但块与块之间容易出现横向条纹和边缘毛刺。SGBM 在块匹配基础上加入了全局能量约束,对弱纹理区域和边缘的处理明显更好,计算量在普通笔记本上也能接受。
深度学习立体匹配在精度上更有优势,但需要预训练模型和 GPU,训练数据里的基线与分辨率如果和你的摄像头不一致,迁移效果往往不如仔细调参后的 SGBM。所以我的结论是:双目视觉尺寸测量项目里,SGBM 是性价比最高的起点。
3.2 SGBM 参数起点表与代码
SGBM 参数多,每一个都在影响视差图质量。下面是一组在 1280×720 分辨率下可用的起点值:
| 参数 | 起点值 | 调整方向 |
|---|---|---|
| minDisparity | 0 | 画面最近物体的视差下界 |
| numDisparities | 96 | 必须是 16 的倍数,越大搜索范围越大 |
| blockSize | 5 | 奇数,弱纹理时调大到 7 或 9 |
| P1 / P2 | 8*3*5*5 / 32*3*5*5 | P2 一般取 P1 的 4~8 倍 |
| preFilterCap | 32 | 光照变化大时可以调大到 63 |
| uniquenessRatio | 10 | 越大匹配越严格,低纹理区容易出现空洞 |
| speckleWindowSize | 100 | 过滤孤立小斑点 |
| speckleRange | 2 | 斑点连通时的视差容差 |
| disp12MaxDiff | 1 | 左右一致性检查阈值,0 最严格 |
代码实现如下:
sgbm = cv2.StereoSGBM_create( minDisparity=0, numDisparities=96, blockSize=5, P1=8 * 3 * 5 * 5, P2=32 * 3 * 5 * 5, disp12MaxDiff=1, preFilterCap=32, uniquenessRatio=10, speckleWindowSize=100, speckleRange=2, mode=cv2.STEREO_SGBM_MODE_SGBM ) disp_l = sgbm.compute(img_l_rect, img_r_rect).astype(np.float32) / 16.0需要说明的是,compute返回的视差图是定点数,真实视差值要除以 16。numDisparities必须是 16 的倍数,因为 SGBM 内部按 16 像素步长搜索。它设置得越大,能测的距离越近,但计算量和误匹配也会增加。如果被测物距离相机 200mm 以上,96 通常够用;如果物体常贴近到 100mm,可能需要调到 128 或 160。
blockSize是匹配窗口边长,窗口越大对噪声越不敏感,但会抹掉边缘细节。P1和P2是像素邻域视差变化的惩罚项,P1 用于相邻像素视差变化较小的情况,P2 用于视差跳变较大的边缘。P2 设置得过小,边缘处视差会“粘连”,物体轮廓向外膨胀;过大则会把细小结构直接平滑掉。
3.3 视差后处理:WLS 滤波能救回多少精度
裸 SGBM 视差图一般会有三种问题:低纹理区空洞、左右边缘毛刺、细小误匹配斑点。cv2.ximgproc里的 WLS 滤波器可以把彩色图作为引导,把视差边缘修整得更贴合物体边界,效果比普通中值滤波好很多:
right_matcher = cv2.ximgproc.createRightMatcher(sgbm) wls = cv2.ximgproc.createDisparityWLSFilter(sgbm) wls.setLambda(8000) # 平滑强度 wls.setSigmaColor(1.5) # 颜色权重,数值越大越看重颜色边缘 disp_r = right_matcher.compute(img_r_rect, img_l_rect).astype(np.float32) / 16.0 disp_wls = wls.filter(disp_l, img_l_rect, disparity_map_right=disp_r)setLambda控制平滑程度,推荐值 3000~10000。lambda 太大,视差图会变得过度平滑,物体的棱角被圆化;太小则滤波效果不明显。setSigmaColor是颜色空间中的高斯权重,拍摄物体颜色单一、背景干净时设为 1.5 即可,纹理复杂时可以调到 2.0 以上。
注意cv2.ximgproc属于opencv-contrib-python,默认安装的opencv-python不包含该模块,需要额外执行pip install opencv-contrib-python。如果不想引入额外依赖,也可以用cv2.medianBlur连续滤波两遍作为替代,但对细小边缘的保持效果会差一些。
3.4 从视差图得到三维点云
视差图本身还不能直接测尺寸,需要结合标定阶段的重投影矩阵Q转成三维坐标:
points_3d = cv2.reprojectImageTo3D(disp_wls, Q) mask_valid = (points_3d[:, :, 2] > 0) & (points_3d[:, :, 2] < 2000)reprojectImageTo3D输出的points_3d每个像素位置对应一个三维坐标,单位是毫米,因为Q矩阵中的平移量在标定时就是以毫米为单位的。mask_valid把视差为 0 或深度异常大的点去掉,这些点通常是低纹理区域或遮挡区域产生的无效值。深度上限 2000 是我根据测量距离设置的,实际使用时按项目场景调整。
如果点云整体出现“弯曲”或某个放大率明显异常,问题几乎不在 SGBM,而在标定质量。这时候回头加拍标定样本,比继续调 SGBM 参数更有效。
4. 物体尺寸计算:用三维点云的主轴代替像素比例尺
4.1 像素比例法的适用条件和坑
很多教程给出一个简化方法:先算平均深度 Z,再用公式物理长度 = 像素长度 * Z / f换算。这个公式成立的前提是物体平面与成像平面近似平行,而且物体在图像中心附近。一旦物体有倾斜或者不在画面中央,误差会明显增大;如果镜头畸变没有完全校正,边缘处的像素比例和中心处也不一致。
双目测量相对单目加固定比例尺的优势就在于:每个像素都有独立的三维坐标,不需要假设物体与相机平行。用三维点云主轴来测量才是合理方案。
4.2 轮廓提取与三维点采样
在左校正图上提取物体轮廓,再把轮廓内部的points_3d点取出来。这里要注意一点:二维轮廓决定了采样范围,所以物体和背景之间要有足够的对比度。如果物体颜色接近背景,二维分割会失败,一个更稳的办法是先用深度范围粗略切出一块 ROI,再做边缘检测:
gray = cv2.cvtColor(img_l_rect, cv2.COLOR_BGR2GRAY) blur = cv2.GaussianBlur(gray, (5, 5), 0) edge = cv2.Canny(blur, 50, 150) contours, _ = cv2.findContours(edge, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) contour = max(contours, key=cv2.contourArea) mask = np.zeros_like(gray) cv2.drawContours(mask, [contour], -1, 255, -1) pts = points_3d[mask > 0].reshape(-1, 3) pts = pts[np.isfinite(pts).all(axis=1) & (pts[:, 2] > 0)]Canny的两个阈值50和150是经验起点,如果物体边缘出现断裂,可以把低阈值降到30;如果背景噪声太多,则适当提高。RETR_EXTERNAL只取外层轮廓,避免物体内部纹路干扰。max(contours, key=cv2.contourArea)保留面积最大的轮廓,前提是画面里只有被测物体一个目标。
4.3 用 PCA 主轴测量物体的长宽实测值
拿到轮廓内的三维点之后,用 PCA 求出点集的主方向。物体长边对应第一主成分方向,短边对应第二主成分方向,第三主成分通常指向物体平面的法线方向。沿主方向计算跨度,就得到物体的物理尺寸:
mean_vec, eigen_vecs = cv2.PCACompute(pts.astype(np.float32), None, maxComponents=3) centered = pts - mean_vec.ravel() proj = centered @ eigen_vecs.T lengths = proj.max(axis=0) - proj.min(axis=0) length, width, thickness = lengths print(f"尺寸: {length:.2f} mm x {width:.2f} mm x {thickness:.2f} mm")这段逻辑的关键是:proj把每个点投影到由 PCA 主轴构成的新坐标系里,max - min就得到沿三个主轴方向的包围盒边长。单位依然是毫米,因为输入pts来自三维点云。这种方法不要求物体摆放成特定角度,即使物体在画面里是斜的,也能测出真实长宽。
PCA 也有缺陷,它对离群点敏感。如果轮廓采样混入背景点,主轴方向会被带偏。所以采样后建议先做一个简单离群点剔除,比如取pts在 Z 方向的中位数,把偏离中位数超过 20mm 的点删掉再算 PCA。对薄片物体,第三主轴方向的厚度测量基本不可信,因为边缘遮挡导致深度噪声大,厚度数值只做参考。
4.4 需要规避的三个测量误差来源
第一,物体边缘的深度值通常不可靠。即使经过 WLS 滤波,轮廓边缘一两层像素的视差仍可能偏向背景,导致测出来的尺寸偏大。对策是在drawContours得到 mask 后,用cv2.erode腐蚀一到两个像素,把边缘不可靠点排除掉。
第二,低于基线平面的区域容易产生视差空洞。比如物体放在桌面上,桌面纹理可能不满足匹配条件,物体底边附近出现黑洞。测量时尽量让物体悬空或使用有纹理的衬垫。
第三,远处的物体在同一视差误差下,深度误差更大。测量距离尽量控制在标定距离区间内,不要标定在 300mm,却测量 600mm 的物体。
5. 评估与验证:用数据说话的三步收尾
5.1 用重复测量标准差证明稳定性
毕业设计答辩时,一张视差图远不如一张重复性测试表有说服力。对同一个物体连续测量 10 次,计算均值和标准差:
samples = [] for i in range(10): # 重新采集左右图、重新计算视差和尺寸 samples.append(length) mean_l = np.mean(samples) std_l = np.std(samples, ddof=1) print(f"mean={mean_l:.2f} mm, std={std_l:.2f} mm")标准差反映测量系统的重复性,与真值比较得到的是系统误差。比如一个 50.00mm 的标准块,10 次测量均值为 50.37mm,标准差 0.12mm,那就可以说系统偏差约 +0.37mm,重复精度约 0.12mm。把这几组数字写进论文,比任何文字描述都直观。
5.2 什么时候必须重新标定
把相机固定在支架上之后,不需要每次开机都重新标定,但有以下任何情况都必须重拍标定板:转动过镜头调焦环、碰撞过支架、更换摄像头、改变了工作分辨率。一个很容易忽略的问题是自动对焦。电脑用 USB 摄像头如果开启自动对焦,焦距 f 会随物体距离变化而变化,标定参数随之失效。用手动对焦把镜头拧到一个固定位置,或者锁定焦距,再开始采集标定样本。
5.3 一个能快速暴露标定问题的斜置测试
找一个平面度好的矩形纸板,先水平正对相机测一次,再把纸板旋转 15 度左右测第二次。如果两次测量的长边差值超过 1mm,说明三维点云存在系统性畸变,最常见原因是标定时的棋盘格姿态不全,或者左右相机曝光差异过大导致视差偏移。这个测试成本低,能帮你在答辩前发现大部分隐患。
最后给一个小技巧:标定时把棋盘格贴在玻璃平板上,测量时用同一块玻璃板垫在被测物体下面。这样做的好处是,如果玻璃板表面与相机光轴不平行,视差图上的棋盘角点和物体边缘会同时出现离焦,检查校正图能直接看出问题。标定和测量共用同一个参考平面,等于把系统误差从物体姿态中分离出去,测量结果的可解释性会明显提升。
本文还有配套的精品资源,点击获取