ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于OpenCV的双目视觉物体尺寸测量:从标定到三维坐标计算

基于OpenCV的双目视觉物体尺寸测量:从标定到三维坐标计算 简介这套基于 Python 与 OpenCV 的双目视觉尺寸测量项目源码及配套文档面向需要完成毕业设计、期末大作业或课程设计的计算机视觉方向读者旨在帮助快速搭建物体尺寸测量系统理解双目视差、相机标定与三角测量等核心原理。资源包共 23 个文件压缩后约 2.27MB内含 21 张左右目测试图像、1 个带详细注释的 Python 主程序以及 1 份 Markdown 说明文档代码覆盖图像预处理、特征提取、立体匹配和尺寸换算等关键环节文档则配合源码讲解项目结构、运行方式与算法思路。整套项目目前已有 83 人学习界面直观、功能完整可在本地直接部署运行项目经过严格调试整体流程清晰适合作为高分离线项目参考。对初学者而言清晰的注释能降低入门门槛对进阶者也能提供从图像标定到实际测距的完整工程参考。1. 双目视觉测尺寸为什么说它比单目靠谱又容易在哪里翻车期末大作业要测一个纸盒的长宽高你首先想到的是用 OpenCV 加单目摄像头把画面里像素换算成毫米。但拍过就知道物体离镜头近一点、远一点同样一个盒子的像素高度变了而单目图像里根本没有深度信息比例尺是猜的。基于 OpenCV 的双目视觉测量被摄物体尺寸本质就是用两个固定间距的摄像头模拟人眼通过左右图像的视差把每个像素的深度算出来再在三维空间里量两点距离。这个方案能解决的正是“物体不在同一平面、相机距离未知”时的尺寸测量问题适合期末大作业、毕设里需要展示完整技术链路的场景。它能同时拿到深度图和三维坐标但前提是标定、立体匹配、测量三环都别出错任何一个环节失守测出来的数字比单目还离谱。2. 从相机标定到极线校正先把双目系统的“尺子”校准2.1 双目测距的原理与坐标系视差、基线、焦距的关系双目测尺寸的核心不是“拍两张图”而是知道左右两个相机之间的空间关系。一个理想的双目系统里左右相机光心距离叫基线b焦距是f空间中某一点 P 在左右相机成像平面上的水平坐标分别是xl和xr视差d xl - xr。根据相似三角形深度z f * b / d。有了 z再加上相机内参就能把像素坐标(u, v)换算成相机坐标系下的三维坐标X (u - cx) * z / fx Y (v - cy) * z / fy Z z所以整个测量流程可以拆成三步先把左右相机各自的内参和畸变算出来这叫单目标定再算两个相机之间的旋转矩阵 R 和平移向量 T这叫双目标定最后把左右图像校正成严格的“左右对齐只差视差”的形态这叫立体校正。校准这一步是后面所有数字的尺子尺子歪了测出来的尺寸全是错的。2.2 标定板采集与 calibrateCamera 参数棋盘格数量与角点提取我一般建议用 OpenCV 的经典标定流程不需要额外下载标定工具。首先打印一张棋盘格贴在硬纸板上。内角点数量建议用 9×6 或者 8×6不要用太少角点太少解出来的内参不稳定。采集时左右相机各拍一组同一时刻拍同一个棋盘格姿势至少拍 15 组角度要覆盖倾斜、平移、远近不同。采集代码如下import cv2 import numpy as np # 棋盘格内角点数比如横向9个纵向6个 pattern_size (9, 6) criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) def collect_chessboard(cap_left, cap_right, save_dir, count20): for i in range(count): ret_l, frame_l cap_left.read() ret_r, frame_r cap_right.read() gray_l cv2.cvtColor(frame_l, cv2.COLOR_BGR2GRAY) gray_r cv2.cvtColor(frame_r, cv2.COLOR_BGR2GRAY) found_l, corners_l cv2.findChessboardCorners(gray_l, pattern_size, None) found_r, corners_r cv2.findChessboardCorners(gray_r, pattern_size, None) if found_l and found_r: 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) cv2.imwrite(f{save_dir}/left_{i:02d}.jpg, frame_l) cv2.imwrite(f{save_dir}/right_{i:02d}.jpg, frame_r) print(f采集第 {i1} 组左右同时检测到角点)cornerSubPix是把角点精度从像素级提升到亚像素级的关键窗口大小取(11, 11)即可。采集时要注意左右两张图必须同时按下快门或者用支持同步的双目摄像头否则棋盘格在运动中拍出来的左右位置不匹配后面的立体标定会出现无法收敛的畸变。保存图像后再统一读取文件做标定避免实时标定时相机还在自动调节曝光。这里有一个细节OpenCV 标定需要的是“世界坐标”的棋盘格角点如果用 9×6 棋盘每个格子边长记得量准确单位用毫米。标定主体代码def calibrate_camera(image_paths, pattern_size, square_size): # 生成棋盘格角点的三维世界坐标假设棋盘格在Z0平面 objp np.zeros((pattern_size[0] * pattern_size[1], 3), np.float32) objp[:, :2] np.mgrid[0:pattern_size[0], 0:pattern_size[1]].T.reshape(-1, 2) objp * square_size obj_points [] img_points [] for path in image_paths: img cv2.imread(path) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners cv2.findChessboardCorners(gray, pattern_size, None) if ret: corners cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)) obj_points.append(objp) img_points.append(corners) ret, mtx, dist, rvecs, tvecs cv2.calibrateCamera( obj_points, img_points, gray.shape[::-1], None, None) return mtx, distsquare_size是实际标定板的格子边长单位必须一致比如 25 毫米后面测尺寸的结果单位就是毫米。如果这里写错内参里的焦距会等比例错最终测出来的长宽高全部偏移。标定完成之后用cv2.projectPoints把角点重新投影回图像计算重投影误差一般小于 0.1 像素算合格。如果误差在 0.5 像素以上多半是采集图像模糊、标定板不平整或者图片数量太少。2.3 立体校正与 stereoRectify 参数alpha 和平衡单目标定之后还需要双目标定来求两台相机的相对位置关系。这里用stereoCalibrate输入左右相机各自的内参和畸变系数以及每一对图像的角点坐标输出旋转矩阵R、平移向量T。拿到R和T后用stereoRectify生成校正映射def stereo_rectify(mtx_l, dist_l, mtx_r, dist_r, R, T, image_size): R1, R2, P1, P2, Q, _, _ cv2.stereoRectify( mtx_l, dist_l, mtx_r, dist_r, image_size, R, T, alpha0, flagscv2.CALIB_ZERO_DISPARITY) map1_l, map2_l cv2.initUndistortRectifyMap( mtx_l, dist_l, R1, P1, image_size, cv2.CV_32FC1) map1_r, map2_r cv2.initUndistortRectifyMap( mtx_r, dist_r, R2, P2, image_size, cv2.CV_32FC1) return map1_l, map2_l, map1_r, map2_r, QstereoRectify的alpha参数控制校正后图像保留多少原始像素。alpha0会裁剪掉图像边缘的非公共区域导致视差图有效区域变小alpha1会保留全部像素但边缘会出现黑色条带。我一般取alpha0让有效区域最大化同时配合CALIB_ZERO_DISPARITY让主点保持一致这样后续 SGBM 搜索范围更整齐。Q是重投影矩阵后面从视差图转三维坐标全靠它。校正之后可以用cv2.remap生成校正图再用鼠标点同一物理点在左右图上的像素坐标看是否在同一水平线上明显不在就说明标定失败。3. 用 OpenCV 实现视差图与深度图SGBM 参数是唯一玄学3.1 视差计算的最小闭环SGBM 创建与参数初始化标定和校正都通过后下一步是立体匹配。OpenCV 里有两种常见的匹配器StereoBM和SGBM。BM 快但噪声大SGBM 慢但更平滑实测中 SGBM 配合 WLS 滤波是期末作业和毕设里最稳妥的组合。最小闭环代码def create_sgbm(min_disparity0, num_disparities64, block_size11): left_matcher cv2.StereoSGBM_create( minDisparitymin_disparity, numDisparitiesnum_disparities, blockSizeblock_size, P18 * 3 * block_size ** 2, P232 * 3 * block_size ** 2, disp12MaxDiff1, uniquenessRatio10, speckleWindowSize100, speckleRange32, modecv2.STEREO_SGBM_MODE_SGBM_3WAY ) return left_matchernumDisparities必须能被 16 整除它决定了搜索的视差范围。如果被测物体离相机较近视差范围大取 96 或 128物体在 1 米以外64 就够。blockSize必须是奇数值越大对噪声越不敏感但也会抹掉边缘细节。P1和P2是平滑惩罚项控制视差图的平滑程度。经验值是小 blockSize 时用8 * 3 * blockSize**2和32 * 3 * blockSize**2当 blockSize 增大时P1、P2 按比例调大。uniquenessRatio用于排除低置信度的匹配点10 到 15 之间比较合适。这些参数没有万能取值我用一组默认值跑完再根据视差图里的空洞和横条纹调。3.2 从视差到深度重投影矩阵 Q 与 point cloud 的换算得到原始视差图disp后需要用reprojectImageTo3D把它转换成三维点集。这个函数用到双目标定时输出的Q矩阵def compute_depth(disp, Q, min_depth100, max_depth2000): # disp 是 int16 格式值表示实际视差 * 16 disp_float disp.astype(np.float32) / 16.0 disp_float[disp_float 0] 0.1 # 避免除零 points_3d cv2.reprojectImageTo3D(disp_float, Q, handleMissingValuesTrue) # 取出每个像素的深度 Z depth_map points_3d[:, :, 2] depth_map[depth_map min_depth] 0 depth_map[depth_map max_depth] 0 return points_3d, depth_map一个常见的坑SGBM 输出的disp是定点数真实视差必须除以 16。如果漏掉这一步深度会小 16 倍测出来的尺寸也小 16 倍。另外handleMissingValuesTrue会把无效点置成 0但处理时会引入边缘的异常值所以后面必须做深度裁剪。min_depth和max_depth根据你的拍摄距离设置比如物体离相机 50 到 150 厘米就设 500 和 1500 毫米能过滤掉大部分噪声。如果你只想看深度可以用cv2.normalize把深度图映射到 0-255 显示但测量尺寸时一定要用三维坐标点而不是直接用视差换算的近似深度。3.3 深度图后处理WLS 滤波与空洞填充的参数选择原始视差图通常有大量黑色空洞尤其是纹理少的区域比如纸箱的纯色表面。OpenCV 的ximgproc模块提供了 WLS 滤波需要左右两个方向的一致性检查。做法是先创建右匹配器算出右视差图再传给createDisparityWLSFilterdef wls_filter(left_matcher, left_img, right_img, disp_l, disp_r, sigma1.5, lmbda8000): right_matcher cv2.ximgproc.createRightMatcher(left_matcher) disp_r right_matcher.compute(right_img, left_img) wls_filter cv2.ximgproc.createDisparityWLSFilter(left_matcher) wls_filter.setLambda(lmbda) wls_filter.setSigmaColor(sigma) filtered_disp wls_filter.filter(disp_l, left_img, disparity_map_rightdisp_r) return filtered_displmbda是平滑权重值越大视差图越平滑但实际物体的棱角也会被抹平导致边缘向外扩测出来的尺寸偏大。我用 8000 到 10000 之间sigma用 1.5。滤波后仍然会有小块空洞可以再做一步中值滤波或者形态学闭合。注意createRightMatcher使用的参数必须与左匹配器完全一致否则左右一致性检查会失效。这一章做完你已经能从一对图像中得到每个像素的三维坐标接下来该怎么量尺寸取决于被测物体放在哪里、你要量的是哪个部位。4. 测量被摄物体的尺寸像素坐标到世界坐标的换算与 ROI 操作4.1 目标分割与基准平面拟合在最简单场景下先跑通测量尺寸时最直接的思路是鼠标在左图上框出被测区域然后取这一区域内的三维点云计算两点间距离。但要保证精度必须知道物体放置的平面。如果物体放在桌面上我们可以先用标定板或者一个已知平面拟合出桌面方程这样物体表面即使不平整也能用平面投影来消除视角误差。一个更简单的方案适合期末大作业把被测物体放在一个棋盘格标定板上用标定板所在的平面作为基准面。检测棋盘格角点得到该平面的位姿然后把物体上的像素点投影到这个平面。代码思路如下def segment_object_by_roi(image, depth_map, roi): x, y, w, h roi roi_depth depth_map[y:yh, x:xw] # 用ROI内深度中值作为物体表面深度 valid_depth roi_depth[roi_depth 0] if len(valid_depth) 0: return None surface_depth np.median(valid_depth) # 得到ROI中心像素的三维坐标 cx x w // 2 cy y h // 2 # 根据深度和相机内参反算三维坐标 fx mtx_l[0, 0] fy mtx_l[1, 1] cx_intr mtx_l[0, 2] cy_intr mtx_l[1, 2] X (cx - cx_intr) * surface_depth / fx Y (cy - cy_intr) * surface_depth / fy Z surface_depth return (X, Y, Z)这个简化模型假设物体表面是一个平面并且深度取中值。实测时如果物体的厚度不能忽略比如一个盒子要分别测顶面和放置平面的距离来得到高度而不是直接取表面深度。roi可以用cv2.selectROI让用户手动框选这比自动分割更稳因为复杂背景下自动分割经常把阴影和反光也算进去。4.2 尺寸计算两点间欧氏距离与尺度因子有了两点的三维坐标长宽就是三维空间里的欧氏距离。但如果你已经让物体贴着基准平面放置测量顶面两个边缘点之间的距离时需要把这两个点投影到同一平面上否则因为透视倾斜算出来的距离比真实值偏大。我一般先拟合一个平面到点云数据然后把待测点垂直投影到这个平面再算投影后的距离。拟合平面可以用最小二乘OpenCV 没有直接封装但可以用np.linalg.lstsqdef fit_plane(points): # points: Nx3 矩阵 centroid points.mean(axis0) centered points - centroid _, _, vt np.linalg.svd(centered) normal vt[-1] d -np.dot(normal, centroid) return normal, d def project_to_plane(point, normal, d): # 点到平面投影 dist np.dot(normal, point) d return point - dist * normal这个函数可以复用标定板的角点把检测到的棋盘格角点三维坐标喂进去得到标定板平面方程。测量物体轮廓上的任意点时候先投影到该平面再计算两点距离能显著减少相机倾斜带来的误差。实际操作中我会在左图上用鼠标点击物体的两个角点然后去三维点云里取这两个像素坐标对应的 XYZ。4.3 把测量程序封装成函数输入输出与可视化为了让代码看起来像一个完整的“项目源码”最好把整个流程封装成一个类或者一组函数方便期末答辩时演示。下面是一个最小封装class StereoMeasurer: def __init__(self, mtx_l, dist_l, mtx_r, dist_r, R, T): self.mtx_l mtx_l self.dist_l dist_l self.dist_r dist_r self.R R self.T T self.map1_l, self.map2_l, self.map1_r, self.map2_r, self.Q None, None, None, None, None def rectify(self, image_size, alpha0): self.R1, self.R2, self.P1, self.P2, self.Q, _, _ cv2.stereoRectify( self.mtx_l, self.dist_l, self.mtx_r, self.dist_r, image_size, self.R, self.T, alphaalpha) self.map1_l, self.map2_l cv2.initUndistortRectifyMap( self.mtx_l, self.dist_l, self.R1, self.P1, image_size, cv2.CV_32FC1) self.map1_r, self.map2_r cv2.initUndistortRectifyMap( self.mtx_r, self.dist_r, self.R2, self.P2, image_size, cv2.CV_32FC1) def compute_disparity(self, left, right, sgbm_params): left_rect cv2.remap(left, self.map1_l, self.map2_l, cv2.INTER_LINEAR) right_rect cv2.remap(right, self.map1_r, self.map2_r, cv2.INTER_LINEAR) matcher create_sgbm(**sgbm_params) disp matcher.compute(left_rect, right_rect) return disp def measure_distance(self, left, right, p1, p2, sgbm_params): disp self.compute_disparity(left, right, sgbm_params) _, depth_map compute_depth(disp, self.Q) # p1, p2 是左图上的像素坐标 d1 depth_map[p1[1], p1[0]] d2 depth_map[p2[1], p2[0]] if d1 0 or d2 0: return None # 根据深度和相机内参计算3D点 pt1 pixel_to_3d(p1, d1, self.mtx_l) pt2 pixel_to_3d(p2, d2, self.mtx_l) return np.linalg.norm(pt1 - pt2)pixel_to_3d函数就是前面提到的反投影公式。可视化部分可以用cv2.imshow把视差图和深度图并排显示再画上你点击的测量点和连线。所有这些代码合在一起就是一个可供答辩演示的“基于 OpenCV 的双目视觉尺寸测量”项目。5. 双目测尺寸的常见坑与排查从标定到测量的 5 条血泪经验5.1 重投影误差看着低测出来却差一截现象calibrateCamera返回的重投影误差只有 0.08 像素但测量一个 100 毫米的标准块结果偏差超过 10 毫米。原因单目标定的重投影误差只代表单目内参自洽不代表双目标定里的R、T准确。常见的是左右相机采集棋盘格时两组图像没有严格同步或者棋盘格一半被遮挡角点检测到的位置有偏移。另一个隐藏原因是标定板不平整打印纸贴在弯曲的纸板上角点世界坐标和真实位置有偏差。解决把左右相机固定在一个刚性支架上用外触发或者同时拍照每张图像检查棋盘格所有内角点都被完整检测到标定板用玻璃贴片或硬塑料板。我习惯把采集的图像数量从 15 组增加到 25 组并保证棋盘格在画面四个角和中心都出现过。5.2 视差图全是横纹或者“鬼影”现象SGBM 计算出的视差图有一条条的水平条纹物体边缘像带锯齿的重影深度图看起来是脏的色块。原因左右图像校正不彻底极线没有严格对齐。stereoRectify之后没有验收直接丢给 SGBM。SGBM 的搜索窗口默认只做水平搜索如果极线有哪怕 1-2 个像素的偏差匹配就全乱。解决用cv2.remap校正后在左右图上画一条水平线观察同一个角点是否落在同一条水平线上。如果有偏差检查stereoRectify的入参是不是左右内参写反了或者R、T方向不对。另一个常见原因是相机分辨率不小但标定时没有传入正确的imageSize导致映射矩阵尺寸和原图对不上。5.3 纯色物体测出来的尺寸忽大忽小现象一个白色纸箱放在浅色桌面上视差图里纸箱表面有大片空洞边缘被噪声覆盖连续测量两次结果差了 20 毫米。原因SGBM 依赖纹理来匹配左右图像。纯色区域没有梯度匹配置信度低OpenCV 只能靠平滑项去猜猜出来的视差自然不稳。解决最省事的办法是在物体表面贴上带纹理的贴纸或撒一点细小颗粒物如果不想破坏外观就把blockSize调大让平滑窗口覆盖更大区域或者用 WLS 滤波把空洞补上。但要记住blockSize也不能无脑大它会把边缘往里收窄导致测出来的尺寸偏小需要拿标准块标定一次补偿值。5.4 测量时相机被碰一下所有数据全废现象标定和测量都正常但某次测量前相机支架被碰歪了之后测出的尺寸直接飘了。原因双目测量的前提是两个相机的相对位姿不变。只要左右相机之间有任何微小的旋转和平移R、T和Q矩阵全部失效视差和深度跟着错。解决相机支架要用螺丝锁死不要用夹子或双面胶。记录每个标定板角点的三维坐标如果发现前后两次标定得到的R、T变化超过 0.1 毫米量级就重新标定。另一个实用方案是做“在线校验”每次开机拍一张固定位置的棋盘格计算当前的R、T如果和标定结果偏差大就报错提醒。5.5 手动点击测量点时亚像素精度不够现象用鼠标点击角点时的像素坐标是整数取深度时也是取单点像素的值导致两三次测量重复性差。原因整数值像素对边缘而言有 0.5 像素的量化误差深度图的单点噪声又会被放大测出来的距离自然抖。解决测量点不要在 ROI 里取单个像素而是取该点周围 3×3 或 5×5 窗口的深度中值。如果物体边缘有清晰的角点用cv2.cornerSubPix先求亚像素角点再测。另外深度图本身要先做中值滤波cv2.medianBlur(depth, 5)能有效抑制单点噪声但要注意滤波核太大会把边缘弄圆。6. 进阶用自动标定与单应性简化测量附验证方法和验收习惯如果你的项目时间不够不想走完整双目标定流程还有一种适合固定场景的简化方案把相机固定在一个已知高度的支架上拍摄一个贴着棋盘格的平面然后用单应性矩阵建立像素到真实平面坐标的映射。这种方法只适合物体完全贴合这个平面、且没有明显厚度的场景比如测量一张纸的长度、一个 PCB 板的尺寸。它不需要 SGBM直接以棋盘格角点为基准计算单应性矩阵H然后测量任意两点在平面上的距离。这个方案速度更快精度取决于标定板到相机的距离和棋盘的角点检测精度通常能达到 0.5 毫米以内前提是物体高度差不超过 2 毫米。它的局限是量不了高度量不了圆柱和斜面。验证测量结果时不要只测一个标准块就宣布成功。我建议准备三组参照物硬币直径已知 25 毫米、A4 纸宽 210 毫米和一个手机长度可查。每个物体测 10 次记录每次的像素坐标和计算结果计算均值和标准差。如果标准差超过 1 毫米说明图像采集或者 SGBM 的稳定性不够检查光线是否变化、物体是否反光、相机是否用了自动对焦——自动对焦是测量的大敌必须手动锁焦。另外每次测量前都先测一次固定位置的棋盘格用重投影误差作为当日系统的“健康指标”。我最后的习惯是把每一组测量数据连同当时的视差图、深度图一起存成一个文件夹命名带上日期和工况。这样答辩时老师问“这个结果怎么来的”我能直接调出原始数据复现而不是只给一张截图。这个习惯帮我挽回过好多次因为环境光变化导致结果无法复现的局面。双目测尺寸这条路真正难的不是算法是你有没有把所有会导致误差的细节都看住。希望这篇笔记能帮你在期末或毕设里少走几步弯路。本文还有配套的精品资源点击获取
返回列表