ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于OpenCV的双目视觉尺寸测量:从标定到三维重建的完整实现

基于OpenCV的双目视觉尺寸测量:从标定到三维重建的完整实现 简介基于Python与OpenCV实现的双目视觉测量被摄物体尺寸项目是面向高校计算机、自动化、人工智能等专业学生及初学者的毕业设计级源码包。项目从双目图像采集、标定到视差计算与尺寸测量均有完整实现代码经调试可运行支持在此基础上二次扩展适合课程设计、期末大作业及毕设参考。资源压缩包共23个文件以jpg图像样本、Python主程序及md说明文档为主整体体积约2.26MB其中多组左右视图图片可用于验证算法效果README文档辅助快速理解项目结构与运行方式。目前已有200人学习使用反馈具备较好的学习借鉴价值。通过该项目读者可以掌握OpenCV双目视觉的基本流程、图像处理与测量计算方法获得可直接运行的工程代码和实验素材有助于从理论走向实践。1. 单目测不出尺寸双目视觉为什么能拍一张照片想量出物体的真实尺寸单目相机做不到。像素没有物理意义同一个目标放在1米和5米外成像大小差五倍没有深度信息时任何像素换算都是猜。双目视觉借用人眼原理左右两个相机看同一个物体时物点在两幅图像上的投影位置存在偏移这个偏移量叫视差它与物点到相机的距离成反比。测出视差深度就恢复了再配合相机内参就能把像素距离还原成毫米。pythonopencv在这条链路里提供了标定、校正、匹配、三维重建的完整API但真正决定测量精度的不是API数量而是你搞没搞清楚每个环节的物理含义。这篇内容按标定、校正、匹配、重建四条主线把基于opencv的双目尺寸测量拆成可以直接复现的代码和参数并指出误差出在哪里、优先级怎么排。适合正在做毕设、比赛或小型测量项目的读者。2. 相机标定双目测量的第一道门槛不标定后面全是废的2.1 内参、外参和畸变标定到底在解什么双目测量里的标定分三层内参、外参和畸变系数。内参是 fx、fy、cx、cyfx 事实上已经是像素单位而不是物理焦距它描述三维点投影到像素平面时的尺度换算cx、cy 是主点坐标标定后主点与画面中心偏差过大意味着标定图采集质量不高。外参在这套系统里特指右相机相对左相机的旋转矩阵 R 和平移向量 T它决定了两个光心之间的相对位置也就是基线。基线越宽近距离测量精度越高但两个相机的视野重叠区域会缩小。畸变系数由径向畸变 k1、k2、k3 和切向畸变 p1、p2 组成。镜头的畸变在画面边缘最严重一个没标定畸变的系统边缘像素坐标本身可能偏移几十像素把这样的坐标送进后续匹配量出的尺寸必然带系统性偏移。所以标定不是走流程而是整个双目测量里投入产出比最高的一步。棋盘格标定有一个常见误区OpenCV 的 calibrateCamera 只关心角点数量不关心棋盘格实际多大但如果后面要的是毫米值标定时必须把格子边长量准并传入。plenty of 毕设里把 square_size 随手写成 1内参也能收敛结果算出来的深度整体被缩放测什么都是错的。这类错误用已知尺寸物体一验就能发现但很多人会在误差源排查里绕很多圈才回到这。2.2 用OpenCV跑张正友标定的最小流程我一般用 10x7 格子的棋盘格内角点取 9x6贴在一个平面板上拍摄 20 到 30 张不同角度。采集时不要让棋盘只在画面中央要覆盖四角和边缘因为畸变参数主要靠边缘图像约束。左右相机分别采集每张图都要保证棋盘完全在视野内、没有反光遮挡。下面是最小标定流程import cv2 import numpy as np import glob pattern_size (9, 6) # 内角点数宽9、高6 square_size 20.0 # 棋盘格实际边长单位毫米必须实测 objp np.zeros((np.prod(pattern_size), 3), np.float32) objp[:, :2] np.indices(pattern_size).T.reshape(-1, 2) objp * square_size obj_points [] img_points [] for f in sorted(glob.glob(left/*.jpg)): img cv2.imread(f) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners cv2.findChessboardCorners(gray, pattern_size, None) if not ret: continue criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 1e-3) corners cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) obj_points.append(objp) img_points.append(corners) ret, mtx, dist, rvecs, tvecs cv2.calibrateCamera( obj_points, img_points, gray.shape[::-1], None, None)findChessboardCorners 返回角点的像素坐标cornerSubPix 通过灰度梯度把角点精度推到亚像素级别不做这一步重投影误差通常差一个数量级。calibrateCamera 默认会同时优化内参和畸变ret 是重投影误差单位像素一般应小于 0.3。左相机和右相机要分别这样标定最终拿到 K1、D1 和 K2、D2。如果误差偏大优先增加图片数量和角度覆盖其次检查棋盘格是否反光或板面弯曲。2.3 双目标定与立体矫正参数单独标完左右相机后还需要做一次 stereoCalibrate 来确定 R 和 T。输入必须是同一时刻左右相机拍摄的同一块棋盘格所以采集时要同步。用 USB 双相机最好加硬触发线没有硬触发就把棋盘固定不动拍完左再拍右只要棋盘完全静止也可以接受。ret, K1, D1, K2, D2, R, T, E, F cv2.stereoCalibrate( obj_points, left_points, right_points, K1, D1, K2, D2, image_size, flagscv2.CALIB_FIX_INTRINSIC, criteria(cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 100, 1e-6))flags 用 CALIB_FIX_INTRINSIC表示左右内参保持单目标定结果不变只优化 R 和 T。加这个标志位比不加要稳单目标定的误差被限制住双目标定不容易发散。R 是旋转矩阵T 是平移向量基线长度是 np.linalg.norm(T)单位由 square_size 决定square_size 写毫米基线数值就是毫米。标定完成后立刻用 stereoRectify 生成校正映射表并保存到 npz 文件后续每次启动直接加载不用重新标定。注意相机被碰过、焦距被拧动、两个相机相对位置改变后标定结果必须重做。很多测量误差突然变大的求助根因都是相机被动过却没重标。3. 立体校正与SGBM视差计算从两幅图到一张深度图3.1 极线约束为什么能降低匹配难度双目匹配的第一步是立体校正。两个相机光心的连线叫基线三维空间点在左右成像面的投影满足对极几何约束左图上的一个像素点在右图上对应的搜索位置落在那条极线上二维搜索因此变成一维搜索。但默认摆放的两个相机极线是倾斜的且没有规律直接在这个状态下匹配既慢又容易误配。stereoRectify 通过 Bouguet 算法把左右图像重新投影让两个成像面共面、极线水平对齐。校正后的图像同一个特征点在左右图中的行坐标应该一致。验证方式很直接在左图上点一个棋盘格角点右图同一行上能看到同样的角点说明对齐成功。如果行坐标对不上优先检查左右相机采集是否同步其次检查 stereoCalibrate 时左右角点顺序是否一致。3.2 用SGBM计算视差的参数表与调参逻辑视差匹配算法里SGBM 是精度和速度的平衡点也是 OpenCV 里最常用的选择。它的思路是让匹配代价在整条极线上做能量最小化而不是简单比较局部像素差。默认参数能跑出大致结果但想用于尺寸测量下面几个参数必须手动确认。sgbm cv2.StereoSGBM_create( minDisparity0, numDisparities16 * 4, blockSize11, P18 * 3 * blockSize ** 2, P232 * 3 * blockSize ** 2, disp12MaxDiff1, uniquenessRatio10, speckleWindowSize100, speckleRange32, modecv2.STEREO_SGBM_MODE_SGBM ) disparity sgbm.compute(left_rect, right_rect) disparity (disparity / 16.0).astype(np.float32)compute 输出是固定点格式真实视差值要除以 16这一步漏掉会让深度整体小 16 倍属于最容易踩的低级错误。常用参数的作用如下参数典型值作用与影响numDisparities16 ~ 16*6最大视差搜索范围必须是 16 的整数倍范围太小会导致近处物体无匹配太大会引入噪声blockSize5 ~ 15匹配窗口边长必须是奇数窗口大更抗噪但边缘被平滑尺寸测量易偏小P1、P2P183bs², P2323bs²平滑惩罚项P2 控制深度不连续处的容忍度物体边缘明显阶梯时适当调大 P2disp12MaxDiff0 ~ 2左右一致性检查阈值大于该值的像素标记为无效能过滤遮挡区域误匹配uniquenessRatio5 ~ 15匹配唯一性比率越大越严格能过滤弱纹理区域错误匹配speckleWindowSize50 ~ 200去小噪点的连通域窗口0 表示关闭去斑speckleRange1 ~ 2斑块内视差允许的波动幅度调参有一个现实顺序先固定其他参数调 numDisparities 让被测物体在视差图里呈现清晰的灰阶分层再调 blockSize 观察边缘是否锐利最后开 disp12MaxDiff 和 speckleWindowSize 去噪。不要一上来就开大窗口blockSize 超过 15 后边缘的圆角效应会让边长测量整体偏小。调试时把视差图转成伪彩色cv2.applyColorMap(cv2.normalize(disparity, None, 0, 255, cv2.NORM_MINMAX), cv2.COLORMAP_JET)近处物体应是高亮远处偏暗。如果画面像雪花一样优先怀疑同步问题其次才是参数问题。3.3 视差图后处理掩膜、中值滤波与空洞填补SGBM 产生的无效点在测量前必须处理。常见处理顺序是将无效视差置为 NaN用左右图有效区域生成掩膜再对掩膜内的视差做中值滤波。disparity[disparity 0] np.nan mask left_rect 0 disparity[~mask] np.nan disparity_clean cv2.medianBlur( np.nan_to_num(disparity).astype(np.float32), 5)用中值而不是高斯滤波是因为视差图的错误通常是离群大跳变中值滤波专门处理这类椒盐式噪声在保留边缘的同时把跳变抹掉高斯滤波会模糊边缘导致后续尺寸测量偏短。这一步的细节直接决定测量稳定性尤其是物体边缘附近的噪声最终都会反映在毫米误差里。4. 用重投影矩阵把视差换算成毫米尺寸4.1 Q矩阵与reprojectImageTo3D有了校正后的视差图深度恢复只剩最后一步。stereoRectify 输出的 Q 矩阵同时编码了焦距、主点和基线信息本质是一个把像素坐标和视差映射到三维坐标的变换[X, Y, Z, W]^T Q * [x, y, disparity, 1]^T三维坐标由 X/W、Y/W、Z/W 给出Z 就是深度。OpenCV 封装成一行调用points3d cv2.reprojectImageTo3D(disparity, Q, handleMissingValuesTrue)handleMissingValuesTrue 会把无效视差点的三维坐标填成 10000测量前必须过滤。如果你不想用黑盒接口手动算深度的公式是Z -fx * Tx / (disparity (cx - cx))其中 fx、cx 是左相机焦距和主点cx 是右相机主点Tx 是基线水平分量。这个公式能帮你快速定位问题当视差为 0 时深度无穷大当视差接近 -(cx - cx) 时分母趋近 0这些区域都应视为无效。4.2 测单个点的三维坐标再算两点距离对尺寸测量来说最稳的路径不是把整个点云做表面重建而是在目标物体的关键特征点上取视差逐个算出三维坐标再求欧氏距离。这样绕开了点云分割的不确定性误差模型也更简单。def get_3d_point(x, y, disparity, Q): d disparity[y, x] if not np.isfinite(d) or d 0: return None vec Q np.array([x, y, d, 1.0], dtypenp.float64) if abs(vec[3]) 1e-12: return None return vec[:3] / vec[3] def distance_between_pixels(p1, p2, disparity, Q): pt1 get_3d_point(p1[0], p1[1], disparity, Q) pt2 get_3d_point(p2[0], p2[1], disparity, Q) if pt1 is None or pt2 is None: return None return float(np.linalg.norm(pt1 - pt2))像素坐标的选取方式决定测量对象。测矩形物块边长时用 cv2.minAreaRect 提取最小外接矩形取四个角点测圆形零件直径时先提取轮廓再拟合最小外接圆。这两种方式都比手动点两个点可重复性更高。注意角点的像素精度直接影响毫米结果标定板上的亚像素角点只对标定板可靠普通工件上用轮廓或边缘交叉点更务实。4.3 一个完整的矩形工件测量流程下面把双目图像从校正到出毫米尺寸完整跑一遍。map_x、map_y 和 Q 来自 stereoRectify 后保存的结果。data np.load(stereo_params.npz) left cv2.imread(capture/left.png) right cv2.imread(capture/right.png) left_rect cv2.remap(left, data[map1_x], data[map1_y], cv2.INTER_LINEAR) right_rect cv2.remap(right, data[map2_x], data[map2_y], cv2.INTER_LINEAR) disparity sgbm.compute(left_rect, right_rect).astype(np.float32) / 16.0 disparity[disparity 0] np.nan gray cv2.cvtColor(left_rect, cv2.COLOR_BGR2GRAY) _, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) contours, _ cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) cnt max(contours, keycv2.contourArea) rect cv2.minAreaRect(cnt) box cv2.boxPoints(rect).astype(np.int32) for i in range(4): p box[i] region disparity[max(0,p[1]-3):p[1]4, max(0,p[0]-3):p[0]4] d np.median(region[np.isfinite(region)]) if np.any(np.isfinite(region)) else None box[i] (p[0], p[1], d) edge1 distance_between_pixels(box[0][:2], box[1][:2], disparity, Q) edge2 distance_between_pixels(box[1][:2], box[2][:2], disparity, Q) print(f测量结果: {edge1:.2f}mm x {edge2:.2f}mm)每个角点取周围 3x3 窗口内视差的中位数比直接用单像素视差稳得多。视差图在物体边缘容易出现 1 到 2 像素误差单像素取到噪点的概率很高中位数能把离群值剔掉。被测物体表面反光严重时建议先喷一层哑光漆这是工业现场处理纹理反光的常见做法。5. 精度验证与排错为什么测出来偏大或偏小5.1 三组必做的验证实验拿到测量结果先别急着写结论按三组实验验证。第一组是反投影误差检查重新拍一张标定板用 findChessboardCorners 找角点再通过标定结果把三维角点投影回图像统计重投影误差应小于 0.3 像素。第二组是已知尺寸验证用游标卡尺量一个矩形工件在 500mm、800mm、1200mm 三个距离各测十次均值和真实值的差是系统误差标准差是重复性。第三组是位姿互换性验证把被测物体旋转 90 度、换个位置再测如果两次结果差超过 1%优先怀疑标定板尺寸和立体校正。5.2 常见误差源排查顺序整体偏大或偏小先查标定时 square_size 是否写错这条排在第一位因为系统误差是这个环节埋下的。只在画面某一侧偏大检查两个相机是否严格水平安装以及 stereoRectify 后主点偏差是否异常。边缘毛刺导致测量结果起伏把 blockSize 调小同时确认 disp12MaxDiff 等于 1 或 2。物体表面没纹理时 SGBM 几乎必然失败不要在参数上死磕换打光方向或者给物体补纹理效果立竿见影。工业上同类方案经常用 Halcon它的标定和定位算子确实开箱即用但 OpenCV 路线的优势是可控性强、无授权成本毕设和小批量测量完全够用。5.3 一个提高精度的捷径平面拟合代替单点测距被测物体表面是平面时在局部区域取 9 个点的三维坐标拟合出平面方程再在平面模型上进行尺寸计算。这个方法相当于用平面先验压制视差噪声比单点转换稳定很多。拟合平面用最小二乘解即可from numpy.linalg import lstsq def fit_plane(points): # points: Nx3至少3个非共线点 centroid points.mean(axis0) _, _, vh np.linalg.svd(points - centroid) normal vh[-1] d -np.dot(normal, centroid) return normal, d取点时把测量区域按 3x3 网格均匀采样每个点的视差先做邻域中值再进入拟合。平面拟合后同一平面上的两个点的距离直接在这个平面上计算相当于把单点噪声平均掉了。对毕设里的精度指标提升非常明显值得作为最后一招用上。提示全部参数调完后把标定矩阵、SGBM 参数、测量函数版本一起存进实验记录参数一变前后对比就失去意义。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进