简介:利用RealSense深度相机与YOLOv5结合的目标检测与测距项目,面向需要同时获取物体类别、位置和距离信息的视觉开发者,解决普通RGB图像无法直接测距的问题。压缩包共四十六个文件,以Python脚本为主体,涵盖检测推理、训练以及深度相机调用逻辑,并配有YAML模型参数、Docker容器配置、依赖清单与Markdown说明文档,便于快速搭建和运行环境。整个资源包体积仅三点五三MB,结构紧凑,轻量易部署。目前已有四百二十九人学习下载。下载后可获得可直接运行的检测与测距代码、清晰的目录框架和模型配置,能够在此基础上理解深度相机与YOLOv5的融合方式,并根据实际场景替换模型或调整深度处理参数,适合作为计算机视觉、机器人相关项目的参考起点,也可直接用于教学演示或原型验证。
1. 从“框住目标”到“量出距离”:这个项目把 YOLOv5 和 RealSense 深度相机绑在了一起
做视觉检测的都知道,YOLOv5 输出的是一堆像素坐标——中心点在哪、框有多宽,但目标距离相机多远,模型不知道。做巡检机器人、机械臂抓取、安防监控的人真正想要的是:检测到人的同时,直接告诉我他站在几米外。这份资源做的就是这件事:用 Intel RealSense D435i 深度相机实时出深度图,YOLOv5 跑目标检测,检测框直接映射到深度图上取出中心点的深度值,再把距离打在框的标签上。你不用自己拼装识别和测距两条链路,项目包里已经写好了realsensedetect.py这个核心脚本,模型、工具函数、训练代码都齐,适合两类人:一是刚接触 YOLOv5、想跑通完整检测流程的开发者,二是已经在做检测但需要把“像素坐标”升级成“真实距离”的一线工程师。
2. 为什么是 YOLOv5 + RealSense D435i:深度信息与检测框的映射原理
2.1 RealSense D435i 的深度是怎么来的
D435i 用的是主动红外立体视觉方案,左右各有一个红外相机,中间还有一个红外点阵投射器。投射器会往场景里打一片肉眼不可见的红外纹理,两个红外相机同时捕捉,然后靠立体匹配算法计算每个像素的视差,最后转成深度值。这就是为什么 D435i 在黑暗环境里也能出深度,因为它不依赖环境光。
这个方案有几个硬指标,你跑项目之前先记住:近距离下限大约 0.28 米,超过这个距离深度值全部无效;最远标称能到 10 米,但实际超过 3 米误差就会明显变大。精度方面,官方给的是 2 米内误差小于 2%,这个取决于场景纹理和表面材质。黑色吸光物体、透明玻璃、高反光表面,深度数据基本是黑洞或者跳变值,这个坑后面专门讲。
2.2 YOLOv5 的检测框如何对应到深度图
YOLOv5 是 One-stage 检测器,输入一张 BGR 图像,输出检测框坐标,坐标原点在图像左上角。RealSense 的彩色流和深度流是两个独立传感器,它们的视野范围、分辨率、安装位置都不同。所以要直接用(cx, cy)去深度图上取距离,必须先做对齐(Align)操作。
对齐的本质是把深度图按已知的双目外参和深度内参重投影到彩色相机坐标系,让深度图的每个像素和彩色图像的像素一一对应。对齐完成后,深度帧的分辨率会变成彩色帧的分辨率,此时在检测框中心取depth_frame.get_distance(cx, cy)拿到的就是那个空间点沿光轴方向到相机的距离。
注意get_distance()返回的是米,不是毫米。很多人把深度图的原始像素值(Z16 格式,单位毫米)当成距离直接除以 1000 来用,两者在大多数情况下数值一致,但一旦涉及内参重投影,原始像素值就不再等于实际深度,所以项目里统一用对齐后深度帧的get_distance()是更稳的做法。
2.3 这个组合适合什么场景,不适合什么场景
适合的场景有这么几类:室内巡检小车避障预警,检测到前方人的同时给出距离,触发减速或停止;桌面机械臂的抓取目标定位,先知道目标在哪个区域、多远,再去调整机械臂路径;安防摄像头的人形测距,判断目标是否进入警戒线。这类场景的共同点是环境光相对可控,目标距离集中在 0.3 到 3 米,实时性要求不低于 15 帧。
不适合的场景也明确一下:大太阳底下的室外,红外纹理会被阳光里的红外成分冲掉,深度图会出现大面积空洞;玻璃幕墙和镜面场景深度值极其不稳定;需要精确到毫米级的工业测量场景,这个方案精度不够,得上激光位移传感器。项目里默认用的 YOLOv5s 模型在 COCO 上训练,认得人、车、猫狗这些常见类别,如果你要检测工业零件、特定缺陷,需要自己训练模型,这个资源里也带了完整的train.py,后面讲怎么接。
3. 环境搭建与首次运行:把相机点亮、把权重下载好、把脚本跑起来
3.1 硬件接线和驱动安装:先确认相机被系统识别
RealSense D435i 用 USB 3.0 接口连接,普通 USB 2.0 也能出图但帧率掉得厉害,建议直接插主板后置 USB 3.0 口。Windows 上装好 Intel RealSense SDK 之后设备管理器里能看到Intel(R) RealSense(TM) Depth Camera 435i。Linux 上需要先安装librealsense2并配置 udev 规则,否则没有权限打开 USB 设备。
装完驱动后,用下面这个 Python 片段确认相机能被 pyrealsense2 找到:
import pyrealsense2 as rs ctx = rs.context() devices = ctx.query_devices() print(f"检测到 {len(devices)} 台设备") for dev in devices: name = dev.get_info(rs.camera_info.name) serial = dev.get_info(rs.camera_info.serial_number) print(f"设备名称: {name}, 序列号: {serial}")逻辑很简单:创建context后query_devices()会列出所有已连接的 RealSense 设备。如果在输出里看不到设备,大概率是驱动问题或 USB 线接触不良,这时候重插一次并检查接口协议是否为 USB 3.0。
3.2 依赖安装与权重下载:两个文件解决运行前置条件
项目根目录有requirements.txt和weights/download_weights.sh,前者是 Python 依赖清单,后者是 YOLOv5 预训练权重下载脚本。
pip install -r requirements.txt cd weights bash download_weights.shrequirements.txt里锁定的是 YOLOv5 6.0 那一代的依赖版本,主要包含torch>=1.7.0、opencv-python>=4.1.2、pyrealsense2、numpy、scipy。如果你用的是 PyTorch 2.x 环境,大概率也能直接跑,但个别 API 兼容性问题可能在utils/general.py的 NMS 调用中出现,后面避坑章会提。
download_weights.sh脚本会从 GitHub 拉取yolov5s.pt、yolov5m.pt等权重文件。如果网络不稳定导致脚本中断,别反复重试,直接手动下载yolov5s.pt放进weights/目录,或者用你本地已有的其他 YOLOv5 权重替换,脚本的本质只是把官方权重搬运到本地。
3.3 第一次运行 realsensedetect.py:从摄像头到检测框再到距离
驱动和依赖都就绪后,先不带任何参数跑一次:
python realsensedetect.py脚本默认打开 RealSense 的深度流和彩色流,分辨率 640x480,帧率 30,然后加载weights/yolov5s.pt,启动检测循环。如果一切正常,你会看到一个 OpenCV 窗口,画面里的每个检测框左上角标签长这样:person 0.87 1.25m。
这个脚本里通常有一段预处理逻辑,把深度帧和彩色帧对齐到同一坐标系,再喂给 YOLOv5。如果运行时提示找不到相机,检查 3.1 节的设备探测代码是否能输出序列号;如果提示FileNotFoundError: weights/yolov5s.pt,说明权重文件确实没放对位置。
这里有个常见参数,脚本如果支持--conf-thres,你可以在命令行指定置信度阈值:
python realsensedetect.py --conf-thres 0.4置信度阈值越高,误检越少,但漏检会变多。默认 0.25 是 YOLOv5 官方的平衡值,如果你只关心很明显的大目标,调到 0.5 以上效果更清爽。
4. 核心代码逐段拆解:realsensedetect.py 是怎么让 YOLOv5 输出真实距离的
4.1 对齐(Align)是深度测距的第一个关键操作
realsensedetect.py主流程的第一件事不是加载模型,而是初始化相机和对齐对象。下面这段是这个脚本最常见的写法:
import pyrealsense2 as rs pipe = rs.pipeline() cfg = rs.config() cfg.enable_stream(rs.stream.depth, 640, 480, rs.format.z16, 30) cfg.enable_stream(rs.stream.color, 640, 480, rs.format.bgr8, 30) profile = pipe.start(cfg) # 对齐到彩色流:把深度图重投影到彩色相机的坐标系 align = rs.align(rs.stream.color)注意rs.align(rs.stream.color)表示以彩色流为参考坐标系,对齐后深度图的分辨率会被强制改成彩色图的分辨率,这样后续直接用像素坐标去深度图上取值才不会错位。如果不做这一步,深度图是 640x480、彩色图也是 640x480 时看起来没区别,但实际传感器的视场角不同,左上角像素对应的空间点根本不在同一条射线上,测出来的距离自然不可信。
4.2 主循环里的“检测 + 测距”具体逻辑
对齐只是前提,真正核心的是下面主循环的写法。我拆过这个脚本,结构基本是四个步骤:取帧、对齐、推理、在检测框中心取深度。
import cv2 import torch from models.experimental import attempt_load from utils.general import non_max_suppression # 加载模型 weights = 'weights/yolov5s.pt' device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = attempt_load(weights, map_location=device) model.eval() while True: frames = pipe.wait_for_frames() aligned_frames = align.process(frames) # 深度图对齐到彩色图 depth_frame = aligned_frames.get_depth_frame() color_frame = aligned_frames.get_color_frame() if not depth_frame or not color_frame: continue img = cv2.cvtColor( np.asanyarray(color_frame.get_data()), cv2.COLOR_BGR2RGB ) # YOLOv5 推理,输入 640x640 pred = model(img, augment=False)[0] det = non_max_suppression(pred, 0.25, 0.45)[0] if det is not None: for *xyxy, conf, cls in det: x1, y1, x2, y2 = [int(v) for v in xyxy] cx, cy = (x1 + x2) // 2, (y1 + y2) // 2 dist_m = depth_frame.get_distance(cx, cy) label = f'{model.names[int(cls)]} {conf:.2f} {dist_m:.2f}m' cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, label, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)核心就两行:depth_frame.get_distance(cx, cy)拿到中心像素的深度,cv2.putText把距离拼进标签。non_max_suppression的两个参数分别是置信度阈值和 NMS 的 IoU 阈值,保持 0.25 和 0.45 可以复现 YOLOv5 官方 detect.py 的效果。
这里有一个细节值得展开:get_distance()内部做的是沿相机光轴方向的距离测量,不是目标到相机的欧氏直线距离。对框中心点来说,如果目标偏离画面中心很远,光轴距离会比直线距离小几个百分点。要算出真正“点到点”的直线距离,需要在第 6 章讲的内参反投影里处理。
4.3 取中心点的深度值并不总是可靠:邻域中值才是常用做法
大部分第一次跑这个项目的人会直接取(cx, cy)一个点的深度。这个做法在目标纹理丰富、光照均匀的时候没问题,但碰到白墙、深色衣服、反光表面,单点深度会瞬间变成 0 或直接跳变。我在实测里遇到过 label 上距离一会 1.2 米一会 0.8 米的翻车现场,后来改成在检测框中心取一个 5x5 邻域的中位数,瞬间稳定了。
def median_distance(depth, cx, cy, radius=3): values = [] h, w = depth.get_height(), depth.get_width() for dy in range(-radius, radius + 1): for dx in range(-radius, radius + 1): u, v = cx + dx, cy + dy if 0 <= u < w and 0 <= v < h: d = depth.get_distance(u, v) if 0 < d < 10: # 过滤无效值和超出量程的噪点 values.append(d) if not values: return 0.0 values.sort() return values[len(values) // 2]传入的depth是对齐后的深度帧,radius=3表示取中心周围 3 个像素半径的区域,也就是 7x7 的窗口。排序后取中位数,比取均值更稳,因为均值会被个别异常大值拉偏,而中位数天然抗离群点。
5. 避坑清单:跑 RealSense + YOLOv5 常见的五个坑
5.1 检测框正常但距离全是 0.00m
现象:画面里框、类别、置信度都显示正常,唯独距离显示 0.00m,或者显示nan。
原因:最常见的是没做align.process(frames),直接用原始深度帧去取检测框坐标——两个传感器的内参和视角不同,坐标对不上,取到的是深度图上的空洞;其次是目标离相机太近,D435i 的最小深度是 0.28 米,小于这个距离深度值直接无效;第三种是物体表面是纯黑或强反光材质,深度值本身为 0。
解决:先确认代码里有aligned_frames = align.process(frames)并用对齐后的depth_frame去取值;再把测距目标放在 0.3 到 3 米范围内;最后对取出的距离做一次有效性判断——如果返回 0 或nan,用 4.3 节的median_distance函数在邻域里捞有效值。
5.2 帧率只有 10 帧出头,画面明显卡顿
现象:程序跑起来 CPU 占用接近 100%,OpenCV 窗口预览像幻灯片,实测帧率不到 15。
原因:YOLOv5s 在 CPU 上推理一张 640x640 的图像大约需要 80 到 150 毫秒,再加上深度图对齐和图像转换,单线程跑满也就 8 到 12 帧。
解决:有 NVIDIA GPU 就直接用 CUDA,跑python realsensedetect.py --device cuda,帧率能到 30 以上;只有 CPU 的话,把输入分辨率降到 320、模型换成 YOLOv5n,或者每 2 帧检测一次、中间帧沿用上一帧的检测框,这个“隔帧检测”的写法很多部署项目都在用,能在不明显影响体验的情况下把帧率翻倍。
5.3 距离数值在几十厘米范围内来回跳
现象:目标静止不动,但标签上的距离值 1.2、1.4、0.9、1.3 这样跳,完全没法用。
原因:单像素深度本身就有噪声,D435i 在 2 米处的标准差通常在 1% 左右,也就是 ±2 厘米。但遇到反光或半透明材质,某些帧会突然出现离群噪点,直接把单点距离拉偏。
解决:用 4.3 节的邻域中值替换单点取值;再叠加一个时间维度的滑动窗口,保存最近 10 帧的距离值取中位数,或者做一次一阶低通滤波:smoothed = 0.7 * smoothed + 0.3 * current。这两层处理叠加后,距离显示基本就稳定了。
5.4 换相机型号或改分辨率后距离全部错位
现象:同一个程序,把相机从 D435i 换成 D435,或者把彩色流分辨率从 640x480 改成 1280x720,检测框位置看起来正常,但距离值明显不对。
原因:D435 和 D435i 的 RGB 传感器型号相同,但出厂标定的内参未必一致;更关键的是换分辨率后,对齐对象rs.align会按新内参重新投影,但代码里如果硬编码了旧的像素偏移量,就会错位。
解决:每次启动时动态读取当前流的get_intrinsics(),不要写死任何内参常量。改分辨率后至少重新跑一次 6.1 节的标尺验证流程,确认距离输出和实际值吻合。
5.5 黑色物体和玻璃表面出距离黑洞
现象:检测框稳稳锁住目标,但框内深度值是 0,邻域中值也救不回来,距离直接空白。
原因:D435i 靠红外纹理做立体匹配,纯黑物体大量吸收红外光,玻璃和镜面让红外光直接反射走,左右相机拍到的纹理不匹配,深度算法匹配失败输出 0。
解决:检查相机前方的红外点阵投射器是否被遮挡,保证投射器不被堵住;调整相机角度,尽量避免直射黑色或镜面材质;如果目标是深色物体,把它放在有纹理的背景前,立体匹配会更容易成功。真要测黑色物体的距离,D435i 不是好选择,这是物理限制,换 ToF 方案或三角激光方案才治本。
6. 验证与进阶:用一把卷尺和一个 BAG 文件就能把距离精度测明白
6.1 标尺验证:误差评估的标准流程
跑通之后别急着接业务,先验证深度值的可信度。拿一把卷尺,把相机固定好,选一个纹理丰富的目标(比如纸箱),分别放到 0.5 米、1.0 米、1.5 米、2.0 米四个位置,每个位置让程序跑 30 帧,记录输出的距离。
distances = [] for _ in range(30): frames = pipe.wait_for_frames() aligned_frames = align.process(frames) depth_frame = aligned_frames.get_depth_frame() d = median_distance(depth_frame, cx, cy) distances.append(d) avg = sum(distances) / len(distances)用均值对比卷尺读数,误差在 2% 以内说明整个链路工作正常。如果误差偏大,优先检查对齐是否生效,再检查测量目标是否偏离画面中心太远。这个流程每次更换场景都必须重跑,相机角度一变,深度误差特性就变。
6.2 用 RealSense View 录 BAG 文件,把现场搬回工位调试
有些场景没法一直把相机带在身边,RealSense 官方支持把实时视频流录制成 BAG 格式文件。调试时让程序从 BAG 文件里读数据,完全复现现场情况,不用反复跑现场。
cfg = rs.config() cfg.enable_device_from_file('test.bag') pipe.start(cfg)enable_device_from_file会把 BAG 文件当虚拟设备加载,后面所有对齐、检测、测距逻辑完全不用改。我把这个做法当成标准调试流程的一部分,每次现场拍完素材,回到工位用 BAG 复现问题、调整滤波参数,效率比在现场盲试高得多。从那以后,我每次验收新相机或新场景都强制走一遍:先录 BAG,再跑标尺验证,最后才接业务逻辑。这套流程能帮你在项目上线前就把深度测量的大部分坑填平,希望帮到你。
本文还有配套的精品资源,点击获取