1. はじめに
ロボットシステムを構築するとき,「今どこにいるのか」,「センサーやカメラはどの方向を向いているのか」といった空間的な関係を正しく理解することは非常に重要です.ROS2では,これらの座標変換(transform)を統一的に扱うための仕組みとしてtfが用意されています.たとえば,移動ロボットには「マップ座標系(map)」「オドメトリ座標系(odom)」「ロボット本体の座標系(base_link)」「センサー座標系(lidar_linkやcamera_link)」といった複数の座標系が存在します.それぞれの関係をリアルタイムで管理しなければ,カメラ画像やLiDAR点群を正しい位置に重ね合わせることができません.tfは,これらフレーム間の位置・姿勢関係を時間とともに記録・共有するためのライブラリです.
これからROS 2を使ってロボット開発を行う方にとって,tfは避けて通れない基礎要素です.tfがどのようにロボットの「空間理解」を支えているのかをしっかり掴みましょう.
2. tfの基本概念
ROS 2のtf(正確にはtf2)は,ロボット内のさまざまな座標系(frame)同士の関係を管理するための仕組みです.ロボットは通常,1つの座標系だけで動作しているわけではありません.例えば以下のように,複数のframe(ある基準点を持つ座標系のこと)が存在します.
| フレーム名 | 説明 |
|---|---|
map |
地図全体の基準座標系。SLAMや経路計画で使用 |
odom |
オドメトリ(走行距離計)に基づくローカルな座標系 |
base_link |
ロボット本体の中心に設定される座標系 |
camera_link |
カメラの座標系 |
lidar_link |
LiDARの座標系 |
2.1 Transform(変換)とは
transform(変換)とは,あるframeから別のframeへの位置(translation)と姿勢(rotation)の関係を表します.
たとえば,「カメラはロボットの中心から前方0.2 m、上方0.3 mにある」といった関係を表すのがtransformです.
数学的には,変換は4×4の同次変換行列として表現されます:
T=\left[
\begin{matrix} R & t \\ 0 & 1 \end{matrix}
\right]
ここで,R:3×3の回転行列(姿勢),t:3×1の並進ベクトル(位置)である.
tf2では,この変換をgeometry_msgs::msg::TransformStampedとして扱います.
2.2 tfツリー(Transform Tree)とは
複数のframeは,tfの中でツリー構造(Transform Tree)として管理されます.
例えば次のような構造を考えます:
map
└── odom
└── base_link
├── camera_link
└── lidar_link
ここでは,
- map → odom の変換は,地図上でロボットの位置
- odom → base_link は,オドメトリに基づくロボットの自己位置
- base_link → camera_link / lidar_link は,センサーの取り付け位置
をそれぞれ表しています.tf2を使うと,ある任意の2つのframe間(例:map→camera_link)の関係を自動的に求めることができます.
3. ROS 2 Humbleでのtf2パッケージ構成
ROS 2 Humbleで座標変換を扱う際には,単に「tfを使う」といっても,実際には複数の関連パッケージが連携して動作しています.これらを理解しておくことで,「どのライブラリをimport / includeすべきか」「どのノードが何をしているのか」が明確になります.
3.1 tf2関連パッケージの全体像
ROS 2のtf機能は,大きく以下の5つのパッケージで構成されています.
| パッケージ名 | 役割 | 利用場面 |
|---|---|---|
| tf2 | コアライブラリ.座標変換のロジック・データ構造を提供 | 内部演算・変換管理 |
| tf2_ros | ROS 2インタフェース層.ノード間通信をサポート | ブロードキャスタ・リスナ実装 |
| tf2_geometry_msgs |
geometry_msgsとの互換性を提供 |
PoseStamped, PointStampedなどの変換 |
| tf2_sensor_msgs | センサーデータの座標変換をサポート |
LaserScan, PointCloud2など |
| tf2_tools | デバッグ・可視化ツール群 | tfツリーの可視化・確認 |
これらのパッケージは階層的に関係しており,図で表すと次のようになります:
┌──────────────────────────────────┐
│ tf2_tool │ ← デバッグ・可視化
├──────────────────────────────────┤
│ tf2_ros │ ← ROS通信層(Publisher / Subscriber)
├──────────────────────────────────┤
│ tf2_geometry_msgs / sensor_msgs │ ← メッセージ型変換層
├──────────────────────────────────┤
│ tf2 │ ← コアライブラリ
└──────────────────────────────────┘
3.2 tf2:コアライブラリ
tf2は,すべてのtf関連処理の基盤となるコア部分です.変換行列の生成,時間付きデータの管理,バッファリングなどの内部ロジックを担当します.
主な機能:
-
TransformStampedの格納・検索 - 時間補間(interpolation)
- 変換の逆算(inverse transform)
- ループ構造検出(frameの循環)
tf2自体はROSメッセージを扱わないため,単体では通信は行いません.その代わり,後述のtf2_rosがROS 2ノード間通信を担当します.
3.3 tf2_ros:通信インタフェース層
tf2_rosは,tf2のロジックをROS 2ネットワーク上で利用するための通信レイヤです.このパッケージが,実際のtfブロードキャスタ・リスナ機能を提供します.
代表的なクラス/ノード:
| 名称 | 説明 |
|---|---|
TransformBroadcaster |
動的なtransformを送信する(例:ロボットの動きに合わせて更新) |
StaticTransformBroadcaster |
静的なtransformを一度だけ送信する(例:センサー固定位置) |
TransformListener |
他ノードから送られてくるtransformを受信して管理する |
Buffer |
過去のtransformを時間付きで保存・検索する |
3.4 tf2_geometry_msgs:メッセージ型変換
tf2_geometry_msgsは,geometry_msgs型(Pose、Point、Vectorなど)をtf2の変換システムに統合するための拡張モジュールです.
3.5 tf2_sensor_msgs:センサーデータ変換
LiDARやカメラなどのセンサー情報を,別のframeへ変換するときに使われるのがtf2_sensor_msgsです.
特に,LaserScanやPointCloud2といった空間的データを扱う際に便利です.
3.6 tf2_tools:デバッグと可視化ツール
tfの状態を確認・可視化するためのツール群を提供するのがtf2_toolsです.代表的なコマンドとして:
- tfツリーをPDFに出力
ros2 run tf2_tools view_frames
- 特定のframe間の関係を表示
ros2 run tf2_ros tf2_echo base_link camera_link
これらを活用すると,transformが正しく送信・受信できているかを簡単に検証できます.
4. tfブロードキャスタとリスナの実装
ここからは,tf2_rosを使って実際にtfデータを送受信する方法を学びます.
tfの基本的な通信モデルは非常にシンプルで,以下の2種類のノードで構成されます.
- ブロードキャスタ(Broadcaster):座標変換を送信する側
- リスナ(Listener):座標変換を受信・利用する側
4.1 Transform Broadcaster(ブロードキャスタ)
ブロードキャスタは,2つのframe間の変換(TransformStamped)を生成し,それをトピック/tfまたは/tf_staticへ定期的に送信します.例えば,「ロボットの中心(base_link)に対して,カメラ(camera_link)がどこにあるか」を知らせるのがこのノードです.
4.2 Transform Listener(リスナ)
リスナは,ブロードキャスタが送信した変換を受け取り,指定した2つのframe間の関係を計算します.この処理は,tf2_ros.Bufferを用いて行われます.
4.3 動作確認
いくつかの実装例を提示します.まずは,動作確認用のパッケージを作成します.ここから先は,~/workspace/transform配下でチュートリアルを実装していくことを想定しています.
パッケージのひな形を作成
source /opt/ros/humble/setup.bash
mkdir -p ~/workspace/transform/src
cd ~/workspace/transform/src
ros2 pkg create --build-type ament_python transform_tutorial
ノードを追加
下記の説明を読んでプログラムを追加してください.
動的なtransformを送信するプログラム
このノードを起動すると,/tfトピック上でbase_link→camera_linkの変換が定期的に配信されます.rviz2を起動すれば,tfツリーにこの関係が自動的に追加されていることを確認できます.
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import TransformStamped
import tf2_ros
import math
class DynamicTFBroadcaster(Node):
def __init__(self):
super().__init__('dynamic_tf_broadcaster')
self.broadcaster = tf2_ros.TransformBroadcaster(self)
self.timer = self.create_timer(0.1, self.broadcast_callback)
def broadcast_callback(self):
t = TransformStamped()
t.header.stamp = self.get_clock().now().to_msg()
t.header.frame_id = 'base_link'
t.child_frame_id = 'camera_link'
t.transform.translation.x = 0.2
t.transform.translation.y = 0.0
t.transform.translation.z = 0.3
# 回転を簡単な正弦波で変化させる例
t.transform.rotation.z = math.sin(self.get_clock().now().seconds_nanoseconds()[0])
t.transform.rotation.w = 1.0
self.broadcaster.sendTransform(t)
def main():
rclpy.init()
node = DynamicTFBroadcaster()
rclpy.spin(node)
rclpy.shutdown()
if __name__ == '__main__':
main()
静的なtransformを送信するプログラム
このコマンドを実行すると,base_link→camera_linkの固定変換が/tf_staticトピックに一度だけ送信されます.
ros2 run tf2_ros static_transform_publisher 0.2 0 0.3 0 0 0 base_link camera_link
指定した2つのframe間の関係を計算するプログラム
このプログラムでは,1秒ごとにbase_linkとcamera_link間の変換を取得し,位置情報を出力します.
import rclpy
from rclpy.node import Node
import tf2_ros
from geometry_msgs.msg import PointStamped
class TFListenerExample(Node):
def __init__(self):
super().__init__('tf_listener_example')
self.tf_buffer = tf2_ros.Buffer()
self.listener = tf2_ros.TransformListener(self.tf_buffer, self)
self.timer = self.create_timer(1.0, self.timer_callback)
def timer_callback(self):
try:
now = rclpy.time.Time()
trans = self.tf_buffer.lookup_transform(
'base_link', 'camera_link', now)
self.get_logger().info(
f"Translation: {trans.transform.translation.x:.2f}, "
f"{trans.transform.translation.y:.2f}, "
f"{trans.transform.translation.z:.2f}, "
f"Rotation: {trans.transform.rotation.x:.2f}, "
f"{trans.transform.rotation.y:.2f}, "
f"{trans.transform.rotation.z:.2f}, "
f"{trans.transform.rotation.w:.2f}")
except Exception as e:
self.get_logger().warn(f"Transform not available: {e}")
def main():
rclpy.init()
node = TFListenerExample()
rclpy.spin(node)
rclpy.shutdown()
エントリポイントを設定
setup.pyのentry_points にコンソールスクリプトを登録します.
from setuptools import find_packages, setup
from glob import glob
import os
package_name = 'transform_tutorial'
setup(
name=package_name,
version='0.0.0',
packages=find_packages(exclude=['test']),
data_files=[
('share/ament_index/resource_index/packages',
['resource/' + package_name]),
('share/' + package_name, ['package.xml']),
('share/' + package_name + '/launch', glob('launch/*.launch.py')),
],
install_requires=['setuptools'],
zip_safe=True,
maintainer='cool4',
maintainer_email='cool4@todo.todo',
description='TODO: Package description',
license='TODO: License declaration',
tests_require=['pytest'],
entry_points={
'console_scripts': [
'dynamic_transform_publisher_node = transform_tutorial.dynamic_transform_publisher_node:main',
'transform_listener_node = transform_tutorial.transform_listener_node:main',
],
},
)
launchファイルの作成
ブロードキャスタとリスナを同時に起動するlaunchファイルを作成します.
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
Node(package='transform_tutorial', executable='dynamic_transform_publisher_node', name='broadcaster'),
Node(package='transform_tutorial', executable='transform_listener_node', name='listener')
])
ビルドと実行
cd ~/workspace/transform/
colcon build --packages-select transform_tutorial
source install/setup.bash
ros2 launch transform_tutorial broadcaster_listener.launch.py
次のような出力結果が得られます.
$ ros2 launch transform_tutorial broadcaster_listener.launch.py
[INFO] [launch]: All log files can be found below /home/cool4/.ros/log/2025-10-14-20-16-22-066504-cool4-12323
[INFO] [launch]: Default logging verbosity is set to INFO
[INFO] [dynamic_transform_publisher_node-1]: process started with pid [12324]
[INFO] [transform_listener_node-2]: process started with pid [12326]
[transform_listener_node-2] [INFO] [1760440583.358506064] [listener]: Translation: 0.20, 0.00, 0.30, Rotation: 0.00, 0.00, -0.69, 0.73
[transform_listener_node-2] [INFO] [1760440584.349093105] [listener]: Translation: 0.20, 0.00, 0.30, Rotation: 0.00, 0.00, -0.22, 0.97
[transform_listener_node-2] [INFO] [1760440585.349261694] [listener]: Translation: 0.20, 0.00, 0.30, Rotation: 0.00, 0.00, 0.57, 0.82
[transform_listener_node-2] [INFO] [1760440586.348812682] [listener]: Translation: 0.20, 0.00, 0.30, Rotation: 0.00, 0.00, 0.70, 0.71
dynamic_transform_publisher_nodeで出力されたtfが broadcaster_listener.launch.pyで読み取られて標準出力されます.
5.実践例
ここでは,tf2_rosを使って実際にセンサーデータの座標を変換する例を紹介します.ロボットにはさまざまなセンサーが搭載されており,それぞれ独自のframe(座標系)を持っています.例えば,LiDARの計測結果をロボット中心(base_link)の座標に変換したり,カメラ画像の位置をマップ座標(map)に変換することで,ロボット全体として整合のとれた空間情報を扱うことができます.
5.1 LiDARデータをbase_linkに変換する
LiDARセンサーは通常,lidar_linkというframeを基準に距離データを出力します.しかし,多くのロボットアプリケーションでは,ロボット中心であるbase_linkを基準として環境を認識する方が便利です.ここでは,LiDARが出力した点群データをlidar_linkからbase_linkへ変換する簡単な例を示します.
import rclpy
from rclpy.node import Node
import tf2_ros
from geometry_msgs.msg import PointStamped
from tf2_geometry_msgs import do_transform_point
class LidarTransformExample(Node):
def __init__(self):
super().__init__('lidar_tf_example')
self.tf_buffer = tf2_ros.Buffer()
self.listener = tf2_ros.TransformListener(self.tf_buffer, self)
self.timer = self.create_timer(1.0, self.timer_callback)
def timer_callback(self):
# LiDARの座標で1点を仮定
lidar_point = PointStamped()
lidar_point.header.frame_id = 'lidar_link'
lidar_point.point.x = 1.0
lidar_point.point.y = 0.0
lidar_point.point.z = 0.0
try:
# lidar_link → base_linkへの変換を取得
transform = self.tf_buffer.lookup_transform(
'base_link',
'lidar_link',
rclpy.time.Time()
)
# 座標を変換
base_point = do_transform_point(lidar_point, transform)
self.get_logger().info(
f"LIDAR点のlidar_link座標:x={lidar_point.point.x:.2f}, "
f"y={lidar_point.point.y:.2f}, z={lidar_point.point.z:.2f}"
f", LIDAR点のbase_link座標:x={base_point.point.x:.2f}, "
f"y={base_point.point.y:.2f}, z={base_point.point.z:.2f}"
)
except Exception as e:
self.get_logger().warn(f"変換に失敗しました:{e}")
def main():
rclpy.init()
node = LidarTransformExample()
rclpy.spin(node)
rclpy.shutdown()
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
# 静的Transformを送信するノード
Node(
package='tf2_ros',
executable='static_transform_publisher',
name='static_tf_pub_lidar',
arguments=['0.2', '0.0', '0.3', '0', '0', '0', 'base_link', 'lidar_link'],
output='screen'
),
# LidarTransformExampleノード
Node(
package='transform_tutorial',
executable='lidar_link_to_base_link_converter',
name='lidar_tf_example',
output='screen'
)
])
5.2 カメラ画像の座標をマップ座標に変換する
自己位置推定やSLAMを行う場合,カメラの座標を地図上(map座標)に変換して処理することが多いです.ここでは,map → odom → base_link → camera_linkというtfツリーを想定し,カメラ座標にある点をマップ座標へ変換する例を示します.
import rclpy
from rclpy.node import Node
import tf2_ros
from geometry_msgs.msg import PoseStamped
from tf2_geometry_msgs import do_transform_pose
class CameraToMapExample(Node):
def __init__(self):
super().__init__('camera_to_map_example')
self.tf_buffer = tf2_ros.Buffer()
self.listener = tf2_ros.TransformListener(self.tf_buffer, self)
self.timer = self.create_timer(1.0, self.timer_callback)
def timer_callback(self):
camera_pose = PoseStamped()
camera_pose.header.frame_id = 'camera_link'
camera_pose.pose.position.x = 0.5
camera_pose.pose.position.y = 0.0
camera_pose.pose.position.z = 0.0
try:
transform = self.tf_buffer.lookup_transform(
'map',
'camera_link',
rclpy.time.Time()
)
pose_in = camera_pose.pose
pose_out = do_transform_pose(pose_in, transform)
self.get_logger().info(
f"カメラ位置(map座標):x={pose_out.position.x:.2f}, "
f"y={pose_out.position.y:.2f}, "
f"z={pose_out.position.z:.2f}"
)
except Exception as e:
self.get_logger().warn(f"変換に失敗しました:{e}")
def main():
rclpy.init()
node = CameraToMapExample()
rclpy.spin(node)
rclpy.shutdown()
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
# map → odom(テスト用に同一座標とする)
Node(
package='tf2_ros',
executable='static_transform_publisher',
name='static_tf_map_to_odom',
arguments=['0', '0', '0', '0', '0', '0', 'map', 'odom'],
output='screen'
),
# odom → base_link(テスト用に同一座標とする)
Node(
package='tf2_ros',
executable='static_transform_publisher',
name='static_tf_odom_to_base',
arguments=['0', '0', '0', '0', '0', '0', 'odom', 'base_link'],
output='screen'
),
# base_link → camera_link(前0.5 m,上0.0 m,姿勢0)
Node(
package='tf2_ros',
executable='static_transform_publisher',
name='static_tf_base_to_camera',
arguments=['0.5', '0.0', '0.0', '0', '0', '0', 'base_link', 'camera_link'],
output='screen'
),
# camera_link_to_map_converter.py
Node(
package='transform_tutorial',
executable='camera_link_to_map_converter',
name='camera_to_map_example',
output='screen'
),
])
このように,tf_buffer.lookup_transform()で任意の2つのframe間の関係を取得すれば,センサーデータや位置情報を簡単に別の座標系に変換できます.
6.デバッグとツール
tfはロボットシステム全体の「座標のつながり」を支える重要な仕組みですが,複数のframeが関わるため,設定ミスや通信遅延などによってエラーが発生しやすい部分でもあります.この章では,tfの状態を確認・可視化・デバッグするための代表的なツールとコマンドを紹介します.
6.1 tfツリーの可視化:view_frames
tfツリーの全体構造を確認したいときに便利なのが,tf2_toolsパッケージに含まれるview_framesコマンドです.このツールは,現在システムで送信・受信されているすべてのtf情報を収集し,PDFとしてツリーを生成します.
実行方法
ros2 run tf2_tools view_frames
出力結果
- カレントディレクトリにframes.pdfとframes.yamlが生成される
- frames.pdfには,frame同士の関係がツリー状に描画される
- 各ノード(frame)の下には最終更新時刻(Last Update)と送信周期が表示され,通信状態の把握に役立つ
6.2 transformの確認:tf2_echo
ある特定のframe間のtransformをリアルタイムで確認するには,tf2_echoコマンドを使います.
実行方法
ros2 run tf2_ros tf2_echo base_link camera_link
出力例
At time 1707923456.123
- Translation: [0.2, 0.0, 0.3]
- Rotation: in Quaternion [0.0, 0.0, 0.0, 1.0]
ポイント
- 出力はリアルタイムに更新され,frame間の距離・姿勢を継続的に確認できる
- 変換が取得できない場合は「Lookup would require extrapolation into the future」などのエラーが出る
→ この場合は,タイムスタンプの不整合やframe名のミスを疑うとよい.
6.3 よくある問題と確認ポイント
| 問題 | 原因 | 対処方法 |
|---|---|---|
| Transformが見つからない | frame名のスペルミス,送信が未実行 |
tf2_echoで存在確認.view_framesでツリー構造を確認 |
| 「Lookup would require extrapolation into the future」 | タイムスタンプが未来時刻 | 送信ノードの時刻同期を確認.use_sim_time設定を見直す |
| ループ構造ができている | frameを循環参照している | view_framesのPDFで検出し,frame設計を修正 |
| Transformが古すぎる | 送信間隔が長い/停止している | ブロードキャスタの周期を短く設定する |
7. まとめ
tfはロボットに「空間の意味」を与えるライブラリです.どんなに高度なAIや経路計画アルゴリズムを使っても,座標変換が正しく行われていなければ,ロボットは環境を正確に理解することができません.ROS 2 Humbleのtf2は,そのための強力で柔軟な基盤を提供します.本記事を通してtfの仕組みと実装の流れをつかみ,自分のロボットプロジェクトでぜひ活用してみてください.