ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Python+OpenCV双目测距:标定、SGBM匹配与深度图实现全攻略

Python+OpenCV双目测距:标定、SGBM匹配与深度图实现全攻略 简介这份毕业设计论文围绕Python与OpenCV实现双目视觉匹配测距系统展开面向计算机视觉、机器人感知以及自动驾驶相关方向的本科生可作为完成毕业设计或课程项目的完整参照。论文先介绍双目视觉的基本原理与研究现状再深入讲解图像采集、深度图像获取、视差计算与立体匹配方法同时配合Python编程和OpenCV库的应用完整呈现系统架构与硬件搭建、软件开发、系统调试过程并通过不同距离与光照条件下的实验验证系统精度与鲁棒性。资源为单个docx文档大小约32KB目录涵盖引言、基础知识、系统设计、实现与调试、实验与结果、总结与展望六大章节可直接查阅或按需修改。目前已有352人学习适合需要快速掌握双目视觉测距落地流程并撰写规范论文的高年级本科生。1. 双目测距的精度瓶颈标定误差比匹配误差更难排查单目相机拍到的是一张没有深度信息的平面图人眼能看出远近靠的是两只眼睛的视差。要在 Python 环境下复现这套机制常规做法是两个摄像头同步采集左右视图经过标定、极线校正、立体匹配得到视差图再用三角测量换算物理距离。很多人第一次跑通 SGBM 算法后测距误差高达 10% 以上第一反应是调匹配参数但问题往往出在更前面的环节——相机标定和内参外参的准确性。这篇把基于 Python 与 OpenCV 的双目视觉匹配测距系统从标定到测距的完整链路拆开给出可直接运行的代码和参数起点也把容易踩的坑一起讲清楚。适合正在做双目测距毕业设计、机器人避障或低成本 3D 视觉方案的开发者。2. 相机标定与极线校正立体匹配的前置条件双目测距不是简单地把两个摄像头拍到的图拼在一起。左右相机之间存在位置偏移和视角差异图像上的像素点不会天然对齐。SGBM 这类立体匹配算法依赖一个假设同名点在某一条直线上搜索。这个直线关系就是靠标定和极线校正建立起来的。2.1 双目几何模型内参、外参与基线先建立坐标系概念。每个相机有四个参数组需要关注内参矩阵 K [[fx, 0, cx], [0, fy, cy], [0, 0, 1]]。fx f / dxfx 是像素焦距dx 是单个像元的物理尺寸cx 和 cy 是光心在图像平面的像素坐标。畸变系数 dist (k1, k2, p1, p2, k3)。k 系列是径向畸变p 系列是切向畸变。右相机相对左相机的旋转矩阵 R 和平移向量 T。T 的模长就是基线长度 B这是后面测距公式中的关键量。本质矩阵 E 和基础矩阵 F记录了左右视图之间的对极几何约束。这些参数决定了空间一点 P 在左右相机成像平面上的投影位置。理想情况下如果两个相机完全平行安装且没有畸变左右图像的对应点只存在于同一行求视差只需要水平方向搜索。实际安装不可能做到绝对平行畸变也客观存在所以必须先标定再校正。2.2 棋盘格标定获取内参与双目标定参数OpenCV 提供了完整的标定工具链calibrateCamera做单目标定stereoCalibrate做双目标定。标定板的选型直接影响精度我一般用 9x6 内角点的棋盘格格子边长 20mm 或 30mm。拍摄时让棋盘格出现在画面不同位置和角度至少采集 15~20 对图像。import cv2 import numpy as np # 棋盘格内角点尺寸9x6 表示横向9个内角点、纵向6个 pattern_size (9, 6) square_size 0.03 # 每个格子的实际边长单位米 # 生成世界坐标系下的角点坐标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_l [] # 左图像坐标 img_points_r [] # 右图像坐标 left_images [...] # 你的左相机标定图片路径列表 right_images [...] # 对应的右相机标定图片路径列表 for left_img_path, right_img_path in zip(left_images, right_images): left cv2.imread(left_img_path) right cv2.imread(right_img_path) gray_l cv2.cvtColor(left, cv2.COLOR_BGR2GRAY) gray_r cv2.cvtColor(right, cv2.COLOR_BGR2GRAY) ret_l, corners_l cv2.findChessboardCorners(gray_l, pattern_size, None) ret_r, corners_r cv2.findChessboardCorners(gray_r, pattern_size, None) if ret_l and ret_r: # 亚像素细化提高角点定位精度 criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) 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) # 单目标定得到左右相机各自的内参和畸变系数 ret_l, K_l, dist_l, rvecs_l, tvecs_l cv2.calibrateCamera( obj_points, img_points_l, gray_l.shape[::-1], None, None) ret_r, K_r, dist_r, rvecs_r, tvecs_r cv2.calibrateCamera( obj_points, img_points_r, gray_r.shape[::-1], None, None) # 双目标定得到 R 和 T ret, K_l, dist_l, K_r, dist_r, R, T, E, F cv2.stereoCalibrate( obj_points, img_points_l, img_points_r, K_l, dist_l, K_r, dist_r, gray_l.shape[::-1], criteriacriteria)代码逻辑说明findChessboardCorners负责在灰度图上查找棋盘格内角点返回的角点是像素级精度cornerSubPix用迭代法把角点位置细化到亚像素级这步直接决定了标定结果的稳定性。stereoCalibrate返回的 R 和 T 是右相机相对于左相机的外参T 的模长就是基线长度 B。参数说明square_size必须与你打印棋盘格时的实际尺寸一致单位建议统一用米。如果打印出来 30mm 的格子填成 0.03最后测距结果会出错。另外需要确认返回值ret重投影误差通常小于 0.3 像素才算合格如果大于 0.5 像素说明照片里有模糊或反光的棋盘格建议重新采集。提示标定板不一定必须是棋盘格也可以用cv2.findCirclesGrid配合圆点标定板。圆点板在极端角度下比棋盘格更稳定适合广角镜头。2.3 极线校正把二维匹配降阶为一维搜索标定完成后左右相机的位置关系和畸变参数已经确定但两个成像平面仍然存在夹角。stereoRectify会计算两个重投影矩阵把左右视图重映射成行对齐的形式。校正之后左右图中对应的同名点竖直方向偏移为零SGBM 的匹配搜索从二维降为一维速度和准确性都会有明显提升。# 图像尺寸注意顺序是 (width, height) image_size (gray_l.shape[1], gray_l.shape[0]) # alpha0 表示只保留有效像素区域 R1, R2, P1, P2, Q, roi1, roi2 cv2.stereoRectify( K_l, dist_l, K_r, dist_r, image_size, R, T, alpha0) # 计算映射表 map1_l, map2_l cv2.initUndistortRectifyMap( K_l, dist_l, R1, P1, image_size, cv2.CV_32FC1) map1_r, map2_r cv2.initUndistortRectifyMap( K_r, dist_r, R2, P2, image_size, cv2.CV_32FC1) # 对每一帧图像做重映射 rectified_l cv2.remap(left, map1_l, map2_l, cv2.INTER_LINEAR) rectified_r cv2.remap(right, map1_r, map2_r, cv2.INTER_LINEAR)initUndistortRectifyMap生成的映射表是浮点坐标remap按照映射表从原图采样生成新图INTER_LINEAR表示双线性插值。校正完成后做一个快速验证把左右图水平并排鼠标在左图选一个纹理明显的点右图同一行上应该能找到对应的点。如果竖直方向偏差超过 1~2 个像素说明标定参数不够准强行跑 SGBM 会产生大量误匹配。3. SGBM 匹配参数调优从视差图到去除离群点极线校正之后左右图像在水平方向对齐接下来的任务是对左图的每个像素在右图的同一行中找到它的对应点两个点的列坐标差值就是视差 d。视差越大物体离相机越近。OpenCV 里常用的立体匹配算法有 BM、SGBM、全局匹配选哪个、参数怎么定直接决定最终测距精度。3.1 为什么选 SGBM 而不是 BM 或全局匹配BMBlock Matching是典型的局部匹配算法对每个像素在邻域窗口内做代价聚合速度快但容易在弱纹理区域产生空洞和条纹噪声。全局匹配把问题建模成能量函数最小化用图割或置信度传播求解精度高但一张 VGA 图像需要几秒无法用于实时测距。SGBMSemi-Global Block Matching的思路介于两者之间将二维图像的能量最小化近似为多个一维扫描线路径上的动态规划在代价中引入平滑惩罚项既保留了全局结构约束又能在 CPU 上跑到接近实时的速度。Python 侧直接用cv2.StereoSGBM_create创建匹配器即可不需要自己实现底层优化过程。3.2 SGBM 参数起点与调优思路StereoSGBM_create的参数很多但真正影响结果的只有少部分。先用一组经过多次实验验证的起点值再根据实际图像微调参数起点值作用调整规则minDisparity0最小视差近距离场景可设为负数以扩大范围numDisparities64最大视差与最小视差的差必须是 16 的倍数blockSize9匹配块边长奇数越大越平滑越小细节越丰富P18 * blockSize²视差变化 1 像素的惩罚小数值保留更多边界细节P232 * blockSize²视差变化超过 1 像素的惩罚建议为 P1 的 4 倍左右uniquenessRatio10最小代价与次小代价的比例阈值提高可减少误匹配但过大丢边缘speckleWindowSize100去除小连通域的窗口大小0 表示关闭去斑speckleRange32连通域内视差允许的最大差异视差尺度大时适当加大preFilterCap63预处理截断值抑制光照差异带来的噪声# 创建 SGBM 匹配器 left_matcher cv2.StereoSGBM_create( minDisparity0, numDisparities64, blockSize9, P18 * 9 * 9, P232 * 9 * 9, disp12MaxDiff1, # 左右一致性检查允许的最大差值 uniquenessRatio10, speckleWindowSize100, speckleRange32, preFilterCap63, modecv2.STEREO_SGBM_MODE_SGBM ) # 输入必须是灰度图 disparity left_matcher.compute( rectified_l_gray, rectified_r_gray ).astype(np.float32) # OpenCV 输出的视差值是固定小数点格式需要除以 16 disparity disparity / 16.0代码里有几个关键点。disp12MaxDiff控制左右一致性检查的容差它要求左图匹配到的点在右图上反向匹配回来视差差值小于该值才算有效这是遮挡区域和误匹配的主要过滤手段。preFilterCap在匹配前对图像做梯度截断可以抑制左右相机曝光差异造成的亮度不一致。最容易踩的坑是最后一行SGBM 输出的视差值是固定小数点存储真实视差要除以 16忘记这步会让深度结果偏大 16 倍。注意修改 blockSize 后P1 和 P2 需要同步重新计算。很多人只改了 blockSizeP1/P2 还停留在原值导致平滑效果不符合预期。3.3 WLS 滤波与视差图去噪裸 SGBM 输出的视差图存在噪声和空洞直接用于测距会产生大量离群点。OpenCV 的 ximgproc 扩展模块提供了 WLSWeighted Least Squares滤波用左图作为引导图对视差图做边缘保持平滑能明显改善测距稳定性。import cv2.ximgproc as ximgproc # 右图也计算一次视差WLS 滤波依赖左右两张视差图 right_matcher cv2.ximgproc.createRightMatcher(left_matcher) disparity_left left_matcher.compute( rectified_l_gray, rectified_r_gray ).astype(np.float32) disparity_right right_matcher.compute( rectified_r_gray, rectified_l_gray ).astype(np.float32) # 创建 WLS 滤波器 wls_filter ximgproc.createDisparityWLSFilter(left_matcher) wls_filter.setLambda(8000.0) # 平滑强度 wls_filter.setSigmaColor(1.5) # 颜色高斯核标准差 filtered_disparity wls_filter.filter( disparity_left, rectified_l_gray, disparity_map_rightdisparity_right ).astype(np.float32) / 16.0 # 把无效视差置为 NaN便于后续掩码处理 filtered_disparity[filtered_disparity 0] np.nanWLS 滤波的原理是视差图作为待优化信号以左图颜色信息构造加权矩阵在平滑视差的同时保留物体边缘。lambda控制平滑强度值越大视差图越平滑但可能把细小物体的边缘抹掉sigmaColor表示颜色差异的敏感度值越大边缘保持越弱。真实距离超过 3 米之后WLS 滤波对精度提升的贡献会变小此时更应该关注标定和相机硬件的同步性。4. 视差-深度模型三角测量推导与误差传播有了视差图下一步是把视差值换算成物体到相机平面的物理距离。很多人卡在标定和匹配阶段其实深度换算这一步也有不少细节需要处理。4.1 三角测量原理Z fB/d 是怎么来的简化情况下两个完全平行且内参相同的相机光心距离为 B基线焦距为 f。空间点 P 在左右相机成像平面上的投影点分别为 (x_l, y) 和 (x_r, y)它的深度 Z 与视差 d x_l - x_r 的关系由相似三角形得Z f * B / d这个公式成立的前提是成像平面已经校正为严格平行且点在两个相机公共视野内。实际系统中 f 用像素单位焦距内参矩阵的 K[0,0]B 用毫米或米d 是像素算出的 Z 单位与 B 一致。stereoRectify返回的 Q 矩阵也封装了从视差到三维坐标的投影关系可以用cv2.reprojectImageTo3D直接得到三维坐标但手写公式在调试阶段更直观便于排查单位不一致的问题。4.2 深度图计算代码与无效像素处理# 从内参中读取像素焦距 fx K_l[0, 0] baseline np.linalg.norm(T) # 单位与标定板格子尺寸一致 # 有效视差掩码视差必须大于0且不超过 numDisparities 范围 valid_mask (filtered_disparity 0) (filtered_disparity 64) depth_map np.zeros_like(filtered_disparity) with np.errstate(divideignore): depth_map[valid_mask] fx * baseline / filtered_disparity[valid_mask] # 剔除过远或过近的离群点 depth_map[depth_map 5.0] 0 # 5米以外不可信 depth_map[depth_map 0.1] 0 # 10厘米以内超出视差范围 # 可视化深度图拉伸到 8 位显示 depth_vis np.zeros_like(depth_map, dtypenp.uint8) mask depth_map 0 depth_vis[mask] np.clip(depth_map[mask] * 255.0 / 3.0, 0, 255).astype(np.uint8)这里有几个容易踩的坑。第一单位一致性fx 是像素单位标定板格子边长如果用米T 的模长也是米算出的深度就是米如果混用毫米和米结果差三个数量级。第二除零问题filtered_disparity中可能出现 0 或 NaN必须先做掩码再进入除法。第三有效视差范围近距离物体若超出numDisparities覆盖范围SGBM 会直接丢弃这些像素深度图上表现为黑色空洞此时需要调大numDisparities或增大基线距离。提示手写公式计算的是左相机光轴方向上的深度值。如果要做点云输出用cv2.reprojectImageTo3D(filtered_disparity, Q)拿到完整的三维坐标再配合 Open3D 保存为 ply 格式即可。4.3 误差传播为什么距离越远越测不准深度误差主要来自三个部分视差量化误差、标定误差、匹配误匹配。其中视差量化误差是系统性的SGBM 的代价计算在整数像素粒度上进行亚像素精度有限。对视差公式求导得到dZ/d_d -fB / d² -Z² / (fB)深度误差与 Z 的平方成正比。举个例子fx 600 像素B 60mm物体在 1m 处时视差 d 600×0.06/1 36 像素一个像素的视差误差对应约 28mm 深度误差物体在 3m 处时视差只有 12 像素1 像素误差对应约 250mm 误差误差率超过 8%。这个传播规律决定了双目测距适合中近距离0.3m~3m应用。想测更远有两种途径增大基线 B或者提高图像分辨率让视差计算更精细但两者都会增加硬件成本或计算开销工程上需要做取舍。5. 实测验证与调参收敛技巧系统调通后需要定量验证而不是停留在感觉差不多的状态。5.1 用已知距离的模板做误差标定选一个纹理丰富的图案贴纸或标定板垂直正对相机放置用卷尺量出真实距离读取深度图中心区域的中位数作为测量值。每个距离测 5 组取平均记录成表格真实距离 (mm)测量距离 (mm)绝对误差 (mm)相对误差500518183.6%800825253.1%10001042424.2%15001585855.7%200021401407.0%误差随距离增大的趋势符合第 4 章的结论。如果 1m 以内误差超过 5%优先检查标定参数而不是算法参数。另外图像边缘区域的误差通常比中心大原因是镜头畸变在校正后的残余误差在边缘被放大所以测量物体尽量放在画面中心区域。5.2 三个常见的现场问题第一个是标定图数量不够。少于 15 对图像的标定结果很不稳定换一次光照可能内参就出现可见偏差。建议每个角度拍 3 组覆盖画面的九宫格区域保证棋盘格在画面中占据 1/3 以上面积。第二个是极线校正后没有验证。有人跑完remap直接进 SGBM结果竖直方向偏移 3~5 像素匹配质量大幅下降。校正后把左右图并排显示在左图选几个纹理点看右图同行位置是否对齐这一步只花一分钟能省下大量调参时间。第三个是左右相机曝光不一致。两个 USB 摄像头的自动曝光独立运行画面亮度差异会让 SGBM 的代价计算失真。解决方案有两种在相机驱动层面固定曝光和增益参数或者改用带硬件同步触发功能的双目模组。调参收敛的先后顺序建议是先固定numDisparities64和blockSize9确认极线校正没问题再调uniquenessRatio观察误匹配点数量最后通过setLambda调整 WLS 平滑强度。如果视差图出现大片黑色空洞优先检查numDisparities是否覆盖了场景的最小距离如果物体边缘出现膨胀感把blockSize调小到 5 或 7如果视差图椒盐噪声多先把speckleWindowSize从 100 增加到 150。每次只改一个参数对比前后的视差图和测距误差盲目组合调参会很难收敛到稳定值。本文还有配套的精品资源点击获取
返回列表