Chapter 12: ROS/ROS2 Foundations (Practical Intro)

Lesson 4: TF Concept and Frame Broadcasting (usage only)

This lesson introduces the TF/TF2 system in ROS and ROS2: a time-aware tree of coordinate frames. We focus on using TF to broadcast and query transforms between frames at runtime. The mathematical backbone (rigid-body transforms and their composition) is reviewed at a practical, consistency-check level—without diving into deeper kinematics or Lie algebra, which belong to later courses.

1. Why TF Exists

In Chapter 9 we saw that robots rely on multiple coordinate frames (world, base, sensors, tool). TF (ROS1) / TF2 (ROS1+ROS2) is the middleware layer that keeps these frames consistent over time and across distributed nodes.

Conceptually, TF maintains a directed tree (or forest) of frames. Each edge stores a transform from a parent to a child along with a timestamp, allowing any node to compute transforms between arbitrary frames by composing edges.

flowchart TD
  W["world"] --> B["base_link"]
  B --> L["laser_frame"]
  B --> C["camera_frame"]
  C --> O["object_frame"]
  subgraph TF_tree["Transform tree with timestamps"]
    W
    B
    L
    C
    O
  end
        

TF is especially important in robotics because sensors and bodies move; the transform you need is not just \( T_{ab} \), but a \( T_{ab}(t) \) stored and queried at the correct time.

2. Rigid Transforms as TF Messages

A rigid transform from frame \( b \) to frame \( a \) consists of rotation and translation: \( \mathbf{R}_{ab} \in \mathbb{R}^{3\times 3} \), \( \mathbf{t}_{ab} \in \mathbb{R}^3 \). In homogeneous form:

\[ \mathbf{T}_{ab} = \begin{bmatrix} \mathbf{R}_{ab} & \mathbf{t}_{ab} \\ \mathbf{0}^\top & 1 \end{bmatrix}, \quad \mathbf{T}_{ab} \in \mathrm{SE}(3) \]

Given a point with coordinates \( \mathbf{p}^b \) in frame \(b\), its coordinates in frame \(a\) are:

\[ \begin{bmatrix} \mathbf{p}^a \\ 1 \end{bmatrix} = \mathbf{T}_{ab} \begin{bmatrix} \mathbf{p}^b \\ 1 \end{bmatrix} \quad \Longleftrightarrow \quad \mathbf{p}^a = \mathbf{R}_{ab}\mathbf{p}^b + \mathbf{t}_{ab}. \]

TF messages store \( \mathbf{t}_{ab} \) and a rotation as a quaternion \( \mathbf{q}_{ab} \). The quaternion represents the same rotation matrix but avoids singularities in parameterization.

3. Composition, Inverses, and Tree Consistency

If TF provides \( \mathbf{T}_{ab} \) and \( \mathbf{T}_{bc} \), then the transform from \(c\) to \(a\) is their product:

\[ \mathbf{T}_{ac} = \mathbf{T}_{ab}\mathbf{T}_{bc}. \]

Expanding:

\[ \mathbf{T}_{ab}\mathbf{T}_{bc} = \begin{bmatrix} \mathbf{R}_{ab}\mathbf{R}_{bc} & \mathbf{R}_{ab}\mathbf{t}_{bc}+\mathbf{t}_{ab} \\ \mathbf{0}^\top & 1 \end{bmatrix}. \]

Proposition 1 (Associativity): For any frames \(a,b,c,d\), \( (\mathbf{T}_{ab}\mathbf{T}_{bc})\mathbf{T}_{cd} = \mathbf{T}_{ab}(\mathbf{T}_{bc}\mathbf{T}_{cd}) \).

Proof: Matrix multiplication is associative. Since each \(\mathbf{T}\) is a \(4\times 4\) matrix, the equality follows directly:

\[ (\mathbf{T}_{ab}\mathbf{T}_{bc})\mathbf{T}_{cd} = \mathbf{T}_{ab}(\mathbf{T}_{bc}\mathbf{T}_{cd}) \quad \text{for all conformable matrices}. \]

This guarantees that TF can safely chain along any path in the tree without ambiguity.

Proposition 2 (Inverse transform): The inverse transform exists and is:

\[ \mathbf{T}_{ba} = \mathbf{T}_{ab}^{-1} = \begin{bmatrix} \mathbf{R}_{ab}^\top & -\mathbf{R}_{ab}^\top\mathbf{t}_{ab}\\ \mathbf{0}^\top & 1 \end{bmatrix}. \]

Proof: Since \(\mathbf{R}_{ab}\) is a rotation, \(\mathbf{R}_{ab}^\top\mathbf{R}_{ab}=\mathbf{I}\). Multiply \(\mathbf{T}_{ab}\) by the proposed inverse:

\[ \mathbf{T}_{ab}\mathbf{T}_{ba} = \begin{bmatrix} \mathbf{R}_{ab}\mathbf{R}_{ab}^\top & \mathbf{R}_{ab}(-\mathbf{R}_{ab}^\top\mathbf{t}_{ab})+\mathbf{t}_{ab}\\ \mathbf{0}^\top & 1 \end{bmatrix} = \begin{bmatrix} \mathbf{I} & \mathbf{0}\\ \mathbf{0}^\top & 1 \end{bmatrix}. \]

Thus \(\mathbf{T}_{ba}\) is the inverse. TF uses this to answer queries even if the stored edge direction is opposite to what you ask for.

Tree consistency rule: TF requires a tree (no loops). If a loop existed, you could get conflicting transforms depending on the path. Associativity prevents path-order ambiguity, but cannot resolve contradictory data.

4. Time and Interpolation in TF

TF stores transforms with timestamps \( t_k \). A query requests \( \mathbf{T}_{ab}(t_q) \). If \(t_q\) lies between two stored times, TF2 interpolates.

Translation interpolation (linear): Given translations \(\mathbf{t}_0\) at \(t_0\) and \(\mathbf{t}_1\) at \(t_1\):

\[ \alpha = \frac{t_q - t_0}{t_1 - t_0}, \quad 0 \le \alpha \le 1, \]

\[ \mathbf{t}(t_q) = (1-\alpha)\mathbf{t}_0 + \alpha \mathbf{t}_1. \]

Rotation interpolation (SLERP): For unit quaternions \(\mathbf{q}_0,\mathbf{q}_1\):

\[ \mathbf{q}(t_q) = \frac{\sin((1-\alpha)\theta)}{\sin\theta}\mathbf{q}_0 + \frac{\sin(\alpha\theta)}{\sin\theta}\mathbf{q}_1, \quad \theta = \arccos(\mathbf{q}_0^\top \mathbf{q}_1). \]

SLERP traces the shortest geodesic on the unit 4D sphere, yielding constant angular velocity between samples. TF2 applies this automatically when you request transforms at times between stored samples.

5. Broadcasting Frames in ROS1 and ROS2

A broadcaster publishes a transform from parent to child at some rate. In ROS1, this is typically on the /tf topic; in ROS2, TF2 uses /tf and /tf_static.

flowchart TD
  S["Sensors / robot state"] --> N1["Broadcaster node"]
  N1 --> TF["/tf or /tf_static"]
  TF --> N2["Listener node"]
  N2 --> A["Compute p^a = T_ab(t) p^b"]
  A --> APP["Control / Perception / Logging"]
        

5.1 ROS1 C++ TF2 Broadcaster


#include <ros/ros.h>
#include <geometry_msgs/TransformStamped.h>
#include <tf2_ros/transform_broadcaster.h>
#include <tf2/LinearMath/Quaternion.h>

int main(int argc, char** argv){
  ros::init(argc, argv, "base_to_laser_broadcaster");
  ros::NodeHandle nh;
  tf2_ros::TransformBroadcaster br;
  geometry_msgs::TransformStamped t;

  ros::Rate rate(30.0);
  while (ros::ok()){
    t.header.stamp = ros::Time::now();
    t.header.frame_id = "base_link";
    t.child_frame_id = "laser_frame";

    // Example fixed offset: laser mounted 0.2 m forward, 0.1 m up
    t.transform.translation.x = 0.2;
    t.transform.translation.y = 0.0;
    t.transform.translation.z = 0.1;

    tf2::Quaternion q;
    q.setRPY(0.0, 0.0, 0.0);  // roll, pitch, yaw
    t.transform.rotation.x = q.x();
    t.transform.rotation.y = q.y();
    t.transform.rotation.z = q.z();
    t.transform.rotation.w = q.w();

    br.sendTransform(t);
    rate.sleep();
  }
  return 0;
}
      

5.2 ROS1 Python TF2 Broadcaster


#!/usr/bin/env python3
import rospy
import tf2_ros
from geometry_msgs.msg import TransformStamped
import tf_conversions

rospy.init_node("base_to_camera_broadcaster")
br = tf2_ros.TransformBroadcaster()
rate = rospy.Rate(30)

while not rospy.is_shutdown():
    t = TransformStamped()
    t.header.stamp = rospy.Time.now()
    t.header.frame_id = "base_link"
    t.child_frame_id = "camera_frame"

    t.transform.translation.x = 0.0
    t.transform.translation.y = 0.15
    t.transform.translation.z = 0.25

    q = tf_conversions.transformations.quaternion_from_euler(0.0, 0.0, 1.57)
    t.transform.rotation.x = q[0]
    t.transform.rotation.y = q[1]
    t.transform.rotation.z = q[2]
    t.transform.rotation.w = q[3]

    br.sendTransform(t)
    rate.sleep()
      

5.3 ROS2 C++ TF2 Broadcaster


#include <rclcpp/rclcpp.hpp>
#include <geometry_msgs/msg/transform_stamped.hpp>
#include <tf2_ros/transform_broadcaster.h>
#include <tf2/LinearMath/Quaternion.h>

class BroadcasterNode : public rclcpp::Node {
public:
  BroadcasterNode() : Node("ros2_broadcaster") {
    br_ = std::make_unique<tf2_ros::TransformBroadcaster>(*this);
    timer_ = this->create_wall_timer(
      std::chrono::milliseconds(33),
      std::bind(&BroadcasterNode::tick, this)
    );
  }
private:
  void tick(){
    geometry_msgs::msg::TransformStamped t;
    t.header.stamp = this->now();
    t.header.frame_id = "base_link";
    t.child_frame_id  = "tool_frame";

    t.transform.translation.x = 0.4;
    t.transform.translation.y = 0.0;
    t.transform.translation.z = 0.2;

    tf2::Quaternion q;
    q.setRPY(0.0, 0.0, 0.0);
    t.transform.rotation.x = q.x();
    t.transform.rotation.y = q.y();
    t.transform.rotation.z = q.z();
    t.transform.rotation.w = q.w();

    br_->sendTransform(t);
  }
  std::unique_ptr<tf2_ros::TransformBroadcaster> br_;
  rclcpp::TimerBase::SharedPtr timer_;
};

int main(int argc, char** argv){
  rclcpp::init(argc, argv);
  rclcpp::spin(std::make_shared<BroadcasterNode>());
  rclcpp::shutdown();
  return 0;
}
      

5.4 ROS2 Python TF2 Broadcaster (rclpy)


import rclpy
from rclpy.node import Node
from geometry_msgs.msg import TransformStamped
from tf2_ros import TransformBroadcaster
import math

class TFPublisher(Node):
    def __init__(self):
        super().__init__("ros2_tf_publisher")
        self.br = TransformBroadcaster(self)
        self.timer = self.create_timer(0.033, self.tick)

    def tick(self):
        t = TransformStamped()
        t.header.stamp = self.get_clock().now().to_msg()
        t.header.frame_id = "base_link"
        t.child_frame_id = "imu_frame"

        t.transform.translation.x = 0.0
        t.transform.translation.y = 0.0
        t.transform.translation.z = 0.05

        yaw = 0.0
        t.transform.rotation.z = math.sin(yaw/2)
        t.transform.rotation.w = math.cos(yaw/2)

        self.br.sendTransform(t)

def main():
    rclpy.init()
    node = TFPublisher()
    rclpy.spin(node)
    node.destroy_node()
    rclpy.shutdown()

if __name__ == "__main__":
    main()
      

6. Listener Usage (Brief)

Even though this lesson emphasizes broadcasting, you should know the minimal listener pattern. A listener buffers TF messages and answers queries such as: “give me \(\mathbf{T}_{ab}(t_q)\)”.

\[ \mathbf{T}_{ab}(t_q) = \prod_{(i\rightarrow j)\in \text{path}(a\leftarrow b)} \mathbf{T}_{ij}(t_q), \]

where the product is ordered along the unique tree path. In practice, TF2 returns this as a TransformStamped.


# ROS2 example listener pattern (usage)
from tf2_ros import Buffer, TransformListener
import rclpy
from rclpy.node import Node

class TFListenerNode(Node):
    def __init__(self):
        super().__init__('tf_listener')
        self.buffer = Buffer()
        self.listener = TransformListener(self.buffer, self)

    def query(self):
        try:
            t = self.buffer.lookup_transform(
                'world', 'base_link', rclpy.time.Time())
            self.get_logger().info(str(t.transform))
        except Exception as e:
            self.get_logger().warn(str(e))
      

7. Java and MATLAB/Simulink Usage

7.1 Java (ROS2 rcljava) Broadcaster Skeleton

Java support in ROS2 uses rcljava. The pattern mirrors C++/Python: create a node, a TF broadcaster, and publish TransformStamped.


import org.ros2.rcljava.RCLJava;
import org.ros2.rcljava.node.Node;
import geometry_msgs.msg.TransformStamped;
import tf2_msgs.msg.TFMessage;
// Note: exact TF broadcaster helpers may vary by distro;
// this is a minimal "usage" skeleton.

public class TFJavaBroadcaster {
  public static void main(String[] args){
    RCLJava.rclJavaInit();
    Node node = RCLJava.createNode("java_tf_broadcaster");

    var pub = node.<TFMessage>createPublisher(TFMessage.class, "/tf");

    node.createWallTimer(33, () -> {
      TransformStamped t = new TransformStamped();
      t.getHeader().setFrameId("base_link");
      t.setChildFrameId("gps_frame");
      t.getHeader().getStamp().setSec((int)(System.currentTimeMillis()/1000));

      t.getTransform().getTranslation().setX(0.0);
      t.getTransform().getTranslation().setY(-0.2);
      t.getTransform().getTranslation().setZ(0.3);

      t.getTransform().getRotation().setW(1.0); // identity rotation

      TFMessage msg = new TFMessage();
      msg.setTransforms(java.util.Arrays.asList(t));
      pub.publish(msg);
    });

    RCLJava.spin(node);
    node.dispose();
    RCLJava.shutdown();
  }
}
      

7.2 MATLAB (ROS Toolbox)

MATLAB’s ROS Toolbox provides a TF tree interface. A minimal broadcaster publishes a transform on /tf:


% ROS1 MATLAB TF broadcasting (usage)
rosshutdown;
rosinit;

tftree = rostf;  % TF interface

parent = 'base_link';
child  = 'lidar_frame';

trvec = [0.25 0 0.12];   % translation
rpy   = [0 0 0];         % roll-pitch-yaw
quat  = eul2quat(rpy);

msg = rosmessage('geometry_msgs/TransformStamped');
msg.Header.FrameId = parent;
msg.ChildFrameId   = child;
msg.Transform.Translation.X = trvec(1);
msg.Transform.Translation.Y = trvec(2);
msg.Transform.Translation.Z = trvec(3);
msg.Transform.Rotation.W = quat(1);
msg.Transform.Rotation.X = quat(2);
msg.Transform.Rotation.Y = quat(3);
msg.Transform.Rotation.Z = quat(4);

rate = robotics.Rate(30);
while true
    msg.Header.Stamp = rostime('now');
    sendTransform(tftree, msg);
    waitfor(rate);
end
      

7.3 Simulink (conceptual usage)

In Simulink, use ROS blocks: “Publish Transform” to broadcast on /tf, and “Get Transform” to query TF. Set parent/child frame names and sample time. This is typically embedded in a higher-level model rather than hand-coded.

8. Practical Pitfalls

  • Frame naming inconsistency: TF is string-based. A typo creates a disconnected subtree and queries fail.
  • Loop creation: Broadcasting conflicting transforms can accidentally form a loop. TF rejects loops.
  • Timestamp mismatch: If transforms are late, lookup at time \(t_q\) may throw extrapolation errors.
  • Static vs dynamic: Use static broadcasters for fixed mounts (ROS2: /tf_static) to reduce bandwidth and avoid jitter.

9. Problems and Solutions

Problem 1 (Transform chaining): You have \(\mathbf{T}_{ab}\) and \(\mathbf{T}_{bc}\). Show that a point \(\mathbf{p}^c\) maps to frame \(a\) as \(\mathbf{p}^a = \mathbf{R}_{ab}\mathbf{R}_{bc}\mathbf{p}^c + \mathbf{R}_{ab}\mathbf{t}_{bc} + \mathbf{t}_{ab}\).

Solution: First map \(c \rightarrow b\): \(\mathbf{p}^b = \mathbf{R}_{bc}\mathbf{p}^c + \mathbf{t}_{bc}\). Then map \(b \rightarrow a\):

\[ \mathbf{p}^a = \mathbf{R}_{ab}\mathbf{p}^b + \mathbf{t}_{ab} = \mathbf{R}_{ab}(\mathbf{R}_{bc}\mathbf{p}^c + \mathbf{t}_{bc}) + \mathbf{t}_{ab}, \]

\[ \mathbf{p}^a = \mathbf{R}_{ab}\mathbf{R}_{bc}\mathbf{p}^c + \mathbf{R}_{ab}\mathbf{t}_{bc} + \mathbf{t}_{ab}. \]

Problem 2 (Inverse consistency): Given rotation \(\mathbf{R}_{ab}\) and translation \(\mathbf{t}_{ab}\), verify that \(\mathbf{T}_{ab}^{-1}\) is as in Proposition 2.

Solution: Verify that \(\mathbf{R}_{ab}^\top\mathbf{R}_{ab}=\mathbf{I}\) and compute:

\[ \mathbf{T}_{ab}\mathbf{T}_{ab}^{-1} = \begin{bmatrix} \mathbf{R}_{ab}\mathbf{R}_{ab}^\top & \mathbf{R}_{ab}(-\mathbf{R}_{ab}^\top\mathbf{t}_{ab})+\mathbf{t}_{ab} \\ \mathbf{0}^\top & 1 \end{bmatrix} = \mathbf{I}_4. \]

Problem 3 (Temporal interpolation): Suppose TF stores \(\mathbf{t}_0\) at \(t_0=0\) and \(\mathbf{t}_1\) at \(t_1=1\) s. Compute \(\mathbf{t}(0.25)\) and prove it lies on the line segment between \(\mathbf{t}_0\) and \(\mathbf{t}_1\).

Solution: Here \(\alpha=0.25\), so:

\[ \mathbf{t}(0.25) = 0.75\mathbf{t}_0 + 0.25\mathbf{t}_1. \]

Any convex combination \((1-\alpha)\mathbf{t}_0+\alpha\mathbf{t}_1\) with \(0\le \alpha \le 1\) lies on the line segment connecting the two points in \(\mathbb{R}^3\). Thus TF’s translation interpolation is geometrically consistent.

Problem 4 (Tree vs loop): Explain why TF disallows loops by giving a counterexample with three frames \(a,b,c\) where two different paths imply two different \(\mathbf{T}_{ac}\).

Solution: If edges create a loop \(a\rightarrow b\rightarrow c\rightarrow a\), TF could infer \(\mathbf{T}_{ac}^{(1)}=\mathbf{T}_{ab}\mathbf{T}_{bc}\) and also \(\mathbf{T}_{ac}^{(2)}=\mathbf{T}_{aa}\mathbf{T}_{ac}\) where \(\mathbf{T}_{aa}\) from the loop might not equal \(\mathbf{I}\) due to noise or conflicting broadcasters. Then \(\mathbf{T}_{ac}^{(1)} \ne \mathbf{T}_{ac}^{(2)}\), making the system ill-posed. Enforcing a tree ensures a unique path and unique transform.

10. Summary

TF/TF2 provides a time-stamped tree of rigid transforms among frames. We reviewed the rigid-transform math used by TF (composition and inversion), and practiced broadcasting transforms in ROS1 and ROS2 using C++, Python, Java, and MATLAB/Simulink patterns. This prepares you to read and debug real robot frame trees in the next lessons.

11. References

  1. Quigley, M., Conley, K., Gerkey, B., Faust, J., Foote, T., Leibs, J., Wheeler, R., & Ng, A.Y. (2009). ROS: an open-source Robot Operating System. ICRA Workshop on Open Source Software.
  2. Foote, T. (2013). tf: The transform library. IEEE Robotics & Automation Magazine, 20(3), 90–96.
  3. Smith, R., Self, M., & Cheeseman, P. (1990). Estimating uncertain spatial relationships in robotics. Autonomous Robot Vehicles, 167–193. (Foundational frame-consistency perspective.)
  4. Kuipers, B. (1999). The spatial semantic hierarchy. Artificial Intelligence, 119(1–2), 191–233. (Formal treatment of spatial frames.)
  5. Shoemake, K. (1985). Animating rotation with quaternion curves. SIGGRAPH Proceedings, 19(3), 245–254. (Theoretical basis for SLERP.)
  6. Hartley, R., & Zisserman, A. (2004). Multiple View Geometry in Computer Vision (2nd ed.). Cambridge University Press. (Chapters on rigid transforms and composition.)