mcpField
R
rerun-urdf

rerun-urdf

github.com/rerun-io数据与数据库11.7k125 次安装今天更新
代码仓库

关于 rerun-urdf

用于将机器人 URDF 模型导入 Rerun,并根据关节状态计算位姿的技能,重点说明关节映射、限位检查及场景中独立坐标系的连接。

主要功能

  1. 01流式输出 URDF 网格和固定变换
  2. 02计算正向运动学变换批次
  3. 03检查关节并验证数值限位
  4. 04指导帧前缀与根节点连接

适用场景

  1. 01结合关节状态数据可视化机器人模型
  2. 02处理数据集前检查关节映射
  3. 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-ARM100Simulation/SO100/so100.urdf、Simulation/SO101/so101_new_calib.urdf;普通 URDF,相对网格路径
ALOHA(ViperX 300、WidowX)Interbotix/interbotix_ros_manipulatorsxacro
Franka Panda / FR3frankaemika/franka_descriptionxacro
Universal Robots UR3 至 UR20UniversalRobots/Universal_Robots_ROS2_Descriptionxacro,机械臂参数位于 YAML 中
Kinova Gen3Kinovarobotics/ros2_kortexxacro
UFactory xArm / Lite 6xArm-Developer/xarm_ros2xacro
Unitree Z1、G1、H1、Go2unitreerobotics/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;有些边则根本不存在。单棵连通树不一定能求解。请有意识地解析:

  1. 枚举每个坐标系。 遍历 urdf.joints(),收集 parent_link/child_link 对(树内边,带坐标系前缀);urdf.root_link() 是该 URDF 的根。添加场景所需的每个传感器/世界坐标系(来自 rerun-data-model 表)。确定唯一的预期根。
  2. 按来源分类每条边。 URDF 内的边(FK 关节、fixed 关节)来自 URDF 和关节值。根之间的边(根到每个机器人基座、根到每个固定传感器、机械臂链接到腕部安装的摄像头)来自校准,必须由你自行记录。
  3. 需要计算仅由 fixed 关节连接的两个链接之间的变换时,根据 URDF 合成固定链(例如摄像头支架、工具安装座):通过 urdf.joints() 沿 fixed 关节遍历父链接,将每个关节的 origin_xyz/origin_rpy 转换为齐次矩阵,并沿链相乘。如果遍历无法到达目标链接,请停止并说明原因(“A 到 B 之间没有固定链;停在 C”)。链断裂会导致位姿错误,而不是缺失。
  4. 构建边集合并查找根。 收集你将记录的每个 parent_frame -> child_frame 对(URDF + 校准)。从每个坐标系沿父级向上遍历;任何无法到达预期根的坐标系都是未连接的根,也意味着存在一条缺失的边。这是纯图检查,可以在写入 RRD 前执行。
  5. 补齐每条缺失的边,否则明确报错。 对于每条缺失的边:
    • 如果校准提供了变换,就将其记录下来(见下一节)。
    • 如果数据未提供该变换,树就无法求解。不要让坐标系保持断开(它会塌缩到原点,看起来像合并成一个场景)。要么中止并指出缺失的边,要么记录单位变换并发出明确警告,指出作出的假设。说明你采取了哪种做法。
  6. 确保不同来源的坐标系名称一致。 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,然后在查看任意片段时与之一起打开,而不是将其复制到每个片段中。

可能导致实际故障的问题

  1. 图层为空,且没有报错:关节状态源失效(解码后得到 0 行;请参阅适用于你所用格式的导入器技能),或 JOINT_SOURCE_PATH/组件名称错误。
  2. 姿态看起来无误,实际却不正确:关节数量、顺序或单位错误。
  3. 图层已写入,但始终未附加:recording_id != segment_id。
  4. 坐标系冲突:两个机器人共用了同一个 frame_prefix。
  5. 场景看起来都合并在原点:未连接的根坐标系被记录为单位变换,且没有标定边。
  6. Catalog 导入拒绝或打乱了数据块顺序:跳过了 OBJECT_STORE 优化。
  7. 在正在运行的查看器中打开的图层始终无法合并:按路径打开 .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 并合并存储。
  8. 机器人不可见,但变换正确:网格解析失败(参见“获取 URDF”)。
  9. 将 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(透镜/流、写入/优化机制)

评论

支持 Markdown