ROS TFs: What are they? Why use them?

The TF package in ROS is incredibly useful for handling coordinate transformations. It simplifies managing coordinate frames and allows for a clear representation of transforms between them in robotics applications. I used TFs extensively in my work in the Summer of 2023 with the University of Minnesota's Robotics Perception and Manipulation Laboratory (RPM Lab) under Prof. Karthik Desingh. Working with the Boston Dynamics Spot robot quadruped (Figure 1), I used a system with multiple cameras and actuators and control it completely from the ground-up. TFs were crucial in this process, as they allowed me to keep track of the robot's position and orientation in the world frame.

Boston Dynamics Spot Robot
Figure 1: Boston Dynamics Spot Robot Quadruped that I used at RPM Lab. The robot has multiple sets of cameras, including RGB-D and Pointcloud representation of images from each set. The front has two sets of cameras facing inwards, cameras exist on the right and left sides of the robot, and there are cameras on the back too. There is a set of cameras on the end effector of the arm as well. TFs helped represent each of these in a clean transform manner with respect to the body frame of the robot, with /tf topic(s) representing the transform between the robot and the world frame through GPS and IMU data.


What are TFs?

TFs (Transforms) allow robots to keep track of multiple coordinate frames over time. You would be able to answer questions like:

Fun Fact, TFs are named TFs partially due to the developer who made them - Tully Foote.

Why Use TFs?

TFs make handling complex systems much easier by:

  1. Standardizing coordinate transformations.
  2. Allowing queries about frame relationships at any time.
  3. Reducing manual calculations of transforms.

Getting Started with TFs

The ROS website has links to tutorials to understand how exactly they work but what I am doing here is to highlight the essence of TFs to understand how exactly they are useful while working with robots. Transforms of any sort are required when working with robots of multiple components. Defining a transform between two different links and using it to transform data from one link to another is a common practice in robotics. TFs in ROS provide this functionality in a very easy-to-use manner, which is evident in robots with more than just a couple links or sensors.


Thetf package has been deprecated and the move has been made to the tf2_ros package. tf2 has been the successor to tf and is widely used in ROS2 to represent transforms. Packages like tf2_geometry_msgs and tf2_py are used along with tf2to represent transforms in ROS2.


More links to get started with TFs:

Representing Data in TFs

TFs are represented as TransformStamped messages in ROS. These messages contain the following fields:

std_msgs/Header header
string child_frame_id
Transform transform
      

One of the coolest things about tf2 is the Buffer. The Buffer is a class that stores all the transforms in a tree structure. It can be used to query transforms between different frames. It's a powerful tool that can be used to store all the transforms in a system and query them at any time. Functions like lookup_transform and all_frames_as_yaml are used to query transforms and print all frames in the buffer, respectively. set_transform is used to add a transform to the buffer.


How I used TFs

Here's a piece of code I wrote when working in RPM Lab to get data from Spot. The public Gist is available here. I wrote a _dummy_test() function to test my TF2 Buffer as well.

### import modules ###

# dataset containing rosbag
from rosbag2dataset import dataset

import rospy

# import tf2_geometry_msgs
from geometry_msgs.msg import TransformStamped 
from geometry_msgs.msg import Quaternion as q
from tf2_ros import TransformException
from tf2_ros import Buffer
# import tf2_msgs
import tf2_py as tf2
import sys

##### helper functions #####

def _transfer_to_ref_frame(tf_buffer, topics, topic_ref='body'):
    '''
    Transforms set topics on tf_buffer to reference topic and returns an updated buffer.
    Args:
        tf_buffer (Buffer): tf2 buffer containing all transforms
        topics (list): list of topics to get transforms from
        topic_ref (str): reference topic to get transforms to
            default: 'body'
    Returns:
        tf_buffer_updated (Buffer): updated tf2 buffer containing all transforms to reference topic
    '''

    # using an updated tf_buffer to store transforms to reference frame
    tf_buffer_updated = tf_buffer

    # getting all transforms to reference frame
    for topic in topics:
        try:
            # getting transform from topic to reference topic
            transform = tf_buffer.lookup_transform(topic_ref, topic, rospy.Time())

            # adding transform to updated buffer
            tf_buffer_updated.set_transform(transform, transform.header.frame_id)
            print("------------------------")
            print("pose from " + topic + " to " + topic_ref)
            print("------------------------")
            print(transform)

        except tf2.LookupException as e:
            print("There either is an invalid quaternion or a frame is not found.")
            print(e)

    return tf_buffer_updated

def _dummy_test(tf_buffer, val1=[1, 1, 1, 0, 0, 0, 1], 
                                   val2=[0, 2, 1, 0, 0, 0, 1], val3=[0, 0, 0, 0, 0, 0, 1]):
    '''
    Creates dummy TransformStamped messages to be sent into buffer and displayed.
    Args:
        val1 (list): list of pose values for dummy header w.r.t. dummy child
            default: [1, 1, 1, 0, 0, 0, 1]
        val2 (list): list of pose values for dummy leaf1 w.r.t. dummy child
            default: [0, 2, 1, 0, 0, 0, 1]
        val3 (list): list of pose values for dummy leaf2 w.r.t. dummy child
            default: [0, 0, 0, 0, 0, 0, 1]
    '''

    # creating dummy pose1
    pose1 = TransformStamped()
    pose1.header.frame_id = 'dummy_header'
    pose1.header.stamp = rospy.Time()
    pose1.child_frame_id = 'dummy_child' # dummy child is a child for dummy header
    pose1.transform.translation.x, pose1.transform.translation.y, pose1.transform.translation.z = val1[0:3]
    quat = q(val1[3], val1[4], val1[5], val1[6])
    # quat = q.normalize(quat)
    pose1.transform.rotation.x, pose1.transform.rotation.y, pose1.transform.rotation.z, pose1.transform.rotation.w = quat.x, quat.y, quat.z, quat.w

    # creating dummy pose2
    pose2 = TransformStamped()
    pose2.header.frame_id = 'dummy_child'
    pose2.header.stamp = rospy.Time()
    pose2.child_frame_id = 'dummy_leaf1' # dummy leaf1 is a child for dummy child
    pose2.transform.translation.x, pose2.transform.translation.y, pose2.transform.translation.z = val2[0:3]
    quat2 = q(val2[3], val2[4], val2[5], val2[6])
    # quat2 = q.normalize(quat2)
    pose2.transform.rotation.x, pose2.transform.rotation.y, pose2.transform.rotation.z, pose2.transform.rotation.w = quat2.x, quat2.y, quat2.z, quat2.w

    # creating dummy pose3
    pose3 = TransformStamped()
    pose3.header.frame_id = 'dummy_child'
    pose3.header.stamp = rospy.Time()
    pose3.child_frame_id = 'dummy_leaf2' # dummy leaf2 is a child for dummy child
    pose3.transform.translation.x, pose3.transform.translation.y, pose3.transform.translation.z = val3[0:3]
    quat3 = q(val3[3], val3[4], val3[5], val3[6])
    # quat3 = q.normalize(quat3)
    pose3.transform.rotation.x, pose3.transform.rotation.y, pose3.transform.rotation.z, pose3.transform.rotation.w = quat3.x, quat3.y, quat3.z, quat3.w
    
    # adding dummy poses to buffer
    tf_buffer.set_transform(pose1, pose1.header.frame_id)
    tf_buffer.set_transform(pose3, pose3.header.frame_id)
    tf_buffer.set_transform(pose2, pose2.header.frame_id)
    
    try:
        # printing the two leaves' relation to dummy_child
        print("------------------------")
        print("pose from leaf1 to child")
        print("------------------------")
        print(tf_buffer.lookup_transform("dummy_leaf1", "dummy_child", rospy.Time()))
        print("------------------------")
        print("pose from leaf2 to child")
        print("------------------------")
        print(tf_buffer.lookup_transform("dummy_leaf2", "dummy_child", rospy.Time()))

        # printing one of the two leaves' relation to dummy_header
        print("------------------------")
        print("pose from leaf1 to header")
        print(tf_buffer.lookup_transform("dummy_header", "dummy_leaf1", rospy.Time()))
        print("------------------------")

    except tf2.LookupException as e:
        print("There either is an invalid quaternion or a frame is not found.")
        print(e)
    
    return

##### main function #####

def main():

    # input arguments for topics to get relative transforms from
    topic_ref = sys.argv[1] # --ref == reference topic
    if topic_ref not in ['--ref', '-r']:
        topics = sys.argv[1:]
        topic_ref = 'body'
    else:
        topic_ref = sys.argv[2]
        topics = sys.argv[3:]

    # getting topics from dataset
    # type of static data: TransformStamped
    tf_static_data = list(dict(dataset[0])['tf_static'].transforms)

    #odom_data = dict(dataset[0])['odom']
    # print(odom_data)

    # tf2 buffers usually have a try and except framework,
    # checking for TransformException
    try:

        # creating tf2 buffer
        tf_buffer = Buffer()

        # dummy_test (comment out when actual dataset is used)
        # _dummy_test(tf_buffer)

        # adding static transforms to buffer
        for static_indiv in tf_static_data:
            tf_buffer.set_transform(static_indiv, static_indiv.header.frame_id)
            # print(tf_buffer.lookup_transform(static_indiv.header.frame_id, static_indiv.child_frame_id, rospy.Time()))
        
        # printing all frames in buffer
        # print(tf_buffer.all_frames_as_yaml())

        # getting all transforms to reference frame
        tf_buffer_updated = _transfer_to_ref_frame(tf_buffer, topics, topic_ref)

        # printing all frames in updated buffer
        # print(tf_buffer_updated.all_frames_as_yaml())            

    except TransformException as e:
        # printing error type and error
        print(type(e))
        print(e)


if __name__ == '__main__':
    main()
      

Some other useful things fromtf2:

There are so many more useful things that can be done with tf2 and I hope this post gives you a good idea of what they are and how they can be used. tf2 is a foundation for many robotics projects and is a great tool to have in your toolbox.

The greatest tool for picking up any ROS package is the ROS Wiki and the official ROS website itself.


References and Useful Links