☰
RealSense D435i + YOLOv5目标检测测距:从像素坐标到毫米
2026/10/7 6:37:08 网站建设 项目流程

简介:结合RealSense深度相机与YOLOv5的实战项目,面向需要在目标检测同时获取物体距离信息的开发者,可用于机器人避障、智能监控、AR交互等场景。压缩包共46个文件、约3.53MB,其中18个py脚本负责模型训练、检测推理及深度相机数据读取,8个yaml/yml配置用于定义YOLOv5网络结构与训练参数,另有依赖清单、Dockerfile、Shell脚本、Markdown文档和Jupyter笔记,便于一键复现环境、下载预训练权重和交互式学习。已有429人学习下载。核心脚本realsensedetect.py将深度图与YOLOv5检测框融合,可同步输出目标类别、边界框坐标和距离值;同时保留YOLOv5原生训练与检测能力,支持自定义数据集训练和推理部署;利用深度相机可直接获取像素对应的物理距离,免去单目测距标定步骤。对于正在推进毕设或工程原型的读者,这套代码提供了从RealSense调用到模型集成的完整链路,能大幅缩短开发周期。

1. RealSense 深度相机 + YOLOv5 目标检测测距:检测框里缺的那个距离值

拿到 realsense d435i 深度相机做目标检测的工程师,很快会发现一个尴尬:YOLOv5 把行人、车辆、锥桶标得清清楚楚,框也画得四平八稳,可框里没有“距离”。而机器人避障、交通抓拍、鸟类监测这些场景,要的不只是“这是什么”,还要“它离我多远”。把 YOLOv5 检测框和 RealSense 深度图对齐,就能在输出“是什么、在哪”的同时,给出每个目标的毫米级距离。这个方案适合刚拿到 D435i、想用 YOLOv5 训练自己数据集的开发者,也适合做三维目标检测预研的团队。真正决定成败的往往不是模型,而是 RGB 和深度图之间那一步对齐。

2. 测距原理先立住:深度对齐、像素坐标与毫米值的换算链路

2.1 YOLOv5 检测框为什么给不了距离:单目几何的尺度鸿沟

YOLOv5 的输出是四个像素坐标、一个置信度、一个类别编号。这五个数字都是二维图像坐标系里的东西,不携带任何物理尺度。要把像素换算成距离,通常有人会走单目几何路线:假设行人平均身高 1.7m,用焦距和图像上的像素高度,套相似三角形算出距离。这个做法表面成立,实际很容易翻车——行人可能是小孩,可能是骑车人,检测框高度一变,假设就全部失效。更不用说车辆这种外形差异极大的类别。

D435i 这类 RGB-D 深度相机解决了尺度问题:深度图里每个像素本身就是毫米级距离,不需要猜目标尺寸,直接读就行。所以检测与测距的组合本质是“2D 检测负责语义,深度图负责物理尺度”,两者各干各的,再用坐标映射连接起来。

常见误区是以为“YOLOv5 测距”是把模型改成回归距离,那是另一套做法,需要标注的真实距离数据集,且依赖目标类别和场景,泛化差。用深度相机的好处是测距不经过模型推理,深度值由硬件算好,模型只负责“告诉你在哪取深度”。这套方案要解决的核心问题不是训练,而是让检测框坐标能准确落到深度图的同一物理点上。

2.2 RealSense 的主动红外深度链路:一个像素一把尺子

我一般用 D435i,它属于主动立体视觉方案:机身上有一个红外激光投射器、两个红外相机和一个普通 RGB 相机。激光投射器打出随机散斑图案,两个红外相机从不同视角拍摄,通过视差计算每个像素的深度。输出深度流的格式通常是 z16,也就是 16 位无符号整数,每个像素的值代表该点到相机平面的距离,单位是毫米。0 表示这个像素没有有效深度。量程一般在几厘米到 10 米左右,但可靠范围受物体表面材质、光照和帧率影响,这一点后面避坑章会专门说。

深度流和 RGB 流可以独立配置。RGB 流是 bgr8 彩色图像,深度流是 z16 单通道。做测距时我建议两者分辨率保持一致,比如 640x480@30fps。分辨率不一致不是不能做,但坐标映射要多一次缩放,容易出错。很多新手的首个项目就是在这个环节埋下距离不准的种子。

2.3 rs2::align 对齐:RGB 与深度图逐像素对应的前提

第一次接触 realsense 的开发者,会直接 pipeline.start 后分别取 color_frame 和 depth_frame,然后把 YOLOv5 检测框坐标拿去 depth 图上取值——结果距离完全不对。原因很简单:RGB 镜头和两个红外镜头在相机面板上不在同一个物理位置,视角存在视差。同一个物理点,在彩色图上的像素坐标和在深度图上的像素坐标不是严格对应的,越靠近相机、物体越大,错位越明显。

解决这问题的标准做法是调用 rs2.align 做深度到彩色的对齐:把深度图重投影到彩色相机的视角下,让两幅图像的像素一一对应。代码就一行:

align = rs.align(rs.stream.color)

之后每次拿到 frames,先执行frames = align.process(frames),再从这个结果里取 color_frame 和 depth_frame。对齐后的 depth_frame 和 color_frame 尺寸相同,坐标可直接复用。需要注意 align 的方向:rs.stream.color表示把所有流都对齐到彩色流。如果你把参数写成rs.stream.depth,那就是把彩色图对齐到深度视角,检测框依旧不能直接用,很多项目就在这个看似不起眼的参数上翻车。

之所以能这么简单,是因为 RealSense 固件和 SDK 内部把 RGB 相机与深度相机的内参、外参都做了出厂标定,重投影时先反投影到三维,再投到目标视角,整个过程不需要我们自己算。这也是选 RealSense 而不是随便一个双目相机的原因之一:官方 SDK 把标定和重投影都封装掉了,省掉大量血泪调试。

2.4 检测框到深度值的映射:坐标系与两个易错顺序

深度图在 Python 里是一张 HxW 的单通道 numpy 数组。OpenCV 图像的索引顺序是先行后列,也就是img[y, x];而 YOLOv5 的检测框输出是 x1, y1, x2, y2,也就是 x 对应列、y 对应行。取深度时要写成depth_image[int(yc), int(xc)],而不是depth_image[int(xc), int(yc)]。这个顺序错掉之后,得到的不是目标的距离,而是斜对角某个点的距离,而且是“看似合理实则全错”的数据,最难排查。

取哪一点也有讲究。对行人、车辆、锥桶这类与地面接触的目标,我一般取检测框底部中点,因为那里对应目标与地面的接触点,深度值代表目标离相机最近的实体表面;取框中心点则容易落在行人躯干中间,如果穿黑色吸光衣服,深度可能直接无效。更稳的做法是在底部中点周围取一个 5x5 邻域,去掉 0 和异常值后取中位数。5x5 中位数比单点像素稳很多,能抵抗一两个像素的噪声,后面代码章会给具体实现。

另外,如果你需要的不是“距离”,而是目标在相机坐标系下的三维坐标,那就在取到深度 z 之后,用相机内参 fx、fy、cx、cy 把像素坐标反投影成 X、Y、Z。这是从二维检测到三维目标检测的自然延伸,但 YOLOv5 测距这个需求本身,到 z 值就够了。

3. 在 D435i 上实现 YOLOv5 测距:最小可复现代码

3.1 环境准备:pyrealsense2 与 YOLOv5 的版本搭配

先把环境列清楚:Python 3.8 或 3.10,pyrealsense2、opencv-python、numpy、torch。YOLOv5 的加载我用官方仓库配合 torch.hub,不需要把整个仓库 clone 下来。需要注意 pyrealsense2 的预编译包对较新的 Python 版本支持经常滞后,社区里常见的情况是 Python 3.11、3.12 装不上轮子,只能退回 3.8 或 3.10。如果打算在树莓派这类 ARM 板上部署,pyrealsense2 也有对应版本,但推理帧率要单独评估,测距流程本身不变。

pip install pyrealsense2 opencv-python numpy torch torchvision

torch 安装时建议按自己的 CUDA 版本选择安装命令,CPU 版也能跑 YOLOv5s,但 D435i 的 30fps 流在 CPU 上会比较吃力,推理帧率可能只有 5 到 10fps。如果你用的是 ultralytics 包加载 YOLOv8 或 YOLO11,接口类似,本文按 YOLOv5 的 xyxy 结果来写,逻辑可以直接平移。

模型加载:

import torch model = torch.hub.load('ultralytics/yolov5', 'yolov5s', pretrained=True) model.conf = 0.25 model.iou = 0.45

conf 和 iou 是 YOLOv5 后处理的超参数。conf 太低会出一堆误检框,太高会把远处小目标漏掉;iou 是 NMS 阈值,目标密集时适当调高。加载后模型会自动管理权重下载,网络不顺畅时可以先把 yolov5s.pt 放到 torch 缓存目录,或者改成从本地路径加载,避免每次启动都卡在下载上。

3.2 一版能直接跑的主循环代码

下面这版代码可以直接跑起来:RealSense 输出 RGB 和深度图,经过对齐后交给 YOLOv5 推理,每个检测框画出来并标注类别、置信度和距离。

import pyrealsense2 as rs import cv2 import numpy as np import torch pipe = rs.pipeline() cfg = rs.config() cfg.enable_stream(rs.stream.color, 640, 480, rs.format.bgr8, 30) cfg.enable_stream(rs.stream.depth, 640, 480, rs.format.z16, 30) pipe.start(cfg) align = rs.align(rs.stream.color) model = torch.hub.load('ultralytics/yolov5', 'yolov5s', pretrained=True) model.conf = 0.25 model.iou = 0.45 try: while True: frames = pipe.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 color_image = np.asanyarray(color_frame.get_data()) depth_image = np.asanyarray(depth_frame.get_data()) results = model(color_image) dets = results.xyxy[0].cpu().numpy() # Nx6: x1,y1,x2,y2,conf,cls for det in dets: x1, y1, x2, y2 = det[:4].astype(int) conf = float(det[4]) cls_id = int(det[5]) xc = (x1 + x2) // 2 yb = y2 patch = depth_image[max(0, yb - 2): yb + 3, max(0, xc - 2): xc + 3] valid = patch[(patch > 0) & (patch < 10000)] if valid.size == 0: continue dist_mm = float(np.median(valid)) label = f'{model.names[cls_id]} {conf:.2f} {dist_mm / 1000:.2f}m' cv2.rectangle(color_image, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(color_image, label, (x1, y1 - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imshow('yolov5 + realsense', color_image) if cv2.waitKey(1) & 0xFF == ord('q'): break finally: pipe.stop() cv2.destroyAllWindows()

逻辑说明:pipeline 启动时按 cfg 打开两个流;每次 wait_for_frames 拿到一组原始帧,先交给 align 重投影。对齐后取出来的 depth_frame 和 color_frame 尺寸一致,深度图直接用 numpy 索引。YOLOv5 的results.xyxy[0]是 Nx6 张量,每一行是 x1、y1、x2、y2、conf、cls,坐标已经映射回输入原图尺度,所以可以直接画框和索引深度图。

取深度时用底部中点作为锚点,切一个 5x5 区域,过滤掉 0 值和超过 10000mm 的远点,再取中位数。这样每个目标每帧得到一个稳定的距离值,所有框都会带上实时距离。参数说明:640x480@30fps 是 D435i 在 USB 3.0 下很稳的组合,再往上开 1280x720@60 很容易撞 USB 带宽;5x5 邻域、10000mm 上限、置信度 0.25 是我常用的起点,不要照抄,按实际场景调。

3.3 关键取舍:取中心点还是底部中点、深度值怎么聚合

测距锚点的选择直接影响数据的稳定性,我在不同场合用过三种方案,各有边界:

测距锚点优点缺点适用场景
中心点实现最简单容易落在躯干或头部,黑色吸光物体会读到 0目标较小、远离相机的场景
底部中点对应物体接触面,深度稳定检测框不准时误差较大行人、车辆、锥桶等地面目标
5x5 中值抗噪,单像素跳变不影响区域横跨两个物体边缘时会混合通用,我默认用这个

使用时要留意:YOLOv5 的检测框如果没有完全包住目标,底部中点可能落在目标边缘外,所以先确认检测质量,再做测距。测距读的不是框,是框背后那块深度。如果深度无效值占比太高,比如 5x5 区域内超过一半像素都是 0,我宁可让这一帧不输出距离,也不要给下游一个错误值。

3.4 多目标同时测距:把距离写进每个检测框

上面的循环天然支持多目标:每个框独立取深度,互不影响。如果要在画面上区分远近,可以把 dist 映射成颜色,距离越近越偏红,越远越偏蓝:

dist = dist_mm / 1000.0 color = (int(min(255, dist * 40)), 0, int(max(0, 255 - dist * 40))) cv2.rectangle(color_image, (x1, y1), (x2, y2), color, 2)

这只是可视化技巧,多个目标出现在画面里时能一眼看出前后顺序。真正的多目标测距难点不在单帧,而在连续帧里同一个目标的 ID 保持,那要接跟踪器,最后一章再展开。

4. 让距离读数可信:深度滤波、置信度阈值与标定验证

4.1 深度图不稳的三个来源:反光、黑体与物体边缘

主动红外的深度相机怕三类表面:黑色吸光、高反光和透明材质。黑色衣服、头发会让投射的红外散斑无法被相机有效识别,深度图上表现为空洞;镜面、白墙和车窗会出现反射干扰,深度值来回跳变;玻璃瓶这类透明物体会直接丢失有效深度。环境光过强也是大问题,尤其是户外太阳光,红外散斑被淹没,深度图会大片失效。

物体边缘是另一类问题:前景和背景之间深度突变,边缘像素经常混入背景值。YOLOv5 的框是垂直于图像坐标的外接矩形,检测框边界会压到背景上,如果在框边缘取深度,很容易取到后面背景的距离。这就是为什么我坚持用区域聚合而不是单点取值。

4.2 RealSense 官方滤波链路与推荐参数

pyrealsense2 内置了几种深度滤波算法:decimation_filter 降采样深度图、spatial_filter 做空间平滑、temporal_filter 做时间域平滑、hole_filling_filter 填洞。我常用的参数组合如下:

# decimation_filter 会降低深度图分辨率,测距场景默认注释掉 # dec = rs.decimation_filter() # dec.set_option(rs.option.filter_magnitude, 2) spat = rs.spatial_filter() spat.set_option(rs.option.filter_smooth_alpha, 0.5) spat.set_option(rs.option.filter_smooth_delta, 20) temp = rs.temporal_filter() temp.set_option(rs.option.filter_smooth_alpha, 0.4) temp.set_option(rs.option.filter_smooth_delta, 20) fill = rs.hole_filling_filter() fill.set_option(rs.option.holes_fill, 2)

处理顺序上,spatial、temporal、hole_filling 这类不改变分辨率的滤波器,可以在 align 之后逐帧处理;而 decimation 会把深度图缩小,如果要用,必须在 align 之前处理原始帧集,让 align 完成重投影。我在测距项目里一般不用 decimation,因为 640x480 的深度图配合 5x5 邻域已经够用,降采样后一个像素代表的物理区域变大,底部中点的定位精度反而下降。

滤波器解决什么关键参数副作用
decimation噪声和小洞,降分辨率换信噪比filter_magnitude=2分辨率减半
spatial空间噪声smooth_alpha=0.5, smooth_delta=20边缘轻微磨平
temporal时间抖动smooth_alpha=0.4, smooth_delta=20动态目标轻微拖影
hole_filling孤立空洞holes_fill=2边缘可能填充错误值

注意:hole_filling 只适合填补孤立的小洞。如果深度图大片失效,填洞只会编造出一片“看起来很合理实则没有测量依据”的距离,这种情况要直接判定为不可信,而不是依赖滤波器。

4.3 YOLOv5 后处理超参数怎么配合测距

模型 conf 默认 0.25,如果目标是远处小目标,建议降到 0.15 到 0.2,但要注意误检框也会带着距离一起上屏。误检框的深度值往往来自背景,一眼就能看出异常:一个“行人”在 8 米外的墙上。对测距来说,漏检比误检更可惜,漏检意味着这一帧这个目标完全没有距离输出,下游没法处理。

NMS 的 iou 阈值 0.45 对常见场景够用,人群密集时可以调到 0.5 到 0.55,但不要超过 0.6,否则两个挨着的目标很容易被合并成一个框,框中心落在两人之间,距离会读取背景值。imgsz 建议保持 640,不要为了帧率降到 320:深度图本身是 640x480,检测图片降低后坐标映射回原图时定位误差会被放大,底部中点取点质量下降。如果换了鸟类检测数据集,记得把类别过滤加上,只对需要测距的类别取深度,避免每帧对全类别输出无效数据。

4.4 用卷尺或激光测距仪校准:误差目标控制在 2% 以内

拿到距离后别急着接业务,先做一组标定。把相机固定,在 1m、2m、3m、5m 处放目标物,比如一个纸箱或一个人站在卷尺边上,记录算法输出的距离和真实距离的差值。我一般每个距离采 50 帧,算均值和标准差。如果均值偏差稳定在固定值,比如每次都近 3cm,可以做线性校正dist_corrected = dist_raw - 0.03;如果偏差随距离增大,说明深度标定有问题,检查相机是否摔过、是否需要重新做出厂标定。

标准差超过 5cm 的场景,先怀疑深度滤波没开,或者测距锚点选在了物体边缘。如果目标是行人,相机安装俯仰角会影响底部中点:安装角度倾斜较大时,框底部对应的物理点不是脚底接触点,而是地面前方一点,距离会偏大。这时要么调相机角度,要么对底部中点加一个固定像素偏移,具体数值以实测为准,这一步没法拍脑袋定。

5. RealSense 与 YOLOv5 测距的 5 个高频坑与避坑方案

5.1 检测框和深度图错位,距离取到背景上

现象:检测框精确框住行人,取出来的距离却是 4m、6m,和肉眼明显不符。

原因:最典型的是直接用未对齐的 color_frame 和 depth_frame。RGB 镜头和深度相机的物理位置不同,未对齐前,同一物体的像素坐标在两幅图里存在视差。还有一种隐藏原因:深度图被滤波器降采样过,分辨率变了,但检测框坐标还是按彩色图来的,两者对不上。

解决:统一用rs.align(rs.stream.color)处理后再取值。滤波器只处理对齐后的帧,如果用了 decimation 这类改变分辨率的滤波,要么在 align 之前处理并让 align 重投影,要么手动把坐标按分辨率缩放。我的检查顺序是:先关掉所有滤波器,确认基础对齐正确,再加滤波。

5.2 距离值突然变 0 或跳到最大量程

现象:目标明明在 2m 位置,距离显示 0.00m,或者跳到 9.9m 这种接近量程顶的值。

原因:0 是深度无效标记位,跳到最大量程往往是因为邻域内没有有效深度,中值滤波取到了背景或者空洞填充填出来的值。黑色衣服、黑色头发、玻璃表面都会触发。

解决:取深度前先过滤 0 和超过 10000mm 的值,无效值占比超过 50% 就放弃这一帧的目标距离,返回“不可信”,而不是给一个错值。再加 hole_filling 减少孤立 0 点。注意 temporal_filter 只能减少时间上的闪烁,救不了完全无效的像素。

5.3 USB 带宽不足导致帧率暴跌与深度横纹

现象:画面卡顿,深度图上出现横向条纹,帧率从 30 掉到 10 以内,RGB 和深度帧不同步。

原因:多个流同时开高分辨率高帧率会超过 USB 3.0 的带宽。常见误操作是同时开了 depth、color、left IR、right IR 四个流,还都跑在 1280x720@60。

解决:把 color 和 depth 都设成 640x480@30,只开这两个流。插到主板原生 USB 3.0 接口,扩展坞和 HUB 经常掉链子。如果还掉帧,把帧率降到 15fps,或者把 RGB 分辨率降低,但检测框定位精度会随之下降。

5.4 户外强光下 D435i 深度大面积失效

现象:室内距离正常,搬到太阳下深度图大片黑色噪声,检测框没问题但距离不可用。

原因:主动红外散斑在强烈阳光照射下信噪比急剧下降,红外相机拍到的主要是环境光,深度匹配失败。这不是 YOLOv5 或代码的问题,是 D435i 这类相机的物理限制。

解决:能避就避,把测光场景放室内或阴凉处。户外场景优先考虑 D455 这类激光方案,强光表现更好但同样有上限。在户外测距时,temporal_filter 能缓解闪烁,但救不了大片空洞;不要试图用 hole_filling 把大片无效区填满,那些值是编出来的,不能用。

5.5 中心点落在头发、眼镜或反光面上,距离瞬间漂移

现象:同一个站着不动的人,距离在 1.6m 和 2.3m 之间来回跳。

原因:框中心点如果恰好落在黑头发、眼镜框、反光皮肤上,深度像素无效或误匹配,单点取值就会漂移。YOLOv5 框本身是人体的外接矩形,中心点不一定落在有效深度区。

解决:用底部中点代替中心点,再套 5x5 中值。对戴帽子、长发遮挡的情况,底部中点仍可能落在边缘,可以尝试把测距点下沉到 y2 上方 2 到 4 个像素位置。连续测距阶段,保留最近 3 帧的距离做指数平滑,基本能消除单帧漂移。

6. 从单帧测距到连续测距:跟踪、平滑与误差验证

单帧测距能在画面上显示距离,但直接接到业务里,会发现连续流中距离抖动仍然不小,而且目标 ID 会来回跳。我一般会在测距后面加两级处理。第一级是 EMA 平滑,给每个目标维护一个距离状态,每次新值进来按比例混入:

ema = ema * 0.7 + dist_mm * 0.3

0.3 这个比例对 30fps 的流大概 5 帧能跟上目标变化,比直接输出单帧值稳得多。第二级是接目标跟踪器,比如 ByteTrack 或 deepsort,对每个 track 维护独立的 EMA 距离,ID 切换时重置。改完之后,测距输出从“每帧计算的散点”变成“每个目标连续的物理量”,才能交给避障或抓取逻辑使用。如果项目在 ROS 里做,把结果打包成带距离的目标列表发到话题上,就是“yolov5 + realsense 无人小车”这一类方案的常见做法。

结果记录也建议趁早做。每个目标输出一帧数据:帧号、类别 id、置信度、x1、y1、x2、y2、距离 mm,直接追加到 csv,后面离线分析误差不用重跑相机。验证时拿激光测距仪在 2m、4m、6m 各采 100 帧,对比均值与标准差。标准差小于 3cm、均值误差小于 2% 是这套方案能稳定达到的指标,如果没达到,先回第 5 章排查,不要急着换模型。

我的教训是:测距的精度瓶颈从来不在 YOLOv5,而在深度图上那一个像素背后是否有效。把深度图当作和 RGB 同样重要的一路信号来对待,配置滤波、验证对齐、保留无效值判断,比换更重的模型有效得多。希望帮到你。

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

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

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

立即咨询