简介针对英特尔RealSense深度相机D435i、D435、D415的数据采集演示项目面向机器视觉、机器人导航、增强现实与三维扫描等领域的开发者和研究者解决如何稳定获取二维彩色/红外图像与三维点云原始数据的问题。压缩包共15个文件涵盖C工程源码、CMakeLists构建配置、YAML相机参数、PNG示例图像、三类点云数据压缩包PCD/PLY/XYZRGB及Markdown说明文档整体仅23.47MB目录结构清晰便于查阅或二次开发。目前已有856人学习或下载项目已在D435i、D435、D415等设备上验证通过整体工程可直接编译运行。通过该演示程序使用者可快速掌握相机初始化、参数调节、数据流读取与输出等关键方法获得彩色图、红外图、深度图及三维点云数据这些数据可直接用于目标检测、三维重建、机器人避障等算法验证为空间感知类智能应用提供可靠基础对算法测试和空间感知应用开发尤为重要。1. 通过英特尔RealSense设备获取2D图像和3D点云等原始数据先把管线顺序摆对再谈算法第一次拿到D435i我用官方查看器点了几下RGB、深度、红外同时铺在屏幕上感觉这设备没难度。等真开始写代码才发现通过英特尔RealSense设备获取2D图像和3D点云等原始数据这件事顺序错了就是不行。有人先转灰度再做掩码有人直接把深度帧当普通图像读还有人把深度内参和彩色内参混着用结果点云要么错位要么尺寸离谱。这篇笔记把我拆过的一套采集工程讲透从SDK环境、管线配置、内参映射到点云反投影和批量验证新手照着能跑熟手可以直接跳去避坑章节。适合做三维重建、机械臂抓取、视觉SLAM以及任何需要干净深度数据的场景。2. 采集环境与相机选型确认手里的D435i到底多了什么2.1 快速分辨D435和D435iIMU只在后缀带i的型号上D435和D435i硬件差异实际很小光学模组几乎一样唯一的明显区别就是D435i多了一颗6轴IMU可以输出加速度和角速度。别小看这个差异如果你的项目里要做视觉惯性里程计或者给机械臂做运动补偿IMU是刚需如果只是静态扫描桌面物体那D435就够用。很多同学把两个型号当成同一个设备去搜视频教程看到D435能跑就以为D435i也一样结果要IMU数据时才发现设备枚举里根本没有运动传感器。选型时要留意几个点深度分辨率、彩色分辨率、同时开启的限制和USB带宽。D435i的深度图像最大能到1280x720帧率最高90fps彩色能到1080p30fps但在USB3.0带宽下深度和彩色同时跑到最高组合几乎不可能。我一般会固定到640x48030fps起步调试通过以后再往上调。D455这类升级型号在近距离精度上有改进但它的深度窗口和RGB窗口做了硬件级同步是否带IMU要以官方参数表确认不要想当然认为所有深度相机都带惯性单元。还需要理解RealSense的深度原理它是主动红外立体不是ToF。左右两个红外相机形成基线结合红外投影仪投射的散斑图案来估计深度。深度引擎在设备端完成CPU几乎不参与所以从SDK拿到的是已经计算好的16位深度图而不是传感器裸数据。很多人看到“raw”就去找raw12、raw16其实在SDK语境下depth、color、infrared、gyro、accel这几类帧就是最原始的可用数据。参数项D435D435i说明深度分辨率最高1280x720最高1280x720组合受USB带宽限制RGB分辨率最高1920x1080最高1920x1080同时开高分辨率会掉帧IMU无有6轴IMU加速度角速度深度单位z16毫米z16毫米需要用depth_scale换算典型场景静态重建VIO/抓取/在线重建D435i覆盖更多感知需求D435i在设备枚举后通常会有多个传感器节点一个深度传感器、一个彩色传感器、一个运动传感器。如果你只按照网络上的教程读取depth和color大概率没碰到IMU但真正做融合的时候会发现IMU的数据流策略完全不同。我在资源包里会额外提供imu_stream示例这里先记住一点IMU流不需要设置分辨率直接设频率常见做法是200Hz读取时用motion_frame而不是frameset.get_depth_frame()。2.2 安装librealsense与pyrealsense2版本匹配决定你踩不踩坑写代码前先把SDK装对。Python环境建议用虚拟环境避免系统python和项目依赖互相污染。最常见的安装方式是直接装预编译包# Python绑定直接装预编译包 pip install pyrealsense2 # 确认安装后的版本 python -c import pyrealsense2 as rs; print(rs.__version__)预编译包的好处是省事缺点是不一定和新版固件完全匹配。旧SDK连接新固件可能出现设备枚举失败我踩过一次排查半天发现是SDK版本和固件版本差太多。所以拿到设备后第一步应该先看固件版本# 固件工具列出连接的设备信息 rs-fw-update -l在Python里也可以查这样代码和命令行都能用import pyrealsense2 as rs ctx rs.context() devices ctx.query_devices() for dev in devices: print(dev.get_info(rs.camera_info.name)) print(dev.get_info(rs.camera_info.firmware_version)) print(dev.get_info(rs.camera_info.serial_number))这段代码从设备名、固件版本、序列号三个维度确认相机是否正常。query_devices()返回一个list一台设备也会返回长度1的list不要直接取下标[1]导致越界。查不到设备时优先检查USB接口RealSense深度流必须USB3.0插在USB2.0接口上经常只能枚举、开不了深度流。版本匹配还有一个容易踩的坑pip的pyrealsense2包和系统librealsense运行库版本不一致时模块虽然能import但部分选项不生效。我一般会统一用pip包不额外装系统运行库避免两个版本叠加。如果你在Linux下装了多个版本的librealsense优先更新一遍udev规则否则设备权限不够每次都要sudo才能跑。还有一类常见报错值得提前说启动时如果提示Couldnt resolve requests大概率是分辨率、帧率、流类型组合超过了USB带宽。这时候别怀疑设备坏了先把其中一个流的分辨率降下来。如果提示Device is busy说明另一个进程占用了设备最常见的是官方查看器还开着或者上一个Python进程没有正常pipeline.stop()就退出了。3. 2D图像获取RGB、深度与红外帧的管线设置3.1 最小可用的数据管线分辨率、帧率和深度预设先给一套能直接跑的最小管线后面所有数据都从这里出来import pyrealsense2 as rs import numpy as np import cv2 # 创建pipeline和配置对象 pipeline rs.pipeline() config rs.config() # D435i上可用的四类流depth, color, infrared, gyro/accel config.enable_stream(rs.stream.depth, 640, 480, rs.format.z16, 30) config.enable_stream(rs.stream.color, 640, 480, rs.format.bgr8, 30) config.enable_stream(rs.stream.infrared, 1, 640, 480, rs.format.y8, 30) # 启动后记录profile后续读取内参要用 profile pipeline.start(config) # 前几帧用来让自动曝光稳定 for _ in range(5): frames pipeline.wait_for_frames() try: for i in range(30): frames pipeline.wait_for_frames() depth_frame frames.get_depth_frame() color_frame frames.get_color_frame() ir_frame frames.get_infrared_frame() if not depth_frame or not color_frame: print(f{i}: no frame) continue depth_image np.asanyarray(depth_frame.get_data()) color_image np.asanyarray(color_frame.get_data()) ir_image np.asanyarray(ir_frame.get_data()) if ir_frame else None if i 29: cv2.imwrite(color.png, color_image) finally: pipeline.stop()这段代码的逻辑很直白pipeline.start(config)按配置启动所有流wait_for_frames()阻塞等待一组同步到达的帧get_depth_frame()和get_color_frame()分别取出对应帧。需要说明的是wait_for_frames()返回的是原始帧组RGB和深度各自有独立的时间戳并不保证同一时刻。np.asanyarray(frame.get_data())得到的是numpy数组视图它背后是相机的内存缓冲pipeline.stop()之后不要继续引用否则数据可能变成野指针。参数说明z16是16位无符号整数深度格式单位是毫米bgr8是OpenCV习惯的彩色顺序如果直接存jpg不会偏色红外流infrared需要指定编号1或2对应左右两个红外相机这里只开1号流。640x48030fps是一组比较稳妥的开发参数兼顾分辨率和带宽。如果同时开两个红外流帧率会明显下降。深度预设也是这段管线的关键参数。有些同学觉得深度图噪声大就想着自己去滤波其实设备端已经内置了多种深度预设只是默认值不一定适合你的场景。# 获取深度传感器 depth_sensor profile.get_device().query_sensors()[0] depth_scale depth_sensor.get_depth_scale() # 查询visual preset取值范围再设置 preset_range depth_sensor.get_option_range(rs.option.visual_preset) print(preset min/max:, preset_range.min, preset_range.max) # 0Custom, 1High Accuracy, 2High Density, 3Balanced depth_sensor.set_option(rs.option.visual_preset, 3)这里有个细节query_sensors()返回设备的传感器列表第一项通常是深度传感器但有的设备还带运动传感器顺序不保证。稳妥的做法是按传感器类型过滤或者直接用first_depth_sensor()。visual_preset直接影响深度引擎的平滑策略High Accuracy对静态目标更稳High Density保留更多边缘细节但噪声更大。改完预设建议重新start pipeline否则部分固件不生效。3.2 内参与元数据读取不要只取图不看参数很多采集代码只保存了图像没保存内参等后面做点云重建才发现相机标定数据丢了。RealSense的内参不是固定的同一型号也有个体差异必须从当前设备的stream profile里读。# 从pipeline启动时返回的profile里取深度流内参 depth_profile profile.get_stream(rs.stream.depth) intr depth_profile.as_video_stream_profile().get_intrinsics() print(depth intrinsics: fx{:.2f} fy{:.2f} ppx{:.2f} ppy{:.2f}.format( intr.fx, intr.fy, intr.ppx, intr.ppy)) # 彩色内参独立获取 color_profile profile.get_stream(rs.stream.color) cintr color_profile.as_video_stream_profile().get_intrinsics() print(color intrinsics: fx{:.2f} fy{:.2f} ppx{:.2f} ppy{:.2f}.format( cintr.fx, cintr.fy, cintr.ppx, cintr.ppy))D435i的深度内参和彩色内参差异明显fx/fy接近但ppx、ppy不同。后面生成点云时如果直接用彩色内参处理对齐后的深度图就没问题但很多人拿深度内参去投影彩色对齐后的深度图结果边缘错位。还有一个必须记录的量是depth_scale。它表示深度图每个单位对应的米数D435i通常返回0.001也就是深度像素值10000等于10米。但不要想当然固件不同这个值可能有变化一定要运行时读取# 深度缩放比例深度值乘以它得到米 depth_scale profile.get_device().first_depth_sensor().get_depth_scale() print(depth scale (m per unit):, depth_scale)另外帧的元数据也可以用来做时间戳对齐。get_timestamp()单位是毫秒get_frame_number()是自增帧号。批量采集中我用它来判断RGB和深度是否是同一时刻采集误差超过5ms的帧对就丢弃。print(depth ts:, depth_frame.get_timestamp(), depth frame no:, depth_frame.get_frame_number()) print(color ts:, color_frame.get_timestamp(), color frame no:, color_frame.get_frame_number())提示深度图的像素值不等于真实距离。先把depth_scale乘上去再做任何几何计算否则点云尺寸会差三个数量级。4. 3D点云生成从深度图像到点云的正确姿势4.1 用自己的内参反投影先搞懂每一步再交给库函数点云生成的数学并不复杂对于深度图像中的每个像素(u, v)深度值为d相机坐标就是X (u - ppx) / fx * dY (v - ppy) / fy * dZ d其中d必须是从depth_scale换算出的米。用Python写一个向量化版本既快又容易排查问题def depth_to_pointcloud(depth_frame, intrinsics, depth_scale): # 深度帧转numpy并换算成米 depth np.asanyarray(depth_frame.get_data()).astype(np.float32) depth depth * depth_scale h, w depth.shape # 生成像素网格坐标 u, v np.meshgrid(np.arange(w), np.arange(h)) # 反投影到相机坐标 x (u - intrinsics.ppx) / intrinsics.fx * depth y (v - intrinsics.ppy) / intrinsics.fy * depth z depth points np.stack([x, y, z], axis-1) # 过滤无效深度0值不可用过近和过远也去掉 valid_mask (depth 0) (depth 0.1) (depth 3.0) points points[valid_mask] return points这段代码把反投影写成了纯numpy操作不存在Python循环几百万像素的深度图也可以秒级完成。meshgrid生成和深度图尺寸一致的坐标表np.stack把XYZ叠成(H, W, 3)最后用有效掩码过滤。注意我是先乘depth_scale再做投影如果顺序反了x和y会同步放大点云整体比例全错。过滤条件里加上物理范围很有必要。D435i近距测量范围大约0.1米起跳超过3米后精度下降所以我把无效区间直接裁掉。如果做远距离重建可以把上限调成5米但不要一次性放到10米那个距离的深度值噪声很大。4.2 使用Open3D生成与保存点云省事但有三处易错自己反投影适合理解原理实际项目中用Open3D更省事尤其是带彩色贴图的点云。但Open3D的接口有几个坑最容易翻车的是depth_scale参数的含义。import open3d as o3d def real_depths_to_pcd(color_image, depth_image, intrinsics): h, w depth_image.shape # 构建Open3D针孔内参 o3d_intr o3d.camera.PinholeCameraIntrinsic( w, h, intrinsics.fx, intrinsics.fy, intrinsics.ppx, intrinsics.ppy) # 这里depth_scale1000表示深度值除以1000得到米 # 不要直接把pyrealsense2的0.001填进来 depth_o3d o3d.geometry.Image(depth_image) color_o3d o3d.geometry.Image(color_image) rgbd o3d.geometry.RGBDImage.create_from_color_and_depth( color_o3d, depth_o3d, depth_scale1000.0, convert_rgb_to_intensityFalse) pcd o3d.geometry.PointCloud.create_from_rgbd_image(rgbd, o3d_intr) return pcdOpen3D的depth_scale是“把深度像素值除以该值得到米”所以D435i的z16毫米值要填1000.0而不是0.001。这两个参数方向相反我见过不少人都折在这一点上。如果填错点云尺寸会放大1000倍一个5厘米的小盒子在点云里看起来像50米高的大楼。第二个注意点是彩色图和深度图的分辨率。create_from_color_and_depth要求两张图尺寸一致否则会直接抛异常或者生成空点云。我自己写采集脚本时会先做一步对齐处理# 把深度图对齐到彩色坐标系 align_to_color rs.align(rs.stream.color) frameset align_to_color.process(frames) aligned_depth_frame frameset.get_depth_frame() color_frame frameset.get_color_frame() depth_aligned np.asanyarray(aligned_depth_frame.get_data()) color_aligned np.asanyarray(color_frame.get_data())rs.align的作用是把不同传感器采集的帧映射到同一个坐标系和分辨率。对齐后的深度图对应的内参应该是彩色内参而不是深度内参。很多示例代码没有强调这一点导致点云贴图边缘对不齐。第三个注意点是坐标系。D435i的相机坐标是右手系x向右、y向下、z向前Open3D默认可视化也是这个方向大部分场景不需要额外翻转。但如果把深度图保存成npy再用其他软件加载对方一旦用了图像坐标系就可能出现左右镜像。解决办法是在最终点云上做pcd.transform([[1,0,0,0],[0,-1,0,0],[0,0,-1,0],[0,0,0,1]])这一步取决于目标工具不是必须的。保存点云用一行代码# 保存带颜色的PLY点云 o3d.io.write_point_cloud(frame.ply, pcd, write_asciiFalse) print(point count:, len(pcd.points))保存下来的PLY可以直接拖进常用点云查看工具。如果你只需要点坐标不带颜色也可以从反投影函数里拿points直接保存成npynp.save(points.npy, points)5. 常见问题与排查帧同步、空指针、点云尺寸异常都怎么处理5.1 帧获取与对齐时的三个典型坑坑1彩色和深度时间戳对不上。现象快速移动手部时彩色图里手在左侧深度图里手在右侧两张图叠在一起轮廓错位匹配特征失真。原因深度传感器和彩色传感器硬件位置不同曝光时间不同wait_for_frames()返回的是各自最近的帧并不保证同一时刻。快速运动时这种时间差就会变成空间错位。解决先把深度图对齐到彩色坐标系用rs.align(rs.stream.color).process(frames)再通过get_timestamp()筛选差值小于5ms的帧对。对齐能解决几何关系时间戳筛选能解决时间差异两个手段建议同时用。批量采集中我把时间差大于5ms的帧对直接丢弃宁缺毋滥。坑2get_depth_frame()经常返回None。现象程序启动后反复打印no frame或者运行十几秒后突然一直取不到深度帧进入死循环。原因前几帧还在自动曝光和降噪预热没进入稳定状态也有可能是同时开启了太多流USB带宽不够深度帧被挤出队列。解决pipeline.start()之后先丢弃前5帧让设备稳定。如果再出现None就降分辨率、关掉一路红外流。还有一种情况是官方查看器还开着抢占了设备直接关掉就能恢复。这个坑通常不是代码逻辑问题而是设备调度问题。坑3对齐后深度图的内参用错。现象用rs.align对齐了颜色和深度生成的点云彩色贴图在边缘还是错位物体轮廓对不齐。原因对齐后的深度图已经被重投影到彩色坐标系像素坐标不再对应原始深度坐标系但代码仍然用depth_profile的内参反投影导致每个像素的角度偏差。解决对齐后保存的深度帧反投影时使用color_profile的内参。我在代码里会先判断aligned_depth_frame.get_width()和color_frame.get_width()是否一致不一致就先取彩色宽度重新配置对齐目标再进行点云生成。5.2 点云与带宽配置中的两个坑坑4点云尺寸放大1000倍。现象一个实际5cm的小盒子点云测量出来显示50m或者反过来小到不可见。原因这是最典型的单位换算错误。RealSense返回的depth_scale是0.001表示每单位0.001米而Open3D的depth_scale参数是除数表示把深度值除以多少得到米。把0.001填进Open3D等于把深度值乘以1000再当成米。解决自己反投影时乘depth_scale用Open3D时填1000.0。资源包里的自检函数会打印当前depth_scale和点云包围盒尺寸包围盒长宽高在0.2到2米之间一般是正常范围超过10米基本就是单位错了。坑5点云出现成片NaN和黑洞。现象点云里有一大片空洞加载后部分坐标是NaN统计点云数量时数值少得离谱。原因深度图0值被当成有效点参与反投影得到不可用的坐标或者物体距离太近小于D435i最小工作距离约0.1米深度引擎无法稳定输出。解决反投影前做掩码过滤valid_mask (depth 0) (depth 0.1) (depth 3.0)把无效深度全部滤掉再进点云生成。如果使用Open3D的RGBD接口它内部也会处理部分0值但不会处理小于最小工作距离的脏值所以自己加一道过滤更稳。6. 进阶用法多帧时间戳同步与自检回归批量采集最怕数据脏了还当成干净的用。我现在的习惯是每次采集前先让pipeline跑30帧用一个自检函数把每帧的时间差和有效深度比例打出来低于阈值就立刻停下调整硬件或配置。def check_frame(depth_frame, color_frame, depth_scale): d np.asanyarray(depth_frame.get_data()) c np.asanyarray(color_frame.get_data()) if c.shape[:2] ! d.shape: # 出现这种情况需要rs.align对齐后再传进来 print(size mismatch, align needed) return False ts_diff abs(depth_frame.get_timestamp() - color_frame.get_timestamp()) valid_ratio (d 0).astype(np.float32).mean() print(fts diff(ms): {ts_diff:.1f}, valid ratio:{valid_ratio:.3f}) return (ts_diff 5) and (valid_ratio 0.3)自检函数返回布尔值连续30帧里失败超过3次就可以判断是硬件连接不稳、曝光异常、还是某组参数设错。valid_ratio是有效深度像素占比室内场景通常超过50%如果低于20%基本可以断定深度流出了问题这时候不用继续采集先修设备状态。我把这些检查和批量采集脚本放在配套工程里每帧自动记录元数据彩色内参、深度内参、depth_scale、timestamp、frame_number、有效深度比例、点云数量。跑完一轮采集后回头看日志就能定位是从哪一帧开始变脏的不用重新花时间排查整段数据。验证方法也简单连续采集100帧生成100个点云检查每一帧的点云数量和包围盒尺寸是否处于同一个数量级只要有一帧异常日志里对应的编号会直接指出来。从那以后我每次采集前都强制走一遍这个自检流程确认内参、深度尺度和点云有效占比都落在正常区间才敢说这批数据是干净的。希望帮到你。本文还有配套的精品资源点击获取