Files
rerun-io--rerun/examples/python/robot_data_preprocessing/robot_data_preprocessing.py
T
2026-07-13 13:05:14 +08:00

197 lines
7.9 KiB
Python

"""
Demonstrates how to use Rerun's chunk processing API to assemble a robot recording
from multiple file sources (MCAP, custom data, URDF, …):
- fix recording errors
- add external static data
- compute joint transforms using URDF
- insert URDF assets
- …
The resulting merged stream is saved to an RRD file, which can be
opened in the Rerun viewer or registered to a dataset catalog.
"""
from __future__ import annotations
import json
from pathlib import Path
import pyarrow as pa
import rerun as rr
from rerun.experimental import Chunk, DeriveLens, LazyChunkStream, McapReader, MutateLens, OptimizationProfile, Selector
from rerun.urdf import UrdfTree
PARENT_DIR = Path(__file__).parent
DATA_DIR = PARENT_DIR / "input_data"
OUTPUT_DIR = PARENT_DIR / "output"
def json_transforms_stream(json_path: Path) -> LazyChunkStream:
"""Loads transform data saved in JSON as a chunk stream of static Transform3D."""
with json_path.open() as f:
transforms = json.load(f)["transforms"]
chunk = Chunk.from_columns(
"/tf_static/robot_offsets",
indexes=[],
columns=rr.Transform3D.columns(
translation=[transform["translation"] for transform in transforms],
quaternion=[transform["quaternion_xyzw"] for transform in transforms],
parent_frame=[transform["parent"] for transform in transforms],
child_frame=[transform["child"] for transform in transforms],
),
)
return LazyChunkStream.from_iter([chunk])
def change_albedo_factor_lens(new_albedo: rr.components.AlbedoFactor) -> MutateLens:
"""Replaces Asset3D albedo factors with a fixed color."""
return MutateLens(
"Asset3D:albedo_factor",
Selector(".").pipe(lambda old_albedo: pa.array([new_albedo] * len(old_albedo), type=old_albedo.type)),
)
def joints_batch_lens(robot_urdf: UrdfTree, to_entity: str = "/tmp") -> DeriveLens:
"""Computes intermediate transform batches from each joint state message using the URDF."""
return DeriveLens("schemas.proto.JointState:message", output_entity=to_entity).to_component(
"rerun.urdf.JointTransformBatch",
Selector(".").pipe(
lambda joint_state_messages: robot_urdf.compute_joint_transform_batches(
names=Selector(".joint_names").execute(joint_state_messages),
values=Selector(".joint_positions").execute(joint_state_messages),
)
),
)
def output_transforms_lens() -> DeriveLens:
"""Scatters transform batches into final Transform3D rows per joint."""
return (
DeriveLens("rerun.urdf.JointTransformBatch", output_entity="/tf", scatter=True)
.to_component(
rr.Transform3D.descriptor_translation(),
Selector(".[].translation"),
)
.to_component(
rr.Transform3D.descriptor_quaternion(),
Selector(".[].quaternion"),
)
.to_component(
rr.Transform3D.descriptor_parent_frame(),
Selector(".[].parent_frame"),
)
.to_component(
rr.Transform3D.descriptor_child_frame(),
Selector(".[].child_frame"),
)
)
def main() -> None:
"""Run the main chunk-processing pipeline for this example."""
OUTPUT_DIR.mkdir(exist_ok=True)
# Create a chunk stream from the MCAP file.
# The reader uses Rerun's MCAP importer (like the viewer or `rerun mcap convert` CLI),
# so we get Rerun components that we can process in-stream.
mcap_stream = McapReader(DATA_DIR / "episode.mcap").stream()
# The world-to-base transform offsets of the two robots are stored in a separate JSON file.
robot_offsets_stream = json_transforms_stream(DATA_DIR / "offsets.json")
# Load the same robot URDF twice, with distinct entity path and frame name prefixes for each robot.
robot_urdf_left = UrdfTree.from_file_path(
DATA_DIR / "robot.urdf",
entity_path_prefix="robot_left",
frame_prefix="left_",
static_transform_entity_path="/tf_static/left_robot",
)
robot_urdf_right = UrdfTree.from_file_path(
DATA_DIR / "robot.urdf",
entity_path_prefix="robot_right",
frame_prefix="right_",
static_transform_entity_path="/tf_static/right_robot",
)
# Load the scene URDF (table & external cameras).
scene_urdf = UrdfTree.from_file_path(DATA_DIR / "scene.urdf", static_transform_entity_path="/tf_static/scene")
# The external camera calibration in our example MCAP has swapped width/height.
# We can fix this with a MutateLens.
mcap_stream = mcap_stream.lenses(
MutateLens(
"Pinhole:resolution",
Selector(".").pipe(
lambda resolution: pa.array(
[(height, width) for width, height in resolution.to_pylist()], type=resolution.type
)
),
),
content=["/external/cam_low", "/external/cam_high"],
output_mode="forward_unmatched",
)
# For each robot, compute the joint transforms in batches and convert to the final Transform3D chunks.
# We keep the original joint states in the stream ("forward_all") while dropping the temporary batch values.
mcap_stream = (
mcap_stream
.lenses(joints_batch_lens(robot_urdf_left), content="/robot_left/joint_states", output_mode="forward_all")
.lenses(output_transforms_lens(), content="/tmp", output_mode="drop_unmatched")
.lenses(joints_batch_lens(robot_urdf_right), content="/robot_right/joint_states", output_mode="forward_all")
.lenses(output_transforms_lens(), content="/tmp", output_mode="drop_unmatched")
)
# We also modify each robot's visual meshes to have custom colors / transparency by mutating the albedo factor.
robot_urdf_left_stream = robot_urdf_left.stream().lenses(
change_albedo_factor_lens(rr.components.AlbedoFactor([80, 120, 175, 125])),
content="/robot_left/wxai/visual_geometries/**",
output_mode="forward_unmatched",
)
robot_urdf_right_stream = robot_urdf_right.stream().lenses(
change_albedo_factor_lens(rr.components.AlbedoFactor([200, 120, 90, 125])),
content="/robot_right/wxai/visual_geometries/**",
output_mode="forward_unmatched",
)
# Drop the collision meshes from each URDF.
# (you can also disable them in the viewer, but here we demonstrate how to drop them entirely)
robot_urdf_left_stream = robot_urdf_left_stream.drop(content="/robot_left/wxai/collision_geometries/**")
robot_urdf_right_stream = robot_urdf_right_stream.drop(content="/robot_right/wxai/collision_geometries/**")
# Merge the streams in logical groups (base recording and URDF data).
# (alternatively we could also merge everything in one stream here, if desired)
data_stream = LazyChunkStream.merge(
mcap_stream,
robot_offsets_stream,
)
urdf_stream = LazyChunkStream.merge(
robot_urdf_left_stream,
robot_urdf_right_stream,
scene_urdf.stream(),
)
# Run the pipeline, materialize into a ChunkStore and optimize it before writing to an RRD.
# Here we use an optimization profile suited for object-store (query & stream applications).
data_stream.collect(optimize=OptimizationProfile.OBJECT_STORE).write_rrd(
OUTPUT_DIR / "data.rrd",
application_id="rerun_example_robot_data_preprocessing",
recording_id="episode",
)
# Write also the URDF streams to an RRD.
# Note how we use the same `recording_id` here to group the two RRD layers into the same logical recording.
# https://rerun.io/docs/concepts/logging-and-ingestion/recordings#logical-vs-physical-recordings
urdf_stream.collect(optimize=OptimizationProfile.OBJECT_STORE).write_rrd(
OUTPUT_DIR / "urdf.rrd",
application_id="rerun_example_robot_data_preprocessing",
recording_id="episode",
)
print(f"\nWrote output RRDs to: {OUTPUT_DIR}")
if __name__ == "__main__":
main()