Python+OpenCV双目视觉尺寸测量:从标定到平面拟合的完整实战 简介这是一套面向计算机视觉初学者、课程实践者与期末作业需求者的双目立体视觉尺寸测量方案基于Python与OpenCV实现通过双摄像头采集图像计算目标物体的高度、宽度等物理尺寸稍作调整还可扩展距离测量功能适用于学术研究、课程设计与工程验证等场景。资源包共28个文件约2.71MB以21张jpg标定与左右目图像、1个py主程序、1个md说明文档及若干zbak备份文件为主图像用于棋盘格标定与立体像对测试脚本可直接运行复现测量流程。目前已有105人学习下载。项目目录结构清晰imgs存放标准棋盘格标定图主程序启动后同步显示深度计算与校正视图用户在校正窗口左目区域拖拽即可输出所选线段端点的像素坐标及对应实际物理长度配套开发文档便于理解立体匹配与坐标映射思路适合对照复现与二次开发。1. 从两个摄像头到一把尺子这套双目测量系统到底能干什么去年帮一个做质检的朋友看产线改造方案对方开口就要“非接触式尺寸测量”预算却只够买两个普通工业相机。当时我第一反应就是上双目视觉——用 Python 加 OpenCV 搭一套成本压到最低精度够用就行。后来这套思路被整理成了一份完整的开发文档加源码包也就是今天要拆的这份资源PythonOpenCV 双目视觉物体尺寸测量系统。它的核心逻辑不复杂两个摄像头从不同角度拍同一物体通过视差算出深度再把像素坐标映射到物理尺寸。听起来像教科书里的标准流程但真正落地时标定、匹配、单位换算每一步都有坑。这份资源的价值在于它把从相机标定到尺寸输出的完整链路都跑通了还附了开发文档适合做期末作业的学生、想快速验证方案的工程师以及需要非接触测量原型的从业者。如果你手头正好有两个摄像头和一台能跑 Python 的机器跟着走一遍大概率能省掉两周的试错时间。2. 双目测距的数学底子视差、标定与坐标系转换2.1 视差为什么能换算成距离双目视觉的根基是三角测量。两个相机光心之间的距离叫基线物体在左右图像上的成像位置差就是视差。深度 Z 和视差 d 的关系是 Z f * B / d其中 f 是焦距B 是基线。这个公式简单到一行就能写完但实际用的时候f 和 B 的单位必须统一d 的精度直接决定 Z 的精度。常见做法是先把相机标定好拿到内参矩阵和畸变系数再用立体校正把左右图像对齐让同名点只在水平方向上有位移。这一步不做后面算出来的视差全是噪声。标定这块OpenCV 提供了calibrateCamera和stereoCalibrate两个函数。单目标定解决每个相机自己的内参和畸变双目标定则在此基础上算出两个相机之间的旋转和平移关系。很多人图省事直接用厂家给的参数结果测出来误差大得离谱。我一般会老老实实打棋盘格拍 15 到 20 组不同角度的图让重投影误差降到 0.3 像素以内再往下走。2.2 从像素到毫米坐标系转换的完整链条拿到深度 Z 之后还需要把像素坐标转成物理尺寸。这里涉及四个坐标系世界坐标系、相机坐标系、图像坐标系、像素坐标系。OpenCV 的projectPoints和reprojectImageTo3D能帮你完成大部分转换但前提是你得知道物体所在平面的深度。如果物体表面不平或者倾斜角度大直接拿单个点的深度去算尺寸就会翻车。常见做法是先用findContours提取物体轮廓再对轮廓上的点做平面拟合或者用solvePnP估计物体的位姿把像素尺寸投影到物理平面上。这份资源的开发文档里把这条转换链写得很清楚先立体校正再计算视差图然后通过reprojectImageTo3D得到三维点云最后在点云上做尺寸测量。每一步都有对应的 Python 代码参数也标了推荐范围。比如视差计算的numDisparities要设成 16 的倍数blockSize一般取 5 到 11 之间的奇数这些细节文档里都给了。2.3 标定实操棋盘格拍摄与参数验证标定是整套系统的地基地基没打好后面全是玄学。我一般会按下面的步骤走import cv2 import numpy as np import glob # 棋盘格内角点数量比如 9x6 chessboard_size (9, 6) criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) # 准备世界坐标系下的角点 objp np.zeros((chessboard_size[0] * chessboard_size[1], 3), np.float32) objp[:, :2] np.mgrid[0:chessboard_size[0], 0:chessboard_size[1]].T.reshape(-1, 2) objpoints [] # 三维点 imgpoints_left [] # 左图二维点 imgpoints_right [] # 右图二维点 # 读取左右相机拍摄的棋盘格图片 left_images sorted(glob.glob(calib_data/left/*.jpg)) right_images sorted(glob.glob(calib_data/right/*.jpg)) for left_path, right_path in zip(left_images, right_images): img_l cv2.imread(left_path) img_r cv2.imread(right_path) 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_size, None) ret_r, corners_r cv2.findChessboardCorners(gray_r, chessboard_size, None) if ret_l and ret_r: objpoints.append(objp) # 亚像素级角点优化 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) imgpoints_left.append(corners_l) imgpoints_right.append(corners_r) # 单目标定 ret_l, mtx_l, dist_l, _, _ cv2.calibrateCamera(objpoints, imgpoints_left, gray_l.shape[::-1], None, None) ret_r, mtx_r, dist_r, _, _ cv2.calibrateCamera(objpoints, imgpoints_right, gray_r.shape[::-1], None, None) # 双目标定 ret, 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_l.shape[::-1], criteriacriteria, flagscv2.CALIB_FIX_INTRINSIC ) print(基线长度, np.linalg.norm(T)) print(重投影误差, ret)这段代码的关键在stereoCalibrate的flags参数。如果单目标定已经做得很准就加上cv2.CALIB_FIX_INTRINSIC让双目标定只优化外参避免把内参带偏。criteria控制迭代精度一般 30 次迭代加 0.001 的阈值够用。跑完之后重点看两个数基线长度和重投影误差。基线长度应该和你实际测量的相机间距差不多误差超过 1 毫米就要检查是不是棋盘格没放平。重投影误差最好在 0.5 像素以内超过 1 像素的话后面测尺寸基本没法看。提示棋盘格一定要贴在硬质平板上拍摄时覆盖画面的各个区域包括四个角和中心。只拍中间几组图畸变系数会不准。3. 从视差图到尺寸输出SGBM 匹配与轮廓测量3.1 SGBM 参数怎么调才不糊立体匹配是双目视觉里最吃参数的一步。OpenCV 提供了StereoSGBM比 BM 算法稳但参数也多。我一般会按这个顺序调# 立体校正 rectify_scale 0 # 0 表示不裁剪1 表示裁剪掉无效区域 R1, R2, P1, P2, Q, _, _ cv2.stereoRectify( mtx_l, dist_l, mtx_r, dist_r, gray_l.shape[::-1], R, T, flagscv2.CALIB_ZERO_DISPARITY, alpharectify_scale ) # 生成映射表 map1_l, map2_l cv2.initUndistortRectifyMap(mtx_l, dist_l, R1, P1, gray_l.shape[::-1], cv2.CV_16SC2) map1_r, map2_r cv2.initUndistortRectifyMap(mtx_r, dist_r, R2, P2, gray_r.shape[::-1], cv2.CV_16SC2) # 读取待测图像并校正 img_l cv2.imread(test/left.jpg) img_r cv2.imread(test/right.jpg) 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) # SGBM 参数 min_disp 0 num_disp 16 * 6 # 必须是 16 的倍数 block_size 5 stereo cv2.StereoSGBM_create( minDisparitymin_disp, numDisparitiesnum_disp, blockSizeblock_size, P18 * 3 * block_size ** 2, P232 * 3 * block_size ** 2, disp12MaxDiff1, uniquenessRatio10, speckleWindowSize100, speckleRange32 ) disparity stereo.compute(rect_l, rect_r).astype(np.float32) / 16.0numDisparities决定了搜索范围设小了远处物体匹配不到设大了计算量飙升。常见做法是先估算物体最近和最远距离用公式num_disp (f * B) / Z_min算个大概再取 16 的倍数往上靠。blockSize影响匹配窗口大小纹理丰富的物体可以取 5纹理少的取 7 或 9。uniquenessRatio是唯一性检查值越大越严格一般 10 到 15 之间。speckleWindowSize和speckleRange用来过滤小噪点如果视差图上有零星的黑斑就把这两个值调大。3.2 轮廓提取与物理尺寸换算拿到视差图之后用reprojectImageTo3D转成三维点云再在点云上做测量。但点云数据量大直接处理慢我一般会先在二维图像上提取轮廓再映射到三维。# 生成三维点云 points_3d cv2.reprojectImageTo3D(disparity, Q) # 用左图校正后的图像做轮廓提取 gray_rect cv2.cvtColor(rect_l, cv2.COLOR_BGR2GRAY) _, thresh cv2.threshold(gray_rect, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) contours, _ cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 取最大轮廓 c max(contours, keycv2.contourArea) rect cv2.minAreaRect(c) box cv2.boxPoints(rect) box np.int0(box) # 计算像素尺寸 pixel_width np.linalg.norm(box[0] - box[1]) pixel_height np.linalg.norm(box[1] - box[2]) # 取轮廓中心点的三维坐标 center_x int(rect[0][0]) center_y int(rect[0][1]) center_3d points_3d[center_y, center_x] # 用深度和焦距换算物理尺寸 focal_length P1[0, 0] # 校正后的焦距 physical_width (pixel_width * center_3d[2]) / focal_length physical_height (pixel_height * center_3d[2]) / focal_length print(f物理宽度{physical_width:.2f} mm) print(f物理高度{physical_height:.2f} mm)这里有个容易翻车的地方center_3d[2]是物体中心点的深度但如果物体倾斜用中心点深度去换算整个轮廓的尺寸会有透视误差。更稳的做法是对轮廓上的每个点都取三维坐标然后拟合平面在平面上算距离。这份资源的开发文档里给了两种方案的对比简单场景用中心点深度就够精度要求高的话就上平面拟合。3.3 测量结果验证用已知尺寸的物体做校准系统搭好之后别急着测真东西先找个已知尺寸的物体验证。我一般会拿一个标准量块或者打印一个边长 100 毫米的正方形放在不同距离下测。如果误差在 1% 以内说明标定和参数都没大问题。如果误差随距离变大而增加多半是基线长度或者焦距不准。如果误差忽大忽小检查视差图是不是有空洞或者噪点。验证的时候还要注意光照。双目视觉对光照很敏感左右相机的曝光不一致会导致匹配失败。常见做法是固定曝光时间或者用自动曝光但加同步触发。如果两个相机是独立运行的尽量让它们同时采集否则运动物体会产生视差错误。4. 避坑指南标定、匹配、换算里最容易翻车的五件事4.1 棋盘格拍摄不规范导致标定参数漂移现象重投影误差看着不大但测出来的尺寸总差几毫米换个角度差得更多。原因棋盘格没贴平或者拍摄时只覆盖了画面中间区域导致畸变系数估计不准。边缘区域的畸变没被正确建模校正后图像仍然有残余畸变。解决把棋盘格贴在玻璃板或铝板上确保平整。拍摄时让棋盘格出现在画面的四个角和中心每组图的角度变化大一些。标定完成后用undistort看校正后的棋盘格直线是否真的直了不直就重新拍。4.2 SGBM 参数设错导致视差图大面积空洞现象视差图上一片黑或者只有物体边缘有值中间全是空洞。原因numDisparities设小了物体视差超出搜索范围或者blockSize太小纹理弱的区域匹配不上。解决先估算物体最近距离用num_disp (f * B) / Z_min算最小搜索范围再往上取 16 的倍数。纹理弱的物体把blockSize调到 7 或 9同时把speckleWindowSize设成 200 以上过滤噪点。4.3 用单点深度换算整个物体尺寸现象测一个倾斜的物体宽度和高度都偏大或偏小换个角度结果又不一样。原因物体表面不在同一深度用中心点深度代表整个物体透视误差被放大了。解决对轮廓上的点做平面拟合或者用solvePnP估计物体平面把像素坐标投影到物理平面上再算距离。简单场景可以取轮廓上多个点的深度平均值比单点稳。4.4 左右相机不同步导致运动物体测量失败现象静态物体测得好好的一放上传送带就全乱了。原因两个相机独立采集时间差导致运动物体在左右图中的位置不对应视差计算错误。解决用硬件触发同步或者选支持外触发模式的相机。如果只能用软触发尽量降低物体运动速度或者用全局快门相机减少卷帘效应。4.5 忽略镜头畸变导致边缘测量偏差大现象物体放在画面中心时测得很准移到边缘就差很多。原因只用了内参矩阵做校正没有正确应用畸变系数或者畸变系数标定不准。解决标定时确保棋盘格覆盖画面边缘畸变系数至少标到 k2 和 p2。校正时用initUndistortRectifyMap生成映射表不要直接拿原始图像做匹配。如果边缘误差仍然大考虑换畸变更小的镜头。5. 进阶技巧用平面拟合把测量误差压到 0.5% 以内前面说的中心点深度换算法在物体倾斜 15 度以上时误差能到 3% 到 5%。如果想把精度再往上提平面拟合是性价比最高的方案。具体做法是从轮廓区域里随机采样一批点用 RANSAC 拟合平面再把轮廓点投影到这个平面上最后在平面上计算距离。这样即使物体倾斜测出来的也是真实尺寸。def fit_plane_and_measure(points_3d, contour, focal_length): # 从轮廓区域采样三维点 mask np.zeros(points_3d.shape[:2], dtypenp.uint8) cv2.drawContours(mask, [contour], -1, 255, -1) valid_points points_3d[mask 255] valid_points valid_points[np.isfinite(valid_points).all(axis1)] # RANSAC 拟合平面 best_plane None best_inliers 0 for _ in range(200): idx np.random.choice(len(valid_points), 3, replaceFalse) p1, p2, p3 valid_points[idx] normal np.cross(p2 - p1, p3 - p1) norm np.linalg.norm(normal) if norm 1e-6: continue normal normal / norm d -np.dot(normal, p1) distances np.abs(np.dot(valid_points, normal) d) inliers np.sum(distances 2.0) # 2mm 阈值 if inliers best_inliers: best_inliers inliers best_plane (normal, d) if best_plane is None: return None normal, d best_plane # 把轮廓点投影到平面上 contour_points contour.reshape(-1, 2) projected [] for px, py in contour_points: p3d points_3d[py, px] if not np.isfinite(p3d).all(): continue dist np.dot(normal, p3d) d projected.append(p3d - dist * normal) projected np.array(projected) if len(projected) 4: return None # 在平面上计算最小外接矩形 # 先构建平面局部坐标系 u projected[1] - projected[0] u u / np.linalg.norm(u) v np.cross(normal, u) v v / np.linalg.norm(v) coords_2d np.array([[np.dot(p - projected[0], u), np.dot(p - projected[0], v)] for p in projected]) rect cv2.minAreaRect(coords_2d.astype(np.float32)) width, height rect[1] return width, height这段代码的核心是 RANSAC 拟合平面阈值设 2 毫米迭代 200 次。实际用的时候如果点云噪声大可以把阈值放宽到 3 毫米迭代次数加到 500。投影之后在平面局部坐标系里算最小外接矩形得到的就是真实物理尺寸。我拿这个方法测过一个 200 毫米长的铝块倾斜 30 度的情况下误差从 4.2% 降到了 0.4%。还有个小技巧如果物体表面反光严重视差图会有大片空洞平面拟合的点不够。这时候可以在物体表面贴一层哑光纸或者用偏振片消反光。另外标定的时候尽量让相机光轴和物体平面垂直倾斜角度越大校正后的有效像素越少精度也会下降。从那以后我每次搭双目系统都会先拿标准件跑一遍平面拟合确认误差在 0.5% 以内再上真东西。这套流程走下来不敢说能替代工业级测量设备但应付期末作业、原型验证、产线快速抽检足够了。希望帮到你。本文还有配套的精品资源点击获取