关于 rerun-urdf
用于将机器人 URDF 模型导入 Rerun,并根据关节状态计算位姿的技能,重点说明关节映射、限位检查及场景中独立坐标系的连接。
主要功能
- 01流式输出 URDF 网格和固定变换
- 02计算正向运动学变换批次
- 03检查关节并验证数值限位
- 04指导帧前缀与根节点连接
适用场景
- 01结合关节状态数据可视化机器人模型
- 02处理数据集前检查关节映射
- 03排查网格不可见或坐标系未连接
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ rerun-io/ rerun --skill rerun-urdf安装后在 Claude Code 中输入/rerun-urdf确认已加载。
附带文件scripts/check_joint_mapping.py
详细介绍
Rerun URDF 导入
URDF 描述了机器人的几何形状及其运动学树。
导入它需要在一个 rerun.urdf.UrdfTree 上调用两次 API:先流式传输静态模型,再根据关节状态通过正向运动学驱动模型。生成的变换是派生数据,因此它们属于图层,而不是基础数据(rerun-data-model 表中的 URDF + joints (computed) 行)。
此技能介绍 UrdfTree API,以及它无法替你做出的两个判断:关节值如何映射到 URDF 关节,以及场景中的非连通坐标系如何连接到同一个根节点。流式处理和透镜相关的配置(LazyChunkStream、DeriveLens、Selector,以及 RRD 的写入和优化)已在 rerun-chunk-processing 中介绍;请直接使用,不要重新实现。以下内容不依赖任何数据格式:关节名称、关节值和标定数据的来源,都由你自行接入。
获取 URDF
数据集很少会附带 URDF,而直接从机器人自己的代码库获取也不总是最快的方式。 按以下顺序查找:
robot_descriptions.py— 运行pip install robot_descriptions,然后执行from robot_descriptions import ur5e_description; ur5e_description.URDF_PATH。 它会下载并缓存上游描述文件,并提供一个普通路径,网格文件也已放在其旁边。 先查看它的索引;它涵盖了下表中的大多数机器人。- 如果描述文件比软件包锁定的版本更新,或软件包中没有该文件,请查看厂商代码库。
- MuJoCo Menagerie — 使用的是 MJCF,而不是 URDF,因此需要转换。只有在没有其他选择时才值得使用。
| 机器人 | 来源 | 备注 |
|---|---|---|
| SO-100 / SO-101 (LeRobot) | TheRobotStudio/SO-ARM100 | Simulation/SO100/so100.urdf、Simulation/SO101/so101_new_calib.urdf;普通 URDF,相对网格路径 |
| ALOHA(ViperX 300、WidowX) | Interbotix/interbotix_ros_manipulators | xacro |
| Franka Panda / FR3 | frankaemika/franka_description | xacro |
| Universal Robots UR3 至 UR20 | UniversalRobots/Universal_Robots_ROS2_Description | xacro,机械臂参数位于 YAML 中 |
| Kinova Gen3 | Kinovarobotics/ros2_kortex | xacro |
| UFactory xArm / Lite 6 | xArm-Developer/xarm_ros2 | xacro |
| Unitree Z1、G1、H1、Go2 | unitreerobotics/unitree_ros | 每种机器人均提供普通 URDF |
在 FK 开始运行前,有两个常见问题需要注意:
- xacro 不是 URDF。 首先运行
xacro arm.urdf.xacro > arm.urdf(pip install xacro),并传入文件所需的参数(夹爪类型、机械臂尺寸)。 将 xacro 文件交给UrdfTree.from_file_path会导致解析失败。 package://网格 URI。 导入器会扫描ROS_PACKAGE_PATH(ROS 1)和AMENT_PREFIX_PATH(ROS 2),以查找对应的软件包;如果找不到,则将 URI 的其余部分视为相对于 URDF 所在目录的路径。 在未加载 ROS 工作空间的环境中,请导出上述任一变量,或将网格文件放在相对于 URDF 的位置。 网格文件解析失败时,机器人在运动学上仍然正确,但不可见。
API
from rerun.urdf import UrdfTree
urdf = UrdfTree.from_file_path(
urdf_path,
entity_path_prefix="robot", # links log under /robot/<link>
frame_prefix="", # prepended to every frame name
static_transform_entity_path="robot/tf_static",
)
entity_path_prefix为实体树设置命名空间。每个机器人实例使用一个。frame_prefix为坐标系名称设置命名空间(base_link->arm_base_link)。 同一条记录中的两台机器人需要使用不同前缀,否则它们的根节点会冲突, 坐标变换也会串线。只有一台机器人时留空。static_transform_entity_path是记录 URDF 固定关节变换的位置 (默认为/tf_static)。
也可以对树进行自省(完整接口见 help(UrdfTree))。
对于一次性、非流式的用法,可以使用 joint.compute_transform(value)、joint.compute_transform_columns(values)(供 rr.send_columns 使用),以及 urdf.log_urdf_to_recording(),通过经典日志 API 记录整个模型(animated_urdf 示例 https://github.com/rerun-io/rerun/tree/main/examples/python/animated_urdf 就采用了这种方式)。
对于流水线,UrdfTree 可以完成两件事。
1. 流式传输静态模型。 将视觉网格(Asset3D)和固定关节变换作为分块输出。这就是完整的“记录 URDF”步骤:
model = (
urdf.stream(include_joint_transforms=True).drop( # rest-pose joint transforms too
content="/robot/**/collision_geometries/**"
) # unless you need collision meshes
)
使用 Asset3D:albedo_factor 上的 MutateLens 为机器人的网格重新着色(参见 rerun-chunk-processing)。
2. 求解正向运动学。 给定关节名称和对应的关节值,它会为每个输入行返回一个 rerun.urdf.JointTransformBatch。每个批次都是一个关节条目列表,包含 parent_frame、child_frame、translation 和
quaternion:
batches = urdf.compute_joint_transform_batches(names, values, clamp=False)
# names, values: pyarrow arrays, one list per timestamp (names aligned to values)
# clamp=True clamps out-of-limit values and warns, useful while debugging units
你几乎总会在流中运行此操作,以保持列式和惰性,
使用双透镜模式:先生成批次,再通过 scatter=True 将其写入 Transform3D.descriptor_translation/quaternion/parent_frame/child_frame。完整结构见下文“最小结构”一节;robot_data_preprocessing 示例(见参考资料)提供了一个完整可运行的实例。唯一与 URDF 相关的部分是第一个透镜中的 compute_joint_transform_batches 调用;
其余都是通用的流处理机制(rerun-chunk-processing)。
合并模型流与 FK 流,执行 collect(optimize=OBJECT_STORE),然后执行 write_rrd(..., recording_id=<segment_id>)。recording_id 必须与基础片段 ID 相同,否则图层不会附加;注册时会丢弃 application_id。
将关节状态映射到 URDF 关节(需要你提供映射;数据不会提供)
compute_joint_transform_batches 的结果是否正确,完全取决于你传入的 names/values,而 URDF 中并不包含这种映射。以下三类问题可能会悄无声息地导致错误:
- 顺序。 构建一个明确的
names数组,使其与读取到的values对齐。 切勿假定消息中的字段顺序与 URDF 的<joint>顺序一致。 - 数量。 URDF 中非
fixed关节的数量很少与报告的值数量相同。以一个值发送的夹爪,在 URDF 中通常对应两个移动关节; 消息中也可能省略 mimic 关节。请明确协调数量,并使用 API 完成:遍历urdf.joints(),按joint_type和mimic分类。设置了mimic的关节,其值由驱动关节按driver * multiplier + offset计算;应传入这个值,而不是消息字段。请根据urdf.joints()核对数量,而不是根据消息长度。 - 单位。 URDF 关节使用弧度(旋转关节)和米(移动关节)。如果来源使用其他单位,请进行转换。
以上任一项出错,FK 仍会运行,并写入看似可信但实际错误的姿态。
验证关节映射和限位
源数据中的关节值不一定在 URDF 关节限位范围内。 任何格式(MCAP、Parquet 等)都可能出现这种情况。 因此,检查数据中的关节值是否符合 URDF 模型的限位很有用。 超出范围的值可能表明索引错误或模型不匹配等问题,在计算正向运动学之前应先弄清楚原因。
获取 URDF 模型的限位:
limits = {joint.name: (joint.limit_lower, joint.limit_upper) for joint in urdf.joints() if joint.joint_type != "fixed"}
# then compare each column's min/max against its joint's range
scripts/check_joint_mapping.py 针对一种源数据形态实现了这项检查:每帧关节值存储在 LeRobot 风格的 parquet 列中。
请将其作为示例,根据自己的格式进行复制和调整;如果你的数据已经是 LeRobot 风格的 parquet 文件,也可以直接运行:
uv run scripts/check_joint_mapping.py <urdf> <values.parquet> --column observation.state --degrees
重要: 这项验证应在完整处理之前进行——不要对大型数据集中的每个文件都运行它。 选择一个有代表性的文件或一小部分文件进行验证。
对于每个关节,脚本会尝试常见的候选修正方式(原值、取反、% 360、±90°/±180° 偏移,以及这些方式的组合),并报告哪些修正后的值落在该关节的限位范围内。
报告结果说明的是匹配情况,而不是最终答案:如果一个关节有两个候选修正方式都符合,就表示存在歧义,单靠范围检查无法区分它们。
请根据数据集中的已知姿态或几何信息验证有歧义的映射。
根据异常的形态判断问题。
整列数据都超出限位范围,说明符号、单位或偏移有误——这是程序错误,会导致姿态看起来完全可信但实际错误。
只有少数帧超出限位几度,属于标定余量——这是预期情况,应使用 clamp=True 修正,而不是绕过问题。
clamp=True 会针对每个超限行警告一次,因此在排查映射时运行它:大量警告表明映射可能有问题,而只有少量警告出现在某个关节的极限值附近,说明记录情况良好。
先确保关节状态数据可读
关节值的来源不属于此技能要处理的问题,但如果关节数据源没有数据,FK 层会为空,且不会报错:例如,源路径没有匹配到任何内容、解码器返回零行,或者读取器静默丢弃了消息。无论数据源是什么,在排查 FK 之前,都要先确认关节状态流中有数据行。针对对应源格式的导入器技能会介绍其空数据流的故障模式。
变换如何组合(在记录任何内容之前先弄清楚)
Rerun 会通过将变换从某个坐标系逐级链接到根坐标系来解析姿态。链中的边有两种定义方式,而 URDF 导入会混合使用这两种方式:
- 通过实体路径层级定义。 如果
/a/b上的Transform3D未命名坐标系,那么它表示/a/b相对于其父路径/a的变换。组合时遵循实体树。 - 通过显式坐标系图定义。 如果
Transform3D包含parent_frame和child_frame,它就定义两个具名坐标系之间的边,与记录位置在实体树中的位置无关。URDF FK 使用这种方式:每个关节变换都会为其父链接和子链接的坐标系命名。
因此,URDF 导入会构成一个具名坐标系图。只有某个 Transform3D 明确命名了完全匹配的 parent_frame -> child_frame 对,边才存在。没有入边的坐标系就是根坐标系。查看器会将每个根坐标系渲染在世界原点,这就是两个互不连接的机器人会静默重叠而不是报错的原因。
解析变换森林(数据不一定总能提供的部分)
URDF 是一棵以其基座链接为根的树。FK 和固定关节提供该树内的每条边。真实场景则是一片由 URDF 从未连接的根组成的森林:世界或场景坐标系、每个机器人的基座,以及每个摄像头或传感器坐标系。连接这些根的边来自校准(旁路文件中的外参、TF 静态发布器、手眼标定结果),而非 URDF;有些边则根本不存在。单棵连通树不一定能求解。请有意识地解析:
- 枚举每个坐标系。 遍历
urdf.joints(),收集parent_link/child_link对(树内边,带坐标系前缀);urdf.root_link()是该 URDF 的根。添加场景所需的每个传感器/世界坐标系(来自rerun-data-model表)。确定唯一的预期根。 - 按来源分类每条边。 URDF 内的边(FK 关节、
fixed关节)来自 URDF 和关节值。根之间的边(根到每个机器人基座、根到每个固定传感器、机械臂链接到腕部安装的摄像头)来自校准,必须由你自行记录。 - 需要计算仅由
fixed关节连接的两个链接之间的变换时,根据 URDF 合成固定链(例如摄像头支架、工具安装座):通过urdf.joints()沿fixed关节遍历父链接,将每个关节的origin_xyz/origin_rpy转换为齐次矩阵,并沿链相乘。如果遍历无法到达目标链接,请停止并说明原因(“A 到 B 之间没有固定链;停在 C”)。链断裂会导致位姿错误,而不是缺失。 - 构建边集合并查找根。 收集你将记录的每个
parent_frame -> child_frame对(URDF + 校准)。从每个坐标系沿父级向上遍历;任何无法到达预期根的坐标系都是未连接的根,也意味着存在一条缺失的边。这是纯图检查,可以在写入 RRD 前执行。 - 补齐每条缺失的边,否则明确报错。 对于每条缺失的边:
- 如果校准提供了变换,就将其记录下来(见下一节)。
- 如果数据未提供该变换,树就无法求解。不要让坐标系保持断开(它会塌缩到原点,看起来像合并成一个场景)。要么中止并指出缺失的边,要么记录单位变换并发出明确警告,指出作出的假设。说明你采取了哪种做法。
- 确保不同来源的坐标系名称一致。 FK 推导出的
parent_frame/child_frame必须与urdf.stream()为静态几何体输出的名称一致,校准边也必须使用相同名称,否则链接会脱离网格。frame_prefix用于保持名称一致;请在所有地方重复使用它。
正确的采集结果应恰好只有一个根,且每个坐标系都能沿路径到达该根。
正确记录连接
连接边是携带桥接坐标系名称的静态 Transform3D。它应为静态(无时间索引),这样在整个录制过程中都有效;创建图边的是坐标系名称,而非实体路径。这个 Chunk.from_columns 是罕见的旁路文件例外——它用于记录没有任何读取器或 FK 层能生成的校准变换;不要将其泛化到读取器输出的变换(frame_transforms 主题 → Transform3D)或 FK 推导出的变换:
import rerun as rr
from rerun.chunk import Chunk, LazyChunkStream
# world -> this robot's base, from your calibration (translation + xyzw quaternion)
edge = Chunk.from_columns(
"/world/robot_base", # any sensible bridging entity path
indexes=[], # no index == static
columns=rr.Transform3D.columns(
translation=[translation],
quaternion=[quaternion_xyzw],
parent_frame=["world"], # must match the root frame name
child_frame=["arm_base_link"], # must match the URDF root frame (prefixed)
),
)
edges = LazyChunkStream.from_iter([edge]) # merge alongside model + FK streams
以相同方式记录固定链结果(步骤 3),将 parent_frame 和 child_frame 设置为该链跨越的两个链接坐标系。将所有边块合并到与模型和 FK 流相同的录制中,以便它们共享图。
最小结构(单个机器人,通用关节源)
import rerun as rr
from rerun.chunk import DeriveLens, LazyChunkStream, OptimizationProfile, Selector
from rerun.urdf import UrdfTree
urdf = UrdfTree.from_file_path(urdf_path, entity_path_prefix="robot", static_transform_entity_path="robot/tf_static")
model = urdf.stream(include_joint_transforms=True).drop(content="/robot/**/collision_geometries/**")
joints = source_joint_state_stream() # your reader; one message column of names+values
fk = (
joints
.lenses(
DeriveLens(JOINT_MSG_COMPONENT, output_entity="/tmp/batches").to_component(
"rerun.urdf.JointTransformBatch",
Selector(".").pipe(lambda msgs: urdf.compute_joint_transform_batches(read_names(msgs), read_values(msgs))),
),
content=JOINT_SOURCE_PATH,
output_mode="forward_all",
)
.lenses(
DeriveLens("rerun.urdf.JointTransformBatch", output_entity="/robot/transforms", 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")),
content="/tmp/batches",
output_mode="drop_unmatched",
)
.filter(content="/robot/transforms")
)
LazyChunkStream.merge(model, fk).collect(optimize=OptimizationProfile.OBJECT_STORE).write_rrd(
out_path,
application_id="urdf",
recording_id=segment_id,
)
read_names、read_values、JOINT_MSG_COMPONENT 和 JOINT_SOURCE_PATH 是唯一与数据相关的部分;上述映射部分决定了它们是否正确。
整个数据集共用一个 URDF(资源)
urdf.stream() 只会输出静态数据块——网格和固定关节变换,不包含时间索引。
模型不应放在逐片段层中,否则每个片段都要重复存储相同的网格。
将其注册一次,作为数据集资源:
urdf.stream().collect(optimize=OptimizationProfile.OBJECT_STORE).write_rrd(
model_rrd, application_id="urdf", recording_id="so100_model"
)
asset_id = dataset.register_asset("s3://bucket/so100_model.rrd") # uri must be readable by the server
这里按静态数据与时序数据划分:模型是资源,而 FK 变换仍保留在逐片段层中,并以 recording_id 为键。
在资源中保留 include_joint_transforms=True 不会增加成本,还能为 FK 层未覆盖的片段或时间范围提供静止姿态。
- 查看器会在打开片段时一并加载数据集的资源,并在不同片段间缓存这些资源。
dataset.segment_store(segment_id)会包含资源;传入include_assets=False可跳过每个资源额外的清单请求。- DataFrame 查询不会看到资源——资源仍归数据集自身的片段所有。
没有目录时,同样的划分仍然有用:只写入一次模型 RRD,然后在查看任意片段时与之一起打开,而不是将其复制到每个片段中。
可能导致实际故障的问题
- 图层为空,且没有报错:关节状态源失效(解码后得到 0 行;请参阅适用于你所用格式的导入器技能),或
JOINT_SOURCE_PATH/组件名称错误。 - 姿态看起来无误,实际却不正确:关节数量、顺序或单位错误。
- 图层已写入,但始终未附加:
recording_id != segment_id。 - 坐标系冲突:两个机器人共用了同一个
frame_prefix。 - 场景看起来都合并在原点:未连接的根坐标系被记录为单位变换,且没有标定边。
- Catalog 导入拒绝或打乱了数据块顺序:跳过了
OBJECT_STORE优化。 - 在正在运行的查看器中打开的图层始终无法合并:按路径打开
.rrd会将其注册为独立数据集,并替换其application_id,因此它会出现在片段旁边,而不是合并到片段中。 若要在本地快速查看图层,可以将模型、FK 和基座合并到一个 RRD 中,或者通过指向查看器的RecordingStream(application_id, recording_id=segment_id)同时发送它们,并调用rec.connect_grpc(url="rerun+http://127.0.0.1:9876/proxy"),这样会保留 ID 并合并存储。 - 机器人不可见,但变换正确:网格解析失败(参见“获取 URDF”)。
- 将 URDF 添加到现有记录后没有 3D 视图:自动布局不一定会针对新实体重新构建。请发送显式蓝图,并指定
rrb.Spatial3DView(origin="/robot")(参见rerun-blueprint)。
参考资料
https://github.com/rerun-io/rerun/tree/main/examples/python/robot_data_preprocessing(FK 双透镜模式、两个机器人与场景 URDF、前缀、重新着色、标定偏移)https://github.com/rerun-io/rerun/tree/main/examples/python/animated_urdf(经典日志记录 API:log_urdf_to_recording、逐关节compute_transform)- Catalog 对象模型:资源(
register_asset、限制,以及如何读取资源) rerun-data-model(此技能使用的映射表)- 适用于你的关节状态源格式的导入器技能(使源数据可读)
rerun-chunk-processing(透镜/流、写入/优化机制)