简介:一套基于YOLOv5与英特尔RealSense深度相机的目标检测工程,面向计算机视觉、人工智能方向的开发者和学习者,用于解决常规二维检测缺乏空间信息的问题。工程在识别目标的同时,会输出目标anchor中心的像素坐标与深度值,可应用于机器人避障、增强现实、智能监控等需要三维感知的实战场景,尤其在移动机器人领域具有较高实用价值。压缩包共84个文件,以Python脚本(27个)、YAML模型配置(25个)和预训练权重为核心,并包含依赖清单、说明文档、PyInstaller编译缓存及工程配置文件,整体约13.23MB;目录按照模型定义、工具函数、配置参数等模块划分,方便直接定位和二次开发。目前已有6199人学习下载,适合希望从算法原理走向工程实现的读者,也适合作为课程设计或竞赛项目的参考起点。工程内置多种YOLOv5规格(如n/s/m/l/x)的yaml配置和对应权重,并提供推理脚本,演示如何将RealSense深度图与检测结果对齐,从而准确获取目标中心点的距离信息,同时可根据实际相机型号调整输入参数,灵活性较高。
1. 用深度相机补上YOLOv5缺失的那一维
第一次用 Intel Realsense D435i 跑 YOLOv5 目标检测时,我以为把两个官方示例拼起来就行。实际上彩色图和深度图来自两个传感器,像素坐标不经过对齐就采样深度,得到的数据全是偏的。标题里的“标注目标物和目标anchor中心的像素坐标和深度信息”这句话,本质就是解决 2D 检测框如何从深度相机那里精确取到距离的问题。
这份 yolov5_detection 资源自带完整 YOLOv5 模型结构,models/common.py 和 models/yolo.py 都在,权重放的是 yolov5s.pt,入口脚本是 rstest.py。它把目标检测、深度相机、像素坐标与深度信息融合到了一个流程里,适合已经会跑基础目标检测、现在要做移动机器人和三维目标检测的开发者。下面按我的拆库顺序,从原理讲到工程实现。
2. YOLOv5网络结构、anchor与Realsense深度图的对齐
在讲拼接代码之前,得先把两件事说清楚:YOLOv5 的预测结果里到底哪一项是目标 anchor 中心,Realsense 深度相机又是怎样把深度图和彩色图对齐到同一个视角。这两件事没理清,后面所有深度数值都不可信。
2.1 从yolov5s.pt推理结果看anchor与预测框
YOLOv5 的 yolov5 网络结构由 Backbone、Neck、Head 三段组成。Backbone 用 CSPDarknet 提取不同尺度特征,Neck 用 PANet 做自上而下和自下而上的特征融合,Head 最终在 80×80、40×40、20×20 三个特征层输出预测。每个网格会放置多个不同长宽比的 anchor,anchor 只是预定义的先验框,模型回归的是相对于 anchor 的偏移量,最后解码出目标框和置信度。
资源里的 models/yolov5s.yaml 定义了 Backbone 每一层的通道数,weights/yolov5s.pt 则保存了在 COCO 数据集上训练好的参数。推理时输入一张 640×640 的 RGB 图,输出可以整理成一个 1×25200×85 的张量:25200 是三个特征层所有 anchor 位置相加的总数,85 由 4 个框坐标、1 个置信分数和 80 个类别分数组成。注意这里“anchor 中心”在最终结果里并不是原始先验框中心,而是模型预测框的中心,但它仍然由 anchor 解码而来,所以工程里习惯叫它目标 anchor 中心。
import torch from models.experimental import attempt_load device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = attempt_load('weights/yolov5s.pt', map_location=device) model.eval() dummy = torch.rand(1, 3, 640, 640).to(device) with torch.no_grad(): pred = model(dummy)[0] print(pred.shape) # (1, 25200, 85)上面代码中 attempt_load 是 YOLOv5 本地工程加载权重的方式,和 torch.hub.load 相比不依赖网络缓存。dummy 的 640×640 是常用输入分辨率,实际使用时可以换成 416 或 1280,但要注意模型权重的 anchor 在训练时是为特定尺度设计的。pred.shape 里的第一维是 batch size,第二维是所有预测位置总和,第三维是预测信息。拿到 pred 后还需要按 confidence 过滤并用 NMS 去重,真正画框用的是经过 NMS 的列表。
2.2 Realsense D435i彩色图与深度图对齐:align的用法
Intel Realsense D435i 的左右红外相机输出深度图,右侧的 RGB 模块输出彩色图。由于两个模组在物理上存在位置偏移,同一物体在深度图上的像素坐标和彩色图上的像素坐标并不一致。这就是为什么不能直接拿深度帧的 (u,v) 去读检测框中心,必须先做对齐。
pyrealsense2 的 rs.align 是最常用的对齐方式。它接收一个目标坐标系的 stream 类型,内部通过硬件标定参数把深度图重投影到彩色图坐标系,得到一张逐像素与彩色图对齐的新深度帧。
import pyrealsense2 as rs pipeline = rs.pipeline() config = rs.config() config.enable_stream(rs.stream.depth, 640, 480, rs.format.z16, 30) config.enable_stream(rs.stream.color, 640, 480, rs.format.bgr8, 30) profile = pipeline.start(config) align = rs.align(rs.stream.color) frames = pipeline.wait_for_frames() aligned_frames = align.process(frames) aligned_depth_frame = aligned_frames.get_depth_frame() color_frame = aligned_frames.get_color_frame()这里有两个参数值得说明:第一个是 depth 和 color 的分辨率都设成 640×480,对齐后的深度图可以直接用彩色图像的像素坐标索引;第二个是 rs.align 的构造参数 rs.stream.color,它表示把深度帧对齐到彩色帧。如果写成 rs.align(rs.stream.depth),方向就反了,后续从彩色检测框中心读取深度会得到偏移量很大的结果。在嵌入式平台上,如果觉得 640×480 深度空洞明显,可以尝试降低到 424×240,但检测框分辨率也会跟着下降。
2.3 深度内参与反投影:像素坐标到三维坐标
对齐完成后,深度图上的每个 (u,v) 就和彩色图对应。此时读取到的深度值是 Z,单位由 API 决定;pyrealsense2 的 get_distance 返回的是浮点米,直接访问像素数据得到的是 uint16 毫米。三维坐标可以按小孔成像模型反投影:X = (u - cx) * Z / fx,Y = (v - cy) * Z / fy。这里 fx、fy、cx、cy 是深度相机内参,通常可以用 K 矩阵从 rs.intrinsics 读取。
| 参数 | 含义 | 对融合结果的影响 |
|---|---|---|
| fx | 水平方向焦距 | fx 不准确会让 X 方向距离误差放大 |
| fy | 垂直方向焦距 | 同理影响 Y 方向 |
| cx | 图像主点 x 坐标 | 偏移会导致目标越靠边缘偏差越大 |
| cy | 图像主点 y 坐标 | 影响目标竖直位置 |
| depth_scale | 原始深度值到米的系数 | 项目里通常为 0.001,过滤时要用 |
在工程实现中,我一般直接用 depth_frame.get_distance(cx, cy) 取 center_px 的深度,而不是频繁做内参矩阵计算。因为很多场景只需要中心到相机的直线距离,并不需要重建点云。只有要做三维目标检测、计算实际尺寸时才需要把整个公式写出来。这一点也决定了后面的深度信息字段具体怎么输出。
3. 环境配置与yolov5s.pt推理链路搭建
原理讲清楚了,接下来要能跑。这份 zip 里的目录结构一看就是官方 YOLOv5 的目录结构:models 目录放网络结构定义,utils 目录放数据集处理和评估工具,weights 目录下放的是 yolov5s.pt,根目录的 rstest.py 是接 Realsense 的检测脚本。之所以说它是工程包而不是教学代码,是因为它没有把模型加载写在主循环里,更好的组织方式是启动时加载一次,然后循环读帧。
3.1 requirements.txt与Realsense SDK安装
先处理依赖。requirements.txt 里包含了 torch、torchvision、opencv-python、numpy、pyrealsense2 等常用库。如果之前已经装过 YOLOv5 官方环境,那么只需要单独补装 pyrealsense2。这里最容易踩的坑是 Python 版本和 torch 版本不匹配。YOLOv5 对 torch 的版本要求并不苛刻,但 Windows 下 pyrealsense2 的预编译 wheel 通常比 Linux 的发布晚,如果 pip 找不到对应版本,建议用 conda 建一个 Python 3.8 环境。
python -m venv .venv source .venv/bin/activate # Windows 下是 .venv\Scripts\activate pip install -r requirements.txt python rstest.py --weights weights/yolov5s.pt --conf 0.4命令里的 --conf 0.4 是置信度阈值,低于 0.4 的检测结果会被过滤。如果检测不到小目标,可以调低到 0.25;如果误报太多,调到 0.6。第一次运行建议在 USB 3.0 接口上插相机,Realsense D435i 的深度流在 USB 2.0 下带宽不够,经常报 failed to set streaming request 之类的错误。如果已经安装过重型的 opencv,建议把 opencv-python 换成 opencv-python-headless,避免和 GUI 工具冲突。
3.2 加载yolov5s.pt并完成单帧检测
工程里加载权重有两种方式:一是官方 detect.py 的 attempt_load,二是 torch.hub.load。离线环境建议用第一种,因为 attempt_load 直接读取项目内 models/yolo.py,不需要联网下载仓库。加载后必须调用 model.eval(),否则模型里的 dropout 和 BatchNorm 层仍处于训练模式,同一张图每次预测结果都可能不一样。
import cv2 import torch from models.experimental import attempt_load device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = attempt_load('weights/yolov5s.pt', map_location=device) model.eval() img_bgr = cv2.imread('frame.jpg') img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) outputs = model(img_rgb, size=640) print(type(outputs)) # DetectionModel 的 forward 结果 print(outputs.xyxy[0][0]) # [x1, y1, x2, y2, conf, cls]上面代码最关键的是彩色图通道顺序。opencv 默认读进来是 BGR,而 YOLOv5 训练时使用的是 RGB,不转换会导致颜色语义错乱,尤其是区分蓝色和绿色物体时准确率大幅下降。model(img_rgb, size=640) 内部会完成 letterbox 缩放和归一化,返回的 xyxy 坐标已经映射回原始图像尺寸,不需要自己再做一次等比缩放。outputs.xyxy[0] 是一个 tensor,每一行包含 6 个值,最后两个分别是 confidence 和 class id。
3.3 构造检测-深度融合循环
实时目标检测应用里,核心循环要处理四件事:取彩色帧、做检测、取对齐深度帧、读取中心深度。建议把模型加载和相机 pipeline 放在循环外,把单帧检测和深度读取封装成函数。下面是一个最简的循环骨架:
import cv2 while True: frames = pipeline.wait_for_frames() aligned_frames = align.process(frames) color_frame = aligned_frames.get_color_frame() depth_frame = aligned_frames.get_depth_frame() if not color_frame or not depth_frame: continue img = np.asanyarray(color_frame.get_data()) results = model(img, size=640) dets = results.xyxy[0].cpu().numpy() for det in dets: x1, y1, x2, y2, conf, cls = det if conf < 0.4: continue cx = int((x1 + x2) / 2) cy = int((y1 + y2) / 2) depth = depth_frame.get_distance(cx, cy) cv2.rectangle(img, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2) cv2.putText(img, f'{depth:.2f}m', (int(x1), int(y1) - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imshow('yolov5 + realsense', img) if cv2.waitKey(1) & 0xFF == ord('q'): break这里第一处值得注意的地方是 results.xyxy[0] 的坐标是浮点类型,画框前要转 int;第二处是 depth_frame.get_distance(cx, cy) 返回单位是米,和真实距离一致。循环里没有做边界检查,如果检测框中心恰好在图像边缘,会触发 out of bounds,这个留给第4章专门处理。for 循环里每个 det 都要追加深度字段,最终得到带距离的检测结果列表。
4. 标注目标框与anchor中心的像素坐标、深度信息
这一章做的是把第3章的循环拆细。目标检测结果通常以 x1,y1,x2,y2 的形式给出,这是左上角和右下角的像素坐标。目标 anchor 中心的像素坐标就是框的中心点,计算方式简单,但要考虑位置有效性、深度图范围和相机量程三个限制条件。缺失这些限制,深度信息这张表就是虚的。
4.1 从检测框解算目标anchor中心像素坐标
先明确计算口径:YOLOv5 输出的 x1,y1,x2,y2 是在原始彩色图像尺寸上的坐标,不是 640×640 特征图尺寸。所以 cx = (x1 + x2) / 2,cy = (y1 + y2) / 2 得到的就是目标在彩色图中的 anchor 中心像素坐标。这个中心点对应深度图上同一个位置,因为前面已经做了 align。
def get_center_from_bbox(box): x1, y1, x2, y2, conf, cls = box cx = int((x1 + x2) / 2) cy = int((y1 + y2) / 2) return cx, cy假如发现检测框中心不在物体上,通常是因为检测框太瘦或置信度太低。比如人形目标只有上半身漏出来时,中心点可能落在背景上,深度会偏到更远的墙。此时可以改用框的下三分位点作为深度采样点,或者先过滤掉宽度或高度小于 10 像素的框。这种启发式规则在移动机器人场景里很常见,只要记录规则说明,后期调参并不困难。
这里还有一个经常被忽略的细节:YOLOv5 在推理时对输入图像做了 letterbox,多余部分用灰色像素填充,因此返回的 xyxy 已经通过坐标逆变换映射回原图尺寸。也就是说,只要用的是官方推理接口,检测框中心直接落在原图像素上,不需要自己做 unletterbox。如果你从 raw 输出张量手写解码,就必须在解码后手动恢复坐标变换,否则中心点会整体偏移,深度采样全部失效。
4.2 从对齐深度帧读取目标中心深度值
读取深度时,pyrealsense2 的 get_distance 方法返回浮点米,而通过 numpy 直接访问 depth_frame 的 buffer 得到的是 uint16 毫米。两种方式选一种,统一换算成米。还要做三类过滤:坐标越界、深度为零或超量程、中心点落在深度空洞。Realsense 在强光、黑色物体表面经常产生零值空洞,这时候直接丢弃深度比强行插值更安全。
def read_depth_mm(aligned_depth_frame, cx, cy): h = aligned_depth_frame.get_height() w = aligned_depth_frame.get_width() if cx < 0 or cy < 0 or cx >= w or cy >= h: return None depth = aligned_depth_frame.get_distance(cx, cy) if depth <= 0 or depth > 10.0: return None return depth这段过滤逻辑里,10.0 是 D435i 在 640×480 分辨率下的典型量程上限。实际量程会随分辨率、环境光照和配置参数改变,不要把这个数当成固定阈值。get_distance 返回 0 表示该点无效,大于 10 可能是反射造成的飞点。把返回 None 的检测结果标记成 depth=0,或者在可视化时用不同颜色显示,比直接崩溃更容易定位问题。
4.3 输出字段与可视化标注
最终要把检测框、像素坐标和深度组合成一条记录。为了后面接机器人导航或三维目标检测,我习惯把这条记录直接写成字典,而不是只打印。这样既能存 log,又能方便地转成 ROS topic。
| 字段 | 类型 | 示例 | 来源 |
|---|---|---|---|
| class_id | int | 0 | 模型输出 |
| class_name | str | person | 从 COCO names 查表 |
| score | float | 0.86 | 模型输出置信度 |
| bbox_xyxy | tuple | (120,45,310,220) | 模型输出映射到原图 |
| center_px | tuple | (215,132) | 由 bbox 计算 |
| depth_m | float | 1.23 | 对齐深度帧采样 |
可视化时,cv2.rectangle 画检测框,cv2.circle 在中心点画一个小圆,再用 putText 把 depth_m 和 score 拼到一起写到检测框上方。如果发现深度数值在画面里缓慢漂移,优先怀疑深度相机没有在运行时做温度补偿;如果数值突然跳变到极大值,大概率是中心点落到了金属反光区域。
5. 工程化排错:深度值滤波、小目标检测与推理提速
到了真正接进系统的阶段,最影响交付质量的是三个细节:深度值抖动、小目标漏检和推理帧率。这三个问题互相关联,处理顺序建议是先滤波,再调网络输入,最后做硬件加速。
5.1 中心点深度抖动:邻域中值滤波
单像素深度在物体边缘上波动非常大,尤其是衣服边缘和金属轮廓。直接取中心像素会让距离值有时跳到前景、有时跳到背景。常见做法是取以中心点为中心的 5×5 邻域,计算这些有效深度值的中位数。中位数比均值稳健,不会被一两个背景飞点带偏。
import numpy as np def get_median_depth(aligned_depth_frame, cx, cy, radius=2): depth_arr = np.asanyarray(aligned_depth_frame.get_data()) patch = depth_arr[cy-radius:cy+radius+1, cx-radius:cx+radius+1] valid = patch[(patch > 0) & (patch < 10000)] # mm if valid.size == 0: return None return float(np.median(valid)) / 1000.0这个函数把深度帧转成 numpy 数组后截取邻域,再按毫米值中值滤波。参数 radius=2 对应 5×5 窗口,窗口越大对噪声越平滑,但同时也会让距离响应变慢。如果目标靠近相机,窗口应适当缩小,因为深度变化剧烈的大场景里大窗口容易把表面起伏平均掉。
5.2 小目标检测:调整输入分辨率与anchor配置
小目标漏检在固定距离的监控场景里最明显。YOLOv5 默认在 640×640 下训练,COCO 数据集中小目标占比不高。如果深度相机距离目标 5 米以上,一个行人可能只有十几个像素,这种情况下有两种调整:第一,把模型输入尺寸从 640 提高到 960 或 1280,小目标分辨率变大,召回率上涨;第二,重训时会用到 models/yolov5n.yaml 或 yolov5s.yaml 里的 anchor,anchor 是模型预测的基准框,默认参数并不一定适合你的场景,需要使用 autoanchor 重新聚类。
使用 autoanchor 的常见命令是在训练参数里开启 --noautoanchor 决定是否使用默认 anchor。更推荐直接用 YOLOv5 训练脚本在自定义数据集上做一轮自动 anchor 分析,它会输出不同尺度下的最佳 anchor 长宽比。对于近距离的深度相机项目,目标通常比 COCO 更大,所以 anchor 的整体尺度也可以等比放大。单纯调高 confidence 阈值并不会改善小目标,反而可能把低置信度的正确框滤掉。
5.3 推理提速:从CUDA到TensorRT
深度相机每秒输出 30 帧,目标检测如果卡在每帧 200 毫秒,整个系统就失去了实时意义。优先检查模型是否跑在 CUDA 上,yolov5s.pt 在 RTX 3060 上通常能跑到 30 帧以上。没有 GPU 的边缘设备,可以先把检测尺寸降到 416 或 480,再把模型导出成 ONNX 或 TensorRT。
import torch model = attempt_load('weights/yolov5s.pt', map_location='cpu') model.model[-1].export = True dummy = torch.zeros(1, 3, 640, 640) torch.onnx.export(model, dummy, 'yolov5s.onnx', opset_version=12)ONNX 导出时 model.model[-1].export = True 是为了让检测头输出原始格式,否则导出后的模型会多一层推理后处理。真正部署到 Jetson 等平台时,TensorRT 的 INT8 量化通常比 ONNX Runtime 再快一倍,但量化需要校准集,校准集应当包含目标物实际位置的深度分布,否则量化误差会放大到检测置信度上。
本文还有配套的精品资源,点击获取