ROS 2 TF2 Guide 2026Broadcasters · Lookup · Debug · Frame Tree
Complete 2026 guide to TF2 — the ROS 2 transform library. Covers static and dynamic transform broadcasters, Python lookup_transform API with point transformation, CLI debug tools (view_frames, tf2_echo), robot_state_publisher integration, the standard REP-105 robot frame tree, and the five most common TF errors with fixes.
Standard Robot TF Frame Tree (REP-105)
| Frame | Parent | Published By | Purpose |
|---|---|---|---|
| map | — | SLAM toolbox / AMCL | Global fixed frame |
| odom | map | SLAM / AMCL | Continuous, no jumps |
| base_footprint | odom | DiffDriveController | Ground projection |
| base_link | base_footprint | robot_state_publisher | Robot body center |
| lidar_link | base_link | Static broadcaster | Lidar sensor origin |
| camera_link | base_link | Static broadcaster | Camera optical frame |
All frames must connect back to a common root. map → odom → base_link → sensors is the REP-105 standard.
Broadcast Transforms
Static Transform Broadcaster
Publish a fixed transform between two frames — e.g. sensor mounting position on the robot. Published once and latched.
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from tf2_ros import StaticTransformBroadcaster
from geometry_msgs.msg import TransformStamped
class StaticFramePublisher(Node):
def __init__(self):
super().__init__("static_tf_publisher")
self._broadcaster = StaticTransformBroadcaster(self)
self._publish()
def _publish(self):
t = TransformStamped()
t.header.stamp = self.get_clock().now().to_msg()
t.header.frame_id = "base_link" # parent frame
t.child_frame_id = "lidar_link" # child frame
# Translation: lidar is 0.2 m forward, 0.1 m above base
t.transform.translation.x = 0.2
t.transform.translation.y = 0.0
t.transform.translation.z = 0.1
# Rotation: no rotation (facing same direction as base_link)
t.transform.rotation.x = 0.0
t.transform.rotation.y = 0.0
t.transform.rotation.z = 0.0
t.transform.rotation.w = 1.0
self._broadcaster.sendTransform(t)
self.get_logger().info("Published static transform base_link -> lidar_link")
def main():
rclpy.init()
node = StaticFramePublisher()
rclpy.spin(node)
rclpy.shutdown()
if __name__ == "__main__":
main()💡 Static transforms are published once and latched. Use for sensor mounts and fixed joints — no periodic republishing needed.
Dynamic Transform Broadcaster
Publish time-varying transforms — e.g. robot odometry, moving joints. Must republish at the sensor/control rate.
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from tf2_ros import TransformBroadcaster
from geometry_msgs.msg import TransformStamped
from nav_msgs.msg import Odometry
class OdomTFBroadcaster(Node):
def __init__(self):
super().__init__("odom_tf_broadcaster")
self._tf_broadcaster = TransformBroadcaster(self)
self.create_subscription(
Odometry, "/odom", self._odom_callback, 10
)
def _odom_callback(self, msg: Odometry):
t = TransformStamped()
# Timestamp MUST match the odometry header stamp
t.header.stamp = msg.header.stamp
t.header.frame_id = "odom"
t.child_frame_id = "base_link"
t.transform.translation.x = msg.pose.pose.position.x
t.transform.translation.y = msg.pose.pose.position.y
t.transform.translation.z = msg.pose.pose.position.z
t.transform.rotation = msg.pose.pose.orientation
self._tf_broadcaster.sendTransform(t)
def main():
rclpy.init()
node = OdomTFBroadcaster()
rclpy.spin(node)
rclpy.shutdown()
if __name__ == "__main__":
main()💡 The TF timestamp must match the source data timestamp exactly — mismatches cause TF lookup failures.
Look Up Transforms
Look Up a Transform (Python)
Query the TF buffer for the transform between any two frames and use it to convert a point.
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from tf2_ros import Buffer, TransformListener, LookupException, ExtrapolationException
from tf2_geometry_msgs import do_transform_point
from geometry_msgs.msg import PointStamped
import rclpy.duration
class TFLookupNode(Node):
def __init__(self):
super().__init__("tf_lookup_node")
self._tf_buffer = Buffer()
self._tf_listener = TransformListener(self._tf_buffer, self)
self.create_timer(0.5, self._lookup_timer)
def _lookup_timer(self):
try:
# time=rclpy.time.Time() means "most recent available"
transform = self._tf_buffer.lookup_transform(
target_frame="map",
source_frame="base_link",
time=rclpy.time.Time(),
timeout=rclpy.duration.Duration(seconds=1.0)
)
pos = transform.transform.translation
self.get_logger().info(
f"map<-base_link: x={pos.x:.3f} y={pos.y:.3f}"
)
# Transform a point 1 m ahead of the robot into map frame
pt_in_base = PointStamped()
pt_in_base.header.frame_id = "base_link"
pt_in_base.header.stamp = self.get_clock().now().to_msg()
pt_in_base.point.x = 1.0
pt_in_base.point.y = 0.0
pt_in_base.point.z = 0.0
pt_in_map = do_transform_point(pt_in_base, transform)
self.get_logger().info(
f"Point in map: ({pt_in_map.point.x:.2f}, {pt_in_map.point.y:.2f})"
)
except (LookupException, ExtrapolationException) as e:
self.get_logger().warn(f"TF lookup failed: {e}")
def main():
rclpy.init()
node = TFLookupNode()
rclpy.spin(node)
rclpy.shutdown()
if __name__ == "__main__":
main()⚡ Install tf2_geometry_msgs: `sudo apt install ros-jazzy-tf2-geometry-msgs`. Required for do_transform_point.
TF from URDF — robot_state_publisher
TF from URDF — robot_state_publisher
robot_state_publisher reads joint states and broadcasts the complete kinematic TF tree automatically.
# Start robot_state_publisher with a URDF
ros2 run robot_state_publisher robot_state_publisher \
--ros-args -p robot_description:="$(xacro /path/to/robot.urdf.xacro)"
# Publish fake joint states for static visualization
ros2 run joint_state_publisher_gui joint_state_publisher_gui
# Required packages
sudo apt install \
ros-jazzy-robot-state-publisher \
ros-jazzy-joint-state-publisher \
ros-jazzy-joint-state-publisher-gui \
ros-jazzy-xacro✅ robot_state_publisher publishes fixed joint transforms to /tf_static and moving joint transforms to /tf.
CLI Debug Tools
TF2 CLI Debug Tools
Debug transform trees and echo live transforms from the terminal.
# View the entire TF tree as a PDF diagram
ros2 run tf2_tools view_frames
# Saves frames.pdf in the current directory
# Echo transforms live between two frames
ros2 run tf2_ros tf2_echo map base_link
# Monitor latency between frames
ros2 run tf2_ros tf2_monitor map base_link
# Inspect TF messages directly
ros2 topic echo /tf
ros2 topic echo /tf_static
# Install tf2 tools if missing
sudo apt install ros-jazzy-tf2-tools ros-jazzy-tf2-rosCommon TF2 Errors & Fixes
ExtrapolationException: Lookup would require extrapolationLookupException: Frame 'X' does not existTransform direction is wrongStatic transform not visible in RViz2TF tree has disconnected islands