
简介一套基于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入口普通图片/视频检测入口不依赖 realsenseweights/yolov5s.pt权重3.1 版的官方预训练权重models/yolo.py模型定义3.1 版的 Darknet 类加载权重的核心utils/general.py预处理/后处理含 letterbox、non_max_suppressionrequirements.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 python3.8 -y conda activate realsense pip install torch1.7.1 torchvision0.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]这个 keyimport torch from models.yolo import Darknet model Darknet(cfgmodels/yolov5s.yaml, ch3, nc80).to(cuda:0) checkpoint torch.load(weights/yolov5s.pt, map_locationcuda: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加载后打印 keysckpt torch.load(xxx.pt, map_locationcpu) 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 流也有一个好处可以拿它对测距结果做交叉验证具体用法在下一章讲对齐时展开。1280x72030fps 是 D455 的标称规格如果工控机比较弱可以改成 640x48030帧率代价小一些但测距精度会跟着像素密度一起下降。get_intrinsics()返回的内参是关键fx 和 fy 是焦距的像素单位表示测距公式里直接用到 fx。注意分辨率变了fx 会变。同一个 D4551280x720 下的 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_thres0.35, iou_thres0.45): img letterbox(frame, new_shape416, autoFalse)[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_thresconf_thres, iou_thresiou_thres)letterbox把不等比缩放的图像填充到 416x416避免目标变形img.float() / 255.0做归一化model(img)[0]在 3.1 里返回的是一个 tuple第一个元素才是预测张量。non_max_suppression内部做背景过滤和框去重返回的每个检测是一个长度为 6 的张量x1, y1, x2, y2, 置信度, 类别索引。conf_thres0.35意味着置信度低于 0.35 的框全被丢弃这个值在人员检测场景可以适当放低到 0.25不然远处的小目标会丢iou_thres0.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%。所以这个表要按自己的场景改。类别默认高度(米)适用场景建议person1.70站立行人密集人群或骑行时要改成 1.5car1.50轿车越野车会偏大按车型调bus3.20客车城市公交接近truck3.00货车空载和满载高度差明显bicycle1.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_size5): buf deque(maxlenwindow_size) buf.append(new_dist) if len(buf) window_size: return new_dist return float(np.median(buf))window_size5时大约滞后 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 BottleneckCSP5.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, typefloat, default1025.0) parser.add_argument(--height-map, defaultperson:1.70,car:1.50) opt parser.parse_args()--focal-length默认值是当前分辨率下的实测值换分辨率、换摄像头都得重新标定。标定完成后做一轮验证让目标站在 2、4、6、10 米处各记录 10 帧输出距离的平均值和最大偏差偏差超过 15% 就回来检查高度表不要动焦距。从那以后我每次换摄像头、换分辨率、换安装高度第一件事都是强制走一遍这个标定流程——不花十分钟但省下来的是后面几天的现场调试。测距这种事模型再准几何参数没对齐照样是玄学。希望帮到你。本文还有配套的精品资源点击获取