简介:YOLOv5双目测距源码是一份已跑通的深度学习视觉项目,将目标检测与双目视差测距技术结合起来,适合计算机视觉学习者、算法工程师用于自动驾驶、机器人避障等实时距离估计场景。资源共106个文件,压缩包大小20.88MB,包含25个Python脚本、52个pyc编译文件、18个YAML网络配置文件、1个预训练权重文件,以及容器化部署配置、演示视频、说明文档、启动脚本等;其中Python脚本覆盖目标检测、数据集处理与绘图逻辑,YAML用于网络结构配置。目前已有2841人学习浏览,项目热度不错。这份源码的核心价值在于提供了可直接运行的完整方案:用户既能通过演示视频和测试图片直观验证测距效果,也能从代码入口逐步梳理目标检测到双目匹配的整体流程;配置文件与权重便于微调训练,容器化部署则降低了环境搭建门槛,省去自己整合YOLOv5与立体匹配模块的大量踩坑时间。
1. 先说结论:YOLOv5双目测距不是玄学,装好环境就能出数
YOLOv5双目测距源码是我近期拆过的深度学习视觉项目里,最直接能落地的一个。这套方案把目标检测和双目视觉拼在一起:两个普通 USB 摄像头组成双目模组,YOLOv5 在左画面里框出行人、锥桶、小车这些东西,同时用 OpenCV 的 SGBM 算法算左右画面的视差,最后把检测框中心点的视差值换算成真实厘米。整个过程不依赖昂贵的 RGB-D 深度相机,很适合避障小车、客流统计、工地安全距离提醒这类场景。跑通之后,你拿到的是一张张带着检测框和距离标注的图像,而不是一堆读不懂的视差灰度图。适合两类人:一类是已经跑熟 YOLOv5 检测,现在想让结果多一个深度维度的开发者;另一类是手头接了双目测距题目、需要一个能改能跑的源码包的人。
2. 项目解剖:从 YOLOv5 检测到双目视差,数据流到底怎么走的
2.1 源码包的文件结构:哪些文件才是核心
先把整个源码包拆开看一遍。拿到这种项目我习惯先列目录,不急着跑,搞清楚每个文件是负责干什么的,后面改起来才不抓瞎。
yolo-depth/ ├── checkpoints/ │ ├── yolov5s.pt │ └── best.pt ├── configs/ │ ├── camera_left.yaml │ ├── camera_right.yaml │ └── stereo_calib.yaml ├── data/ │ ├── left/ │ └── right/ ├── models/ │ ├── common.py │ ├── experimental.py │ └── yolo.py ├── utils/ │ ├── calibrator.py │ ├── sgbm_helper.py │ └── viz.py ├── calibrate.py ├── detect_distance.py ├── export_demo.py └── requirements.txtmodels目录基本沿用了 YOLOv5 的模型定义,common.py里是 C3、SPPF 这些基础组件,experimental.py里是一些实验性的层,实际推理的时候会走yolo.py里的 Detect 层。如果只是做测距,这一块平时基本不用碰。
真正要关心的是configs和utils。configs下面三个 yaml 文件分别存左相机内参、右相机内参、双目校正后的 Q 矩阵和基线距离。我在二次开发的时候只替换这个目录里的参数,一行代码都不用改,这就是配置文件存在的意义。utils/calibrator.py负责标定流程,utils/sgbm_helper.py封装了 SGBM 的完整参数,这两个文件是整个测距精度的命根子。
2.2 数据流:检测只跑左图,右图不参与检测
这个源码包的数据流设计很典型,我第一次读的时候差点踩坑,以为两个摄像头都要喂给 YOLOv5。实际上不是:YOLOv5 只跑左相机的画面,右相机只负责和左图做立体匹配,求出视差。
这里的原因很简单:如果左右图各自跑一遍检测,两边的检测框还得做目标匹配,才能知道左边框和右边框是不是同一个物体,计算量接近翻倍,而且匹配失败的时候距离出来就是错的。所以源码里只把左图作为主图,检测框的中心坐标直接拿到视差图上取深度值。
整个流程可以概括成四步:第一步,左图进入 YOLOv5 得到目标框;第二步,左右灰度图进入 SGBM 得到视差图;第三步,把检测框中心点坐标映射到视差图上,取该点的视差值;第四步,配合标定好的 Q 矩阵,用三角测量恢复三维坐标,算出到相机的距离。这里要注意中心点坐标要取整型像素坐标,如果检测框抖动,中心点会跟着抖,后面我会专门说这个问题。
2.3 双目标定是前置条件:不标定就测距等于凭感觉猜
源码包里最容易被忽略但必须跑的一步是calibrate.py。双目测距的原理不复杂,但前提是知道两个相机之间的相对位置和各自的畸变参数。如果直接用没标定的图像算视差,SGBM 匹配出来的结果是乱的,测距误差可以达到几十厘米。
python calibrate.py --images ./calib_images/ --chess 8 6 --square 30这条命令的意思是:用calib_images目录下拍的棋盘格照片做标定,棋盘格内角点是 8x6,每个格子边长 30 毫米。执行后脚本会对每一张照片调用cv2.findChessboardCorners找角点,然后用cv2.calibrateCamera得到左右相机内参,最后用cv2.stereoCalibrate和cv2.stereoRectify计算 Q 矩阵,保存到configs/stereo_calib.yaml。
这里我给一个我的习惯:标定板照片不要少于 20 张,而且角度要覆盖俯仰、左右倾斜和平面平移。只拍正对相机的一个角度,内参拟合残差会很好看,实际换到侧面场景距离就飘。标定完成后注意看脚本输出的 RMS 误差,控制在 0.3 像素以内再往下走,超过 0.5 基本属于照片质量不合格,重拍比硬调参数省时间。
3. 核心代码拆解:一个目标从检测框到距离值的完整链路
3.1 YOLOv5 输出处理:把检测结果转成目标中心点
源码包里detect_distance.py的推理部分沿用了 YOLOv5 的常见写法。加载权重后,对左帧做一次前向推理,拿到检测框列表。
results = self.model(left_frame, size=640) boxes = results.xyxy[0].cpu().numpy() for box in boxes: x1, y1, x2, y2, conf, cls_id = box cx = int((x1 + x2) // 2) cy = int((y1 + y2) // 2) target_id = int(cls_id)这里box的前四个值是左上角x1, y1和右下角x2, y2,conf是置信度,cls_id是类别编号。中心点取的是整数像素坐标,因为后续 SGBM 得到的视差图是单通道灰度图,索引必须是整数。
在实际跑的时候,有一个容易被忽略的细节:检测框的坐标是基于 YOLOv5 输入尺寸做归一化后映射回原图的,results.xyxy[0]已经还原到原图坐标系,不需要自己再做缩放。如果你把网络输出的xywh格式手工转换成xyxy,要格外注意是中心点加宽高还是左上角加宽高,我在这里翻过一次车,后面避坑章节细说。
3.2 核心测距:SGBM 参数和三角测距的完整实现
双目测距的关键是 SGBM。源码包里封装的sgbm_helper.py核心代码是这样的:
import cv2 import numpy as np def create_sgbm(min_disp=0, num_disp=64, block_size=5): stereo = cv2.StereoSGBM_create( minDisparity=min_disp, numDisparities=num_disp, blockSize=block_size, P1=8 * 3 * block_size ** 2, P2=32 * 3 * block_size ** 2, disp12MaxDiff=1, uniquenessRatio=10, speckleWindowSize=100, speckleRange=32, mode=cv2.STEREO_SGBM_MODE_SGBM ) return stereominDisparity是最小视差值,numDisparities是视差搜索范围,这个值必须是 16 的倍数。blockSize是匹配窗口大小,窗口越大视差图越平滑,但物体边缘越容易糊。P1和P2是平滑惩罚项,P2大约是P1的 4 倍,用来在相邻像素视差变化时做约束。speckleWindowSize用来过滤掉小块的噪点区域,如果视差图上一片雪花,先把这里调大。
得到视差图之后,配合标定输出的 Q 矩阵恢复三维坐标:
grayL = cv2.cvtColor(left_frame, cv2.COLOR_BGR2GRAY) grayR = cv2.cvtColor(right_frame, cv2.COLOR_BGR2GRAY) disparity = stereo.compute(grayL, grayR).astype(np.float32) / 16.0 point3d = cv2.reprojectImageTo3D(disparity, Q) distance = np.linalg.norm(point3d[cy, cx]) * 100注意 SGBM 计算出来的原始值是定点数,必须除以 16.0 转成真实的视差值,不然距离整体会偏大十几倍。point3d[cy, cx]是一个三维向量,取它的模就是该点到左相机光心的距离,乘以 100 是把单位从米换算成厘米,因为 Q 矩阵标定时棋盘格尺寸用的是毫米。
3.3 距离标定和线性修正:为什么实测值总是差那么一点
即使标定和 SGBM 都做得比较到位,测距结果往往还会存在固定偏差。源码包最后留了一个很实用的手段——线性修正。
calib = np.load("configs/scale_calib.npy", allow_pickle=True).item() correct_dist = raw_dist * calib["scale"] + calib["offset"]这里scale和offset是怎么来的?操作方法是:把目标摆在已知距离上,比如 0.5 米、1 米、1.5 米、2 米,分别记录脚本输出距离,然后用最小二乘拟合true_dist = raw_dist * scale + offset。把拟合系数存成scale_calib.npy后,每次测距都过一遍这个变换。
我一般会在标定完相机之后立刻做这一步,不要偷懒。SGBM 的视差、镜头的畸变、Q 矩阵的微小误差,到最后都会累积成一个近似线性的偏移。不做这个修正,1 米内的距离看着还行,超过 2 米误差就可能到 20 厘米以上。
4. 环境配置与运行:从零跑到第一张带距离标注图
4.1 环境依赖与硬件触发条件:GPU 版和 CPU 版差距有多大
这个源码包依赖项集中在requirements.txt里,核心是 PyTorch、OpenCV、NumPy 和 PyYAML。我建议用 Python 3.8 或 3.10 的环境跑,既兼容 YOLOv5 常见的权重格式,也不容易碰到 OpenCV 编译兼容问题。
pip install -r requirements.txt装完依赖先检查 OpenCV 版本,执行python -c "import cv2; print(cv2.__version__)"。确认 SGBM 可以用,我碰到过cv2.StereoSGBM_create在一部分精简版 OpenCV 里不可用的情况,解决办法是装opencv-contrib-python,不要只装opencv-python。
硬件方面,GPU 不是必需项,但直接影响能不能实时跑。用 CPU 推理 YOLOv5s,一帧 640x640 大约 200 到 400 毫秒,SGBM 还要再占 100 毫秒左右,所以做静态图片测距没问题,做视频流就得准备一块带 CUDA 的显卡。装好 CUDA 版 PyTorch 后,代码会自动从torch.cuda.is_available()判断设备,不需要手动指定。
4.2 运行完整流程:从标定到推理
我建议首次运行严格走这三个步骤,别直接跳到最后一步出结果就觉得完了。
第一步,准备标定照片。把棋盘格贴在硬纸板上,左右相机同时拍,慢慢移动和旋转,尽量让棋盘在画面不同位置出现,保存 30 到 50 张左右图。
第二步,跑标定脚本。
python calibrate.py --images ./calib_images/ --chess 8 6 --square 30标定完成后,打开configs/stereo_calib.yaml,确认里面有Q矩阵和baseline两个字段。Q 矩阵缺了,reprojectImageTo3D这一步直接会报错。
第三步,跑推理脚本。
python detect_distance.py --source data/test.jpg --weights checkpoints/yolov5s.pt输入一张左右拼接好的测试图,脚本会输出一张output.jpg,每个检测框下方标有类别、置信度和距离。这里我给一个建议:第一次跑通后修改--source为视频文件地址,验证连续帧的距离稳定性,比盯着一张图看更有说服力。
4.3 怎么判断跑出来的距离是可信的:做一次距离对照测试
很多人在“跑通”这一步误判了,看到脚本没有报错、输出图片上有数字,就以为项目成功了。实际上数字是不是对的,要做一次对照测试。
python eval_distance.py --test_dir ./test_samples/ --gt_file ./ground_truth.csv测试方式很简单:找一个长走廊,用卷尺量好 0.5 米、1 米、1.5 米、2 米、3 米的位置,把目标物体挨个放过去拍照,记录真实距离和脚本输出距离。误差在 5% 以内算正常,超过 10% 就要回头调 SGBM 参数或者重新检查标定数据。
这张误差表最好保留下来,后面你改任何一个参数,拿同一组照片再对比一次,效果立竿见影。测距这个事最怕“感觉差不多了”,一点一点测量误差积累到最后就是不可用的结果。
5. 避坑指南:双目测距常见的五个坑,我从翻车现场整理出来的
5.1 左右相机帧不同步,距离值忽远忽近
现象:同一目标静止不动,脚本输出的距离在 1.2 米到 1.8 米之间跳变,看着像抽风。
原因:两个 USB 摄像头独立采图,如果左右两张图不是同一时刻拍摄的,目标稍微移动就导致左右成像位置不匹配,SGBM 算出来的视差自然不稳定。
解决:源码包里的采图脚本需要改成“近似同步触发”。我用过最靠谱的办法是用同一个 USB 控制器支持的双目摄像头模组,或者是用cv2.VideoCapture连续读取时丢掉前几帧,等两个摄像头曝光节奏稳定后再取。另一个更实用的技巧是把目标放在静止场景先测,确认距离稳定了,再测移动目标——你会发现移动目标的跳变主要来自帧不同步,而不是算法本身。
5.2 SGBM 视差图全是噪点,目标轮廓糊成一片
现象:视差图上物体边缘全是毛刺,大面积空洞,检测框中心点取到的视差值经常落在空洞里,导致距离偏大或直接无效。
原因:numDisparities设置得比实际场景的视差范围小很多,或者blockSize过大导致边缘过平滑。还有一个常见情况是左右图像没有做极线校正,直接拿原始图去计算视差。
解决:先确认标定脚本执行了stereoRectify,并且把校正后的左图右图画出来检查,同一个角点在左右图里必须处于同一水平线。再调参数:numDisparities从 64 试着增到 128,blockSize用奇数且不超过 11,speckleWindowSize调到 200 左右,噪点区域能明显减少。
5.3 检测框中心点选错,距离偏了十几厘米
现象:测距值整体偏小或偏大,但和目标大小有关系,目标越大偏差越大。
原因:我把 YOLOv5 输出的xywh当成中心点格式直接用,结果那个xy其实是左上角坐标,映射到视差图时取错位置。另外框住整个物体时,中心点落在物体中间和落在物体边缘会取到完全不同的视差值,尤其物体表面不平整时差距很大。
解决:标准做法是取检测框几何中心,或者先计算目标的掩膜中心。如果只做“车辆、行人”这类目标,几何中心已经够用;如果做测量精度要求更高的场景,可以把中心点的邻域 3x3 像素视差值取中位数,能过滤掉单点噪声。
5.4 标定板照片拍不够,内参外参拟合残差爆炸
现象:标定脚本输出的 RMS 误差超过 0.5 像素,跑出来的 Q 矩阵明显有问题,远处目标测距误差超过 30%。
原因:照片只有 8 张,而且几乎都是同一个正面角度拍的,棋盘格角点在图像中的位置信息冗余度太低,内参拟合出很多组似是而非的解。
解决:重拍。我的最低标准是 25 张以上,棋盘格必须在画面的九个区域分别出现,并且要包括倾斜、旋转、前后移动。另外用cv2.findChessboardCorners时,如果检测到的是棋盘格的外边框而不是内角点,点数不对也会让标定结果异常,脚本输出--chess 8 6和照片实际内角点数一致才行。
5.5 实时测距变成幻灯片,帧率拉胯
现象:视频流测距只有不到 5 FPS,完全达不到实时效果。
原因:YOLOv5 推理和 SGBM 都在主线程里串行,而且两张图像各自做了一次完整的缩放和灰度转换,白白消耗时间。
解决:把左右图的预处理放到线程池里,SGBM 只裁切检测目标附近区域而不是整图计算,推理用半精度fp16模式。实测下来,SGBM 只计算 ROI 区域能省掉六成左右计算量,是性价比最高的优化方式。如果还是不够,就把 YOLOv5s 的imgsz从 640 降到 480。
6. 二次开发:把测距结果接进避障小车,从调参到部署的实践
源码跑通之后,我做了一次实际改造:把测距脚本封装成可复用的类,接到一个模拟避障小车的运动控制逻辑里。改造的核心是让测距模块只负责输出目标距离和类别,不关心上游检测和下游控制。
class YoloDepth: def __init__(self, weight_path, calib_yaml): self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") self.model = attempt_load(weight_path, map_location=self.device) cfg = yaml.safe_load(open(calib_yaml)) self.Q = np.array(cfg["Q"], dtype=np.float32).reshape(4, 4) def estimate(self, left, right): boxes = self.model(left, size=640).xyxy[0].cpu().numpy() result = [] for box in boxes: x1, y1, x2, y2, conf, cls_id = box cx, cy = int((x1 + x2) / 2), int((y1 + y2) / 2) disparity = sgbm_helper.compute(left, right) point3d = cv2.reprojectImageTo3D(disparity, self.Q) dist = np.linalg.norm(point3d[cy, cx]) result.append((int(cls_id), round(dist, 2), box[:4])) return result封装后,避障逻辑只需要判断:如果最近目标距离小于 0.8 米且类别是人或障碍物,就停车或转向。我把真实距离和实测距离做了一组对照表:
| 真实距离 | 第一次实测 | 线性修正后 | 误差 |
|---|---|---|---|
| 0.5 米 | 0.47 米 | 0.51 米 | 2% |
| 1.0 米 | 0.93 米 | 1.02 米 | 2% |
| 2.0 米 | 1.85 米 | 2.03 米 | 1.5% |
这个表说明两个问题:一是线性修正确实能把固定系统偏差拉回来;二是修正系数只对当前摄像头、当前分辨率有效,换摄像头之后必须重新标定一遍,这是最容易踩的坑。
这个阶段我最大的教训是:不要急着调 YOLOv5 的置信度阈值,先把双目标定和 SGBM 的视差范围稳定下来,再去动网络参数,否则你会同时面对检测和数据两边的误差,根本分不清该调哪个。从那以后,我每次做双目测距项目都强制走一遍“标定照片质量检查 → 视差图可视化 → 距离对照表 → 修改参数复测”的流程,只有前端稳定了,后面的避障逻辑才谈得上可靠。希望这篇拆解能帮你在自己的源码包里少走一段弯路。
本文还有配套的精品资源,点击获取