1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ROS2のtfについて

Last updated at Posted at 2025-10-14

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です.
特に,LaserScanPointCloud2といった空間的データを扱う際に便利です.

3.6 tf2_tools:デバッグと可視化ツール

tfの状態を確認・可視化するためのツール群を提供するのがtf2_toolsです.代表的なコマンドとして:

  • tfツリーをPDFに出力
bash
ros2 run tf2_tools view_frames
  • 特定のframe間の関係を表示
bash
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配下でチュートリアルを実装していくことを想定しています.

パッケージのひな形を作成

bash
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_linkcamera_linkの変換が定期的に配信されます.rviz2を起動すれば,tfツリーにこの関係が自動的に追加されていることを確認できます.

transform_tutorial/transform_tutorial/dynamic_transform_publisher_node.py
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_linkcamera_linkの固定変換が/tf_staticトピックに一度だけ送信されます.

bash
ros2 run tf2_ros static_transform_publisher 0.2 0 0.3 0 0 0 base_link camera_link
指定した2つのframe間の関係を計算するプログラム

このプログラムでは,1秒ごとにbase_linkcamera_link間の変換を取得し,位置情報を出力します.

transform_tutorial/transform_tutorial/transform_listener_node.py
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 にコンソールスクリプトを登録します.

transform_tutorial/setup.py
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ファイルを作成します.

transform_tutorial/launch/ broadcaster_listener.launch.py
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')
    ])

ビルドと実行

bash
cd ~/workspace/transform/
colcon build --packages-select transform_tutorial
source install/setup.bash
ros2 launch transform_tutorial broadcaster_listener.launch.py

次のような出力結果が得られます.

bash
$ 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へ変換する簡単な例を示します.

transform_tutorial/transform_tutorial/lidar_link_to_base_link_converter.py
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()
transform_tutorial/launch/lidar_tf_example.launch.py
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座標)に変換して処理することが多いです.ここでは,mapodombase_linkcamera_linkというtfツリーを想定し,カメラ座標にある点をマップ座標へ変換する例を示します.

transform_tutorial/transform_tutorial/camera_link_to_map_converter.py
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()
transform_tutorial/launch/camera_to_map_example.launch.py
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としてツリーを生成します.

実行方法

bash
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コマンドを使います.

実行方法

bash
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の仕組みと実装の流れをつかみ,自分のロボットプロジェクトでぜひ活用してみてください.

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?