使用 Rerun 可视化 ROS 2 机器人数据:从 ROS Topic 订阅到 Rerun 日志转换的完整实践指南
【免费下载链接】rerunVisualize, query, and stream to train on multimodal robotics data.项目地址: https://gitcode.com/GitHub_Trending/re/rerun
导读
Rerun 目前还没有对 ROS 的原生支持,但 ROS 与 Rerun 的许多核心概念(时间线、坐标变换、图像与点云)能够很好地相互对应。本指南基于仓库中的 ROS node 示例 及其配套文档 Use Rerun with ROS 2,讲解如何编写一个最小的 ROS 2 Python 节点,订阅常见的 ROS Topic(TF、里程计、激光扫描、代价地图、相机图像与深度图、URDF 等),逐类转换为 Rerun 日志调用并实时可视化。读完本文,你将掌握 ROS 2 消息与 Rerun archetype 之间的映射套路、ros_time时间线同步方法,以及如何在 Nav2 Turtlebot 4 仿真环境中直接运行该示例。
背景:为什么需要自己写一个 ROS 节点
Robot Operating System(ROS)通过软件库和工具帮助开发者构建机器人应用。Rerun 本身没有 ROS 的原生支持,但你可以很容易地创建一个基础的 ROS 2 Python 节点,订阅常见 Topic 并将它们记录到 Rerun 中。
本示例的定位是"玩具级"(toy example):它的目的不是做一个完整的 ROS 桥接方案,而是展示ROS 概念如何映射到 Rerun。在示例中,Rerun 可视化的是仿真数据,包括机器人位姿、图像、相机位置、激光扫描、点云和速度,覆盖了机器人导航过程中的典型多模态数据。
官方对更完善 ROS 支持的追踪 issue 见 re_importer 相关的 ROS 支持讨论(对应 issue #1537);LaserScan 原生投影原语的支持跟踪见 docs/content/concepts/logging-and-ingestion/mcap.md 所引用的 issue #1534。
用到的 Rerun 类型
本示例涉及以下 Rerun archetype,全部可以在 crates/store/re_log_types 的类型体系与 rerun_py/rerun_sdk/rerun 的 Python SDK 中找到对应实现:
| ROS 消息 | Rerun archetype | 用途 |
|---|---|---|
sensor_msgs/Image(RGB) | rr.Image | 彩色图像 |
sensor_msgs/Image(深度) | rr.DepthImage | 深度图 |
sensor_msgs/CameraInfo | rr.Pinhole | 针孔相机内参、视锥可视化 |
tf2_msgs/TFMessage | rr.Transform3D | 坐标变换树 |
nav_msgs/OccupancyGrid | rr.GridMap | 地图与代价地图栅格 |
sensor_msgs/PointCloud2(由 LaserScan 投影) | rr.LineStrips3D | 激光扫描线束 |
nav_msgs/Odometry | rr.Scalars | 线速度/角速度时间序列 |
std_msgs/String(/robot_description) | rr.log_file_from_contents | URDF 机器人模型 |
环境与依赖准备
系统级依赖:ROS 2 与 Nav2
与仓库中大多数示例不同,本示例除了requirements.txt中的 Python 包之外,还需要系统级安装 ROS。README 中的命令基于 Ubuntu 发行版上 ROS 的官方安装路径编写;如果你通过 Robostack 安装 ROS,命令可以类推使用。
示例开发和测试基于ROS 2 Kilted Kaiju(即 ROS 2 的 Kilted 发行版),搭配Nav2以及nav2_bringup包中的Turtlebot 4 仿真示例。如果你使用其他版本的 ROS(例如 Jazzy),只需把下面命令中的kilted替换为jazzy即可。
ROS 的安装本身不在示例范围内,但你需要安装等价于以下命令的软件包:
sudo apt install ros-kilted-desktop ros-kilted-nav2-bringup其中ros-kilted-nav2-bringup会作为依赖拉入我们所需的所有导航与仿真包(如果尚未安装的话)。
克隆仓库并准备 Python 环境
git clone https://github.com/rerun-io/rerun.git # Clone the repository cd rerun git checkout latest # Check out the commit matching the latest SDK release提示:本仓库对应分支路径为
GitHub_Trending/re/rerun,其中的示例源码位于 examples/python/ros_node/main.py。
务必使用 Python 虚拟环境,这里以venv为例(sudo apt install python3-venv):
python3 -m venv --system-site-packages rerun-ros-example source rerun-ros-example/bin/activate使用--system-site-packages创建虚拟环境是关键一步:这样 venv 才能看到系统安装的 ROS 2 Python 包(rclpy、cv_bridge、laser_geometry等)。
然后安装最新版 Rerun SDK 以及本示例requirements.txt中指定的库:
pip install --upgrade rerun-sdk pip install -r examples/python/ros_node/requirements.txtrequirements.txt 内容解析
examples/python/ros_node/requirements.txt 内容如下:
# NOTE: numpy has to be downgraded to be compatible with ROS packages # that were built against the 1.x version of python3-numpy in Ubuntu 24.04. numpy<2.0 opencv-python pycollada pyyaml rerun-sdk scipy yourdfpy两点值得注意:
numpy<2.0的版本约束:文件头部注释明确说明,Ubuntu 24.04 中 ROS 包是针对 python3-numpy 1.x 构建的,因此 numpy 必须降级到 2.0 以下才能与 ROS 包兼容;yourdfpy与pycollada:用于 URDF/COLLADA 网格解析,与 docs/content/howto/logging-and-ingestion/urdf.md 中提到的 URDF 加载管线相关;opencv-python提供cv_bridge底层所需的图像编码支持。
激活 ROS 环境
在安装requirements.txt中的依赖之外,你还需要 source ROS 的 setup 脚本:
source /opt/ros/kilted/setup.bash运行示例:连接 Nav2 Turtlebot 仿真
第一步:启动 Turtlebot 4 仿真
在一个终端中启动 Nav2 turtlebot demo:
source /opt/ros/kilted/setup.bash ros2 launch nav2_bringup tb4_simulation_launch.py headless:=False这会打开 Gazebo 和 RViz 两个窗口。使用 RViz 窗口初始化位姿估计(把机器人放到地图上),然后设置一个导航目标让机器人移动起来。
第二步:运行 ROS 节点
在另一个终端中连接正在运行的 ROS 系统:
source /opt/ros/kilted/setup.bash python examples/python/ros_node/main.py # run the example运行后,Rerun Viewer 会自动打开(由rr.script_setup的默认行为触发),实时展示从 ROS 订阅到的机器人数据。
第三步:自定义与探索 CLI
如果你想自定义或探索更多功能,使用--help选项查看 CLI 指南:
python examples/python/ros_node/main.py --helpmain.py中通过rr.script_add_args(parser)注册 Rerun 的通用参数(如--headless、--connect、--serve等),通过rr.script_setup(args, "rerun_example_ros_node")完成 SDK 初始化;剩余的未知参数则传给rclpy.init(args=unknownargs),因此你可以同时透传rclpy的启动参数。参见 examples/python/ros_node/main.py。
核心架构:订阅 → 转换 → 日志
从源码结构看,main.py 的实现模式非常统一:对每个感兴趣的 ROS Topic 创建一个订阅者,配一个回调函数,回调内部做数据转换,然后调用 Rerun 日志 API。核心类为TurtleSubscriber(Node),它继承自rclpy.node.Node,节点名为rr_turtlebot(见 main.py)。
订阅列表与 QoS 处理
在构造函数中,示例通过自定义的subscribe辅助方法订阅了一组 Topic(见 main.py):
| Topic | 消息类型 | 回调 | 说明 |
|---|---|---|---|
/tf | TFMessage | tf_callback | 动态变换 |
/tf_static | TFMessage | tf_callback | 静态变换,latching |
/odom | Odometry | odom_callback | 里程计 |
/scan | LaserScan | scan_callback | 激光扫描 |
/rgbd_camera/camera_info | CameraInfo | cam_info_callback | 相机内参 |
/rgbd_camera/image | Image | image_callback | RGB 图像 |
/rgbd_camera/depth_image | Image | depth_callback | 深度图 |
/robot_description | String | urdf_callback | URDF 描述,latching |
/map | OccupancyGrid | occupancy_grid_callback | 静态地图,latching |
/global_costmap/costmap | OccupancyGrid | occupancy_grid_callback | 全局代价地图 |
/local_costmap/costmap | OccupancyGrid | occupancy_grid_callback | 局部代价地图 |
subscribe方法内部(见 main.py)对 QoS 做了区分:
- 对需要latching(静态数据,如
/tf_static、/map、/robot_description)的订阅使用QoSProfile(depth=1, durability=QoSDurabilityPolicy.TRANSIENT_LOCAL); - 其余动态数据使用整数
10(即 history depth 为 10 的默认 QoS); - 所有订阅都挂在
ReentrantCallbackGroup()上,允许回调并发执行。
执行模型:MultiThreadedExecutor
入口处使用rclpy.spin(turtle_subscriber, executor=rclpy.executors.MultiThreadedExecutor())(见 main.py)。文档 Use Rerun with ROS 2 对此的解释是:ReentrantCallbackGroup配合MultiThreadedExecutor可以让每个回调线程独立处理消息,避免某个回调做 TF 查询等耗时操作时阻塞其他消息的接收。更高级的 ROS 执行模型与异步 TF 查询不在本指南范围内。
时间同步:用ros_time时间线对齐所有数据
所有传感器数据能否在 Rerun 时间轴上正确对齐,是可视化的关键。示例的每个回调都遵循同一模式:从消息头的 stamped 时间更新 Rerun 的ros_time时间线,而不是使用消息被接收或转发到 Rerun 的时间:
def some_msg_callback(self, msg: Msg): time = Time.from_msg(msg.header.stamp) rr.set_time("ros_time", timestamp=np.datetime64(time.nanoseconds, "ns"))这段代码使用rclpy.time.Time.from_msg解析msg.header.stamp,得到纳秒精度的时间戳,再以np.datetime64设置到名为ros_time的 Rerun 时间线上。该时间戳会持续作用于该回调线程后续的所有日志调用,直到再次更新时间(见 docs/content/howto/integrations/ros2-nav-turtlebot.md 的 "Updating time" 一节)。这也与 Rerun 的数据模型一致——时间线是 Rerun 检索与回放数据的核心维度。
各类 ROS 消息到 Rerun 的映射详解
TF 变换 →rr.Transform3D
ROS 的 tf2 变换直接对应 Rerun 的Transform3Darchetype。自 Rerun 0.28 起,Transform3D支持 parent/child frame 关系,使转换变得直接:注意TFMessage是多个独立时间戳的变换的容器,所以需要为每个 transform 单独设置时间。通过指定 parent/child frame,所有变换都可以记录到同一个实体路径"transforms"下,与 ROS 的 TF topic 行为类似。
def tf_callback(self, tf_msg: TFMessage) -> None: for transform in tf_msg.transforms: time = Time.from_msg(transform.header.stamp) rr.set_time("ros_time", timestamp=np.datetime64(time.nanoseconds, "ns")) rr.log( "transforms", rr.Transform3D( translation=[ transform.transform.translation.x, transform.transform.translation.y, transform.transform.translation.z, ], rotation=rr.Quaternion( xyzw=[ transform.transform.rotation.x, transform.transform.rotation.y, transform.transform.rotation.z, transform.transform.rotation.w, ] ), parent_frame=transform.header.frame_id, child_frame=transform.child_frame_id, ), )为了让这些变换在其余日志数据中发挥作用,还需要用rr.CoordinateFrame将实体关联到对应的命名 frame。这正是 Rerun 的命名变换帧(named transform frames)机制:每个实体先用CoordinateFrame关联到某个命名 frame,两个 frame 之间的几何关系由带child_frame/parent_frame的Transform3D确定。这与 ROS tf2 的约定一致——每个 frame 在同一时刻只有一个 parent(见 docs/content/concepts/logging-and-ingestion/transforms.md)。命名 frame 相比实体路径层级变换的优势在于:拓扑可以随时间变化、多个实体可以关联到同一 frame、实体路径可以留给语义组织。
URDF 机器人模型 → 内置 URDF importer
Rerun 内置了 URDF importer(详见 docs/content/howto/logging-and-ingestion/urdf.md),所以/robot_descriptiontopic 上收到的字符串可以直接转发给它:
def urdf_callback(self, urdf_msg: String) -> None: # NOTE: file_path is not known here, robot.urdf is just a placeholder to let # Rerun know the file type. Since we run this example in a ROS environment, # Rerun can use AMENT_PREFIX_PATH etc to resolve asset paths of the URDF. rr.log_file_from_contents( file_path="robot.urdf", file_contents=urdf_msg.data.encode("utf-8"), entity_path_prefix="urdf", static=True, )这里robot.urdf只是一个占位文件名,用于让 Rerun 识别文件类型;由于运行在 ROS 环境中,Rerun 可以通过AMENT_PREFIX_PATH等环境变量解析 URDF 引用的资源(网格等)路径。importer 会自动完成网格路径解析、网格与形状加载为 Rerun 实体、关节变换与 link 的 frame ID 关联。此后可以通过发送带parent_frame/child_frame的Transform3D来更新关节位姿。
LaserScan →rr.LineStrips3D
Rerun 目前还没有原生的LaserScan类型原语(跟踪 issue #1534),所以需要额外做一步转换(见 docs/content/howto/integrations/ros2-nav-turtlebot.md 的 LaserScan 一节):
def scan_callback(self, scan: LaserScan) -> None: time = Time.from_msg(scan.header.stamp) rr.set_time("ros_time", timestamp=np.datetime64(time.nanoseconds, "ns")) # Project the laser scan to a collection of points points = self.laser_proj.projectLaser(scan) pts = point_cloud2.read_points(points, field_names=["x", "y", "z"], skip_nans=True) pts = structured_to_unstructured(pts) # Turn every pt into a line-segment from the origin to the point. origin = (pts / np.linalg.norm(pts, axis=1).reshape(-1, 1)) * 0.3 segs = np.hstack([origin, pts]).reshape(pts.shape[0] * 2, 3) rr.log("scan", rr.LineStrips3D(segs, radii=0.0025, colors=[255, 165, 0])) rr.log("scan", rr.CoordinateFrame(frame=scan.header.frame_id))转换思路分三步:
- 用 ROS 生态的
laser_geometry.LaserProjection().projectLaser()将扫描投影成点云,再用sensor_msgs_py.point_cloud2.read_points读取 x/y/z 坐标并转换为非结构化 NumPy 数组; - 对每个点生成一个从原点沿射线方向伸出 0.3m 的起点,然后用
np.hstack与reshape把两组点交错排列成"起点-终点-起点-终点…"的线段序列——这正是rr.LineStrips3D期望的输入格式(可以想象成 Gazebo 中激光线的显示方式); - 同时把
scan.header.frame_id作为CoordinateFrame记录,确保 Rerun 在正确的变换层级位置可视化这些线段。
OccupancyGrid →rr.GridMap
ROS 的nav_msgs/OccupancyGrid消息直接对应 Rerun 的GridMaparchetype。示例订阅了静态地图以及全局/局部代价地图,分别使用 Rerun 的 RViz 兼容配色RvizMap与RvizCostmap,并通过draw_order定义图层顺序(见 main.py)。
大部分字段是 1:1 映射:占用数据成为GridMap的图像数据,info.resolution成为 cell size,info.origin定义地图位姿。唯一的坑是行顺序:ROS 栅格地图从地图左下角单元格开始,而 RerunGridMap使用的常规图像缓冲区是首行在顶部,因此示例在记录前要np.flipud翻转行(见 main.py):
# ROS maps start at the bottom-left cell; Rerun image buffers are top-row first. data = np.asarray(grid.data, dtype=np.int8).reshape((grid.info.height, grid.info.width)) image_data = np.flipud(data).astype(np.uint8, copy=False) rr.log( entity_path, rr.GridMap( data=image_data.tobytes(), format=rr.components.ImageFormat( width=grid.info.width, height=grid.info.height, color_model="L", channel_datatype="U8", ), cell_size=grid.info.resolution, translation=[grid.info.origin.position.x, grid.info.origin.position.y, grid.info.origin.position.z], quaternion=rr.Quaternion( xyzw=[ grid.info.origin.orientation.x, grid.info.origin.orientation.y, grid.info.origin.orientation.z, grid.info.origin.orientation.w, ] ), colormap=colormap, draw_order=draw_order, opacity=opacity, ), )同时记录地图的CoordinateFrame(frame=grid.header.frame_id),让地图在变换树中与机器人、传感器数据对齐。
相机内参与图像 →rr.Pinhole+rr.Image
ROS 图像可以用cv_bridge包轻松映射:cv_bridge.imgmsg_to_cv2的输出可以直接喂给rr.Image。相机内参 topic 则用image_geometry包:PinholeCameraModel.from_camera_info()暴露的相机内参矩阵结构与 Rerunrr.Pinhole一致(见 main.py):
def cam_info_callback(self, info: CameraInfo) -> None: time = Time.from_msg(info.header.stamp) self.pinhole_model.from_camera_info(info) rr.set_time("ros_time", timestamp=np.datetime64(time.nanoseconds, "ns")) rr.log( "rgbd_camera/camera_info", rr.Pinhole( resolution=[info.width, info.height], image_from_camera=self.pinhole_model.intrinsic_matrix(), image_plane_distance=1.0, parent_frame=info.header.frame_id, # Specifying a `child_frame` for the 2D image plane allows Rerun to # visualize the pinhole frustum together with the image in 3D views. # This has to match the coordinate frames used when logging images, # see `image_callback` below. child_frame=info.header.frame_id + "_image_plane", ), )这里的关键技巧是为 2D 图像平面建立一个独立的 frame:ROS 概念里没有"图像平面"这个坐标系,为了让 Rerun 在 3D 视图中把图像投影到针孔视锥(pinhole frustum)里,需要建立 3D 外参相机坐标系(即消息的frame_id)与 2D 图像平面之间的关系。示例的做法是用_image_plane后缀命名图像平面 frame,并确保 Pinhole 与图像日志都使用它:
def image_callback(self, img: Image) -> None: time = Time.from_msg(img.header.stamp) rr.set_time("ros_time", timestamp=np.datetime64(time.nanoseconds, "ns")) rr.log("rgbd_camera/image", rr.Image(self.cv_bridge.imgmsg_to_cv2(img))) # Make sure the image plane frame matches what we set in `cam_info_callback`. rr.log("rgbd_camera/image", rr.CoordinateFrame(frame=img.header.frame_id + "_image_plane"))深度图处理方式类似,只是用rr.DepthImage并指定meter=1.0、colormap="viridis",编码通过cv_bridge指定为"32FC1"(见 main.py)。从 Rerun 的变换系统看,Pinhole 不只是可可视化的 archetype,它本身就是一种"3D 空间到 2D 子空间"的投影关系,配合正确的 frame 设置即可实现 2D 图像嵌入 3D 场景、3D 对象投影到 2D 相机视图等强大效果(见 docs/content/concepts/logging-and-ingestion/transforms.md 的 Pinhole projections 一节)。
Odometry →rr.Scalars
里程计数据被拆成时间序列标量记录:
def odom_callback(self, odom: Odometry) -> None: time = Time.from_msg(odom.header.stamp) rr.set_time("ros_time", timestamp=np.datetime64(time.nanoseconds, "ns")) # Capture time-series data for the linear and angular velocities rr.log("odom/twist/linear/x", rr.Scalars(odom.twist.twist.linear.x)) rr.log("odom/twist/angular/z", rr.Scalars(odom.twist.twist.angular.z))线速度(x 方向)与角速度(z 方向)分别记录到独立实体路径,在 Rerun 中以时间序列曲线呈现,便于观察机器人运动状态。
总结与扩展方向
虽然示例代码量不小,但没有一段是复杂逻辑:每个消息回调独立运作——接收消息、适配为 Rerun 数据、再记录。这套"订阅 → 转换 → 日志"的模式可以很容易推广到本文未覆盖的其他 ROS 消息类型(例如更多相机位姿、点云、IMU 数据等)。
几个可以继续深入的仓库资源:
- 完整代码:examples/python/ros_node/main.py
- 依赖清单:examples/python/ros_node/requirements.txt
- 配套图文教程:docs/content/howto/integrations/ros2-nav-turtlebot.md
- URDF 加载原理:docs/content/howto/logging-and-ingestion/urdf.md
- 变换与坐标帧概念:docs/content/concepts/logging-and-ingestion/transforms.md
- MCAP 记录数据回放:docs/content/howto/logging-and-ingestion/mcap.md
如果你已经录制了 ROS 数据(例如 .mcap 文件),也可以直接参考 MCAP 文档让 Rerun 读取录制的数据,而无需运行实时节点。Rerun 对激光扫描等原语的原生支持仍在演进中(issue #1534),随着新功能落地,这套桥接代码会进一步简化。
【免费下载链接】rerunVisualize, query, and stream to train on multimodal robotics data.项目地址: https://gitcode.com/GitHub_Trending/re/rerun
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考