简介:一套基于YOLOv5与RealSense D455深度相机的单目测距系统源码,面向目标检测与距离估计需求的研究者、算法工程师或嵌入式视觉开发者,可满足实时目标定位与距离感知类项目的前期验证与开发需求。项目依托YOLOv5-3.1实现,运行主脚本即可对相机画面进行实时目标检测与单目测距;训练自定义模型时仍需使用3.1版本源码,YOLOv5-5.0及其以上版本训练的权重无法直接调用。压缩包共47个文件,以Python脚本、YAML模型配置、预训练权重、容器化配置、依赖清单及使用说明为主,总大小17.76MB,目录按推理、训练、输出、权重等模块划分,层次清晰。目前已有246人学习,内容涵盖检测推理入口、训练验证工具、模型定义与通用工具模块,以及超参进化脚本,并附示例图片和演示视频,便于完整把握单目测距框架与代码流程,适合直接参考或二次开发与实验复现。
1. 拿到的源码能干什么:yolov5-3.1 与 D455 组成的单目测距入口
一个装着 realsense-D455 的工控机,插上 USB 后跑一条python realsensedetect.py,屏幕上实时出现目标框,框底跟着一行数字——这是目标到相机的距离。这就是这份源码包在干的事:yolov5 负责检出画面里的目标,D455 负责把画面变成可计算的流,单目测距用一条小孔成像公式把像素高度换算成物理距离。它不是把 D455 的双目深度拿出来用,而是只吃一路 RGB 图像做几何估距,所以叫"单目测距"。
这套代码适合三类人:做毕设想快速演示检测+测距的学生,在仓库、施工区做人员距离提醒的现场工程师,以及想在自己数据集上跑通"检测→测距"完整链路但不想从零写的开发者。需要先接受一个前提:这份源码是 yolov5-3.1 版本实现,训练自己模型也得用 3.1 源码,5.0 及以上版本训练出的权重不能直接调用,后面我会专门讲这个问题。下面从环境开始,逐步把这套系统拆开。
2. 环境与权重兼容性:先搞懂 3.1 版的依赖边界,再动手
2.1 解压出来的目录,先分清哪些是运行时真正要用的
解压后目录里文件不少,但真正参与realsensedetect.py运行的只有一条链:入口文件realsensedetect.py调用models/下的模型定义,再调用utils/下的预处理和后处理工具,最后加载weights/yolov5s.pt权重。其他像train.py、test.py、evolve.sh属于训练和调参辅助,跑推理时不碰。
| 文件/目录 | 环节 | 说明 |
|---|---|---|
| realsensedetect.py | 入口 | 主运行脚本,D455 取流 + yolov5 推理 + 测距 |
| newdetect.py | 入口 | 普通图片/视频检测入口,不依赖 realsense |
| weights/yolov5s.pt | 权重 | 3.1 版的官方预训练权重 |
| models/yolo.py | 模型定义 | 3.1 版的 Darknet 类,加载权重的核心 |
| utils/general.py | 预处理/后处理 | 含 letterbox、non_max_suppression |
| requirements.txt | 依赖 | 3.1 版的 Python 包清单 |
| 0210901110009.jpg | 测试图 | 可以先拿它验证 newdetect.py 通路 |
| detect.py / train.py | 训练/推理辅助 | 改数据集训练时会用到 train.py |
我一般拿到压缩包第一件事不是装环境,而是先看utils/general.py里有没有letterbox和non_max_suppression,这两个函数决定推理链路能不能走通。3.1 版和 5.0 版这两个函数的实现差异很大,如果你把 5.0 的utils/混进来,光预处理就会出现try: import ... except ImportError还不报错、运行到一半才崩的情况。
2.2 conda 环境与依赖安装:把 torch 锁在 1.7.x 这条线上
yolov5-3.1 发布时的 torch 生态对应 1.5 到 1.7 这一段。我用的是 Python 3.8 + torch 1.7.1 + torchvision 0.8.2 的组合,跑下来最省事。torch 1.9 以上会出现nn.Upsample的align_corners参数问题,torch 2.x 更是一上来就报_remove_zero_regions之类的算子缺失。先建一个干净的 conda 环境:
conda create -n realsense python=3.8 -y conda activate realsense pip install torch==1.7.1 torchvision==0.8.2 PyYAML tqdm opencv-python scipy matplotlib pillow requests pip install pyrealsense2pyrealsense2是 Intel 官方提供的 Python 包,和 yolov5 的 requirements 无关,必须单独装。装完验证一下:python -c "import pyrealsense2 as rs; print(rs.__version__)",能打出版本号说明 SDK 没问题。再验证 torch 和 CUDA 是否对上:
python -c "import torch; print(torch.__version__, torch.cuda.is_available())"如果输出True说明 CUDA 可用;False也能跑,D455 取流和 CPU 推理不冲突,只是检测帧率会从三十多帧掉到十帧左右。requirements.txt 里还列了tabulate、seaborn这些训练可视化用的包,推理阶段可以不装,但跑train.py时会用到,建议一次性装齐。
2.3 权重兼容红线:为什么 3.1 源码不能加载 5.0 训练出的权重
这是摘要里特别强调的一点,也是最多人翻车的地方。yolov5 版本迭代期间,模型结构一直在变:3.1 的models/yolov5s.yaml定义的是 Focus + BottleneckCSP 结构,5.0 之后改成了 C3 模块,6.0 还在输出头的 anchor 分支上做了调整。权重文件和结构是一一绑定的,把 5.0 训练的.pt塞进 3.1 源码,torch.load能读出来,但model.load_state_dict()会直接报size mismatch,报错信息里能看到param #1 mismatch定位到是哪个层不匹配。
3.1 源码加载权重的写法是这样的,注意看用的是['model']这个 key:
import torch from models.yolo import Darknet model = Darknet(cfg='models/yolov5s.yaml', ch=3, nc=80).to('cuda:0') checkpoint = torch.load('weights/yolov5s.pt', map_location='cuda:0') model.load_state_dict(checkpoint['model'].float().state_dict()) model.eval()Darknet是 3.1 版的模型类,cfg 指向 yaml 结构文件,nc 对应类别数。如果你想训练自己的数据集,必须用这个 3.1 源码包自带的train.py,而不是去 clone 新版仓库。命令是:
python train.py --data your_data.yaml --cfg models/yolov5s.yaml --weights weights/yolov5s.pt --epochs 100 --batch-size 16--weights填官方 3.1 的yolov5s.pt做预训练,train.py会在训练完成后把权重存到runs/目录,那个权重文件才能拿回realsensedetect.py用。判断手里的权重是哪个版本,有个土办法:把.pt加载后打印 keys:
ckpt = torch.load('xxx.pt', map_location='cpu') print(ckpt.keys()) print(ckpt['model'].state_dict() if 'model' in ckpt else 'no model key')3.1 的权重顶层有'model'、'optimizer'、'epoch'这些 key,模型文件里能搜到model.12.cv2.conv.weight这种带编号的层名。5.0 之后权重文件结构更复杂,出现model.0.conv.weight以外还带anchors前缀的索引结构,看到这种情况就别往 3.1 里塞了。
3. 完整运行链路:realsensedetect.py 从取流到测距是怎么串起来的
3.1 先看 D455 取流:pyrealsense2 的管线初始化
主脚本最开始的一段一定是初始化 realsense pipeline。D455 是 Intel 的主动立体深度相机,但这里只把它当普通 RGB 摄像头用。初始化代码的常见写法是这样:
import pyrealsense2 as rs pipeline = rs.pipeline() config = rs.config() config.enable_stream(rs.stream.color, 1280, 720, rs.format.bgr8, 30) config.enable_stream(rs.stream.depth, 1280, 720, rs.format.z16, 30) profile = pipeline.start(config) color_profile = profile.get_stream(rs.stream.color) intrinsics = color_profile.as_video_stream_profile().get_intrinsics() print(intrinsics.fx, intrinsics.fy, intrinsics.ppx, intrinsics.ppy)这里同时开启了 color 和 depth 两路流。即便做纯单目测距,开着 depth 流也有一个好处:可以拿它对测距结果做交叉验证,具体用法在下一章讲对齐时展开。1280x720@30fps 是 D455 的标称规格,如果工控机比较弱,可以改成 640x480@30,帧率代价小一些,但测距精度会跟着像素密度一起下降。
get_intrinsics()返回的内参是关键,fx 和 fy 是焦距的像素单位表示,测距公式里直接用到 fx。注意:分辨率变了,fx 会变。同一个 D455,1280x720 下的 fx 大概是 640x480 下的两倍,所以内参必须在当前分辨率下重新读取,不能从网上抄一个固定值写死。
3.2 检测与后处理:框怎么来,置信度怎么过滤
取到一帧 BGR 图像后,送入 yolov5-3.1 的推理链路。3.1 版还没有后来的torch.hub.load('ultralytics/yolov5')那种友好接口,推理要自己走预处理、推理、NMS 三步。核心代码:
import torch import numpy as np from utils.general import letterbox, non_max_suppression def yolo_infer(model, frame, device, conf_thres=0.35, iou_thres=0.45): img = letterbox(frame, new_shape=416, auto=False)[0] img = img[:, :, ::-1].transpose(2, 0, 1) img = np.ascontiguousarray(img) img = torch.from_numpy(img).to(device) img = img.float() / 255.0 if img.ndimension() == 3: img = img.unsqueeze(0) with torch.no_grad(): pred = model(img)[0] return non_max_suppression(pred, conf_thres=conf_thres, iou_thres=iou_thres)letterbox把不等比缩放的图像填充到 416x416,避免目标变形;img.float() / 255.0做归一化;model(img)[0]在 3.1 里返回的是一个 tuple,第一个元素才是预测张量。non_max_suppression内部做背景过滤和框去重,返回的每个检测是一个长度为 6 的张量:x1, y1, x2, y2, 置信度, 类别索引。
conf_thres=0.35意味着置信度低于 0.35 的框全被丢弃,这个值在人员检测场景可以适当放低到 0.25,不然远处的小目标会丢;iou_thres=0.45控制重叠框的合并力度,目标挨得近时调高到 0.5 会少吞框。
3.3 测距核心:像素高度换算物理距离的参数逻辑
拿到目标框(x1, y1, x2, y2)后,测距就一条公式:物理距离 = (焦距 fx × 目标实际高度) / 目标像素高度。像素高度取y2 - y1,也就是框的竖直跨度。实际高度需要按类别给一个先验值,代码里维护一张表:
def pixel_to_distance(intrinsics, box, cls_name, cls_height_map): fx = intrinsics.fx x1, y1, x2, y2, conf, cls = box pixel_h = y2 - y1 if pixel_h < 5: return -1.0 real_h = cls_height_map.get(cls_name, 1.50) distance = (fx * real_h) / pixel_h return distancecls_height_map里存的是每个类别在现实中的典型高度,比如person: 1.70、car: 1.50、bus: 3.20。注意几个边界:像素高度小于 5 像素的目标别算,数值炸上天;距离单位是米,real_h也要以米为单位;这个公式假设目标完整出现在画面里且框底贴着目标底部。
代码里的逻辑说明已经比较直白,我再补充一个工程参数:pixel_h用的是框底部到顶部的整段高度,如果目标下半身被遮挡(比如隔着桌子只露出上半身),pixel_h只有真实的一半,算出的距离会翻倍。这是单目测距最典型的误差来源,第 5 章避坑部分会专门处理。
4. 距离准不准的关键:高度先验、帧对齐与滤波的取舍
4.1 高度先验表:单目几何的唯一发力点
单目测距的物理模型是刚体投影,一个目标在画面里占多少像素,由它离相机多远、以及目标实际多大共同决定。公式里real_h是唯一我们能主动给的外部已知量,它的误差会线性传递到距离结果上:real_h给成 1.8 米但实际人是 1.7 米,距离直接偏大 5.8%。所以这个表要按自己的场景改。
| 类别 | 默认高度(米) | 适用场景 | 建议 |
|---|---|---|---|
| person | 1.70 | 站立行人 | 密集人群或骑行时要改成 1.5 |
| car | 1.50 | 轿车 | 越野车会偏大,按车型调 |
| bus | 3.20 | 客车 | 城市公交接近 |
| truck | 3.00 | 货车 | 空载和满载高度差明显 |
| bicycle | 1.80 | 骑行目标 | 算的是人+车整体高度 |
这个表是"先验",先验错了再好的模型也救不回来。我一般在现场会测一组真实目标高度,按目标类别取中位数写进字典。如果场景里出现图中没有的类,cls_height_map.get(cls_name, 1.50)的兜底值 1.5 米至少不会让系统崩,但精度就别指望了。
另一个容易忽略的点:像素高度y2 - y1是目标框的总高度,但目标检测框往往比目标真实轮廓大一圈,尤其带 padding 的 anchor 设计。距离远时框多 5 个像素,距离近时这 5 个像素占比很小,距离远时占比变大,所以远处测距结果系统性偏近。校准办法是后面第 6 章讲的现场标定。
4.2 要不要用深度帧:对齐带来的二次选择
D455 本身有双目深度流,但标题既然是"单目测距",说明这条路主要靠 RGB 几何。不过我建议在realsensedetect.py里保留 depth 流并做对齐,理由只有一个:用深度值校正几何估距的跳变,比纯单目可靠得多。对齐代码是 realsense 的标准写法:
align = rs.align(rs.stream.color) frames = pipeline.wait_for_frames() aligned_frames = align.process(frames) color_frame = aligned_frames.get_color_frame() depth_frame = aligned_frames.get_depth_frame()rs.align(rs.stream.color)的意思是把深度图重投影到彩色相机的坐标系下,这样彩色图上像素(u, v)和深度图上同一个(u, v)指的是空间同一点。D455 的彩色镜头和两个红外镜头装在一条线上但位置不同,不做 align 直接读深度,会差出十几个像素的偏移,距离越近偏移越大。
有了对齐的深度图,我们可以加一条校正逻辑:在目标框底部中心点取深度值depth_frame.get_distance(u, v),如果这个值和几何估距相差在 30% 以内,就取两者的加权平均;相差太大说明几何假设被破坏,用深度值兜底。这样既能保留"单目测距"的技术路线,又能避免人被截断时几何公式完全失效。
4.3 距离波动:中值滤波与置信度联动
没滤波的测距曲线长什么样?目标站在 5 米外不动,每一帧算出的距离在 4.7 到 5.3 米之间跳,原因是检测框本身在抖动,帧与帧之间 y2 差三五个像素,反映到距离上就是几十厘米的波动。D455 输出的深度在静态场景下比较稳定,几何估距的波动完全来自框的抖动。
常用的处理是滑窗中值滤波:
from collections import deque def distance_filter(new_dist, window_size=5): buf = deque(maxlen=window_size) buf.append(new_dist) if len(buf) < window_size: return new_dist return float(np.median(buf))window_size=5时大约滞后 3 帧,对缓慢移动的目标影响不大;目标快速靠近时建议把窗口缩到 3,不然距离曲线会拖尾,报出比真实值更远的距离。还有一种做法是置信度加权:置信度高的帧给更高权重,因为置信度高通常意味着目标完整、遮挡少,测距更接近真实值。
另外,滤波只对静态或慢速目标有效,如果目标在画面里横向快速移动,框高度本身在变,中值滤波会把真实变化抹掉。我见过有人把 window 调到 30,目标走过去了距离还停在原地,这就是滤波参数和场景不匹配的典型翻车。滤波的核心原则是:窗口越小越跟手,越大越平滑,按你的使用场景选,不要迷信固定值。
5. 避坑记录:我在这个源码包上踩过的五个问题
5.1 权重/模块版本错配类
现象一:运行realsensedetect.py报size mismatch或Some weights of the model checkpoint were not used。原因:往 3.1 源码里加载了 yolov5-5.0 以上版本训练的权重。3.1 的Darknet结构里是 Focus + BottleneckCSP,5.0 之后是 C3 模块,层的名字和数量都对不上。解决:换回 3.1 源码包里的yolov5s.pt,或用自己的 3.1train.py重新训练。这个报错最好认,因为 torch 会直接打出哪一层不匹配,看到param #6 mismatch就基本实锤了。
现象二:torch 版本是 2.x,运行时报_remove_zero_regions()或Upsample相关的算子错误。原因:yolov5-3.1 是 2020 年的代码,很多实现依赖旧版 torch API。解决:别升级 torch,用第 2 章的 conda 配方建环境,torch 锁在 1.7.1。这个坑防不胜防,因为 realsense 包的安装不会管 torch 版本,装完 torch 2.x 再跑老代码,报错信息五花八门。
5.2 摄像头与 USB 类
现象三:D455 插上后pipeline.start(config)卡死或报DeviceNotFound/USB descriptor error。原因:D455 要求 USB 3.0 以上的接口带宽,插在 USB 2.0 口上要么识别不了要么取流卡死。解决:换主板后置 USB 口,用 D455 自带的原装线,不要用延长线。如果是虚拟机里跑,注意把 USB 设备直连给虚拟机,否则通过 USB 2.0 桥接设备会直接掉链子。判断是否 USB 3.0:装好 pyrealsense2 后打印device.get_usb_speed(),返回"usb3.0"才及格。
5.3 测量误差类
现象四:目标距离真实 3 米,实测输出 5 米左右,且越近误差越大。原因:目标下半身被遮挡,检测框只框住了上半身,y2 - y1比真实投影高度小了一大截。解决:在代码里加完整性判断,当框底接近画面边缘时标记"可能截断",此时改用深度帧get_distance()校正。如果不想引入深度帧,就得换个安装位置把视野压低,让行人全身进入画面。
现象五:同一目标停在不同位置,测距结果和真实距离不成线性关系,远处偏近,近处偏远。原因:两个因素叠加——检测框的 padding 在远处占比更大,以及real_h先验值不准。解决:先现场标定一次焦距,把内参和高度表修正后再看曲线。具体标定流程见第 6 章,这一步做完,误差能收敛到 10% 以内。
6. 现场标定焦距:用已知距离反推的验证方法
拿到源码先别急着把摄像头装上去调距离,先做一次焦距标定,这是我这套流程里回报最高的一步。找一个身高已知的人或纸箱,放在一个用卷尺量好的距离上,比如 8.0 米处,跑一下检测,记录此时这个目标的像素高度pixel_h。焦距公式是焦距 = 真实距离 × 像素高度 / 实际高度:
def calibrate_focal(distance_m, real_height_m, pixel_height_px): return (distance_m * pixel_height_px) / real_height_m举一组实测数据:一个 1.70 米的标定板站在 8.0 米处,检测出的框高是 218 像素,套进公式得到焦距约(8.0 * 218) / 1.70 = 1025像素。把这个值和第 3 章intrinsics.fx打印的值对比,如果差 5% 以上,说明高度先验或检测框偏移有问题,得先查框。标定时分别在 3 米、5 米、8 米三组距离各取 20 帧,算焦距均值,能消掉单次框抖动的影响。
标定完把数值固化成启动参数:
import argparse parser = argparse.ArgumentParser() parser.add_argument('--focal-length', type=float, default=1025.0) parser.add_argument('--height-map', default='person:1.70,car:1.50') opt = parser.parse_args()--focal-length默认值是当前分辨率下的实测值,换分辨率、换摄像头都得重新标定。标定完成后做一轮验证:让目标站在 2、4、6、10 米处,各记录 10 帧输出距离的平均值和最大偏差,偏差超过 15% 就回来检查高度表,不要动焦距。
从那以后我每次换摄像头、换分辨率、换安装高度,第一件事都是强制走一遍这个标定流程——不花十分钟,但省下来的是后面几天的现场调试。测距这种事,模型再准,几何参数没对齐照样是玄学。希望帮到你。
本文还有配套的精品资源,点击获取