1 of 14

Coordinate Frames and Transformations in ROS2

Transforming coordinates relative to

the sensors, the robot & the world

2 of 14

What is a coordinate frame?

Set of orthogonal axes attached to a body

  • Axes intersect at the origin
  • Right-hand rule
  • ROS uses cartesian coordinates

Why do we need them?

  • Common "language" to describe the position and orientation of rigid bodies in 3D space

Robotics convention:

  • World frame: x is right
  • Robot frame/sensors: x is forwards

3 of 14

Conversion between frames

In ROS, sensor data is always provided relative to the sensor itself

We need to centralize the data in different sensor frames into one shared 'global' frame, for easier processing

  • Global frame (map/odom), robot frame (base_link)

May need to convert from global to local or vice-versa, and even between different sensor frames for e.g. LiDAR-camera fusion

But how do we do that?

4 of 14

Rigid transformations

Frame conversions are a combination of translation and rotation

  • A transformation can represent position and orientation of robot/component

Can be used to convert points from one frame to another

coordinates in

source frame

coordinates in

target frame

source frame

target frame

4x4 Transformation Matrix

inner dot markdown

5 of 14

Rotation representations

Intuitive: Euler angles

  • Roll: rotation around X axis
  • Pitch: rotation around Y axis
  • Yaw: rotation around Z axis

applied in that order (RPY)

gimbal lock

Practical: Rotation matrix

  • Orthogonal matrix
  • Used to apply transformations

→ not compact (9 numbers)

6 of 14

Quaternions

Idea: Rotations can be represented by:

  • An axis (around which the rotation is done)
  • An angle (by which we rotate around the axis)

Compact representation: 4 numbers

For practical reasons (easy to compose & invert), represented like that

Used by ROS to represent rotations

  • Unit vector (why?)
  • q and -q are the same

7 of 14

Transformation operations = matrix operations

8 of 14

Practical application: ROS TF Tree

9 of 14

What is TF2?

A package that handles the coordinate frames present in a system & the transformations between them

Nodes can:

  • Publish (broadcast) to the TF tree (/tf and /tf_static topics)
    • Static transforms
    • Dynamic transforms
  • Subscribe (listen) to the TF tree

10 of 14

TF2 Static Broadcaster (Python)

...

from geometry_msgs.msg import TransformStamped

from tf2_ros.static_transform_broadcaster import StaticTransformBroadcaster

from tf_transformations import quaternion_from_euler

class StaticFramePublisher(Node):

def __init__(self, transformation):

super().__init__('static_turtle_tf2_broadcaster')

self.tf_static_broadcaster = StaticTransformBroadcaster(self)

# Publish static transforms once at startup

self.make_transforms()

def make_transforms(self):

t = TransformStamped()

t.header.stamp = self.get_clock().now().to_msg()

t.header.frame_id = ‘map’

t.child_frame_id = ‘base_link’

t.transform.translation.x/y/z = ...

t.transform.rotation.x,y,z,w = quaternion_from_euler(r,p,y)

self.tf_static_broadcaster.sendTransform(t)

...

parent in TF tree

child in TF tree

map = parent frame

base_link = child frame

11 of 14

TF2 Broadcaster (Python)

...

from geometry_msgs.msg import TransformStamped

from tf2_ros import TransformBroadcaster

class FramePublisher(Node):

def __init__(self, transformation):

super().__init__('turtle_tf2_frame_broadcaster')

self.tf_broadcaster = TransformBroadcaster(self)

self.timer = self.create_timer(1/30.0, self.timer_cb) # 30Hz tf broadcaster

def timer_cb(self):

t = TransformStamped()

t.header.stamp = self.get_clock().now().to_msg()

t.header.frame_id = ‘map’

t.child_frame_id = ‘base_link’

t.transform.translation.x/y/z = ...

t.transform.rotation.x/y/z/w = ...

self.tf_broadcaster.sendTransform(t)

...

parent in TF tree

child in TF tree

map = parent frame

base_link = child frame

12 of 14

TF2 Listener (Python)

The transform from Source Frame to Target Frame is the transform which when applied to an object in Target Frame will represent it in Source Frame.

...

from tf2_ros import TransformException

from tf2_ros.buffer import Buffer

from tf2_ros.transform_listener import TransformListener

class FrameListener(Node):

def __init__(self, transformation):

super().__init__('turtle_tf2_frame_listener')

self.tf_buffer = Buffer()

self.tf_listener = TransformListener(self.tf_buffer, self)

self.timer = self.create_timer(1/30.0, self.timer_cb)

def timer_cb(self):

try:

# map->base_link rigid transformation

self.map_to_base_link_tf = self.tf_buffer.lookup_transform(“map”, ”base_link”, rclpy.time.Time())

except TransformException as ex:

self.get_logger().info(f'Could not transform map to base_link: {ex}')

return

...

map = target frame

base_link = source frame

target frame

source frame

No need to be parent/child (can be anywhere

in the TF tree)

13 of 14

Useful TF2 utilities

Visualize the TF tree, view frames and transform rates (Hz):

ros2 run tf2_tools view_frames

Listen to a transform:

ros2 run tf2_ros tf2_echo [target_frame] [source_frame]

Broadcast a (static) transform:

ros2 run tf2_ros static_transform_publisher [x] [y] [z] [yaw] [pitch] [roll] [parent_frame] [child_frame]

Monitor a transform, or all transforms (including chain, broadcasters, rate, etc.):

ros2 run tf2_ros tf2_monitor ([target_frame] [source_frame])

14 of 14

Resources