# ROS2基础教程 - 11 TF坐标变换

在机器人上会有很多的传感器,例如激光雷达、摄像头、超声波等,有的传感器可以感知机器人周边物体的位置,包括横向、纵向和距离信息,可以帮助机器人定位障碍物,进行避障等处理。但是传感器感知到的障碍物的位置信息是相对于传感器的,不是相对于机器人的,所以需要将感知到的障碍物的位置转换为相对于机器人的位置。


同样,我们在进行机械臂开发的时候,例如机械臂需要夹取目标物,在机器人头部安装摄像头可以检测目标物的坐标,但是这个坐标是相对于摄像头的,需要将目标物的坐标转换为相对于机械臂夹具的坐标,这样才能实现机械臂的控制和目标物的夹取。

机器人上可能安装了多个传感器,如摄像头、激光雷达等,这每个组件有属于每个组件的坐标系,这些传感器可能位于不同的位置,并产生不同坐标系下的数据。通过坐标变换,可以将这些传感器数据整合到一个统一的坐标系中,从而创建一个统一的环境模型。

# 11.1 坐标转换

# 1 右手坐标系

在 ROS 中的坐标系是通过右手坐标系来标定的。

基于上面的坐标系,机器人往前移动,也就是在 X 轴上正向移动,机器人后退是在 X 轴上负向移动。机器人往左平移(是平移,不是左转),是在 Y 轴上正向移动,机器人往右平移,是在 Y 轴上负向移动。当然,如果机器人能上下移动,是在 Z 轴上移动。

# 2 欧拉角

上面介绍了右手坐标系,以及机器人在各个方向上的移动,这里还有一个欧拉角的概念。机器人除了可以在 X/Y/Z 轴上移动,还可以围绕着 X/Y/Z 轴进行旋转。

例如机器人原地左转,那么是围绕着 Z 轴逆时针旋转;机器人朝左前方运动,其实是在 X 轴上向前移动,Z 轴上逆时针旋转。

旋转的时候,顺时针为负值,逆时针为正值。

在 X 、Y、Z 轴上的旋转分别对应了:翻滚、俯仰、偏航。如下图:

沿X轴旋转,翻滚:

沿Y轴旋转,俯仰:

沿Z轴旋转,偏航:

# 3 坐标转换

TF(TransForm Frame,坐标变换),将一个坐标系转换为另一个坐标系,只需要将坐标系进行平移和旋转就可以了。在明确了不同坐标系之间的的相对关系后,就可以实现任何坐标点在不同坐标系之间的转换,但是该计算实现是较为常用的,且算法也有点复杂,因此在 ROS 中直接封装了相关的模块,TF坐标变换,可以很容易的实现坐标在不同坐标系中的转换。

坐标转换分为两种:

  • 静态坐标变换

  • 动态坐标变换

TF 使用树形数据结构来描述和记录整个系统的坐标系关系。每个节点都是一个坐标系,每个节点只能有一个父节点(即每个坐标系都有一个父坐标系),但可以有多个子坐标系。

TF树的建立和维护依赖于广播器发布的坐标系变换信息。一旦TF树建立完成,就可以利用监听器查询任意两个坐标系之间的变换关系。

# 11.2 静态坐标转换

所谓静态坐标变换,是指两个坐标系之间的相对位置是固定的。例如激光雷达相对于机器人底盘,位置是固定的。

要实现将一个坐标系的坐标转换为另一个坐标系的坐标,转换需要两个步骤:

  • 发布两个坐标系的关系;
  • 监听坐标系关系,并通过坐标系关系来进行坐标转换

下面实现一个功能:给出一个坐标系中点的坐标(雷达坐标系中的坐标),计算出这个点相对于另一个坐标系(小车底盘)中的坐标。两个坐标系位置是固定的。

# 11.2.1 Python实现

# 1 新建功能包

新建一个功能包用于演示。功能包的名称:tf_trans_python

ros2 pkg create --build-type ament_python tf_trans_python
1

安装依赖:

sudo apt install ros-humble-tf-transformations  # 我的是humble版本,根据版本安装
# 安装的过程中会安装python3-transforms3d
1
2

# 2 发布坐标关系

tf_trans_python/tf_trans_python 下创建 static_tf_pub.py (名称自定义)脚本文件,主要来发布两个坐标系之间的关系。

内容如下:

import rclpy
from rclpy.node import Node
from tf2_ros import StaticTransformBroadcaster  # 用于发布静态变换的广播器
from geometry_msgs.msg import TransformStamped  # 消息类型,用于传递变换信息
import tf_transformations  # 用于处理旋转和坐标变换的 Python 库

# 定义一个节点类,用于发布静态坐标变换
class StaticTransformPublisher(Node):
    def __init__(self):
        super().__init__('static_transform_publisher')

        # 创建一个静态变换广播器对象
        self.broadcaster = StaticTransformBroadcaster(self)

        # 创建一个 TransformStamped 消息对象,用于定义变换的内容
        static_transform = TransformStamped()

        # 设置变换的时间戳为当前时间
        static_transform.header.stamp = self.get_clock().now().to_msg()

        # 设置父坐标系和子坐标系的名称
        static_transform.header.frame_id = 'base_link'  # 父坐标系
        static_transform.child_frame_id = 'laser_link'  # 子坐标系

        # 设置laser_link坐标系相对于base_link坐标系平移部分的坐标值 (x, y, z)
        static_transform.transform.translation.x = 1.0  # 沿 x 轴平移 1.0 米
        static_transform.transform.translation.y = 2.0  # 沿 y 轴平移 2.0 米
        static_transform.transform.translation.z = 3.0  # 沿 z 轴平移 3.0 米

        # 使用 tf_transformations 生成四元数,四元数不直观,这里使用欧拉角来定义,然后将其转换为四元数
        quat = tf_transformations.quaternion_from_euler(0.0, 0.0, 0.0)

        # 将计算得到的四元数赋值给变换消息的旋转部分
        static_transform.transform.rotation.x = quat[0]
        static_transform.transform.rotation.y = quat[1]
        static_transform.transform.rotation.z = quat[2]
        static_transform.transform.rotation.w = quat[3]

        # 通过广播器发送变换
        self.broadcaster.sendTransform(static_transform)

        # 输出日志,表示静态变换已发布
        self.get_logger().info('Static transform published between base_link and laser_link')

# 主函数,初始化节点并保持运行
def main(args=None):
    # 初始化 ROS2
    rclpy.init(args=args)

    # 创建节点对象
    node = StaticTransformPublisher()

    # 保持节点运行,直到收到中断信号
    rclpy.spin(node)

    # 关闭 ROS2,清理节点
    rclpy.shutdown()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
  • 代码主要是发布 base_linklaser 两个坐标系的关系。使用 TransformStamped 定义子坐标系相对于父坐标系的关系,包括平移和旋转,使用 StaticTransformBroadcaster 广播坐标关系。
  • 静态坐标转换,两个坐标系的关系是固定的,所以只需要发布一次坐标关系就可以了。

# 3 进行坐标转换

tf_trans_python/tf_trans_python 下创建 static_tf_trans.py (名称自定义)脚本文件,主要是监听坐标关系,使用坐标关系来将一个坐标系中的坐标转换为另一个坐标系中的坐标。

内容如下:

import rclpy 
from rclpy.node import Node
from tf2_ros import Buffer, TransformListener  # 用于监听和查询坐标变换
from tf2_ros import LookupException, ConnectivityException, ExtrapolationException  # 异常类,用于处理查询错误
from geometry_msgs.msg import PointStamped  # 消息类型,用于表示带有坐标系的点
import tf2_geometry_msgs  # 确保导入此包以支持 PointStamped 类型转换

# 定义一个节点类,用于监听和使用坐标变换
class TransformListenerNode(Node):
    def __init__(self):
        super().__init__('transform_listener_node')

        # 创建 Buffer 对象,用于存储 TF 变换数据
        self.tf_buffer = Buffer()
        # 创建 TransformListener 对象,将变换数据存入 Buffer
        self.tf_listener = TransformListener(self.tf_buffer, self)

        # 创建一个定时器,每隔 2 秒调用一次 timer_callback 函数
        self.timer = self.create_timer(2.0, self.timer_callback)

    def timer_callback(self):
        # 创建一个 PointStamped 对象,表示需要转换的点
        point_in_laser = PointStamped()
        point_in_laser.header.stamp = self.get_clock().now().to_msg()  # 设置时间戳为当前时间
        point_in_laser.header.frame_id = 'laser_link'  # 设置原始坐标系为 'laser_link'
        point_in_laser.point.x = 1.0  # 点的 x 坐标
        point_in_laser.point.y = 0.0  # 点的 y 坐标
        point_in_laser.point.z = 0.0  # 点的 z 坐标

        try:
            # 尝试将点从 laser_link 转换到 base_link 坐标系
            point_in_base = self.tf_buffer.transform(point_in_laser, 'base_link')

            # 输出转换后的点在 'base_link' 坐标系中的位置
            self.get_logger().info(f"Point in base_link: ({point_in_base.point.x}, "
                                   f"{point_in_base.point.y}, {point_in_base.point.z})")
        except (LookupException, ConnectivityException, ExtrapolationException) as e:
            # 捕获转换失败的异常,并在日志中输出警告
            self.get_logger().warn(f"Failed to transform point: {str(e)}")

# 主函数,初始化节点并保持运行
def main(args=None):
    # 初始化 ROS2
    rclpy.init(args=args)

    # 创建 TransformListenerNode 节点对象
    node = TransformListenerNode()

    # 保持节点运行,直到收到中断信号
    rclpy.spin(node)

    # 关闭 ROS2,清理节点
    rclpy.shutdown()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

在上面的代码中监听了坐标转换的关系,存储到 tf_buffer 中,然后通过 tf_buffer 对坐标点数据进行坐标转换。代码中是启动了一个定时器,每两秒执行一次转换。给出的坐标是 laser_link 坐标系中的,转换为 base_link 坐标系中的坐标。

# 4 配置节点

在功能包下的 setup.py 中配置两个节点,在 entry_points 中配置:

entry_points={
    'console_scripts': [
        'static_tf_pub = tf_trans_python.static_tf_pub:main',
        'static_tf_trans = tf_trans_python.static_tf_trans:main',
    ],
},
1
2
3
4
5
6

# 5 构建项目

现在代码已经编写完成了,需要构建项目。

工作空间下执行如下命令:

colcon build
1

构建完成,会在 install 目录下生成文件。

# 6 运行节点

首先执行 source 命令,在工作空间下执行:

source install/local_setup.sh
1

上面的命令是让 ROS 找到我们的功能包,已经在 HelloWorld 章节说过了。


启动坐标系关系发布节点:

ros2 run tf_trans_python static_tf_pub
1

这样就发布了两个坐标系之间的转换关系:

打开一个新的终端,启动转换坐标数据的节点:

ros2 run tf_trans_python static_tf_trans
1

可以不停的执行转换:

这样就是实现了将一个坐标系中的坐标转换为另一个坐标系中的坐标。

# 11.3 使用终端或launch发布坐标关系

静态坐标转换我们可以直接使用内置的 static_transform_publisher 工具来发布静态坐标转换,类似于 ROS1 的 static_transform_publisher 节点,这样我们就不用自己实现发布坐标系之间的转换关系,只需要使用就可以了。

在 ROS2 中,这个工具位于 tf2_ros 包中,你可以使用以下命令通过终端来发布静态坐标变换:

ros2 run tf2_ros static_transform_publisher x y z qx qy qz qw frame_id child_frame_id

# 例如:发布从 `base_link` 到 `laser_link` 的静态坐标变换,旧格式
ros2 run tf2_ros static_transform_publisher 1.0 2.0 3.0 0.0 0.0 0.0 1.0 base_link laser_link

# 上面传递参数的方式已经不推荐了,建议使用新格式
ros2 run tf2_ros static_transform_publisher --x 1.0 --y 2.0 --z 3.0 --qx 0.0 --qy 0.0 --qz 0.0 --qw 1.0 --frame-id base_link --child-frame-id laser_link

# 新格式,也可以使用欧拉角
ros2 run tf2_ros static_transform_publisher --x 1.0 --y 2.0 --z 3.0 --roll 0.0 --pitch 0.0 --yaw 0.0 --frame-id base_link --child-frame-id laser_link
1
2
3
4
5
6
7
8
9
10

参数说明:

  • x, y, z:平移的三个坐标值。
  • qx, qy, qz, qw:四元数表示的旋转。
  • frame_id:父坐标系的名称。
  • child_frame_id:子坐标系的名称。

发布了坐标关系转换,直接运行前面编写的第二个节点 static_tf_trans 进行坐标数据的转换即可。


同样,我们也可以使用 launch 进行启动:

from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    return LaunchDescription([
        # 启动内置的坐标转换节点
        Node(
            package='tf2_ros',
            executable='static_transform_publisher',
            name='static_transform_publisher',
            arguments=['1.0', '2.0', '3.0', '0.0', '0.0', '0.0', '1.0', 'base_link', 'laser_link']
        ),
        # 启动自己写的转换坐标数据节点
        Node(
            package='tf_trans_python',
            executable='static_tf_trans',
            name='static_tf_trans'
        ),
    ])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

编译运行 launch,就能进行坐标数据的转换了。

# 11.4 动态坐标转换

所谓动态坐标变换,是指两个坐标系之间的相对位置是变化的。

动态坐标转换,还是需要两个步骤:

  • 发布两个坐标系的关系;
  • 监听坐标系关系,并通过坐标系关系来进行坐标转换

因为动态坐标转换中,两个坐标系之间的关系是一直在变化的,所以我们要不停的发布两个坐标系之间的关系。然后使用最新的坐标关系来进行坐标转换。

下面实现一个功能:给出一个坐标系中点的坐标,计算出这个点相对于另一个坐标系中的坐标。两个坐标系位置是变化的。

在小海龟节点中,有一个世界坐标,也就是相对于窗口左下角的坐标,小海龟会一直发布自己的世界坐标,就是相对于窗口的坐标。

我们可以把小海龟想象为一个坐标系,小海龟中心点位 (0,0)

那么海龟头部位置的坐标可以假设为 (0.1) ,那么在海龟头部在世界坐标系中的坐标也可以根据乌龟的坐标换算得到。

虽然海龟头部的位置在海龟坐标系中是不变的,但是随着海龟是运动的,海龟头部的坐标在世界坐标系中的坐标是不断变化的。

那么如何随时求出海龟头部坐标在世界坐标系中的坐标呢?这就涉及到动态坐标转换了,因为海龟坐标系和世界坐标系之间随着乌龟的运动一直在变化。

所以在实现的过程中,要一直监听乌龟的世界坐标,乌龟的世界坐标也就是乌龟坐标系和世界坐标系的关系,然后不停的发布两个坐标系的关系,有了两个坐标系的关系,就可以进行坐标数据的转换了。

# 11.4.1 Python实现

# 1 新建功能包

如果你前面操作了静态坐标转换,那么此步骤省略,直接在静态坐标转换的功能包下新建就可以了。

新建一个功能包用于演示。功能包的名称:tf_trans_python

ros2 pkg create --build-type ament_python tf_trans_python
1

安装依赖:

sudo apt install ros-humble-tf-transformations  # 我的是humble版本,根据版本安装
# 安装的过程中会安装python3-transforms3d
1
2

# 2 发布坐标关系

tf_trans_python/tf_trans_python 下创建 dynamic_tf_pub.py (名称自定义)脚本文件,主要来发布两个坐标系之间的关系。

内容如下:

import rclpy
from rclpy.node import Node
from turtlesim.msg import Pose  # 海龟坐标消息类
from geometry_msgs.msg import TransformStamped  # 消息类型,用于传递变换信息
import tf_transformations  # 用于处理旋转和坐标变换的 Python 库
import tf2_ros

class TurtlePoseBroadcaster(Node):
    def __init__(self):
        super().__init__('dynamic_tf_pub_p')  # 初始化 ROS2 节点

        # 订阅小海龟的坐标
        self.subscription = self.create_subscription(
            Pose,
            '/turtle1/pose',
            self.pose_callback,
            10  # QoS 参数,10 表示队列大小
        )
        self.subscription  # 防止未使用警告
        self.broadcaster = tf2_ros.TransformBroadcaster(self)  # 创建 TF 广播器

    # 小海龟坐标回调
    def pose_callback(self, pose):
        self.get_logger().info(f"监听坐标: x = {pose.x:.2f}, y = {pose.x:.2f}, theta = {pose.theta:.2f}")

        # 创建并配置 TransformStamped,发布两个坐标系的关系
        tfs = TransformStamped()
        tfs.header.stamp = self.get_clock().now().to_msg()  # 使用 ROS2 的时钟
        tfs.header.frame_id = 'world'
        tfs.child_frame_id = 'turtle1'

        # 设置位移
        tfs.transform.translation.x = pose.x
        tfs.transform.translation.y = pose.y
        tfs.transform.translation.z = 0.0

        # 将角度转换为四元数
        qtn = tf_transformations.quaternion_from_euler(0, 0, pose.theta)
        tfs.transform.rotation.x = qtn[0]
        tfs.transform.rotation.y = qtn[1]
        tfs.transform.rotation.z = qtn[2]
        tfs.transform.rotation.w = qtn[3]

        # 发布 TF 变换,也就是两个坐标系的关系
        self.broadcaster.sendTransform(tfs)

def main(args=None):
    rclpy.init(args=args)  # 初始化 ROS2
    node = TurtlePoseBroadcaster()  # 创建节点实例
    rclpy.spin(node)  # 运行节点,保持活跃

    # 清理并关闭节点
    node.destroy_node()
    rclpy.shutdown()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

在上面的代码中,首先监听了小海龟的坐标,小海龟的在世界坐标系中的坐标就是小海龟坐标系与世界坐标系的关系,因为两个关系是随时变化的,所以不停的将这个关系发布出去。

# 3 进行坐标转换

tf_trans_python/tf_trans_python 下创建 dynamic_tf_trans.py (名称自定义)脚本文件,主要是监听坐标关系,使用坐标关系来将一个坐标系中的坐标转换为另一个坐标系中的坐标。

内容如下:

import rclpy  # 导入 ROS2 客户端库
import rclpy.duration
from rclpy.node import Node  # 导入节点基类
import rclpy.time
from tf2_ros import Buffer, TransformListener  # 导入 TF2 缓冲区和监听器
from tf2_geometry_msgs import PointStamped  # 导入带有坐标系信息的点消息类型
from rclpy.duration import Duration  # 导入用于设置超时时间的 Duration

class TFTransformSubscriber(Node):
    def __init__(self):
        super().__init__('dynamic_sub_tf_p')  # 初始化节点并命名为 "static_sub_tf_p"

        # 创建一个 TF2 缓冲区和 TransformListener 对象,监听坐标转换关系
        self.buffer = Buffer()
        self.listener = TransformListener(self.buffer, self)  # 将当前节点作为监听器的依赖

        # 创建一个定时器,每 1 秒钟调用一次 timer_callback 方法
        self.timer = self.create_timer(1, self.timer_callback)

    # 定时器每1秒执行一次
    def timer_callback(self):
        # 创建一个 PointStamped 对象,表示在 "turtle1" 坐标系中的点
        point_source = PointStamped()
        point_source.header.frame_id = "turtle1"  # 设置源坐标系为 "turtle1"

        # 设置时间戳为当前时间
        point_source.header.stamp = (self.get_clock().now() - rclpy.duration.Duration(seconds=1)).to_msg()  
        # 这里假设是小海龟头部的坐标
        point_source.point.x = 1.0  # 设置点的 x 坐标
        point_source.point.y = 0.0   # 设置点的 y 坐标
        point_source.point.z = 0.0   # 设置点的 z 坐标

        try:
            # 将点坐标从 "turtle1" 转换为 "world" 坐标系
            point_target = self.buffer.transform(point_source, target_frame="world", timeout=Duration(seconds=0.5))
            # 打印转换结果
            self.get_logger().info(f"转换结果: x = {point_target.point.x:.2f}, y = {point_target.point.y:.2f}, z = {point_target.point.z:.2f}")
        except Exception as e:
            # 如果发生异常,记录错误信息
            self.get_logger().error(f"异常: {e}")

def main(args=None):
    # 初始化 ROS2 客户端库
    rclpy.init(args=args)
    # 创建 TFTransformSubscriber 节点实例
    node = TFTransformSubscriber()
    # 运行节点并监听回调函数
    rclpy.spin(node)

    # 节点被手动终止时销毁节点实例
    node.destroy_node()
    # 关闭 ROS2 客户端库
    rclpy.shutdown()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

在上面的代码中监听了坐标转换的关系,存储到 tf_buffer 中,然后通过 tf_buffer 对坐标点数据进行坐标转换。代码中是启动了一个定时器,每两秒执行一次转换。给出的坐标是 laser_link 坐标系中的,转换为 base_link 坐标系中的坐标。

这里有一个地方需要注意,也就是 self.get_clock().now() - rclpy.duration.Duration(seconds=1) ,这里获取到当前的时间,然后减去1秒,因为转换坐标数据的这个时间不能比发布坐标转换的时间新(晚),因为发布转换关系的时间是 self.get_clock().now() ,所以转换数据的时间要早一些,否则会报错。但是转换坐标数据的时间也不能比发布坐标转换的时间早太多,因为 TF 缓冲区缓存的数据默认是 10 秒,最多缓存10秒之前的坐标转换关系。

如果 point_source.header.stamp = (self.get_clock().now() - rclpy.duration.Duration(seconds=10)).to_msg() 设置为减10秒,那么启动的前10秒会报错,因为TF 缓冲区还没有缓存比当前时间早10秒的数据。

不过 point_source.header.stamp = rclpy.time.Time().to_msg() ,这里的时间是 0,ROS2 会寻找 距离当前请求时间最近的有效变换

# 4 配置节点

在功能包下的 setup.py 中配置两个节点,在 entry_points 中配置:

entry_points={
    'console_scripts': [
        # ...其他配置
        'dynamic_tf_pub = tf_trans_python.dynamic_tf_pub:main',
        'dynamic_tf_trans = tf_trans_python.dynamic_tf_trans:main',
    ],
},
1
2
3
4
5
6
7

# 5 构建项目

现在代码已经编写完成了,需要构建项目。

工作空间下执行如下命令:

colcon build
1

构建完成,会在 install 目录下生成文件。

# 6 运行节点

首先执行 source 命令,在工作空间下执行:

source install/local_setup.sh
1

上面的命令是让 ROS 找到我们的功能包,已经在 HelloWorld 章节说过了。


启动节点,这里需要打开四个终端,启动四个节点。

首先需要启动小海龟的两个节点:

# 启动小海龟节点
ros2 run turtlesim turtlesim_node

# 启动小海龟控制节点
ros2 run turtlesim turtle_teleop_key
1
2
3
4
5

然后启动发布 TF 坐标变换节点:

ros2 run tf_trans_python dynamic_tf_pub
1

这样就发布了两个坐标系之间的转换关系:

打开一个新的终端,启动转换坐标数据的节点:

ros2 run tf_trans_python dynamic_tf_trans
1

可以不停的执行转换:

然后可以控制小海龟运动,上面的坐标已知的变化,就是小海龟头部在世界坐标系中的坐标。

# 11.5 坐标工具

在坐标系系统中,各个坐标系是构成父子关系的,最终会构成一个树形结构。一个坐标系只能有一个父坐标系,一个父坐标系可以有多个子坐标系。

# 1 查看坐标结构图

在我们发布了多个坐标转换之后,可以通过工具来查看各个坐标系之间的结构图。

首先需要安装 tf2_tools

sudo apt install ros-humble-tf2-tools   # humble版本的
1

然后使用如下命令查看当前 TF 坐标系树的结构图:

ros2 run tf2_tools view_frames
1

此命令运行后,会在当前运行命令的目录下,生成一个文件,通常命名为 frames-xxx.pdf,打开文件可以查看到5秒内的坐标结构图。

例如运行了上面的动态坐标转换的节点后,再运行上面的命令,生成的 frames-xxx.pdf 如下:

# 2 实时查看坐标系变换关系数据

在 ROS 2 中可以通过命令实时查看两个坐标系之间的变换关系,可以通过 tf2_ros 包提供的 tf2_echo 工具来输出实时的变换数据。

命令如下:

ros2 run tf2_ros tf2_echo world turtle1
1
  • world:源坐标系的名称(父坐标系)。

  • turtle:目标坐标系的名称(子坐标系)。

此命令会监听 ROS 2 网络中关于 worldturtle1 这两个坐标系的 TF 变换数据,并在终端中输出它们之间的实时变换,包括平移(位置)和旋转(以四元数表示)。我们可以使用这个命令验证两个坐标系之间的位置和旋转关系,是否符合预期。