简介:利用Intel RealSense深度相机与YOLOv5目标检测框架,提供一套可同时完成物体识别与距离测量的工程代码,面向计算机视觉开发者、机器人或安防测距场景的实践者。压缩包共46个文件、约3.53MB,包含18个Python脚本(如realsensedetect.py、detect.py、train.py)、8个YAML模型配置(覆盖YOLOv5s/m/l/x)、Dockerfile、Shell权重下载脚本以及README说明文档,便于快速搭建环境并理解项目结构。已有429人学习下载,资源以简洁的模块化设计呈现,可直接修改使用。项目内置模型导出与检测流程,并附带示例图片,用户可参考如何将深度图与目标框结合,输出类别、位置和距离信息,实现“识别即测距”的完整流程。适合熟悉YOLO系列、希望扩展深度相机应用的开发者作为实战参考。
1. 用realsense深度相机给yolov5目标检测补上第三维:先看懂这幅图在解决什么
很多做目标检测的人卡在同一个地方:模型能告诉你框里是什么,屏幕上的x、y坐标是有的,但它离我多远完全不知道。做避障、抓取、巡检时,这个第三维才是真正要的答案。标题里这个realsense深度相机加yolov5目标检测的组合,解决的就是把检测框从“像素坐标”升级成“像素坐标+米”。比起纯视觉方案,深度相机直接把三维信息给到程序里,不用靠标定板估单目距离,拿到检测框中心像素后查一下深度图就能输出距离。这套方案适合已经被yolov5环境配置折磨过、想在本地把realsense d435或d435i接进现有检测流程的开发者,也适合刚入门的人沿着一条主线跑通测距。
2. 跑通前的三件事:选对相机、搭好conda环境、搞懂深度对齐
2.1 D435还是D435i:测距场景下选型的核心区别
realsense系列里最常被拿来接yolov5的是D435和D435i。这两个型号在深度成像上几乎没有差别,都靠左右两个红外相机做立体匹配,配合一个主动红外投影仪在弱纹理区域打上纹理点,从而算出视差。D435i只是在主板上多集成了一颗IMU,能输出加速度和角速度,主要服务于SLAM、轨迹估计这类需要位姿融合的应用。对于标题里“检测到目标同时测出距离”这种需求,IMU帮不上什么忙,选D435就够了,价格更低,发热也更可控。
如果你手里已经有一块D435i,那就直接用它,不用纠结。真正要纠结的是“realsense d435i标定”这句话:标定分两个层面,一是双红外相机的出厂内参标定,二是IMU与相机之间的外参标定。出厂时内参已经写在设备里,pyrealsense2会直接读取,你不用跑标定程序;但如果你要做的是把检测结果和机器人里程计融合、长时间在高温或剧烈震动环境下工作,那IMU外参漂移会明显影响位姿估计。单纯做静态测距的话,先把标定放一边,信任出厂参数,后面遇到精度问题再回头查硬件。
选型还要注意使用距离范围。D435/D435i的有效深度测量范围大约在0.2米到3米之间,超出这个区间,深度值的噪声会快速放大。我见过有人把它架在楼道里测10米外的目标,测出来数据漂得厉害,这不是相机坏了,而是超出了立体视觉的可靠工作带。做测距实验前,先看一眼目标离相机的物理距离,别在3米外追求毫米级精度。
2.2 conda yolov5环境配置:pyrealsense2、torch和yolov5源码一次到位
这套方案的最小环境依赖就四样:pyrealsense2、opencv-python、torch、yolov5源码。我习惯先建一个独立conda环境,避免和已有的tensorflow或者老版opencv互相打架。
conda create -n rs_yolo python=3.9 -y conda activate rs_yolo pip install pyrealsense2 opencv-python numpy pip install torch torchvision git clone https://github.com/ultralytics/yolov5.git cd yolov5 pip install -r requirements.txtpyrealsense2是Intel RealSense官方控制仓库对应的Python绑定,装好后把相机插到USB 3.0口,在命令行里执行python -c "import pyrealsense2 as rs; print(rs.pipeline().doc)"能正常输出,就说明驱动和SDK已经通了。opencv-python负责图像读写和画框,numpy用于把RealSense返回的帧数据转成数组。torch和torchvision按你机器的CUDA情况装,先装torch再执行yolov5的requirements.txt是个小技巧,因为requirements里也会声明torch依赖,提前装好可以避免它把GPU版torch覆盖成CPU版。
yolov5环境配置最常见的翻车点不是模块缺漏,而是版本错位。比如pyrealsense2依赖的numpy版本和yolov5里要求的numpy版本冲突,pip在装requirements时会提示“numpy 2.x is incompatible”。遇到这种情况不要硬改版本号,直接在环境里重新pip install numpy==1.26.4这类完全兼容的版本,然后再跑一遍验证脚本,比逐个试错快得多。
2.3 深度图与彩色图对齐:所谓“同时测距”的前提是把两个坐标系对齐
为什么需要对齐?因为realsense的彩色相机和深度相机是两个物理传感器,它们的光心位置、朝向都不一样。彩色图里检测框坐标是(u, v),深度图里同一物理点对应的像素位置并不在同样的(u, v)上。如果不做任何处理,拿着yolov5给出的检测框坐标直接去深度图里取深度值,误差可能达到几十个像素,近处目标到测出来的距离可能偏到完全不可用。
pyrealsense2里处理这件事的组件叫align。它利用设备固件里保存的相机内参和两个传感器之间的外参,把深度图重投影到彩色相机的视角下。对齐之后,彩色图里的像素坐标就能直接作为索引去访问深度图数组,两个图的分辨率相同,每个像素一一对应。这个“查表”动作就是标题里“同时测出距离”的本质:目标检测产生2D框,深度对齐让2D坐标能对应到3D距离。
深度图的数据格式也要搞清楚。d435输出的depth stream默认是z16格式,也就是每个像素用16位无符号整数表示深度值,这个值不是直接的物理距离。要转换成米,必须乘一个系数,叫depth_scale,D435出厂默认大约是0.001,也就是深度值3000表示3米。这个系数通过get_device().first_depth_sensor().get_depth_scale()读取,不要自己硬编码,因为不同固件可能给出不同值。另外深度值为0的像素代表无效测量,不是距离0米,这一点后面取深度时需要过滤。
3. 把yolov5检测框和realsense深度图接在一起:最小可用的测距主循环
3.1 主循环结构:读帧、对齐、推理、取深度、画框标注
环境配好后,先跑通一个最小循环。下面这段代码把realsense的彩色流和深度流同时打开,对齐后喂给yolov5,拿到每个检测框后取中心像素对应的深度值并换算成米,最后用OpenCV画框显示。
import cv2 import numpy as np import torch import pyrealsense2 as rs # 初始化RealSense管线,同时开color和depth流 pipeline = rs.pipeline() config = rs.config() config.enable_stream(rs.stream.color, 640, 480, rs.format.bgr8, 30) config.enable_stream(rs.stream.depth, 640, 480, rs.format.z16, 30) profile = pipeline.start(config) # 把深度图重投影到彩色图坐标系,这是坐标一致性的关键 align = rs.align(rs.stream.color) depth_scale = profile.get_device().first_depth_sensor().get_depth_scale() # 加载yolov5官方模型,也可以换成自己训练的权重 model = torch.hub.load("ultralytics/yolov5", "yolov5s", pretrained=True) model.conf = 0.35 model.iou = 0.45 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 color_image = np.asanyarray(color_frame.get_data()) # BGR rgb_image = cv2.cvtColor(color_image, cv2.COLOR_BGR2RGB) depth_image = np.asanyarray(depth_frame.get_data()) # uint16 results = model(rgb_image) for det in results.xyxy[0]: x1, y1, x2, y2 = [int(v) for v in det[:4]] conf = float(det[4]) cls = int(det[5]) # 检测框中心查深度图,乘depth_scale换算成米 cx, cy = (x1 + x2) // 2, (y1 + y2) // 2 dist_meters = depth_image[cy, cx] * depth_scale label = f"{results.names[cls]} {conf:.2f} {dist_meters:.2f}m" cv2.rectangle(color_image, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(color_image, label, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imshow("realsense_yolov5_distance", color_image) if cv2.waitKey(1) & 0xFF == ord("q"): break pipeline.stop() cv2.destroyAllWindows()代码的执行顺序是固定的:先pipeline.start启动设备,再align.process对齐帧,然后跑yolov5推理,最后用检测框坐标去depth_image里取值。这里有一个容易忽略的点:我把color_image转了RGB再喂给模型,但画框用的是原始的BGR图。pyrealsense2的color stream我配置成bgr8格式,如果你设成rgb8,那就不需要cvtColor这一步。yolov5官方torch.hub接口本身能自动处理常见颜色顺序,但显式转换能保证你从RealSense拿到的数据和自己用cv2.imread读图时的行为一致,排错时少一个变量。
results.xyxy[0]里每一行是[x1, y1, x2, y2, confidence, class_id],坐标已经映射回原图尺寸,因为torch.hub内部做了letterbox并会把坐标还原。如果你脱离torch.hub、自己写预处理,就必须自己做letterbox的逆变换,不然检测框坐标和原图对不上,取到的深度自然全错。这是这套流程里最容易出现“检测很正常但距离完全错误”的环节。
3.2 检测框中心点的深度值怎么取:二维框坐标查深度图
取距离的思路很简单:检测框中心像素在彩色图里是(cx, cy),经过align后深度图与彩色图对齐,所以depth_image[cy, cx]就是这个像素的深度原始值,乘depth_scale就是米。代码里demo只用单点,这是最朴素的实现,足够验证链路通不通。
但单点取值有几个隐含前提:目标表面在这个像素处必须有有效深度,且中心点不能落在目标边缘。如果你在调试时发现输出0.00m,先不要怀疑公式,去看depth_image[cy, cx]是不是0。深度值为0有几种情况:目标太近或太远超出测量范围,表面是镜面或纯黑色吸光材质,目标很薄导致红外投影仪打不出纹理。D435的红外投影仪在近距离上对白色墙面、纸箱这类低纹理物体效果很好,但对透明塑料瓶、黑色轮胎、阳光直射下的物体经常给不出有效值。
另外一个细节是depth_image是二维uint16数组,访问顺序是行在前列在后,也就是depth_image[cy, cx]而不是depth_image[cx, cy]。OpenCV里图像数组也是这个顺序,但很多人刚从三维坐标习惯切过来时会在这一行线上犯迷糊。写成行的形式就是在检测框中心画一个十字线,把中心点可视化,深度值是否对应当前目标,一眼就能看出来。
3.3 yolov5超参数与推理设置:conf、ioup和imgsz的取舍
yolov5的推理参数对测距结果的影响不在深度侧,而在“有没有测对目标”这一侧。model.conf是置信度阈值,低于这个值的检测框会被丢掉;model.iou是NMS的IoU阈值,控制重叠框的去留。测距场景里我一般把conf调到0.25到0.35之间,比纯入稿调低一点。理由很直白:测距的最终消费者往往是避障逻辑或机械臂抓取,漏检的代价远高于误检,宁可让一个可疑目标进入距离计算,也不能让真目标完全消失。
model.imgsz控制输入分辨率,默认640。如果你用的是yolov5s、跑在普通独立显卡上,640分辨率能到30帧每秒左右;如果是树莓派或者CPU推理,建议降到416甚至320,帧率提升比重新换小模型还明显。降分辨率会影响小目标检测能力,但对常见的中大型目标影响有限。测距任务里还有一个被忽略的参数是max_det,默认1000,实际使用中限制成10到20个就够,能略微减少后处理耗时。
如果你训练了自己的数据集,注意类别名称对不上的问题。torch.hub加载本地权重时要用model = torch.hub.load("ultralytics/yolov5", "custom", path="best.pt"),然后results.names就会带着训练时的类别名,代码里取names[cls]才不会报越界。yolov5后处理部分,wanst该改的只有conf和iou,其他参数保持默认往往比乱调更稳。
4. 从“能测”到“测得稳”:多点采样、中值滤波和失效回退
4.1 单点测距为什么翻车:中心像素落在背景或空洞
第3节的demo能跑通,但实际部署时会发现一个很烦人的问题:检测框明明稳稳地框住目标,输出的距离却在目标与背景之间来回跳。原因多数出在中心点上。目标检测框是矩形,中心像素不一定落在目标实体上,可能是目标的镂空处、两个物体之间的缝隙、目标的弧形表面边缘,甚至是目标前面伸出来的一根细枝。中心点到目标中心的距离没问题,一旦落到缝隙里,深度值就变成背景的距离。
另一个重要来源是深度空洞。立体匹配在遮挡边界和低纹理区域会产生无效像素,D435给出的深度图上这类空洞很常见。中心像素一旦遇到空洞,深度值就是0,单点逻辑直接失效。我自己的经验是:单点测距适合实验室验证,不适合任何真实场景。真实场景至少要用一个区域来代替一个点,这个区域就是检测框本身。
4.2 多点采样加中值:把检测框内有效深度值做统计
把取点改成取区域,核心是先用检测框裁剪深度图,过滤掉无效值,再做统计。下面的函数是对中心单点的直接替换:
def robust_depth_at_box(depth_image, depth_scale, x1, y1, x2, y2): roi = depth_image[y1:y2, x1:x2] # 过滤无效深度,0值代表测量失败 valid = roi[roi > 0].astype(np.float32) if valid.size == 0: return None # 中值对离群点更鲁棒,均值容易被边缘噪声拉偏 dist_meters = float(np.median(valid)) * depth_scale return dist_metersmedian和mean的选择值得说一句。深度图的噪声不是均匀的高斯噪声,而是受反光、遮挡、边缘影响产生的大幅离群值。一组深度值里如果混进几个背景值,均值会明显被拉偏,而中值几乎不受影响。对于前方障碍物测距,如果目标是避障逻辑的输入,还可以把median换成percentile,取有效值的10%分位数,代表“离我最近的可靠距离”,这样对凸出的遮挡物更敏感。
框内采样的边界也需要控制。检测框很大时,比如一个人体框,目标本身只占中间一部分,全框采样会把左右两侧的背景墙面也算进去,中值虽然能压住噪声,但测出来的距离仍会偏大。我一般会先按比例收缩检测框,保留中心区域再做统计。具体比例没有定式,常规做法是取框宽高的40%到60%作为中心矩形,避开边缘干扰。
4.3 深度失效回退:当前帧没有有效深度时不能直接输出0
即使做了区域统计,依然会遇到整块区域都没有有效深度的情况。比如目标表面是黑色吸光材质,或者目标进入强红外干扰环境。这时函数返回None,调用方必须有回退逻辑,不能把None当成0.00m输出。一个简单的做法是保留上一帧有效值,并记录连续失效次数:
import collections last_valid = {} fail_count = collections.Counter() for det in results.xyxy[0]: x1, y1, x2, y2 = [int(v) for v in det[:4]] cls = int(det[5]) obj_id = cls dist = robust_depth_at_box(depth_image, depth_scale, x1, y1, x2, y2) if dist is not None: last_valid[obj_id] = dist fail_count[obj_id] = 0 else: fail_count[obj_id] += 1 # 连续5帧无有效深度才放弃,否则沿用旧值 dist = last_valid.get(obj_id, None) if fail_count[obj_id] > 5: dist = None这里按类别做了区分,不同类别各自维护上一帧距离,避免一个目标失效导致所有类别的距离被同一个旧值污染。连续帧数这个参数可以根据你的帧率调整,30帧每秒的情况下连续5帧失效约等于目标丢失0.17秒,视觉上不会察觉距离跳动。如果目标经常被遮挡,比如巡检机器人抓拍动态目标,可以把阈值放宽到10帧,换取更平滑的输出。
时间维度上的滤波同样重要。深度相机单帧噪声在0.5米到2米范围内通常是几个毫米到一两厘米,但叠加温漂和动态场景后,同一目标的读数值会有肉眼可见的抖动。常见的做法是用一个定长deque缓存最近几帧的距离值,取中值作为最终输出:
d = collections.deque(maxlen=5) d.append(dist_meters) smoothed_dist = float(np.median(d))这种滑动中值滤波比简单指数平滑更稳,因为它不会让一次严重的跳变缓慢“拖尾”,而是直接把它当作离群点剔除。
5. 常见问题排查与避坑记录:深度黑屏、测量跳动的五个真实原因
5.1 深度图一片黑,检测框中心深度值为0
现象:彩色图正常,运行窗口里target距离一直显示0.00m,打开深度可视化看到整块区域是黑的。原因:深度值全部是0,硬件层面没拿到有效深度。常见诱因包括目标距离小于0.2米超出最近测量范围、目标表面是透明或强反光材质、红外投影仪被遮挡、USB带宽不足导致深度流降帧。解决:先把目标放在0.3米到1.5米的正常范围,用官方查看器开一下active infrared选项看是否有红外纹理;确认USB线插在主板原生USB 3.0口而不是扩展坞上;代码层面在区域统计中过滤值为0的像素,并用上一帧有效值做回退,而不是让0参与运算。
5.2 RGB图和深度图错位导致测距整体偏差
现象:检测框稳稳框住目标,距离却是目标旁边墙面或背景物体的读数,而且偏差方向和大小固定。原因:直接用未对齐的原始深度图索引彩色图坐标,或者aligned_frames没处理好就进入下一帧。很多时候是代码里忘了调用align.process,直接把frames.get_depth_frame()拿来做坐标映射。解决:统一走aligned_frames,并且在每次wait_for_frames后都重新process,对每一帧都重新对齐。另外注意depth_frame和color_frame都来自同一个aligned_frames对象,不要混用原始frames里的帧。
5.3 静止目标的测距结果反复跳动
现象:目标放在桌上不动,输出距离在0.8m到1.2m之间来回晃,肉眼可见地不稳定。原因:单帧深度噪声、立体匹配在低纹理区域的不确定性、边缘像素把背景值带进统计区域。深度相机的原始输出本身就不是稳定的逐像素“真值”,而是带空间和时间噪声的观测值。解决:先加后处理滤镜,pyrealsense2自带的spatial filter和temporal filter对静态场景改善明显,代价是动态目标会有轻微拖影;再加滑动中值滤波,把最近5帧到10帧的距离做排序取中值;最后检查采样区域是否覆盖了检测框边缘,收缩到中心区域能显著降低背景干扰。
5.4 USB带宽与供电不足引发的间接故障
现象:pipeline.start()偶发性失败,wait_for_frames报“Frame didn't arrive within N ms”,或者彩色图正常但深度图帧率只有个位数。原因:D435是USB 3.0设备,在USB 2.0口或劣质扩展坞上带宽不够,深度和彩色两个流抢通道,导致深度帧丢失。解决:插主板原生USB 3.0口,不要插机箱前面板转接口;必须用扩展坞时选支持USB 3.0 Gen1以上的;代码层面把深度流和彩色流统一降到640x480@15fps或320x240@30fps,带宽占用减少一半以上。这条坑最容易出现在笔记本上,电源策略还会限制USB口供电,必要时把电源模式调到高性能。
5.5 出厂参数与d435i标定:什么时候才需要认真对待
现象:近距离测距误差在几厘米内,距离拉到2米后误差明显膨胀,甚至固定偏移几十厘米。原因:立体视觉的误差本身随距离平方增长,这属于物理限制;另一个原因是相机内部结构受温漂影响,出厂标定的内外参数在温度变化大的环境中会产生偏移。解决:先排除物理因素,在1米和2米各测几组数据,计算平均误差而不是单次误差;如果误差稳定且偏大,考虑让相机开机预热10分钟再工作;如果对精度有强需求,比如机械臂抓取或毫米级测量,再去做d435i标定流程,普通目标检测测距场景下优先保证相对距离稳定,不要追求绝对值做到仪器级精度。
6. 用卷尺给测距结果验验光:精度验证、深度可视化和部署延伸
6.1 用测量仪器做对照实验:误差算出来心里才有底
深度相机测出来的距离不是一个“绝对正确”的数字,工程上需要知道它在什么范围内可信。我习惯的做法是准备一台激光测距仪或一把卷尺,把目标物依次摆到0.5米、1.0米、1.5米、2.0米、2.5米的位置,每个位置连续记录50帧距离值,统计平均值和标准差。下面这个表是一个典型的实验记录模板:
| 设定距离(m) | 平均输出(m) | 标准差(m) | 误差(m) |
|---|---|---|---|
| 0.50 | 0.52 | 0.02 | +0.02 |
| 1.00 | 1.03 | 0.03 | +0.03 |
| 1.50 | 1.55 | 0.05 | +0.05 |
| 2.00 | 2.10 | 0.08 | +0.10 |
表格里的数值是示意,但趋势是真实的:距离越近,绝对误差越小;超过2米后标准差和误差都明显增大。如果测出来的误差稳定在同一个方向,说明存在系统偏差,可以在输出侧做一次线性校正;如果误差正负随机,那就要靠滤波和后处理来压噪声。
6.2 深度可视化:把深度图变成能肉眼检查的图像
调试时只看打印出来的数字不够直观,把深度图转成伪彩色图叠加显示,能立刻发现空洞和错位问题。常用的做法是把uint16的深度图缩放后映射到COLORMAP_JET:
depth_visual = np.clip(depth_image * depth_scale, 0, 3) # 只显示0~3米 depth_visual = (depth_visual / 3.0 * 255).astype(np.uint8) depth_color = cv2.applyColorMap(depth_visual, cv2.COLORMAP_JET)注意这里是“可视化用的深度图”,不是真正用来计算距离的depth_image。JET色图中蓝色代表近、红色代表远,你很容易看出检测框内有没有黑色空洞。调试完再决定要不要把这一路叠加显示关掉,毕竟彩色图加伪彩图双路显示对帧率有影响。
6.3 向嵌入式部署延伸:树莓派和低算力设备上的取舍
这套方案不是只能跑在台式机上。树莓派5上部署自己训练的yolov5模型时,最大的瓶颈在推理耗时和USB带宽两块。模型优先换yolov5n或量化版,输入尺寸降到320x320,实测能在单片机上跑到接近实时;深度流和彩色流分辨率降到640x480@15fps,避免USB带宽不够导致深度帧丢失。测距逻辑不变,只是要把“连续5帧无有效深度才回退”这个参数调大,因为帧率降了一半,5帧对应的真实时间翻倍了。
我在做这类项目时有一个习惯:把每个检测框的距离字段整理成结构化字典输出而不是直接画在图上,这样无论接机械臂控制还是存日志都好用。后来证明这个习惯省了大量调试时间,因为测距逻辑本身和可视化逻辑经常要分开验证。这套组合里最花时间的往往不是模型本身,而是深度图和检测框之间那层数据形态的对齐与过滤,希望帮到你。
本文还有配套的精品资源,点击获取