☰
Realsense D435i多模态采集与坐标系解析完整实践
2026/10/7 12:53:16 网站建设 项目流程

记录一次Realsense D435i多模态采集与坐标系解析的完整实践

做机器人或者三维视觉相关项目的人,大概率都绕不开Intel Realsense这套相机。D435i算是其中性价比很高的一款,深度、RGB、IMU三样齐全,既能当普通彩色相机用,又能直接输出对齐好的深度图,甚至还能给你一坨IMU数据做视觉惯性里程计。但真拿它做多模态采集的时候,很多坑不是看官方文档能看出来的——尤其是坐标系那部分,稍微不注意,后面标定、融合、手眼标定全部要跟着翻车。

这篇文章记录的就是我最近一次用Realsense D435i做多模态图像采集、顺带把坐标系彻底捋清楚的完整过程。内容包括硬件与软件环境的准备、三种数据的采集方案、坐标系之间的转换关系、以及我在实际中踩过的几个典型问题。适合正在做机械臂抓取、SLAM、三维重建、多传感器融合,或者单纯想把D435i吃透的工程师参考。

先说结论:D435i的真正优势不在于某一个传感器有多强,而在于它把彩色图像、深度图像、IMU数据在硬件层面做了时间同步,并且提供了相对完整的SDK。你要做的只是正确地把这些数据取出来,再把它们之间的关系搞清楚。搞清楚坐标系这件事,是整个采集流程中最容易忽视、又最容易出错的一环。

1. 整体方案与设计思路

1.1 为什么选D435i而不是其他相机

选型这事,得看需求。我这次要采集的数据用于后续的机械臂抓取实验,需要同时拿到物体的彩色纹理、空间深度,以及相机运动过程中的姿态变化。如果只用单目相机,深度得靠算法估计,精度和泛化都不够;如果用双目相机,还得自己处理视差计算;如果再加独立IMU,同步问题会让人很痛苦。D435i刚好把这些都打包好了。

D435i的主动立体视觉原理很有意思。它有一个左红外相机、一个右红外相机,外加一个红外点阵投射器。投射器会往场景里打一个不可见的红外纹理图案,相当于给原本平淡无奇的墙面、桌面加上了特征点,左右相机匹配起来就轻松很多。所以它在白墙、无纹理这类让双目立体视觉头痛的场景下,依然能输出相对完整的深度图。这一点在实际使用中非常关键。

还有一个被很多人忽略的好处,是Intel提供了跨平台的SDK——librealsense。官方SDK封装了底层USB通信、图像解拜耳、深度计算、内参获取、IMU数据读取等一套东西,你不需要自己去看传感器寄存器手册,也不用处理RAW数据格式。装上SDK,插上相机,五分钟后就能看到画面,这种开箱体验在工业相机里几乎是不敢想的。

1.2 多模态采集到底要采什么

所谓多模态,在D435i的语境下通常是三类数据:

  • RGB彩色图像:提供颜色和纹理信息,用于目标识别、语义分割、可视化。
  • 深度图像:提供每个像素到相机的距离,用于三维重建、位姿估计、碰撞检测。
  • IMU数据:提供加速度计和陀螺仪读数,用于估计相机自身的运动,配合视觉做VIO。

这三类数据各有各的坐标系,也各有各的采样频率。RGB默认30fps,深度默认30fps,IMU可以到400Hz左右。采集的时候如果只关心某一类数据,那很简单;但如果想把它们融合起来用,就必须解决好两个问题:一是时间对齐,二是空间对齐。时间对齐靠硬件或SDK的同步机制,空间对齐靠内参和外参。

我这次采集的核心目标,是做一份数据集,既能用于离线训练识别模型,也能用于在线跑机械臂手眼标定。所以采集时不仅要保存图像和IMU,还要把每一帧的时间戳、相机内参、IMU到RGB的外参都一并保存下来。很多人采集时只存了图像,后面要做处理发现缺内参,只能重新标定,非常浪费时间。

1.3 坐标系解析在整个流程中的位置

坐标系解析不是独立的一步,而是贯穿采集、标定、使用的全流程。在采集端,你需要知道深度图上的点怎么映射到相机三维坐标系;在标定端,你需要知道相机坐标系和机械臂末端坐标系之间的变换关系;在融合端,你需要把IMU的加速度计读数从IMU坐标系转到相机坐标系。

如果把整个流程比作一条流水线,坐标系就是每个工位之间的“对齐标记”,标记错了,后面出来的产品全是歪的。D435i官方文档里其实已经给出了各个坐标系之间的关系图,但文档描述比较抽象,实际去数每一组坐标轴方向的时候,还是容易搞晕。所以我建议拿到相机后第一件事,就是打印一张官方坐标系定义图放在桌上,写代码的时候随时对照。

2. 核心细节解析:D435i的坐标系到底长什么样

2.1 相机坐标系基本约定

所有相机坐标系本质上都是一个三维直角坐标系,区别只在于原点和轴的朝向。D435i官方定义中,RGB相机坐标系和深度相机坐标系的原点分别在各自镜头的光心位置,Z轴指向相机正前方,X轴指向右,Y轴指向下。这个“Y轴向下”的约定和很多图形学里的习惯一致,但和机器人学里常见的“Y轴向上”不同,做转换的时候务必留心。

有一个常见的坑:深度相机和RGB相机虽然在同一个模组里,但两者的光心并不重合,存在一个固定的平移关系。所以严格来说,深度图上某个像素对应的三维点,是在深度相机坐标系下表达的;如果你想把这个点投影到RGB图像上,需要先利用深度相机内参把像素转到深度相机三维坐标系,再用外参变换到RGB相机坐标系,最后用RGB内参投影到RGB像素平面。官方SDK提供了一个叫做align的功能,能直接输出对齐到RGB视场的深度图,但要注意,这里的对齐只是把深度图重投影到RGB视角,并没有改变深度值本身的三维含义。

2.2 RGB、深度、IMU三者坐标系的转换链条

D435i里有一个重要的外参:IMU坐标系到RGB坐标系(或反之)的变换。官方在出厂时已经标定好,保存在相机固件里,通过SDK可以读取。IMU的安装位置和RGB镜头光心并不在一起,所以加速度计读到的重力方向、陀螺仪读到的角速度,都必须转换到RGB坐标系下才能和视觉数据统一使用。

我建议在做任何融合算法前,先把三个坐标系的转换关系用代码验证一遍,方法很简单:

  1. 拍一张棋盘格,提取角点,得到RGB相机的外参。
  2. 读取深度相机到RGB相机的外参,以及IMU到RGB的外参。
  3. 把深度图上的某个角点对应的三维点转换到RGB坐标系,投影到RGB图上,看是否和RGB角点重合。
  4. 把IMU的静止加速度读数(几乎只有重力)用外参转到RGB坐标系,看计算出的重力方向是否与RGB相机坐标系Z轴大致垂直。

这一步验证虽然是基本功,但能帮你在后续使用中省去大量排查时间。我第一次做的时候没有验证,后来发现IMU加速度计方向和实际运动方向对不上,查了半天才发现是外参用反了。

2.3 内参与畸变参数的使用要点

D435i的RGB相机是有镜头畸变的,官方SDK默认会给你畸变系数,但默认输出图像是已经去畸变过的。如果你用的是rs2::video_stream_profile的get_intrinsics()接口,得到的coeffs数组里存着畸变系数,但实际上一帧图像拿到手时,SDK已经在内部完成了去畸变处理。所以如果你再用这些畸变系数去对图像做一次去畸变,结果会错。

这一点在做相机标定的时候特别容易出问题。比如你用OpenCV的calibrateCamera去标定D435i的RGB相机,假设你已经用了SDK输出去畸变后的图像,那输入给OpenCV的角点坐标就不应该再有畸变,你设定的畸变模型也应该相应调整。最稳妥的做法,是关闭SDK内置的去畸变,拿到原始带畸变图像再自己处理。或者反过来,直接用SDK给的内参,不做二次标定。

对于深度相机同样如此,深度内参的焦距、主点通常和RGB不一样,做深度图到点云转换时要用深度内参,不能用RGB内参代替。虽然对齐后的深度图分辨率与RGB一致,但它的内参本质上仍然是深度相机内参经过去畸变和重投影后的复合内参,直接查表反而容易出错。

3. 实操过程:一台D435i如何完成多模态数据采集

3.1 环境准备与SDK安装

我这次用的环境是Ubuntu 20.04,配ROS Noetic,当然也单独装了librealsense SDK。两种安装方式各有优劣:用apt源安装预编译包比较省事,适合只想跑工具的;自行编译源码则能确保拿到最新版本,并且方便修改底层参数。我建议至少编译一次源码,因为你会从中理解SDK的模块结构,后面调试自定义功能时方便很多。

安装完成后,先跑rs-enumerate-devices查看设备信息,重点确认固件版本、相机序列号、各传感器的可用分辨率和帧率。然后跑rs-viewer看看画面是否正常。这一步不要嫌麻烦,很多后续问题都是因为固件过旧或者USB接口带宽不足导致的,如果这一步都没有稳定图像,后面都白搭。

需要特别提醒的是USB带宽问题。D435i对USB3.0有硬性要求,如果插在USB2.0口上,深度和RGB同时跑30fps会明显丢帧,IMU也可能出现异常。我用的是笔记本扩展坞上的USB3.0口,但扩展坞芯片不给力,实际传输速度只有USB2.0水平,后来换到电脑原生USB3.0口才解决。所以如果你遇到画面断断续续、深度图有大量空洞,先检查USB线缆和接口。

3.2 基于PyRealsense2的采集脚本设计

Python绑定pyrealsense2是采集数据最方便的方式,API简洁,文档也全。我写采集脚本时主要分四步:

第一步,创建pipeline,配置各数据流。RGB分辨率选1280x720、30fps;深度分辨率同样1280x720、30fps;IMU数据流需要单独启用rs2.stream.accel和rs2.stream.gyro。注意,IMU的数据流不能直接和视频流一起用enable_stream,它们属于motion模块,可以通过一个特殊的profile加载。

第二步,设置对齐选项。使用rs2.align(rs2.stream.color)将深度图对齐到RGB视角,这样输出的深度图每个像素和RGB图一一对应,省去自己做投影匹配的麻烦。

第三步,循环取帧。每取一帧frameset,从中分别取出对齐后的深度帧、彩色帧,再通过frameset里的motion_frame读取IMU数据。IMU数据的频率很高,一帧视频数据可能对应多帧IMU,所以需要把时间戳记清楚,我是把每帧视频的frame.get_timestamp()以及每帧IMU的frame.get_timestamp()都保存下来,同时使用SDK的时间域(RS2_TIMESTAMP_DOMAIN_GLOBAL_TIME)确保RGB、深度、IMU的时间戳在同一个时钟域下。

第四步,保存数据。彩色图用PNG保存,深度图保存为16位PNG(原始深度值,单位毫米),IMU数据保存为CSV。另外把相机内参、外参、畸变系数、分辨率等元信息保存成一个JSON文件。这样一套数据就是自描述的,换台电脑也能直接解析。

下面是一个简化版的采集脚本框架,实际使用中需要根据需求调整:

import pyrealsense2 as rs import numpy as np import cv2 import json import time import os pipeline = rs.pipeline() config = rs.config() config.enable_stream(rs.stream.depth, 1280, 720, rs.format.z16, 30) config.enable_stream(rs.stream.color, 1280, 720, rs.format.bgr8, 30) pipeline.start(config) # 对齐深度到彩色 align_to = rs.stream.color align = rs.align(align_to) save_dir = "capture" os.makedirs(save_dir, exist_ok=True) frame_count = 0 try: while True: frames = pipeline.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 color_image = np.asanyarray(color_frame.get_data()) depth_image = np.asanyarray(depth_frame.get_data()) # 保存彩色图和深度图 cv2.imwrite(f"{save_dir}/color_{frame_count:06d}.png", color_image) cv2.imwrite(f"{save_dir}/depth_{frame_count:06d}.png", depth_image) frame_count += 1 time.sleep(0.03) except KeyboardInterrupt: pass finally: pipeline.stop()

如果要把IMU加速度计和陀螺仪也加进来,需要把IMU作为单独传感器启动。这里给一个最小示例:

pipeline = rs.pipeline() config = rs.config() pipeline_wrapper = rs.pipeline_wrapper(pipeline) device = config.resolve(pipeline_wrapper).get_device() # 查找包含IMU的传感器 for s in device.sensors: if s.supports(rs.camera_info.name) and s.get_info(rs.camera_info.name) == 'Stereo Module': sensor = s # 或者直接查询运动模块 imu_sensor = None for s in device.sensors: for p in s.get_stream_profiles(): if p.stream_type() == rs.stream.accel: imu_sensor = s break if imu_sensor: break config.enable_stream(rs.stream.accel) config.enable_stream(rs.stream.gyro) pipeline.start(config)

需要说明的是,上面示例只是展示如何启用IMU流,实际采集时建议把IMU和视频流放进同一个pipeline,或者使用pipeline.start(config, callback)回调方式,这样能更好地保证时间同步。

3.3 参数计算:从深度像素到三维点云

拿到深度图后,最常见的需求是把每个像素转换为三维坐标,生成点云。D435i深度图里的值是毫米单位的距离,但注意这个距离是沿着相机Z轴的距离,而不是欧氏距离。也就是说,深度图上的值等于三维点在深度相机坐标系下的Z坐标,X和Y需要根据像素坐标和内参反算。

转换公式很简单:

[ X = \frac{(u - c_x) \cdot Z}{f_x} ] [ Y = \frac{(v - c_y) \cdot Z}{f_y} ]

其中 ( u, v ) 是像素坐标,( c_x, c_y ) 是主点坐标,( f_x, f_y ) 是焦距,( Z ) 是深度值(单位要统一为毫米)。如果你用RGB对齐后的深度图做转换,最好使用对齐后对应的内参。官方SDK提供rs2.deproject_pixel_to_point函数来做这件事,内部用的就是这套公式,推荐优先用SDK函数,避免自己实现时像素坐标索引搞错。

实际编程中,用NumPy向量化计算会比逐像素循环快很多:

import numpy as np def depth_to_pointcloud(depth_image, intrinsics): h, w = depth_image.shape u = np.arange(w).reshape(1, -1).repeat(h, axis=0).astype(np.float32) v = np.arange(h).reshape(-1, 1).repeat(w, axis=1).astype(np.float32) z = depth_image.astype(np.float32) / 1000.0 # 转为米 x = (u - intrinsics.ppx) * z / intrinsics.fx y = (v - intrinsics.ppy) * z / intrinsics.fy points = np.stack([x, y, z], axis=-1) return points

注意深度值为0或超出有效范围的像素通常表示无效测量,生成点云时要过滤掉,否则会出现大量离群点。我一般会用深度相机的get_depth_scale()接口获取比例因子,而不是直接假设1000毫米等于1米,因为某些固件下depth值可能有不同比例。

3.4 IMU数据的时间戳与频率处理

IMU是D435i里最容易被人忽视、也最容易采错的数据。它的输出频率很高,但SDK在传输过程中可能对数据进行批量打包,所以你拿到的每一帧IMU都有一个时间戳,这个时间戳是基于全局时钟的。采集时一定不要自己给IMU添加系统时间,而是直接用帧自带的时间戳,否则和图像无法同步。

我处理IMU数据时做了三件事:第一,保存原始读数(加速度计三轴m/s^2,陀螺仪三轴rad/s);第二,把时间戳统一成float seconds格式;第三,对加速度计做静止检测。静止检测的意义在于,可以方便地看到重力矢量方向,用来验证坐标系旋转是否正确。你可以让相机静止放平,记录几百帧加速度计数据,求平均后应该接近(0, 0, -9.8)(取决于坐标轴方向),如果差的太远,说明坐标系没有转换对。

IMU数据的第二个用途是做视觉惯性融合,也就是VIO。这一点需要单独展开,不过如果你只是为了采集数据集,把原始IMU数据完整保存下来就够了,后续离线处理时再决定是降采样、滤波还是融合。

4. 常见问题与排查技巧实录

4.1 深度图大量黑色空洞

深度图上的黑色像素表示没有测到深度。常见原因有三个:

  • 场景离相机太近,超出了最小深度距离(D435i最小距离约0.1米左右,实际可靠距离最好在0.3米以上)。
  • 物体表面过暗或反光太强,红外点阵无法形成有效特征。黑色吸光物体、镜面反光物体都容易产生空洞。
  • 室外强红外光干扰,太阳光里含有大量红外成分,会淹没点阵投射器的图案,导致深度失效。

解决办法除了调整距离和角度外,可以尝试打开rs-viewer里的“hole filling”选项做后处理。SDK里也提供了rs2::temporal_filter或spatial_filter,能有效填补小范围空洞。但要注意,这些滤波器会改变深度值的时间或空间分布,如果做精密测量不能盲目使用。

4.2 采集过程中彩色图与深度图错位

如果你使用了对齐功能,理论上每一帧的彩色图和深度图是像素对齐的。但实际采集时偶尔会出现错位,原因多半是深度图本身有部分有效深度值缺失,对齐算法只能针对有效像素做重投影,缺失区域会保留为0,导致看起来像错位。

更常见的情况是你自己手动保存了未对齐的深度图,然后后续离线再去对齐,结果因为保存文件和RGB文件不是同一时刻采集的,导致物体运动后出现错位。解决方法是采集时就完成对齐,彩色图和深度图用同一个文件名序号保存,且不要间隔太久。如果做动态物体的采集,建议将三路数据同时开启硬件时间同步,并尽量使用全局时间戳。

4.3 坐标系方向总是搞反

这几乎是所有D435i用户都会遇到的烦恼。这里我教你一个“土办法”验证坐标系方向:

将相机固定不动,把一条已知方向的直线物体(比如长尺子)放在相机前方,水平移动它,观察三维点云中对应点的坐标变化。根据三维点坐标变化和实际物理运动方向,反推相机的X、Y、Z轴指向。这是一个最笨但是最靠谱的方法。

另一个技巧:把相机正对自己看,屏幕RGB画面里,物体向右移动,意味着它在相机坐标系中的X坐标增加还是减少?根据定义,X轴指向右,所以物体向右移动时X坐标应该变大。Y轴指向下,所以物体向上移动时Y坐标应该变小。验证的时候心里默念这个逻辑,再配合点云可视化工具(比如Open3D)看一眼,很快就能搞清楚。

4.4 OpenCV与Realsense联合标定的坑

在做相机标定时,很多人习惯直接用OpenCV读D435i的彩色图像。但前面说过,SDK给出的彩色图可能已经去畸变,所以直接用OpenCV找角点、直接标定,得到的重投影误差可能很小,但内参和SDK给的不一致。这是因为你基于“无畸变图像”标定,得到的内参实际上是一个等效内参,它也能用,但如果你把它和SDK内参混用,就会出错。

我的建议是:只要你用SDK采集图像,就统一以SDK内参为准;只有当你需要自定义图像处理(比如需要原始未校正图像)时,才重新做标定。如果非要自己标定,可以先用SDK关闭emitter或设置rs2::option.enable_auto_exposure等参数来获得更稳定的图像,但更重要的是意识到“去畸变后图像上的角点坐标”已经不符合原始畸变模型,不能再用原畸变系数去描述它。

4.5 问题排查速查表

现象可能原因解决方案
深度图大面积空洞距离过近、表面反光、红外干扰调整距离,使用滤波器,避免强光
彩色图与深度图错位未对齐或时间戳不对使用rs2.align,并确保同一帧源
IMU数据一直跳动未开机预热、高频振动采集前静止预热10s,固定好相机
画面延迟或丢帧USB带宽不足换USB3.0原生口,降低分辨率帧率
对齐后的深度图颜色偏暗深度Z值转化为视差显示检查显示映射,不影响原始数据

5. 从采集到坐标系解析的完整链路

到这里,单说某一项操作已经没有太大意义,我更想把整条链路串起来讲一遍,因为这才是D435i多模态采集项目的核心价值所在。

完整的采集链路可以归纳为:硬件连好 → SDK读到数据 → 时间戳同步 → 图像对齐 → 保存数据 → 解析内参外参 → 验证坐标系 → 生成三维数据。每一步都有对应的工具和函数,但每一步也都可能出错。我见过不少新手卡在“读取IMU数据”这一步,因为官方教程里给的示例代码大多针对图像,很少专门讲IMU怎么启用。其实只需要在配置里加上enable_stream(rs.stream.accel)和enable_stream(rs.stream.gyro),再用frames.get_motion_frame()去取就行,关键是理解motion_frame的机制和视频帧不同。

另一个值得强调的点是元数据管理。我强烈建议在采集时就把相机序列号、固件版本、内参、外参、时间戳域、对齐方式、滤波器开启情况全部记录在JSON文件里。这些数据看起来不起眼,但当你几个月后回头处理数据集时,如果没有这些信息,很多参数你都只能靠猜。我吃过这个亏,所以现在无论采什么数据,第一件事就是把元数据写入文件。

坐标系解析也应该放在采集流程中做一次,而不是等到算法阶段才想起来。具体做法是:采集几组静止的IMU数据,验证重力方向;采集棋盘格在不同位姿下的图像,验证重投影;用标定板或已知尺寸的物体,验证三维点距离。这些验证如果刚采集完就做,发现问题还能重新采,等数据都处理完了才发现,就只能从头再来。

6. 后处理环节的扩展:点云生成与手眼标定入口

多模态采集完成后,通常下一步就是生成点云或者做手眼标定。这里我简单说一下方向,因为这也是D435i在机械臂场景中最常见的两个出口。

点云生成已经讲过了,如果你要生成带颜色的点云,就用对齐后的深度图和彩色图,逐像素生成三维点,然后把彩色图的RGB值赋给对应点。保存成PLY格式,就能在MeshLab、CloudCompare或Open3D里查看。如果要跑深度学习的抓取检测,通常不需要全分辨率点云,可以先降采样,减少计算量。

手眼标定是“相机在机械臂上”还是“相机固定在外”的两种情况,分别对应眼在手上和眼在手外。不管哪种,都需要相机坐标系到机械臂末端坐标系的变换,即手眼矩阵。D435i在此的优势是深度图和RGB已经对齐,你可以直接用棋盘格标定板来估计RGB相机的外参,再把深度信息和机械臂的位姿关联起来。由于D435i自带IMU,也可以尝试用IMU辅助标定,提高稳定性和精度。

在标定过程中,坐标系解析的知识会再次派上用场。你需要把标定板上的角点在相机坐标系中的坐标、机械臂末端在基坐标系中的位姿、以及二者之间未知的固定变换,抽象成AX=XB问题。OpenCV的calibrateHandEye函数可以解这个方程,但输入参数必须用对坐标系约定,比如旋转矩阵和平移向量的顺序、是否使用四元数等。我建议手写一个最小二乘验证脚本,用几组数据同时解算,并把重投影误差打印出来。误差超过几个毫米或者几度,就要怀疑是坐标系定义不一致。

7. 最后再分享几个提高采集质量的小技巧

关于曝光和白平衡:D435i的自动曝光在室内效果不错,但场景里如果有强光源,RGB图像容易过曝,导致后端识别掉点。采集前最好固定曝光参数,而不是依赖自动曝光。深度相机也有自己的曝光设置,通常不需要动,但如果你发现深度图在某种光照下质量变差,可以手动调整一下深度相机的激光功率(laser power)参数,这是D435i一个非常实用的调节项。

关于采集速度:如果连续采集超过几分钟,相机会因为发热出现轻微漂移,IMU的温漂尤其明显。所以长时间采集,建议给相机装一个小散热片,或者至少放在通风处。另外不要长时间让相机处于高帧率高分辨率模式,这对USB传输和CPU解码都有压力。很多人在采集时喜欢把分辨率和帧率调到最高,但如果后续应用并不需要,这会白白增加数据量,还可能触发丢帧。

关于数据完整性:保存数据时最好边采集边写盘,而不是最后一次性转存。我用的是SSD,直接写入PNG和CSV没有压力。如果你要把数据给团队其他人用,建议附带一个README文件,明确说明每个字段的含义,这样能省去大量沟通成本。

D435i是个很成熟的设备,SDK也做得足够人性化,但真正考验人的地方恰恰是这些细碎的小问题。把坐标系、时间戳、内参外参这些基础概念彻底搞明白,再动手写采集代码,你会发现自己绕过了很多弯路。我这次从零开始重新捋了一遍多模态采集流程,最大的收获不是写了几百行代码,而是建立了对这套传感器体系的整体认识。如果你也在用D435i,不妨按这个思路走一遍,尤其是坐标系验证那部分,做一次之后,后面所有关于外参的问题都会变得简单很多。

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

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

立即咨询