chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:05:14 +08:00
commit 2a547be7fe
7904 changed files with 1000926 additions and 0 deletions
+98
View File
@@ -0,0 +1,98 @@
<!--[metadata]
title = "ROS node"
tags = ["2D", "3D", "Pinhole camera", "ROS", "Time series", "URDF"]
thumbnail = "https://static.rerun.io/ros_node_example_new/e15b81b183ccafd8ee2994a6abf0b06cbdf22741/480w.png"
thumbnail_dimensions = [480, 318]
-->
A minimal example of creating a ROS node that subscribes to topics and converts the messages to Rerun log calls.
The solution here is mostly a toy example to show how ROS concepts can be mapped to Rerun.
<picture>
<img src="https://static.rerun.io/ros_node_example_new/e15b81b183ccafd8ee2994a6abf0b06cbdf22741/full.png" alt="Rerun viewer showing data streamed from the example ROS node">
<source media="(max-width: 480px)" srcset="https://static.rerun.io/ros_node_example_new/e15b81b183ccafd8ee2994a6abf0b06cbdf22741/480w.png">
<source media="(max-width: 768px)" srcset="https://static.rerun.io/ros_node_example_new/e15b81b183ccafd8ee2994a6abf0b06cbdf22741/768w.png">
<source media="(max-width: 1024px)" srcset="https://static.rerun.io/ros_node_example_new/e15b81b183ccafd8ee2994a6abf0b06cbdf22741/1024w.png">
<source media="(max-width: 1200px)" srcset="https://static.rerun.io/ros_node_example_new/e15b81b183ccafd8ee2994a6abf0b06cbdf22741/1200w.png">
</picture>
## Used Rerun types
[`Image`](https://www.rerun.io/docs/reference/types/archetypes/image), [`DepthImage`](https://rerun.io/docs/reference/types/archetypes/depth_image), [`Pinhole`](https://www.rerun.io/docs/reference/types/archetypes/pinhole), [`Transform3D`](https://www.rerun.io/docs/reference/types/archetypes/transform3d), [`GridMap`](https://www.rerun.io/docs/reference/types/archetypes/grid_map), [`Points3D`](https://www.rerun.io/docs/reference/types/archetypes/points3d), [`LineStrips3D`](https://www.rerun.io/docs/reference/types/archetypes/line_strips3d), [`Scalars`](https://www.rerun.io/docs/reference/types/archetypes/scalars)
## Background
The [Robot Operating System (ROS)](https://www.ros.org) helps build robot applications through software libraries and tools.
Although Rerun doesn't have native ROS support, you can easily create a basic ROS 2 Python node to subscribe to common ROS topics and log them to Rerun.
In this example, Rerun visualizes simulation data, including robot pose, images, camera position, laser scans, point clouds, and velocities, as the robot navigates the environment.
## Logging and visualizing with Rerun
Find the detailed code walkthrough and explanation for visualizing this example here: [Using Rerun with ROS 2](https://www.rerun.io/docs/howto/integrations/ros2-nav-turtlebot).
For more information on future improved ROS support, see tracking issue: [#1527](https://github.com/rerun-io/rerun/issues/1537)
## Run the code
### Dependencies
> NOTE: Unlike many of the other examples, this example requires a system installation of ROS
in addition to the packages from requirements.txt.
>
> The commands are focused on the official ROS installation path on an Ubuntu distro, but should also work analogously if you installed ROS through [Robostack](https://robostack.github.io/index.html).
This example was developed and tested on top of [ROS2 Kilted Kaiju](https://docs.ros.org/en/kilted/index.html), with [Nav2](https://docs.nav2.org/) and the Turtlebot 4 simulation example from the [nav2_bringup](https://github.com/ros-navigation/navigation2/tree/main/nav2_bringup) package.
If you use another version of ROS, e.g. [Jazzy](https://docs.ros.org/en/jazzy/index.html), you should be able to just replace `kilted` with `jazzy` in the commands below.
Installing ROS is outside the scope of this example, but you will need the equivalent of the following packages:
```
sudo apt install ros-kilted-desktop ros-kilted-nav2-bringup
```
(`ros-kilted-nav2-bringup` pulls in all the navigation and simulation packages we need as dependencies, if not installed yet)
Then clone the Rerun repository to get the example code:
```bash
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
```
Make sure to use a Python virtual environment. Here, we use `venv` (`sudo apt install python3-venv`):
```bash
python3 -m venv --system-site-packages rerun-ros-example
source rerun-ros-example/bin/activate
```
Then install the latest Rerun SDK and the necessary libraries specified in the requirements file of this example:
```bash
pip install --upgrade rerun-sdk
pip install -r examples/python/ros_node/requirements.txt
```
In addition to installing the dependencies from `requirements.txt` into a venv you will also need to source the
ROS setup script:
```bash
source /opt/ros/kilted/setup.bash
```
### Run the code
First, in one terminal launch the Nav2 turtlebot demo:
```bash
source /opt/ros/kilted/setup.bash
ros2 launch nav2_bringup tb4_simulation_launch.py headless:=False
```
This should open two windows for Gazebo and RViz. Use the RViz window to initialize the pose estimate to put the robot on the map, and set a navigation goal to let it move.
You can now connect to the running ROS system by running this in a separate terminal:
```bash
source /opt/ros/kilted/setup.bash
python examples/python/ros_node/main.py # run the example
```
If you wish to customize it, or explore additional features, use the CLI with the `--help` option for guidance:
```bash
python examples/python/ros_node/main.py --help
```
+311
View File
@@ -0,0 +1,311 @@
#!/usr/bin/env python3
"""
Simple example of a ROS node that republishes some common types to Rerun.
The solution here is mostly a toy example to show how ROS concepts can be
mapped to Rerun. For more information on future improved ROS support,
see the tracking issue: <https://github.com/rerun-io/rerun/issues/1537>.
NOTE: Unlike many of the other examples, this example requires a system installation of ROS
in addition to the packages from requirements.txt.
"""
from __future__ import annotations
import argparse
import sys
from collections.abc import Callable
import numpy as np
import rerun as rr # pip install rerun-sdk
from rerun.components import Colormap
try:
import cv_bridge
import laser_geometry
import rclpy
from image_geometry import PinholeCameraModel
from nav_msgs.msg import OccupancyGrid, Odometry
from numpy.lib.recfunctions import structured_to_unstructured
from rclpy.callback_groups import ReentrantCallbackGroup
from rclpy.node import Node
from rclpy.qos import QoSDurabilityPolicy, QoSProfile
from rclpy.time import Time
from sensor_msgs.msg import CameraInfo, Image, LaserScan
from sensor_msgs_py import point_cloud2
from std_msgs.msg import String
from tf2_msgs.msg import TFMessage
except ImportError:
print(
"""
Could not import the required ROS2 packages.
Make sure you have installed ROS2 (https://docs.ros.org/en/kilted/index.html)
and sourced /opt/ros/kilted/setup.bash
See: README.md for more details.
""",
)
sys.exit(1)
class TurtleSubscriber(Node): # type: ignore[misc]
def __init__(self) -> None:
super().__init__("rr_turtlebot")
# Assorted helpers for data conversions
self.pinhole_model = PinholeCameraModel()
self.cv_bridge = cv_bridge.CvBridge()
self.laser_proj = laser_geometry.laser_geometry.LaserProjection()
self.subscribers: list[rclpy.Subscription] = []
# Subscribe to the topics we want to republish to Rerun.
# See the callback methods below for how each message type is handled.
self.subscribe("/tf", TFMessage, self.tf_callback)
self.subscribe("/tf_static", TFMessage, self.tf_callback, latching=True)
self.subscribe("/odom", Odometry, self.odom_callback)
self.subscribe("/scan", LaserScan, self.scan_callback)
self.subscribe("/rgbd_camera/camera_info", CameraInfo, self.cam_info_callback)
self.subscribe("/rgbd_camera/image", Image, self.image_callback)
self.subscribe("/rgbd_camera/depth_image", Image, self.depth_callback)
self.subscribe("/robot_description", String, self.urdf_callback, latching=True)
self.subscribe(
"/map",
OccupancyGrid,
lambda grid: self.occupancy_grid_callback("/map", grid, Colormap.RvizMap, draw_order=1.0),
latching=True,
)
self.subscribe(
"/global_costmap/costmap",
OccupancyGrid,
lambda grid: self.occupancy_grid_callback(
"/global_costmap_costmap", grid, Colormap.RvizCostmap, draw_order=2.0, opacity=0.75
),
)
self.subscribe(
"/local_costmap/costmap",
OccupancyGrid,
lambda grid: self.occupancy_grid_callback(
"/local_costmap_costmap", grid, Colormap.RvizCostmap, draw_order=3.0, opacity=0.75
),
)
def subscribe(
self, topic: str, msg_type: type, callback: Callable[[rclpy.MsgT], None], latching: bool = False
) -> None:
"""Adds a subscriber to a topic with the given message type and callback."""
# `qos_profile` can either be an int (history depth) or a QoSProfile.
# See: https://docs.ros.org/en/rolling/p/rclpy/rclpy.node.html#rclpy.node.Node.create_subscription
qos_profile = QoSProfile(depth=1, durability=QoSDurabilityPolicy.TRANSIENT_LOCAL) if latching else 10
sub = self.create_subscription(
msg_type=msg_type,
topic=topic,
callback=callback,
qos_profile=qos_profile,
callback_group=ReentrantCallbackGroup(), # allow concurrent callbacks
)
self.subscribers.append(sub)
def cam_info_callback(self, info: CameraInfo) -> None:
"""
Logs CameraInfo as a Rerun Pinhole.
"""
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",
),
)
def odom_callback(self, odom: Odometry) -> None:
"""
Logs data from Odometry as Rerun Scalars.
"""
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))
def image_callback(self, img: Image) -> None:
"""
Logs an RGB image as a Rerun Image.
"""
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"))
def depth_callback(self, img: Image) -> None:
"""
Logs a depth image as a Rerun DepthImage.
"""
time = Time.from_msg(img.header.stamp)
depth_image = rr.DepthImage(
self.cv_bridge.imgmsg_to_cv2(img, desired_encoding="32FC1"),
meter=1.0,
colormap="viridis",
)
rr.set_time("ros_time", timestamp=np.datetime64(time.nanoseconds, "ns"))
rr.log("rgbd_camera/depth_image", depth_image)
rr.log("rgbd_camera/depth_image", rr.CoordinateFrame(frame=img.header.frame_id + "_image_plane"))
def occupancy_grid_callback(
self,
entity_path: str,
grid: OccupancyGrid,
colormap: rr.components.Colormap,
draw_order: float | None = None,
opacity: float | None = None,
) -> None:
"""
Logs a ROS OccupancyGrid as a Rerun GridMap.
"""
time = Time.from_msg(grid.header.stamp)
rr.set_time("ros_time", timestamp=np.datetime64(time.nanoseconds, "ns"))
# Log the coordinate frame ID of the map.
# The local offset of the map frame within the grid is handled by the archetype (see below).
rr.log(entity_path, rr.CoordinateFrame(frame=grid.header.frame_id))
# 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,
),
)
def scan_callback(self, scan: LaserScan) -> None:
"""
Logs a LaserScan after transforming it to line-segments.
Note: we do a client-side transformation of the LaserScan data into Rerun
points / lines until Rerun has native support for LaserScan style projections:
[#1534](https://github.com/rerun-io/rerun/issues/1534)
"""
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))
def urdf_callback(self, urdf_msg: String) -> None:
"""
Forwards the robot description message to Rerun's built-in URDF loader.
Documentation about URDF support in Rerun can be found here:
https://rerun.io/docs/howto/logging-and-ingestion/urdf
"""
# 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,
)
def tf_callback(self, tf_msg: TFMessage) -> None:
"""
Logs TF transforms to Rerun as Transform3D messages,
with `parent_frame` and `child_frame` fields set.
Documentation about transforms in Rerun can be found here:
https://rerun.io/docs/concepts/transforms
"""
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,
),
)
def main() -> None:
parser = argparse.ArgumentParser(description="Simple example of a ROS node that republishes to Rerun.")
rr.script_add_args(parser)
args, unknownargs = parser.parse_known_args()
rr.script_setup(args, "rerun_example_ros_node")
# Any remaining args go to rclpy
rclpy.init(args=unknownargs)
turtle_subscriber = TurtleSubscriber()
# Use the MultiThreadedExecutor so that calls to `lookup_transform` don't block the other threads
rclpy.spin(turtle_subscriber, executor=rclpy.executors.MultiThreadedExecutor())
turtle_subscriber.destroy_node()
rclpy.shutdown()
if __name__ == "__main__":
main()
+10
View File
@@ -0,0 +1,10 @@
# 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