YOLOv5+双目视觉测距:从标定到三维坐标的完整实现
简介适配YOLOV5 6.1的双目相机三维测距项目工程包面向计算机视觉、自动驾驶、机器人导航及无人机避障领域的开发者和研究者核心解决实时目标检测与深度估计协同工作的工程落地问题。压缩包共141个文件大小约40.38MB以Python源码py/pyc、YAML/XML配置、Shell脚本和Markdown文档为主体并包含预训练权重、视频演示及Docker环境定义便于直接运行和调试。目前已有2420人学习下载适合希望快速掌握目标检测与双目匹配联合用法、且具备基础编程能力的读者。工程代码在YOLOV5输出的目标框基础上接入双目视差计算通过预处理减少特征匹配错误可提升三维空间位置估计的稳定性内置的样例图片和视频可对照验证效果配置文件和Docker环境定义可帮助自定义相机参数与训练策略整体是一个可扩展的基线工程。1. 从一张双目照片到三维坐标YOLOv5双目测距项目的落地笔记一个老生常谈的问题想让摄像头告诉你前方那辆车的距离是4.2米到底该怎么实现有人用激光雷达有人用单目加深度学习估计深度但在成本和精度之间折中的方案往往是双目视觉。而我最近拆解的这个项目恰好走的就是这条路——用YOLOv5做目标检测用双目相机做视差计算最终输出每个目标的3D坐标和距离。它不是一个纯粹的算法工程而是一个能直接从照片跑到实时视频的完整系统。整个流程从相机标定、立体校正、视差计算到目标框匹配与三维解算环环相扣任何一步参数不对最终距离就会离谱。这个项目特别适合正在做机器人避障、智能交通或是安防监控的开发者尤其是那些已经跑通YOLOv5、但不知道怎么把像素坐标变成真实空间距离的人。2. 项目拆解与选型为什么是YOLOv5和SGBM2.1 整体流程从双目图像到三维坐标的六步链路这个项目的核心流程并不复杂但每一步都必须严格按照顺序执行。我拆开之后梳理出了六步链路第一步是双目相机标定得到左右相机的内参、畸变系数以及两相机之间的旋转和平移矩阵第二步是对左右图像做立体校正让两帧图像的行对齐也就是极线校正第三步是目标检测用YOLOv5分别或同时处理左右图得到目标的边界框和类别第四步是立体匹配通过校正后的图像计算视差图第五步是把目标框的中心或者底部中点映射到视差图上提取该位置的视差值第六步是三角化计算利用视差值和相机焦距、基线长度恢复目标的深度坐标再结合相机外参得到三维位置。这个流程把深度学习的检测结果和传统立体视觉的几何计算结合在了一起。我见过很多类似项目直接用单目深度估计但那个无法得到真实尺度也见过纯双目不做检测只能计算整幅图的深度而不能定位到具体目标。这个项目的聪明之处在于用检测框来约束视差图的提取区域既减少了噪声干扰又让距离输出直接和目标语义绑定。2.2 技术选型YOLOv5、OpenCV与SGBM的搭配逻辑项目选型很明确目标检测用YOLOv5图像处理和立体匹配用OpenCV。YOLOv5在工程界的普及度太高导出ONNX和TensorRT的路径都成熟预训练权重也容易获取。而OpenCV内置了双目标定和立体匹配的完整工具链其中SGBM半全局块匹配算法是精度和速度比较均衡的选择比BM算法抗噪强比传统全局匹配快得多。我比较欣赏的一点是作者没有为了炫技引入自定义网络而是把这些成熟组件像积木一样搭起来。整个算法框架完全可以用CPU跑只是速度会慢一些。如果有GPUYOLOv5可以加速但SGBM部分仍然吃CPU资源。所以在实际部署时往往需要把检测放到GPU、立体匹配放到CPU用两个线程来并行避免互相等待。这也是我拆这个项目时觉得最值得学习的地方。3. 核心模块实现双目标定、立体匹配与三维解算的完整链路3.1 双目相机标定与立体校正解决两只看不见的手的对齐问题双目测距的第一步是标定。没有标定左右图像的对应点根本不在同一水平线上视差计算无从谈起。这个项目里用的是OpenCV的棋盘格标定法需要先用双目相机拍摄一定数量的标定板图像。我在复现时拍了30组左右各一张然后分别计算单目内参再通过stereoCalibrate求双目的旋转矩阵和平移向量。import cv2 import numpy as np # 棋盘格参数假设棋盘大小为(9,6)单位格边长30mm pattern_size (9, 6) square_size 30.0 # 准备对象点 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 [] # 遍历标定图像 for fname_l, fname_r in zip(left_images, right_images): img_l cv2.imread(fname_l) img_r cv2.imread(fname_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, pattern_size, None) ret_r, corners_r cv2.findChessboardCorners(gray_r, pattern_size, None) if ret_l and ret_r: obj_points.append(objp) # 亚像素精确化 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) img_points_l.append(corners_l) img_points_r.append(corners_r)这段代码的逻辑很直观每张图片上提取棋盘角点然后对角点做亚像素优化因为直接提取的角点坐标精度不高而亚像素化能显著提升标定精度。注意square_size必须和实际棋盘格边长一致否则算出来的距离会有固定比例的偏差。我一般会让标定板充满画面、倾斜多个角度避免所有图像都是正面平视那样外参计算容易退化。标定完成后会得到两个相机的内参矩阵、畸变系数以及右相机相对于左相机的旋转矩阵R和平移向量T。接下来是立体校正用stereoRectify加上initUndistortRectifyMap得到映射表之后就可以用remap来把左右图变换到极线对齐的状态。这一步效果可以直观检查在左右图上画一条水平线若对应目标点都在同一条线上说明校正成功。3.2 基于SGBM的视差图计算深度信息的原始密码立体匹配是双目测距的中间核心。SGBM通过计算像素块之间的匹配代价找到左右图对应点然后输出视差图。视差值越大说明物体距离相机越近。这个项目用的是OpenCV自带的StereoSGBM_create参数设置直接影响深度图的细腻程度和噪声水平。# 假设左右图已经经过remap校正 left_gray cv2.cvtColor(left_rect, cv2.COLOR_BGR2GRAY) right_gray cv2.cvtColor(right_rect, cv2.COLOR_BGR2GRAY) # SGBM参数设置 min_disp 0 num_disp 96 # 必须是16的倍数 block_size 11 P1 8 * 3 * block_size ** 2 P2 32 * 3 * block_size ** 2 sgbm cv2.StereoSGBM_create( minDisparitymin_disp, numDisparitiesnum_disp, blockSizeblock_size, P1P1, P2P2, disp12MaxDiff1, uniquenessRatio10, speckleWindowSize100, speckleRange32, modecv2.STEREO_SGBM_MODE_SGBM ) disparity sgbm.compute(left_gray, right_gray).astype(np.float32) / 16.0参数解释如下numDisparities表示最大视差值值越大能探测越近的物体但计算量也越大blockSize是匹配块窗口大小取奇数块越大对纹理稀疏区域越鲁棒但会丢失边缘细节P1和P2是惩罚参数控制视差平滑度我调整时会先固定其他参数再用一个远距离和近距离的场景来对比效果。disp12MaxDiff是左右一致性检查的最大差值这个参数能有效过滤遮挡区域的错误匹配。uniquenessRatio要求匹配点必须是左右图中唯一的最佳匹配比例越高误匹配越少但也会导致空洞变多。输出的disparity是一个float32数组原始值是固定小数点格式必须除以16才能得到真正的视差值。这一步非常容易漏如果忘除后面所有距离都会偏大16倍。我最初复现时就吃了这个亏检测到的车辆距离显示80米实际只有5米检查半天才发现在这里。3.3 YOLOv5目标检测与目标框映射从像素框到视差图目标检测部分直接使用YOLOv5的Python接口。考虑到实时性复现时我选择了YOLOv5s模型输入尺寸640x640置信度阈值设为0.5NMS阈值设为0.45。检测结果会输出目标的类别、置信度以及边界框坐标。这里有一个关键设计项目并没有在左右图上都做检测而是只对左图检测再用左图检测框去右图寻找对应匹配点这样能减少一半的检测耗时。import torch # 加载模型 model torch.hub.load(ultralytics/yolov5, yolov5s, pretrainedTrue) model.conf 0.5 # 置信度阈值 model.iou 0.45 # NMS阈值 # 检测左图 results model(left_rect) boxes results.xyxy[0].cpu().numpy() # [x1, y1, x2, y2, conf, cls] for box in boxes: x1, y1, x2, y2, conf, cls box # 取目标框底部中心作为测距点 cx int((x1 x2) / 2) cy int(y2) # 在视差图上读取视差值 disp disparity[cy, cx]为什么取底部中心而不是边界框中心因为对地面上的目标行人、车辆、障碍物来说底部中心通常更接近目标的实际接地位置深度相对稳定。如果取框中心可能会落在行人身体中部导致距离跳动很大。这个细节在项目注释里特别强调了属于工程经验。一个坑是如果目标框底部中心落在视差图空洞区域没有匹配成功的像素disp会是无效值。这个项目采用了一个简单的策略在底部中心附近一个5x5的区域里搜索非零视差值取中位数作为最终视差这样能显著增强鲁棒性。我试过如果只取单点雨天或玻璃等反光区域经常测出NaN距离。3.4 三维坐标解算与距离输出三角化的最后一公里得到视差后深度可以由三角化公式计算。以左相机坐标系为参考系假设目标在左图中的像素坐标为uv视差为d相机焦距为f_x基线长度为b那么目标的Z坐标深度就是f_x * b / d。X坐标和Y坐标可以由像素坐标和内参反算def pixel_to_3d(u, v, disp, Q): # Q是stereoRectify得到的重投影矩阵 if disp 0: return None point_3d cv2.perspectiveTransform( np.array([[[u, v, disp, 1.0]]], dtypenp.float32), Q ) x, y, z, w point_3d[0, 0] if w 0: return None return x / w, y / w, z / w这里的Q矩阵由stereoRectify直接生成包含了相机内参、基线长度和主点偏移所以用齐次坐标变换一步到位。另一种常见做法是直接用cv2.reprojectImageTo3D整幅图转换但那样内存占用大而且很多无效点。逐点转换只处理检测框内的像素效率高得多。逻辑说明perspectiveTransform输入的是四维齐次坐标输出是四维结果但真正的三维坐标是前三个分量除以第四个分量w。有时我看到有人直接把前三项当结果没有归一化导致坐标偏差极大。在输出距离前我会将Z值乘以一个尺度校准系数这是因为标定时棋盘格尺寸可能和实际有偏差用已知距离的物体做一次线性校准比纯理论计算更可靠。4. 环境搭建与运行从裸机到输出距离的实操记录4.1 依赖安装与版本兼容复现这个项目时我踩了不少环境坑。项目依赖的核心包是PyTorch、OpenCV、NumPy、SciPy以及YOLOv5仓库本身。PyTorch版本建议选择稳定版比如2.x因为YOLOv5官方仓库会跟随更新。OpenCV必须带有contrib模块吗这里不用因为双目标定用的stereoCalibrate在基础版就有。我用的Python 3.8环境实测兼容性最好。# 创建虚拟环境 conda create -n stereo python3.8 conda activate stereo # 安装PyTorch CPU版如果有GPU则换对应命令 pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu # 安装其他依赖 pip install opencv-python numpy scipy # 拉取YOLOv5 git clone https://github.com/ultralytics/yolov5 cd yolov5 pip install -r requirements.txt我建议不要全局安装YOLOv5的requirements因为它会改动一些依赖版本。更稳妥的方式是先安装好OpenCV和PyTorch再单独安装YOLOv5需要的轻量依赖。如果后面还想导出TensorRT引擎需要注意ONNX版本和TensorRT版本的对应关系这个环节最容易出幺蛾子。4.2 相机配置与标定脚本运行项目里附带了标定脚本但要求图片放在指定目录。我实际操作时用了自己拍摄的棋盘格图像先把左右图命名为left_01.jpg、right_01.jpg格式然后直接运行标定脚本。脚本会输出stereo_camera_params.npz文件里面保存了M1, D1, M2, D2, R, T, E, F这些矩阵。python calibrate.py --left_dir images/left --right_dir images/right --output params.npz标定结果质量验证很重要。我会打印重投影误差一般小于0.5像素就算不错。如果误差大说明某张图片的角点提取有问题或者标定板不平整。另外R矩阵应该是接近单位矩阵T向量中的x分量就是基线长度单位取决于你在标定时设定的square_size。我用30mm的方格所以T的x量纲是毫米换算成米要除以1000。4.3 实时运行与参数调整一切准备好后运行主程序就可以从双目相机实时读取画面并逐帧输出检测结果和距离信息。主程序支持读取摄像头和视频文件两种模式。# 使用双目USB相机通常设备号0和1 python run.py --camera 0 1 --params params.npz # 或者用录好的双目视频 python run.py --video stereo_video.mp4 --params params.npz启动后界面上会显示左图检测框上带有标签和距离值。我测试时用一个卷尺测量了3米、5米、10米处的箱子输出距离和实测相差在5%以内。如果发现测距偏大或偏小应该先检查numDisparities和blockSize增大numDisparities可以改善近处目标但远处视差小反而容易受噪声影响。通常我会先用静止画面调节到距离稳定再换动态场景看跳动幅度。5. 常见踩坑与排查视差空洞、标定误差和目标错位的应对5.1 视差图大范围空洞远处目标完全测不出距离现象是输出窗口里远处物体没有距离值视差图大片黑色。原因是numDisparities设置太小远处物体视差很小比如10米外的物体视差可能只有2-3像素如果numDisparities只有16视差搜索范围是从0到16但SGBM的视差实际是从minDisparity到minDisparitynumDisparities-1默认最小视差为0所以远距离小视差其实已被包括。更可能的原因是uniquenessRatio过高导致远处低纹理区域的匹配被拒绝。解决方法是降低uniquenessRatio到5-10同时把blockSize从11降低到7或者将P2适当减小。我最终参数是numDisparities128、blockSize7、uniquenessRatio5远近效果平衡。5.2 检测框和距离不匹配框在车上但距离却返回零点几米现象是框在车上但输出的Z值几乎为0明显不合常理。原因在于disparity除以16这一步忽略了或者Q矩阵中主点坐标和实际图像尺寸不一致。我耗时最长的排查是这样的用cv2.getOptimalNewCameraMatrix得到的内参与stereoRectify中传入的内参不一致导致Q矩阵里的主点坐标偏移。解决办法是保证stereoRectify使用原始标定内参且remap的映射表也是基于同一套内参生成。另外检查代码中是否写了disp / 16.0。5.3 画面有严重的横向错位左右图极线不校正现象是左右图叠加后同一目标不在同一水平线视差图出现条纹噪声。原因往往是没有执行remap或者remap的映射表是旧的。我在切换摄像头后忘记重新生成映射表结果所有图像都基于之前相机的校正参数。解决方法是每次启动时根据参数文件重新计算map1_l, map2_l, map1_r, map2_r或者干脆把映射表保存成npz但前提是摄像头位置和焦距不能变。5.4 距离跳动严重同一物体相邻几帧差几十厘米现象是物体静止距离输出却忽大忽小。原因有两点一是检测框左右抖动导致底部中心点在视差图上落在不同位置二是视差图本身在物体边缘有噪声。我的解决方式是引入时间维度滤波对同一目标的深度做滑动平均窗口为5帧。同时检测框使用YOLOv5本身的跟踪功能让框更稳定。代码里可以维护一个目标ID到深度队列的映射用deque保存最近5帧深度取中位数输出。5.5 运行一段时间后内存持续增长最终卡死现象是程序跑几分钟后占满内存。原因是SGBM处理每一帧都会生成新的视差图如果之前的大数组没有及时释放Python引用计数会延迟回收。我通常在循环末尾显式删除不再使用的图像变量并调用cv2.gc_collect()或者直接复用缓冲区。另一个隐藏原因是OpenCV的remap每次都会分配新的输出图像如果反复调用旧的内存在Python里虽然不可达但OpenCV底层可能持有引用。我修改为预分配输出图像用dst参数传入。6. 二次开发思路从静态测距到多目标追踪的改造拿到这个项目后最直接的改造方向是融合目标跟踪。目前版本在每一帧都独立检测目标ID会变化距离信息也只是单帧的。如果要让机器人避障或者统计车流你需要知道同一个目标在一段时间内的运动轨迹和速度。我一般会在YOLOv5检测的基础上接一个简单的IOU匹配跟踪器为每个目标分配临时ID然后维护一个目标状态字典保存位置、深度、速度。这样距离输出就不再是孤立的点而是连续的时间序列。另一个值得改的点是立体匹配算法替换。SGBM在纹理稀疏区域表现一般如果场景是室内白墙或者光滑地板视差图会很难看。我试过把SGBM换成ELAS或者基于深度学习的光流网络精度提升明显但速度下降。如果算力够可以在关键帧上用高精度匹配非关键帧用SGBM然后做时间插值。还有一个实用技巧是输出三维坐标而不是只有距离。目前代码已经通过Q矩阵得到了X、Y、Z但没有进一步处理。比如你可以在鸟瞰图上绘制目标的平面位置或者把X、Y坐标投影到机器人坐标系直接生成运动指令。我在改造时添加了一个平面坐标映射函数把摄像头的俯仰角作为输入然后矫正地面上的目标位置。整个过程需要标定相机安装高度和俯仰角但一旦调好目标的世界坐标就很稳定。从那以后我每次拿到一个测距项目都会先强制自己用卷尺做一次静态标定验证确认2米、5米、10米三个距离点的误差都在5%以内接着再去优化速度和外观。这个习惯帮我避免了很多次凭空优化的弯路。希望帮到你。本文还有配套的精品资源点击获取