TF2 - The second generation of the transform library
Introduction
This tutorial will help you understand the flexibility of transforms with the TF2 library. It will introduce Dynamic Transform Broadcasters, Transform Listeners and Static Transform Broadcasters.
For this tutorial, we will use the Turtlesim again. This time, we will insert a second turtle into the simulation. As you drive the first turtle around with keyboard teleop, the second turte will follow it closely. In order to do this, the second turtle needs to know where the first one is, w.r.t. its own coordinate frames. This can be easily achieved using the TF2 library.
The example code is based on tf2_example and is tested with ROS 2 Foxy.
1. Dynamic TF Broadcaster
The word dynamic indicates that the transform that is being published is constantly changing. This is useful for tracking moving parts. In this case, we continuously broadcast the current position of the first turtle.
Create an ament python package with dependencies on tf2_ros (Use the “depend” tag).
ros2 pkg create --build-type ament_python <package_name> --dependencies tf2_ros rclpy
Create a new python script in this package (under the folder with the package name) and register it as an executable in setup.py
. For the purpose of this document, the package name is assumed as tf2_workshop
and executable name as broadcaster
.
Make sure the scipy library is installed:
pip3 install scipy
Copy the code shown below into the new file, save it, and build it.
#! /usr/bin/env python3
import rclpy
import sys
from geometry_msgs.msg import TransformStamped
from rclpy.node import Node
from scipy.spatial.transform import Rotation as R
from tf2_ros.transform_broadcaster import TransformBroadcaster
from turtlesim.msg import Pose
class DynamicBroadcaster(Node):
def __init__(self, turtle_name):
super().__init__('dynamic_broadcaster')
self.name_ = turtle_name
self.get_logger().info("Broadcasting pose of : {}".format(self.name_))
self.tfb_ = TransformBroadcaster(self)
self.sub_pose = self.create_subscription(Pose, "{}/pose".format(self.name_), self.handle_pose, 10)
def handle_pose(self, msg):
tfs = TransformStamped()
tfs.header.stamp = self.get_clock().now().to_msg()
tfs.header.frame_id="world"
tfs._child_frame_id = self.name_
tfs.transform.translation.x = msg.x
tfs.transform.translation.y = msg.y
tfs.transform.translation.z = 0.0
r = R.from_euler('xyz',[0,0,msg.theta])
tfs.transform.rotation.x = r.as_quat()[0]
tfs.transform.rotation.y = r.as_quat()[1]
tfs.transform.rotation.z = r.as_quat()[2]
tfs.transform.rotation.w = r.as_quat()[3]
self.tfb_.sendTransform(tfs)
def main(argv=sys.argv[1]):
rclpy.init(args=argv)
node = DynamicBroadcaster(sys.argv[1])
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
node.destroy_node()
rclpy.shutdown()
if __name__ == "__main__":
main()
1.1 Explanation
This follows the same basic structure for a ROS 2 node. The TF2 specific lines will be explained.
from geometry_msgs.msg import TransformStamped
from scipy.spatial.transform import Rotation as R
from tf2_ros.transform_broadcaster import TransformBroadcaster
Imports the modules required for TF2. Scipy is used to convert from Euler angles to quaternion since the tf_conversions
package has not been ported over to ROS 2 yet.
self.tfb_ = TransformBroadcaster(self)
Initializes a dynamic transform broadcaster which sends the transforms.
self.name_ = turtle_name
The name of the turtle for which we are broadcasting comes from the CLI as an argument.
tfs = TransformStamped()
...
self.tfb_.sendTransform(tfs)
Create a transform datatype with a header and populate it with meaningful data. The parent frame is always “world” and the child frame (i.e. current frame being published) is the name chosen. Transmit this transformation from within the subscriber callback. This implies it updates the frame (nearly) as fast as it receives the pose.
Header
Timestamp : (Determine the moment when this transform is happening. This is mainly rclpy.Time.time() when you want to send the actual transform. This means the transform can change over time to generate a dynamic motion.)
Frame_ID : (The frame ID of the origin frame)
Child Frame ID: (Frame ID to which the transform is happening)
Transform
Position : in meter (X, Y and Z)
Orientation : in Quaternion (You can use the TF Quaternion from Euler function to use the roll, pitch and yaw angles in rad instead)
1.2 Declaring the executable
Now that we have our code written and dependencies setup, we need to tell our build system that the script we created should be treated as an executable. We do this in setup.py
. Look for the section that starts with entry_points={
. We edit this part to declare the executable and its entry point to look like this:
entry_points={
'console_scripts': [
'broadcaster = tf2_workshop.broadcaster:main',
],
},
1.3 Testing the Broadcaster
First, start the turtlesim node :
ros2 run turtlesim turtlesim_node
Then start the broadcaster, with your chosen name for the turtle as the only argument. Here we assume turtle1
:
ros2 run tf2_workshop broadcaster turtle1
If all works well, the broadcaster is now sending the TF data for turtle1. This can be verified with:
ros2 run tf2_ros tf2_echo turtle1 world
or by simply running:
ros2 run tf2_ros tf2_monitor
or also through rviz2 by adding the TF
display module.
2. TF Listener
Once your robot system has a fully fleshed out TF tree with valid data at a good frequency, you can then make use of these transforms for your application through listeners, which actually solve inverse kinematics for you. This is the true power of TF2.
In the following example, we will create a TF listener, which listens to the TF tree to get the transformation between the first and the second turtle.
Create another new python file for the listener and add it as an executable. This document assumes the name listener
for it.
Copy the following code into the file and save it:
#!/usr/bin/env python3
import sys
import math
from geometry_msgs.msg import Twist
import rclpy
from rclpy.node import Node
from rclpy.qos import QoSProfile
from tf2_ros.transform_listener import TransformListener
from tf2_ros.buffer import Buffer
from tf2_ros import LookupException
class TfListener(Node):
def __init__(self, first_turtle, second_turtle):
super().__init__('tf_listener')
self.first_name_ = first_turtle
self.second_name_ = second_turtle
self.get_logger().info("Transforming from {} to {}".format(self.second_name_, self.first_name_))
self._tf_buffer = Buffer()
self._tf_listener = TransformListener(self._tf_buffer, self)
self.cmd_ = Twist ()
self.publisher_ = self.create_publisher(Twist, "{}/cmd_vel".format(self.second_name_),10)
self.timer = self.create_timer(0.33, self.timer_callback) #30 Hz = 0.333s
def timer_callback(self):
try:
trans = self._tf_buffer.lookup_transform(self.second_name_, self.first_name_, rclpy.time.Time())
self.cmd_.linear.x = math.sqrt(trans.transform.translation.x ** 2 + trans.transform.translation.y ** 2)
self.cmd_.angular.z = 4 * math.atan2(trans.transform.translation.y , trans.transform.translation.x)
self.publisher_.publish(self.cmd_)
except LookupException as e:
self.get_logger().error('failed to get transform {} \n'.format(repr(e)))
def main(argv=sys.argv):
rclpy.init(args=argv)
node = TfListener(sys.argv[1], sys.argv[2])
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
node.destroy_node()
rclpy.shutdown()
if __name__ == "__main__":
main()
2.1 Explanation
The new lines are explained here.
from tf2_ros.transform_listener import TransformListener
from tf2_ros.buffer import Buffer
from tf2_ros import LookupException
These modules are required for listeners. Recall that the buffer provides an abstracted API of the core TF2 package which is ROS agnostic.
self._tf_buffer = Buffer()
self._tf_listener = TransformListener(self._tf_buffer, self)
Boilerplate code for setting up a new listener.
trans = self._tf_buffer.lookup_transform(self.second_name_, self.first_name_, rclpy.time.Time())
The main workhorse of this node. The syntax accepts the target frame first and the source frame second. In other words, the pose of the origin of the source frame w.r.t the target frame. It calculates the transformation from the first turtle to the second turtle, which tells us how far away turtle1 is from turtle2.
self.cmd_.linear.x = math.sqrt(trans.transform.translation.x ** 2 + trans.transform.translation.y ** 2)
self.cmd_.angular.z = 4 * math.atan2(trans.transform.translation.y , trans.transform.translation.x)
self.publisher_.publish(self.cmd_)
This part is a very simply controller, that generates velocity commands based on the distance remaining. This part can be replaced by any other controller you wish to try for the following motion.
Make sure to add the listener as an executable as well (refer to the previous section) and then rebuild the package.
2.2 Testing the listener
The turtlesim simulation and teleop node need to be running first.
To test the application, you first need to spawn a second turtle, the name of which is assumed here to be turtle2
:
ros2 service call /spawn turtlesim/srv/Spawn "{x: 2, y: 2, theta: 0.2, name: "turtle2"}"
The parameters indicate the starting pose and name.
You also need to start up another TF broadcaster for this new turtle, similar to the previous one, but with the name of this turtle instead.
Finally, run the executable node for the listener by passing the first argument as the name of the first turtle and the second argument as the name of the second turtle:
ros2 run tf2_workshop listener turtle1 turtle2
Now when you move around the first turtle using the keyboard, the second turtle follows it.
You can once again check the TF tree to observe the new additions.
3. Static Transform Broadcaster
The dynamic transform broadcaster is used for moving objects. But for parts that do not move, the simpler static broadcaster does the job well. This could be used for instances like a camera fixed in a room, or a lidar mounted on a mobile robot.
The static publisher is already available as an executable node, and it can simply be started with the right command line arguments which are in order :
Translation.x, Translation.y, Translation.z, Rotation.yaw, Rotation.pitch, Rotation.roll, Parent frame, child frame
For example :
ros2 run tf2_ros static_transform_publisher 0.1 0 0 -1.57 0.0 0.0 turtle1 turtle_cam1
This will add the frame turtle_cam1
related to the turtle1
frame.
The virtual camera is mounted +0.1 m in x-axis from view of the turtle base and rotated -1.57 rad in yaw. This might not make much real world sense, but hey it’s just an example!
3.1 Testing the static broadcaster
The TF published is exactly of the same format as the dynamic publisher, and this can be easily verified with any of the aforementioned means.