☰
yolov5-3.1+D455单目测距实战:环境配置、代码解析与标定避坑
2026/9/28 5:51:17 网站建设 项目流程

简介:一套基于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 pyrealsense2

pyrealsense2是 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 distance

cls_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.5
car1.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_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% 就回来检查高度表,不要动焦距。

从那以后我每次换摄像头、换分辨率、换安装高度,第一件事都是强制走一遍这个标定流程——不花十分钟,但省下来的是后面几天的现场调试。测距这种事,模型再准,几何参数没对齐照样是玄学。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询