rerun-urdf について
ロボットの URDF モデルを Rerun に取り込み、関節状態から姿勢を計算するスキルです。関節値の対応付けや制限値の確認、シーン内の独立したフレームの接続方法を扱います。
主な機能
- 01URDF のメッシュと固定変換のストリーム出力
- 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
詳細
URDF の取り込み
URDF には、ロボットの形状とキネマティックツリーが記述されています。
これを取り込むには、1 つの rerun.urdf.UrdfTree に対して 2 回の API 呼び出しを行います。まず静的モデルをストリーミングし、次に関節の状態から順運動学を使ってモデルを動かします。生成されるトランスフォームは派生データなので、ベースではなくレイヤーです(rerun-data-model テーブルの「URDF + joints (computed)」の行)。
このスキルは UrdfTree API と、API では判断できない 2 つの事項を扱います。関節値を URDF の関節にどう対応づけるか、そしてシーン内の接続されていないフレームを 1 つのルートにどう接続するかです。ストリーム/レンズの処理(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 — URDF ではなく MJCF のため、変換が必要です。他に入手先がない場合に限り検討する価値があります。
| ロボット | 入手先 | 備考 |
|---|---|---|
| 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 を実行する前に、次の 2 点でつまずくことがあります。
- 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はエンティティツリーの名前空間を設定します。ロボットのインスタンスごとに 1 つ指定します。frame_prefixはフレーム名の名前空間を設定します(base_link->arm_base_link)。 1 つの記録内に 2 台のロボットがある場合は、異なるプレフィックスが必要です。そうしないとルートが衝突し、トランスフォームが混線します。ロボットが 1 台だけの場合は空のままにします。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 は 2 つの処理を行います。
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
)
MutateLens を Asset3D:albedo_factor に適用すると、ロボットのメッシュの色を変更できます(rerun-chunk-processing を参照)。
2. 順運動学を解きます。 ジョイント名と対応するジョイント値を渡すと、入力行ごとに 1 つの 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 に含まれていません。次の 3 点で、エラーが発生しても気づかないことがあります。
- 順序。 読み込んだ
valuesと対応するnames配列を明示的に作成してください。メッセージのフィールド順序が URDF の<joint>の順序と一致すると決めつけてはいけません。 - 個数。 URDF の
fixed以外のジョイント数が、報告される値の個数と一致することはほとんどありません。1 つの値として送られるグリッパーが、URDF では 2 つのプリズマティックジョイントであることもよくあります。また、メッセージから 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 は、1 つのソース形式、つまりフレームごとの関節値を格納した LeRobot 形式の parquet カラムを対象に、この確認を実装したものです。
これを例として読み、ご自身の形式に合わせてコピーして調整してください。データがすでに LeRobot 形式の parquet ファイルであれば、そのまま実行できます。
uv run scripts/check_joint_mapping.py <urdf> <values.parquet> --column observation.state --degrees
重要: これは本格的な処理の前に行う検証です。大規模なデータセットのすべてのファイルに対して実行しないでください。 検証には、代表的なファイルを 1 つ選ぶか、少数のファイルを選択してください。
各関節について、一般的な補正候補(恒等変換、符号反転、% 360、±90°/±180°のオフセット、およびそれらの組み合わせ)を試し、関節の制限内に収まる候補を報告します。
報告されるのは適合状況であり、正解ではありません。候補が 2 つ残る関節は曖昧であり、範囲を確認するだけではどちらかを特定できません。
データセットの既知の姿勢や形状を基準にして、曖昧な対応付けを検証してください。
失敗のパターンを見て判断してください。
カラム全体が範囲外なら、符号、単位、オフセットが誤っています。これはバグであり、姿勢も確信を持てるほど誤ったものになります。
数フレームだけ数度制限を超えている場合は、キャリブレーションの許容誤差です。これは想定内であり、回避策ではなく clamp=True で対処します。
clamp=True は問題のある行ごとに警告を 1 回出すため、対応付けを調べる際に実行してください。警告が大量に出るなら対応付けに問題がある兆候です。一方、1 つの関節の極値で少数の警告が出る程度なら、正常な記録です。
まず関節状態を読み取れることを確認する
関節値の取得元はこのスキルの対象ではありませんが、関節値のソースが機能していないと、エラーなしで FK レイヤーが空になります。たとえば、ソースパスに一致するものがない、デコーダーが 0 行を返す、リーダーがメッセージを黙って破棄する、といった場合です。ソースが何であれ、FK のデバッグを始める前に、関節状態ストリームが行を返すことを確認してください。ソース形式用のインポータースキルでは、その形式でストリームが空になる場合の問題を扱っています。
変換の合成方法(ログを記録する前に考慮する)
Rerun は、フレームからルートまで変換をつなげて姿勢を解決します。このチェーン内のエッジを定義する方法は 2 つあり、URDF の取り込みでは両方が使われます。
- エンティティパスの階層による方法。
/a/b上にフレーム名のないTransform3Dがある場合、それは親パス/aに対する/a/bの変換です。合成はエンティティツリーに従います。 - 明示的なフレームグラフによる方法。
parent_frameとchild_frameを持つTransform3Dは、エンティティツリー上のどこに記録されているかに関係なく、名前付きフレーム間のエッジを定義します。URDF の FK ではこの方法が使われます。各関節の変換は、親リンクと子リンクのフレームを指定します。
したがって、URDF の取り込み結果は名前付きフレームのグラフになります。エッジが存在するのは、ある Transform3D がその正確な parent_frame -> child_frame の組み合わせを指定している場合に限られます。入力エッジのないフレームはルートです。ビューアーはすべてのルートをワールド原点に描画するため、接続されていない 2 台のロボットはエラーにならず、黙って重なります。
変換フォレストの解決(データだけでは常に解決できるとは限らない部分)
URDF は ベースリンクをルートとする1つのツリーです。FK と固定ジョイントによって、そのツリー内のすべてのエッジが得られます。実際のシーンは、URDF で接続されていない複数のルートからなる フォレストです。ワールドまたはシーンのフレーム、各ロボットのベース、すべてのカメラやセンサーのフレームが該当します。これらのルートをつなぐエッジはキャリブレーション(サイドカー内の外部パラメーター、TF の静的パブリッシャー、ハンドアイの結果など)から得られるもので、URDF には含まれません。また、単純に存在しない場合もあります。単一の連結ツリーでは解決できないこともあります。意図的に解決してください。
- すべてのフレームを列挙します。
urdf.joints()を反復処理して、parent_link/child_linkのペア(ツリー内のエッジ。フレームプレフィックス付き)を収集します。urdf.root_link()はその URDF のルートです。さらに、シーンに必要なすべてのセンサー/ワールドフレーム(rerun-data-modelの表から)を追加します。意図するルートを1つ決めます。 - 各エッジをソースごとに分類します。 URDF 内のエッジ(FK ジョイント、
fixedジョイント)は、ジョイント値と URDF から得られます。ルート間のエッジ(ルートから各ロボットのベース、ルートから各固定センサー、アームのリンクから手首に取り付けたカメラなど)はキャリブレーションから得られるため、自分で記録する必要があります。 - 2つのリンク間が
fixedジョイントだけでつながっている場合(カメラブラケット、ツールマウントなど)、URDF から固定チェーンを合成します。urdf.joints()を介して親リンクをたどり、各ジョイントのorigin_xyz/origin_rpyを同次行列に変換して、チェーンに沿って乗算します。たどっても対象のリンクに到達できない場合は、停止してその旨を伝えます(「A から B への固定チェーンがありません。C で行き止まりです」)。チェーンが壊れているとポーズが誤り、欠落として扱われるわけではありません。 - エッジセットを作成し、ルートを見つけます。 記録するすべての
parent_frame -> child_frameペア(URDF + キャリブレーション)を収集します。各フレームから親をたどり、意図するルートに到達しないフレームは未接続のルートであり、不足しているエッジを示します。これは RRD を書き込む前に実行できる、純粋なグラフチェックです。 - 不足しているエッジをすべて解決するか、明確に失敗させます。 各エッジについて、以下のようにします。
- キャリブレーションから変換が得られる場合は、記録します(次のセクションを参照)。
- データから得られない場合、ツリーは解決不能です。フレームを接続しないままにしてはいけません(原点に重なり、1つに統合されたシーンとして表示されます)。処理を中止して不足しているエッジを示すか、恒等変換を記録して、仮定したエッジを明示する大きな警告を出します。どちらの対応をしたか明記してください。
- ソース間でフレーム名を一致させます。 FK 由来の
parent_frame/child_frameは、静的ジオメトリに対してurdf.stream()が出力する名前と一致しなければなりません。また、キャリブレーションのエッジでも同じ名前を使う必要があります。そうしないと、リンクがメッシュから浮いてしまいます。名前を一致させるのはframe_prefixです。どこでも同じものを再利用してください。
正しく取り込まれたデータにはルートがちょうど1つあり、すべてのフレームからそのルートへのパスがあります。
接続を正しく記録する
接続エッジは、接続するフレーム名を持つ静的な 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 にはチェーンがまたぐ2つのリンクフレームを設定します。すべてのエッジチャンクをモデルおよび FK ストリームと同じ記録にマージし、同じグラフを共有させます。
最小構成(1台のロボット、汎用ジョイントソース)
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 がデータ固有の要素です。これらを正しくするのは、上記のマッピングセクションです。
データセット全体で共有する1つの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を渡します。- データフレームクエリにアセットは表示されません。アセットはデータセット自身のセグメントにとどまります。
カタログがなくても、同じ分割にはコストがかかります。モデルをRRDに一度だけ書き込み、各エピソードにコピーするのではなく、確認するエピソードの横に開いてください。
実際に問題を引き起こす注意点
- 空のレイヤーでエラーもない:関節状態のソースが無効です(デコード後の行数が0。形式に対応するインポータースキルを参照してください)。または
JOINT_SOURCE_PATH/コンポーネント名が間違っています。 - 正しそうに見えるが誤ったポーズ:関節数、順序、または単位が間違っています。
- レイヤーは書き込まれるが、アタッチされない:
recording_id != segment_idです。 - フレームが衝突する:2台のロボットで同じ
frame_prefixを共有しています。 - シーンが原点で一体化して見える:接続されていないルートが、キャリブレーションエッジなしで単位変換としてログに記録されています。
- カタログへの取り込みがチャンクを拒否する、または順序を誤る:
OBJECT_STOREの最適化がスキップされています。 - 実行中のビューアーでレイヤーを開いても結合されない:
.rrdを パスで 開くと、独自のデータセットとして登録され、application_idが置き換えられるため、セグメント上ではなく横に配置されます。 レイヤーをローカルで目視確認するには、モデル+FK+ベースを1つの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の2レンズパターン、2台のロボット+シーンのURDF、プレフィックス、色の変更、キャリブレーションオフセット)https://github.com/rerun-io/rerun/tree/main/examples/python/animated_urdf(従来のロギングAPI:log_urdf_to_recording、関節ごとのcompute_transform)- カタログのオブジェクトモデル:アセット(
register_asset、制限事項、アセットの読み戻し方法) rerun-data-model(このスキルが使用するマッピング表)- 関節状態のソース形式に対応するインポータースキル(ソースを読み取り可能にする方法)
rerun-chunk-processing(レンズ/ストリーム、書き込み/最適化の仕組み)