Skip to main content

TF2

TF2 is the ROS 2 library for tracking coordinate frames over time. This page explains frames, parent/child relationships, static vs. dynamic transforms, the standard inspection tools, and mistakes that commonly break TF trees in Mobility and Manipulation applications.

Overview​

A robot system usually has many meaningful coordinate frames at once — a camera, a LiDAR, a robot base, individual arm links, a map origin. TF2 lets every part of the system ask "where is frame A relative to frame B, at time T?" without every node needing to know the whole geometry itself.

Why It Matters​

Almost every perception, navigation, and manipulation feature depends on TF being correct: transforming a detected object's pose into the robot's planning frame (Manipulation's Pose Estimation), placing sensor data on a map (Mobility), or driving a RobotModel display in RViz. A broken or duplicated TF tree causes failures that look unrelated to TF at first (a "planning failed" error, a robot model that does not move) — see URDF Robot Model for how TF and the robot model connect.

Core Concepts​

Frames and the Tree​

Every meaningful coordinate frame is a frame ID (a string, such as base_link, camera_link, or map). TF2 organizes frames into a tree: every frame except the root has exactly one parent, and a transform describes a child frame's position and orientation relative to its parent.

map
└── odom
└── base_link
├── camera_link
└── lidar_link

Within a connected TF tree, there is exactly one path between any two frames. This allows TF2 to compute transforms between indirectly connected frames even when no node publishes that exact frame pair directly.

If two frames belong to disconnected parts of the TF tree, the lookup fails because no transform chain exists between them.

Static vs. Dynamic Transforms​

TypeTopicChanges over time?Typical source
Static/tf_staticNo (fixed for the node's lifetime)Sensor mounting offsets, fixed links published once
Dynamic/tfYes, continuouslyRobot odometry, joint motion, localization

Static transforms are published on /tf_static using transient-local durability, so late-joining subscribers can receive the fixed transform. They are appropriate for relationships that do not change while the system runs, such as a camera rigidly mounted to a chassis.

Dynamic transforms are published repeatedly on /tf for relationships that change over time, such as odom -> base_link from odometry or moving robot links derived from joint states.

robot_state_publisher​

For an articulated robot described by URDF, you do not usually publish per-link transforms yourself. robot_state_publisher subscribes to /joint_states, combines it with the URDF's kinematic structure, and publishes the resulting link transforms on /tf (and any fixed joints as /tf_static). See URDF Robot Model for how the model itself is defined.

Timestamps Matter​

Dynamic transforms are timestamped, and TF2 keeps a history buffer so transforms can be queried at specific times. TF2 can interpolate between available dynamic transforms, but it cannot extrapolate indefinitely into the past or future.

Static transforms represent fixed relationships and are treated as valid independently of the normal dynamic-transform history.

A common failure with dynamic TF is requesting a transform at a timestamp older than the retained buffer, or at a future timestamp for which no transform has been published yet.

Hands-on Steps​

These commands work against any running system that publishes TF, including the simple_publisher/simple_subscriber pair from Publisher/Subscriber run alongside a static_transform_publisher, or any robot bring-up launch file.

1. Publish a Static Transform Manually​

# Translation is in meters; roll, pitch, and yaw are in radians.
ros2 run tf2_ros static_transform_publisher \
--x 0 --y 0 --z 0.1 \
--roll 0 --pitch 0 --yaw 0 \
--frame-id base_link \
--child-frame-id camera_link
tip

Distro-dependent syntax
The static_transform_publisher argument style changed between distros. Humble and Jazzy both accept the named --x --y --z --yaw --pitch --roll --frame-id --child-frame-id form shown above; older distros used positional arguments (x y z yaw pitch roll parent child). Run ros2 run tf2_ros static_transform_publisher --help to confirm the exact form on your installed distro before relying on it.

2. Echo a Transform Between Two Frames​

ros2 run tf2_ros tf2_echo base_link camera_link

This prints the current transform between the two frames, updating whenever a new one is available, and reports an error if no path between the frames exists yet.

3. Generate a Visual TF Tree​

ros2 run tf2_tools view_frames

This listens for a few seconds, then writes a PDF (frames.pdf in the current directory, filename may vary by distro) showing every frame, its parent, and how recently each transform was published — the fastest way to spot a disconnected or unexpectedly duplicated frame.

4. Visualize TF Live in RViz2​

ros2 run rviz2 rviz2

Add a TF display and enable it to see the frame tree update live, alongside a RobotModel display if a URDF is loaded. See RViz2.

Expected Result​

tf2_echo prints a translation and rotation once the two frames are connected; view_frames produces a tree diagram with no isolated frames and no frame listed as its own ancestor; RViz's TF display shows frame axes that move as the underlying robot or sensor moves.

Useful Commands​

ros2 run tf2_ros tf2_echo <source_frame> <target_frame>
ros2 run tf2_tools view_frames
ros2 topic echo /tf_static
ros2 topic hz /tf
ros2 run tf2_ros static_transform_publisher --help

Common Problems​

  • tf2_echo reports "frame does not exist" — no node has ever published a transform involving that frame ID; check spelling (frame IDs are case-sensitive) and confirm the publishing node is running.
  • "Lookup would require extrapolation into the future/past" — the requested timestamp falls outside the small buffer TF2 keeps; use the latest available time (rclpy.time.Time() / zero timestamp for "latest") when an exact historical time is not required, or reduce the delay between data capture and the TF lookup.
  • Duplicate or conflicting transforms for the same frame pair — two different nodes are publishing the same parent/child relationship, which produces warnings and unpredictable results. This most commonly happens when replaying a recorded TF alongside a live robot_state_publisher — see the callout below and Rosbag.
  • RobotModel does not appear in RViz — the TF tree may be incomplete; run view_frames to confirm every link in the URDF has a path back to the root frame.

Never Publish Duplicate Transforms During Playback​

tip

If a rosbag contains recorded /tf and /tf_static data for a robot, and you also run robot_state_publisher (fed by recorded /joint_states) at the same time, both will publish transforms for the same frames.
TF2 does not merge or arbitrate between two authorities for the same frame pair — pick exactly one source: either replay the recorded /tf and /tf_static directly, or replay /joint_states and let robot_state_publisher compute TF from the URDF, but never both at once.
See Rosbag for the full explanation.

Key Takeaways​

  • TF2 organizes frames into a tree; every child frame has exactly one parent at any given time.
  • Static transforms (/tf_static) are for fixed relationships; dynamic transforms (/tf) are for anything that moves.
  • robot_state_publisher derives link transforms from /joint_states and the URDF — you rarely publish per-link transforms by hand.
  • Dynamic transforms are timestamped, and lookups can fail when the requested time falls outside TF2's available history.
  • Never let two sources (e.g., a rosbag and robot_state_publisher) publish the same transform at the same time.

Next​

Continue to Rosbag to learn how to record and safely replay data, including the TF playback rule introduced above.