目录

目录... 1

一、tf2基础理解... 2

1.1 tf2 解决什么问题?... 2

1.2 tf2 核心架构... 2

二、tf2 (Transform Library 2) 3

2.1. 核心数据结构:TF Tree(坐标树)... 3

2.2. 两种变换类型... 3

2.3. 三大核心角色... 3

三、坐标系的变换原理... 4

3.1. 旋转... 4

3.2. 平移... 4

3.3. 坐标点变换原理... 4

四、坐标系的使用... 5

4.1 坐标系启动... 5

4.2 可视化坐标系Rviz. 6

第五章 发布静态坐标系... 10

5.1 创建工作空间,建立坐标系功能包,... 10

5.1  编译,加载,运行,验证... 13

第六章 发布动态坐标系... 16

6.1 什么是动态坐标系... 16

6.2创建动态坐标系文件framepub.py. 17

6.3  添加环境变量,编译,加载,运行,验证... 19

第7章 坐标系的查询... 22

7.1 接收坐标系变化的代码... 22

7.2 添加环境变量,编译,加载,运行... 24

7.3运行结果... 26

                                

一、tf2基础理解

tf2 是 ROS 2 中负责坐标变换(Transform)的核心功能包。

它的主要作用是维护机器人各坐标系之间的空间关系树(TF Tree),并允许节点在任意两个坐标系之间进行实时查询和转换。

1.1 tf2 解决什么问题?

在机器人系统中,传感器数据通常分散在不同的坐标系下:

  • 激光雷达点云 → lidar_link 坐标系
  • 相机图像 → camera_link 坐标系
  • 里程计位置 → odom 坐标系
  • 地图定位 → map 坐标系

tf2 让你无需手动计算矩阵乘法,只需告诉系统"把 A 坐标系下的某点转换到 B 坐标系",它会自动沿着 TF Tree 查找路径并完成变换。

1.2 tf2 核心架构

表格

组件

说明

TF Broadcaster

发布坐标变换关系(如 base_link → lidar_link)

TF Listener / Buffer

缓存历史变换,提供查询接口

TF Tree

所有坐标系构成的有向树结构(每个子帧只有一个父帧)

Static Transform

固定不变的变换(如传感器安装位置),只需发布一次

在 ROS 2 中,参数(Parameter) 是专门用于外部配置节点(Node)的一种机制。你可以把它理解为节点的“全局设置”或“运行时开关”。

二、tf2 (Transform Library 2) 

在 ROS 2 中,tf2 (Transform Library 2) 是核心的坐标系管理框架。它负责维护机器人系统中所有坐标系(Frames)之间的相对位置和姿态关系,并支持随时间变化的动态查询。

简单来说,tf2 回答了机器人学中最基本的问题:“在时刻 T,坐标系 A 相对于坐标系 B 在哪里、朝向哪里?”

以下是 ROS 2 tf2 的核心体系简介:

2.1. 核心数据结构:TF Tree(坐标树)

tf2 将所有坐标系组织成一棵有向树状结构,而非图或网状结构。

  • 节点 (Node):代表一个坐标系(如 base_link, lidar_link, map)。
  • 边 (Edge):代表两个坐标系之间的刚体变换(3D 平移 + 旋转),具有方向性(父 → 子)。
  • 唯一父节点原则:每个坐标系只能有一个父节点,但可以有多个子节点。这保证了任意两帧之间的变换路径是唯一且确定的,避免了多路径冲突和歧义。

2.2. 两种变换类型

表格

类型

说明

典型示例

发布方式

Static Transform

不随时间变化的固定安装关系

雷达安装在底盘上、相机与IMU的相对位姿

static_transform_publisher 或 URDF/Xacro

Dynamic Transform

随时间实时变化的运动关系

里程计(odom)→底盘(base_link)、机械臂关节运动

节点代码实时广播 (Broadcaster)

⚠️ 关键区别:静态变换只需发布一次,tf2 会永久缓存;动态变换需要持续高频发布(通常 50Hz+),否则查询时会因超时失败。

2.3. 三大核心角色

📡 Broadcaster(广播者)

  • 负责发布坐标变换到 /tf 或 /tf_static 话题。
  • 动态广播使用 TransformBroadcaster,静态广播使用 StaticTransformBroadcaster。
  • 数据格式为 geometry_msgs/msg/TransformStamped。

🔍 Listener(监听者)

  • 负责查询任意两个坐标系之间的变换。
  • 使用 Buffer + TransformListener 组合。
  • 核心 API:lookup_transform(target_frame, source_frame, time)。
  • 支持时间旅行:可查询历史时刻的变换(受限于 Buffer 缓存时长,默认 10s)。

🛠️ Tools(调试工具)

  • ros2 run tf2_ros static_transform_publisher ...:命令行快速发布静态变换。
  • ros2 run tf2_tools view_frames:生成当前 TF Tree 的 PDF/SVG 可视化图。
  • ros2 run tf2_ros tf2_echo <target> <source>:实时打印两帧间的变换数值。
  • RViz2 TF Display:图形化查看各坐标系的位置、方向和命名。

三、坐标系的变换原理

一个机器人上可以建立很多个坐标系;

一个不同的坐标系之间的关系;

一个物体在指定坐标系中,经过旋转和运动后新的位置和姿态;

空间不同的坐标系,关系可以分解为 旋转和平移;

基本旋转矩阵(遵循右手螺旋定则)

3.1. 旋转

  坐标系顶点位置一样,但是 xyz 轴不一样,旋转关系;

3.2. 平移

  坐标系顶点位置不一样,但是 xyz 轴平行,平移;

3.3. 坐标点变换原理

    先求坐标系B单位向量(xyz三轴)到坐标系A的 旋转 矩阵;在求出到A坐标系的平移矩阵;

在带入坐标的点,即可把B坐标系上的点移动到A坐标系中的坐标;   系顶点位置一样,但是 xyz 轴不一样,平移;

四、坐标系的使用

4.1 坐标系启动

启动tf2的服务器

终端1

root@LHAYR:~/ros2_ws# ros2 run tf2_ros buffer_server

发布两个世界坐标系 A 和B(3,4,5)

终端2  发布A坐标系

root@LHAYR:~/ros2_ws# ros2 run tf2_ros static_transform_publisher --frame-id World --child-frame-id A

[INFO] [1783082895.914468358] [static_transform_publisher_gzVwOOL4ooXikTYb]: Spinning until stopped - publishing transform

translation: ('0.000000', '0.000000', '0.000000')

rotation: ('0.000000', '0.000000', '0.000000', '1.000000')

from 'World' to 'A'

终端3 发布B坐标系

root@LHAYR:~/ros2_ws# ros2 run tf2_ros static_transform_publisher --frame-id World --child-frame-id B --x 3 --y 4 --z 5

[INFO] [1783082930.165232844] [static_transform_publisher_5lY2hfQ0IZBvkPw7]: Spinning until stopped - publishing transform

translation: ('3.000000', '4.000000', '5.000000')

rotation: ('0.000000', '0.000000', '0.000000', '1.000000')

from 'World' to 'B'

终端4,检测 坐标系B到坐标系A的变换,

root@LHAYR:~/ros2_ws# ros2 run tf2_ros tf2_echo A B

[INFO] [1783083178.193853145] [tf2_echo]: Waiting for transform A ->  B: Invalid frame ID "A" passed to canTransform argument target_frame - frame does not exist

At time 0.0

- Translation: [3.000, 4.000, 5.000]

- Rotation: in Quaternion (xyzw) [0.000, 0.000, 0.000, 1.000]

- Rotation: in RPY (radian) [0.000, -0.000, 0.000]

- Rotation: in RPY (degree) [0.000, -0.000, 0.000]

- Matrix:

  1.000  0.000  0.000  3.000

  0.000  1.000  0.000  4.000

  0.000  0.000  1.000  5.000

  0.000  0.000  0.000  1.000

At time 0.0

- Translation: [3.000, 4.000, 5.000]

- Rotation: in Quaternion (xyzw) [0.000, 0.000, 0.000, 1.000]

- Rotation: in RPY (radian) [0.000, -0.000, 0.000]

- Rotation: in RPY (degree) [0.000, -0.000, 0.000]

- Matrix:

  1.000  0.000  0.000  3.000

  0.000  1.000  0.000  4.000

  0.000  0.000  1.000  5.000

  0.000  0.000  0.000  1.000

At time 0.0

- Translation: [3.000, 4.000, 5.000]

- Rotation: in Quaternion (xyzw) [0.000, 0.000, 0.000, 1.000]

- Rotation: in RPY (radian) [0.000, -0.000, 0.000]

- Rotation: in RPY (degree) [0.000, -0.000, 0.000]

- Matrix:

  1.000  0.000  0.000  3.000

  0.000  1.000  0.000  4.000

  0.000  0.000  1.000  5.000

  0.000  0.000  0.000  1.000

At time 0.0

- Translation: [3.000, 4.000, 5.000]

- Rotation: in Quaternion (xyzw) [0.000, 0.000, 0.000, 1.000]

- Rotation: in RPY (radian) [0.000, -0.000, 0.000]

- Rotation: in RPY (degree) [0.000, -0.000, 0.000]

- Matrix:

  1.000  0.000  0.000  3.000

  0.000  1.000  0.000  4.000

  0.000  0.000  1.000  5.000

  0.000  0.000  0.000  1.000

4.2 可视化坐标系Rviz

       启动Rviz 执行指令rviz2

root@LHAYR:~/ros2_ws# rviz2

[INFO] [1783083881.835511841] [rviz2]: Stereo is NOT SUPPORTED

[INFO] [1783083881.835566972] [rviz2]: OpenGl version: 4.5 (GLSL 4.5)

[INFO] [1783083881.866538282] [rviz2]: Stereo is NOT SUPPORTED

       将全局设置(Global Options)下的固定坐标系(Fixed Frame)设置为World。添加TF可视化功能后,在三维可视化系统中,三个坐标系 World、A、B;

①、设置固定坐标系(Fixed Frame)

在 RViz2 窗口的左侧有一个 Displays 面板。如果没看到,点击顶部菜单 Panels → 勾选 Displays。操作:

  1. 点击 Fixed Frame 旁边输入框中的 map(或当前显示的文本)
  2. 删除它,输入 World(注意大小写,必须和你发布时用的 --frame-id World 完全一致)
  3. 按 Enter 键确认

✅ 此时,RViz2 会以 World 坐标系为世界原点。

②、添加 TF 可视化

操作:

  1. 点击 Add 按钮
  2. 会弹出一个 Create visualization display 对话框
  3. 在左侧列表中找到 By display 标签页,或在搜索框中输入 tf
  4. 选择 TF(图标是一个坐标系样子)
  5. 点击右下角的 OK 按钮

三位坐标系就显示出来了,其中A和World 是在(0,0,0), B(3,4,5)

在发布一个C坐标(2,2,2)

root@LHAYR:~/ros2_ws# ros2 run tf2_ros static_transform_publisher --frame-id World --child-frame-id c --x 2 --y 2 --z 2

[INFO] [1783084990.914592740] [static_transform_publisher_LwEhZguk3RX9Tpr8]: Spinning until stopped - publishing transform

translation: ('2.000000', '2.000000', '2.000000')

rotation: ('0.000000', '0.000000', '0.000000', '1.000000')

from 'World' to 'c'

显示三维

也可以输出绘制的信息使用指令ros2 run tf2_tools view_frames ;可以通过查看输出的pdf文件查看绘制输出的图;

root@LHAYR:~/ros2_ws# ros2 run tf2_tools view_frames

[INFO] [1783085279.141443082] [view_frames]: Listening to tf data for 5.0 seconds...

[INFO] [1783085284.221835049] [view_frames]: Generating graph...

[INFO] [1783085284.222800630] [view_frames]: Result:tf2_msgs.srv.FrameGraph_Response(frame_yaml="B: \n  parent: 'World'\n  broadcaster: 'default_authority'\n  rate: 10000.000\n  most_recent_transform: 0.000000\n  oldest_transform: 0.000000\n  buffer_length: 0.000\nc: \n  parent: 'World'\n  broadcaster: 'default_authority'\n  rate: 10000.000\n  most_recent_transform: 0.000000\n  oldest_transform: 0.000000\n  buffer_length: 0.000\n")

[INFO] [1783085284.223514746] [view_frames]: Exporting graph in frames_2026-07-03_21.28.04.pdf file...

root@LHAYR:~/ros2_ws# code .   #// 打开vs code ,可以打开pdf 文件

root@LHAYR:~/ros2_ws#

通过vs  code 打开frames_2026-07-03_21.28.04.pdf 文件

第五章 发布静态坐标系

       在机器人系统中,固定的障碍物或建筑物等坐标不会随时间发生变化。对于这类静态坐标系,只需向 tf2 注册并发布一次坐标信息即可。在 ROS2 中,tf2_ros 功能包提供的 StaticTransformBroadcaster 类专门用于发布静态坐标变换;同时,geometry_msgs 功能包中的 TransformStamped 消息类型则用于定义具体的坐标系变换数据。

    5.1 创建工作空间,建立坐标系功能包,

创建工作空间,建立坐标系功能包,包的名称叫 tf_test,用于存放案例代码,(在src目录下,创建一个功能包名称叫tf_test,同时创建一个节点(static_frame_pub));打卡vs code(code .);

root@LHAYR:~/ros2_ws# cd src

root@LHAYR:~/ros2_ws/src# ls

action_test  bag_files  json_interface  parameter_test  serviceclient_test  topic_test

root@LHAYR:~/ros2_ws/src# ros2 pkg create --build-type ament_python --node-name static_frame_pub --dependencies geometry_msgs tf2_ros --license MIT tf_test

going to create a new package

package name: tf_test

……

creating ./tf_test/tf_test/static_frame_pub.py

root@LHAYR:~/ros2_ws/src# ls

action_test  json_interface  serviceclient_test  topic_test

bag_files    parameter_test  tf_test

root@LHAYR:~/ros2_ws/src# code .

root@LHAYR:~/ros2_ws/src#

修改static_frame_pub.py 节点代码,如下

import math

import rclpy

from rclpy.node import Node

from geometry_msgs.msg import TransformStamped

from tf2_ros.static_transform_broadcaster import StaticTransformBroadcaster

def quaternion_from_euler(ai, aj, ak):

    """将欧拉角(roll, pitch, yaw)转换为四元数(x, y, z, w)"""

    ai /= 2.0

    aj /= 2.0

    ak /= 2.0

    ci = math.cos(ai)

    si = math.sin(ai)

    cj = math.cos(aj)

    sj = math.sin(aj)

    ck = math.cos(ak)

    sk = math.sin(ak)

    cc = ci * ck

    cs = ci * sk

    sc = si * ck

    ss = si * sk

    q = [0.0] * 4

    q[0] = cj * sc - sj * cs

    q[1] = cj * ss + sj * cc

    q[2] = cj * cs - sj * sc

    q[3] = cj * cc + sj * ss

    return q

class StaticFramePublisher(Node):

    """静态坐标系发布器"""

    def __init__(self):

        super().__init__('static_frame_tf2_broadcaster')

       

        # 声明并获取参数

        self.x = self.declare_parameter('x', 0.0).get_parameter_value().double_value

        self.y = self.declare_parameter('y', 0.0).get_parameter_value().double_value

        self.z = self.declare_parameter('z', 0.0).get_parameter_value().double_value

        self.roll = self.declare_parameter('roll', 0.0).get_parameter_value().double_value

        self.pitch = self.declare_parameter('pitch', 0.0).get_parameter_value().double_value

        self.yaw = self.declare_parameter('yaw', 0.0).get_parameter_value().double_value

        self.child_frame = self.declare_parameter('child_frame', 'frame_A').get_parameter_value().string_value

        self.parent_frame = self.declare_parameter('parent_frame', 'world').get_parameter_value().string_value

        # 初始化静态广播器和日志器

        self.tf_static_broadcaster = StaticTransformBroadcaster(self)

        self.logger = self.get_logger()

       

        # 发布静态坐标变换

        self.pub_transforms()

    def pub_transforms(self):

        """创建并发送静态坐标变换"""

        t = TransformStamped()

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

        t.header.frame_id = self.parent_frame

        t.child_frame_id = self.child_frame

       

        # 设置平移

        t.transform.translation.x = float(self.x)

        t.transform.translation.y = float(self.y)

        t.transform.translation.z = float(self.z)

       

        # 将角度转换为弧度,再转为四元数

        quat = quaternion_from_euler(

            self.roll / 180.0 * math.pi,

            self.pitch / 180.0 * math.pi,

            self.yaw / 180.0 * math.pi

        )

        t.transform.rotation.x = quat[0]

        t.transform.rotation.y = quat[1]

        t.transform.rotation.z = quat[2]

        t.transform.rotation.w = quat[3]

       

        # 打印日志并发布变换

        self.logger.info(f'Publish static tf: {self.parent_frame} -> {self.child_frame}')

        self.tf_static_broadcaster.sendTransform(t)

def main(args=None):

    rclpy.init(args=args)

    node = StaticFramePublisher()

    try:

        rclpy.spin(node)

    except KeyboardInterrupt:

        pass

    finally:

        node.destroy_node()

        rclpy.shutdown()

if __name__ == '__main__':

    main()

这段代码实现了一个通用的 ROS2 静态坐标系发布节点(Static Frame Publisher)。它的核心功能是:通过读取用户传入的参数,计算并广播两个坐标系之间的静态相对位姿(平移和旋转)。

为了让你更清晰地理解,我将这段代码按功能模块进行详细拆解:

1. 欧拉角转四元数工具函数 (quaternion_from_euler)

  • 功能:在 ROS2 中,坐标系的旋转必须使用四元数(Quaternion)来表示。这个函数接收三个欧拉角参数(Roll, Pitch, Yaw),并通过三角函数公式将其转换为四元数 xyzw  。
  • 意义:让开发者在输入参数时可以使用更符合人类直觉的“角度”,而由程序自动完成底层的数学转换。

2. 核心节点类 (StaticFramePublisher)

这是整个程序的主体,继承自 ROS2 的 Node 类,主要包含以下逻辑:

  • 参数声明与获取:
    在 __init__ 中,节点声明了 8 个参数(平移量 x/y/z,旋转角 roll/pitch/yaw,以及父子坐标系名称 parent_frame/child_frame),并设置了默认值。这使得该节点非常灵活,无需修改代码即可通过命令行传参来发布任意坐标系。
  • 初始化组件:
    实例化了 StaticTransformBroadcaster(静态变换广播器)和日志器(Logger),用于发送数据和打印运行状态。
  • 构建并发布变换 (pub_transforms):
    • 创建消息:实例化一个 TransformStamped 消息对象,并打上当前时间戳。
    • 绑定坐标系:将参数中的父、子坐标系名称赋值给消息的 Header。
    • 设置平移:将 x, y, z 参数填入消息的 translation 字段。
    • 设置旋转:将 roll, pitch, yaw 参数除以 180 乘以 π  (将角度转为弧度),然后调用 quaternion_from_euler 函数,将计算出的四元数填入 rotation 字段。
    • 广播与日志:调用 sendTransform(t) 将变换数据发布到 ROS2 网络中,并通过 logger.info 打印发布成功的提示。

3. 程序入口与生命周期管理 (main 函数)

  • 初始化与实例化:调用 rclpy.init() 初始化 ROS2 客户端,并创建 StaticFramePublisher 节点实例。
  • 保持节点运行:调用 rclpy.spin(node) 让节点进入事件循环。虽然静态坐标只需发布一次,但保持节点存活可以确保该节点能被 ROS2 系统正常管理和查询。
  • 优雅退出:使用 try...except KeyboardInterrupt...finally 结构捕获用户的 Ctrl+C 中断信号,确保在程序退出时能正确销毁节点并关闭 ROS2 通信,防止资源泄露。

总结

简单来说,这段代码就是一个“坐标翻译官”。当你运行它并告诉它“A坐标系在B坐标系前方1米,且向上仰起90度”时,它就会把这个空间关系广播给整个机器人系统,让其他节点(如导航、机械臂控制)都能知道这两个坐标系的相对位置。

5.1  编译,加载,运行,验证

编译,在ros2_ws  目录下编译,

 #//编译

root@LHAYR:~/ros2_ws# colcon build --packages-select tf_test

Starting >>> tf_test

Finished <<< tf_test [0.64s]

Summary: 1 package finished [0.80s]

root@LHAYR:~/ros2_ws#

   #//  加载环境变量

root@LHAYR:~/ros2_ws# source install/setup.bash

root@LHAYR:~/ros2_ws#

在终端1. 发布坐标系 A (相对于 world)

root@LHAYR:~/ros2_ws# ros2 run tf_test static_frame_pub --ros-args \

  -p parent_frame:=world \

  -p child_frame:=frame_A \

  -p x:=1.0 -p y:=0.0 -p z:=0.0 \

  -p roll:=0.0 -p pitch:=0.0 -p yaw:=90.0

[INFO] [1783329977.973951153] [static_frame_tf2_broadcaster]: Publish static tf: world -> frame_A

在终端2. 发布坐标系 B (相对于 A);  先加载一下

root@LHAYR:~/ros2_ws# source install/setup.bash

root@LHAYR:~/ros2_ws# ros2 run tf_test static_frame_pub --ros-args \

  -p parent_frame:=frame_A \

  -p child_frame:=frame_B \

  -p x:=1.0 -p y:=0.0 -p z:=0.0 \

  -p roll:=0.0 -p pitch:=0.0 -p yaw:=90.0

[INFO] [1783330133.656663598] [static_frame_tf2_broadcaster]: Publish static tf: frame_A -> frame_B

这三个词代表的是三维空间中物体的欧拉角(Euler Angles),也就是飞行器或机器人最常用的旋转描述方式:

  • roll(横滚角):绕 X 轴旋转的角度。
  • pitch(俯仰角):绕 Y 轴旋转的角度。
  • yaw(偏航角):绕 Z 轴旋转的角度。

打开rviz

终端3,进入/ros2_ws目录

root@LHAYR:~/ros2_ws# rviz2

[INFO] [1783330204.748346811] [rviz2]: Stereo is NOT SUPPORTED

[INFO] [1783330204.748403214] [rviz2]: OpenGl version: 4.5 (GLSL 4.5)

[INFO] [1783330204.784237586] [rviz2]: Stereo is NOT SUPPORTED

配置Rviz 终端 Fixed Frame 选中 world;在点击add,添加tf显示窗口;

坐标系统 三维显示出来了,

终端4,在加上一个坐标点C;先加载,在输入
 

root@LHAYR:~/ros2_ws# source install/setup.bash

root@LHAYR:~/ros2_ws# ros2 run tf_test static_frame_pub --ros-args   -p parent_frame:=frame_B   -p child_frame:=frame_C   -p x:=1.0 -p y:=0.0 -p z:=1.0   -p roll:=0.0 -p pitch:=0.0 -p yaw:=90.0

[INFO] [1783332358.006986734] [static_frame_tf2_broadcaster]: Publish static tf: frame_B -> frame_C

坐标系统 三维显示出来了,

第六章 发布动态坐标系

6.1 什么是动态坐标系

1. 静态坐标系 (Static Transform)

  • 特点:一旦发布,相对位置就固定不变了。
  • 应用场景:机器人的底盘与底盘上的激光雷达、相机之间的相对位置。只要你不把相机拆下来换个位置,它们之间的相对关系就是永远固定的。
  • 广播器:StaticTransformBroadcaster(只需发送一次)。

2. 动态坐标系 (Dynamic Transform)

  • 特点:需要以一定的频率(比如 10Hz、50Hz)持续不断地更新和广播相对位姿。
  • 应用场景:机器人身上正在运动的部件。例如:
    • 机械臂的关节(大臂和小臂之间在转动)。
    • 机器人的轮子(轮子相对于车体在滚动)。
    • 无人机的云台(云台在实时追踪目标并转动)。
  • 广播器:TransformBroadcaster(普通的动态广播器,需要在定时器或循环中不断发送)。

3. 代码层面的区别

如果你要在 ROS2 中发布一个动态坐标系,代码逻辑会发生以下变化:

  1. 广播器改变:将 StaticTransformBroadcaster 替换为普通的 TransformBroadcaster。
  2. 增加定时器:不能只在 __init__ 里发一次了,你需要创建一个定时器(Timer),比如每 50 毫秒(20Hz)触发一次发布函数。
  3. 数据源改变:平移和旋转的参数不能写死,通常需要从硬件传感器(如电机编码器、IMU)实时读取,或者从仿真软件中获取。

总结

简单来说:静态坐标系描述的是“死”的、固定的结构关系;动态坐标系描述的是“活”的、正在运动的部件关系。

    6.2创建动态坐标系文件framepub.py

在tf_test工作空间,创建一动态坐标系文件framepub.py,用于存放动态坐标系发布的节点代码。

在vs code 的 src/tf_test/tf_test目录下,创建一个文件(framepub.py));

修改framepub.py节点代码,如下

import math

import rclpy

from rclpy.node import Node

from geometry_msgs.msg import TransformStamped

from tf2_ros import TransformBroadcaster

def quaternion_from_euler(ai, aj, ak):

    """将欧拉角(roll, pitch, yaw)转换为四元数(x, y, z, w)"""

    ai /= 2.0

    aj /= 2.0

    ak /= 2.0

    ci = math.cos(ai)

    si = math.sin(ai)

    cj = math.cos(aj)

    sj = math.sin(aj)

    ck = math.cos(ak)

    sk = math.sin(ak)

    cc = ci * ck

    cs = ci * sk

    sc = si * ck

    ss = si * sk

    q = [0.0] * 4

    q[0] = cj * sc - sj * cs

    q[1] = cj * ss + sj * cc

    q[2] = cj * cs - sj * sc

    q[3] = cj * cc + sj * ss

    return q

class FramePublisher(Node):

    """动态坐标系发布器"""

    def __init__(self):

        # 修复了 super() 的调用语法

        super().__init__('frame_tf2_publisher')

       

        # 初始化动态 TF 广播器

        self.tf_broadcaster = TransformBroadcaster(self)

       

        # 角度计数器,用于模拟随时间变化的角度

        self.num = 0

       

        # 创建定时器,每 0.03 秒(约 33Hz)调用一次 pub_transforms 函数

        self.create_timer(0.03, self.pub_transforms)

    def pub_transforms(self):

        """创建并发送动态坐标变换"""

        t = TransformStamped()

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

        t.header.frame_id = 'frame_A'

        t.child_frame_id = 'frame_B'

       

        # 将角度计数器转换为弧度

        theta = self.num / 180.0 * math.pi

       

        # 1. 计算平移:让 frame_B 绕 frame_A 做半径为 2 米的圆周运动

        t.transform.translation.x = math.cos(theta) * 2.0

        t.transform.translation.y = math.sin(theta) * 2.0

        t.transform.translation.z = 0.0

       

        # 2. 计算旋转:让 frame_B 的 X 轴始终指向运动方向(切线方向)

        # 在圆周运动中,切线方向的角度比半径方向的角度大 90 度 (math.pi/2)

        quat = quaternion_from_euler(0.0, 0.0, theta + math.pi / 2.0)

        t.transform.rotation.x = quat[0]

        t.transform.rotation.y = quat[1]

        t.transform.rotation.z = quat[2]

        t.transform.rotation.w = quat[3]

       

        # 发送动态变换

        self.tf_broadcaster.sendTransform(t)

       

        # 更新角度计数器,每次增加 1 度

        self.num += 1

        # 达到 360 度后重置为 0,防止数值无限增大

        if self.num >= 360:

            self.num = 0

def main(args=None):

    rclpy.init(args=args)

    node = FramePublisher()

    try:

        rclpy.spin(node)

    except KeyboardInterrupt:

        pass

    finally:

        node.destroy_node()

        rclpy.shutdown()

if __name__ == '__main__':

    main()

这段代码实现了一个动态坐标系发布节点(Dynamic Frame Publisher)。它的核心功能是:让坐标系 B 以 2 米为半径,围绕坐标系 A 做匀速的圆周运动,并且始终保持其 X 轴指向运动的前进方向(切线方向)

核心代码逻辑解析:

  1. 动态更新机制:使用了 self.create_timer(0.03, self.pub_transforms),这意味着程序每隔 0.03 秒就会重新计算并发布一次坐标系 B 的位置。这就是动态 TF 与静态 TF 最大的区别。
  2. 圆周运动轨迹:通过三角函数 x = cos(θ) * 2 和 y = sin(θ) * 2,精确计算出了 B 在半径为 2 米的圆上的实时坐标。
  3. 朝向跟随(运动学处理):代码中 theta + math.pi/2 是一个非常巧妙的处理。在圆周运动中,物体的前进方向(切线)永远比它所在的半径方向超前 90 度。加上这个偏移量,可以让 B 坐标系在绕圈时,X 轴始终指向前方,就像汽车转弯一样自然。

6.3  添加环境变量,编译,加载,运行,验证

在vs code的setup.py文件夹,里面添加

    entry_points={

        'console_scripts': [

            'static_frame_pub = tf_test.static_frame_pub:main',

            'framepub = tf_test.framepub:main',            

        ],

    },

编译,在ros2_ws  目录下编译,

 #//编译

root@LHAYR:~/ros2_ws# colcon build --packages-select tf_test

Starting >>> tf_test

Finished <<< tf_test [0.58s]

Summary: 1 package finished [0.65s]

   #//  加载环境变量

root@LHAYR:~/ros2_ws# source install/setup.bash

root@LHAYR:~/ros2_ws#

root@LHAYR:~/ros2_ws# ros2 run tf_test framepub

打开rviz

终端2,进入/ros2_ws目录

root@LHAYR:~/ros2_ws# rviz2

[INFO] [1783343952.920860794] [rviz2]: Stereo is NOT SUPPORTED

[INFO] [1783343952.920917613] [rviz2]: OpenGl version: 4.5 (GLSL 4.5)

[INFO] [1783343952.954833328] [rviz2]: Stereo is NOT SUPPORTED

配置Rviz 终端 Fixed Frame 选中 frame_A;在点击add,添加tf显示窗口;

坐标系统 三维显示出来了,B坐标系围绕坐标系A在旋转;

第7章 坐标系的查询

    7.1 接收坐标系变化的代码

tf2会把系统里所有的坐标系连成一棵树,只要查这棵树,就能知道任意两个坐标系是怎么变换的。在 tf2_ros 这个包里,有一个叫 TransformListener 的类,就是专门负责‘监听’这些坐标系的。第一步,我们先在 tf_test 功能包里建一个 framesub.py 文件,用来写接收坐标系的节点代码。

在vs code 的 src/tf_test/tf_test目录下,创建一个文件(framesub.py));

修改framesub.py节点代码,如下

import rclpy

from rclpy.node import Node

from tf2_ros import Buffer, TransformListener

class FrameListener(Node):

    def __init__(self):

        # 修复了 super() 的调用语法

        super().__init__('tf2_frame_listener')

       

        # 声明并获取源坐标系和目标坐标系参数,方便外部动态修改

        self.target_frame = self.declare_parameter('target_frame', 'frame_B').get_parameter_value().string_value

        self.source_frame = self.declare_parameter('source_frame', 'frame_A').get_parameter_value().string_value

        

        # 创建 TF 缓存和监听器

        self.tf_buffer = Buffer()

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

       

        # 创建定时器,每 1.0 秒查询并打印一次坐标变换

        self.timer = self.create_timer(1.0, self.show_tf)

        self.get_logger().info(f'正在监听 [{self.source_frame}] -> [{self.target_frame}] 的坐标变换...')

    def show_tf(self):

        """定时查询并打印坐标变换"""

        try:

            # 查询两个坐标系之间的最新变换

            t = self.tf_buffer.lookup_transform(

                self.source_frame,

                self.target_frame,

                rclpy.time.Time()

            )

           

            # 提取平移和旋转数据

            trans = t.transform.translation

            rot = t.transform.rotation

           

            # 格式化打印结果

            self.get_logger().info(

                f'获取到坐标变换 [{self.source_frame}] -> [{self.target_frame}]:\n'

                f'  平移 (x, y, z): [{trans.x:.2f}, {trans.y:.2f}, {trans.z:.2f}]\n'

                f'  旋转 (x, y, z, w): [{rot.x:.2f}, {rot.y:.2f}, {rot.z:.2f}, {rot.w:.2f}]'

            )

           

        except Exception as e:

            # 捕获异常并打印警告(在 TF 还没完全建立时,这里会短暂报错,属于正常现象)

            self.get_logger().warn(f'获取坐标变换失败: {str(e)}')

def main(args=None):

    rclpy.init(args=args)

    node = FrameListener()

    try:

        rclpy.spin(node)

    except KeyboardInterrupt:

        pass

    finally:

        node.destroy_node()

        rclpy.shutdown()

if __name__ == '__main__':

    main()

代码解析

  • Node: ROS2 节点的基础类。
  • Buffer 和 TransformListener: tf2_ros 包中的核心组件。Buffer 负责在内存中缓存整棵“TF树”,而 TransformListener 负责在后台自动接收并更新这些缓存数据。
  • 动态参数化:使用 declare_parameter 声明了 target_frame 和 source_frame。这意味着你不需要修改代码,就可以在启动节点时通过命令行指定要查询的坐标系(例如:ros2 run pkg node --ros-args -p source_frame:=map -p target_frame:=odom)。
  • 监听器绑定:将 tf_buffer 和当前节点 self 传入 TransformListener,监听器就会自动在后台接收系统广播的所有 TF 消息并更新到 Buffer 中。
  • 定时器:设置了一个 1.0 秒的定时器,每秒触发一次 show_tf 函数。
  • lookup_transform:这是 TF2 最核心的 API。它根据传入的源坐标系和目标坐标系,在 TF 树中进行路径搜索,返回一个包含平移(x,y,z)和旋转(四元数 x,y,z,w)的 TransformStamped 消息。
  • 时间参数 rclpy.time.Time():传入空的时间对象表示查询最新可用的坐标变换。
  • 异常处理(非常重要):由于 ROS2 节点启动有先后顺序,监听节点启动时,发布节点可能还没开始发 TF 数据。此时查询会抛出 LookupException 等异常。使用 try...except 捕获异常并打印警告,可以防止节点因为找不到坐标系而崩溃。
  • 标准的 ROS2 节点生命周期管理。rclpy.spin(node) 会让节点进入阻塞状态,持续响应定时器回调和 TF 监听器的后台数据接收。
  • 使用 try...finally 确保在按下 Ctrl+C 退出时,节点资源能被正确销毁和释放。

7.2 添加环境变量,编译,加载,运行

在vs code的setup.py文件夹,里面添加

    entry_points={

        'console_scripts': [

            'static_frame_pub = tf_test.static_frame_pub:main',

            'framepub = tf_test.framepub:main',  

            'frame_sub = tf_test.framesub:main',   #//注意事项,这个名称是frame_sub 

        ],

    },

编译,在ros2_ws  目录下编译,

 #//编译

root@LHAYR:~/ros2_ws# colcon build --packages-select tf_test

Starting >>> tf_test

Finished <<< tf_test [0.60s]

Summary: 1 package finished [0.68s]

root@LHAYR:~/ros2_ws# source install/setup.bash

   #//  加载环境变量

root@LHAYR:~/ros2_ws# source install/setup.bash

运行 ros2 run tf_test frame_sub

root@LHAYR:~/ros2_ws# ros2 run tf_test frame_sub

[INFO] [1783346898.496246288] [tf2_frame_listener]: 正在监听 [frame_A] -> [frame_B] 的坐标变换...

[INFO] [1783346899.490170375] [tf2_frame_listener]: 获取到坐标变换 [frame_A] -> [frame_B]:

  平移 (x, y, z): [1.91, 0.58, 0.00]

  旋转 (x, y, z, w): [0.00, 0.00, 0.80, 0.59]

[INFO] [1783346900.490171243] [tf2_frame_listener]: 获取到坐标变换 [frame_A] -> [frame_B]:

  平移 (x, y, z): [1.29, 1.53, 0.00]

  旋转 (x, y, z, w): [0.00, 0.00, 0.94, 0.34]

[INFO] [1783346901.490044148] [tf2_frame_listener]: 获取到坐标变换 [frame_A] -> [frame_B]:

  平移 (x, y, z): [0.21, 1.99, 0.00]

  旋转 (x, y, z, w): [0.00, 0.00, 1.00, 0.05]

[INFO] [1783346902.490176833] [tf2_frame_listener]: 获取到坐标变换 [frame_A] -> [frame_B]:

  平移 (x, y, z): [-0.91, 1.78, 0.00]

  旋转 (x, y, z, w): [0.00, 0.00, 0.97, -0.23]

[INFO] [1783346903.490082208] [tf2_frame_listener]: 获取到坐标变换 [frame_A] -> [frame_B]:

  平移 (x, y, z): [-1.73, 1.00, 0.00]

  旋转 (x, y, z, w): [0.00, 0.00, 0.87, -0.50]

[INFO] [1783346904.490007240] [tf2_frame_listener]: 获取到坐标变换 [frame_A] -> [frame_B]:

  平移 (x, y, z): [-2.00, -0.14, 0.00]

  旋转 (x, y, z, w): [0.00, 0.00, -0.68, 0.73]

[INFO] [1783346905.490036439] [tf2_frame_listener]: 获取到坐标变换 [frame_A] -> [frame_B]:

  平移 (x, y, z): [-1.60, -1.20, 0.00]

  旋转 (x, y, z, w): [0.00, 0.00, -0.45, 0.89]

[INFO] [1783346906.490267941] [tf2_frame_listener]: 获取到坐标变换 [frame_A] -> [frame_B]:

  平移 (x, y, z): [-0.68, -1.88, 0.00]

  旋转 (x, y, z, w): [0.00, 0.00, -0.17, 0.98]

[INFO] [1783346907.490098519] [tf2_frame_listener]: 获取到坐标变换 [frame_A] -> [frame_B]:

  平移 (x, y, z): [0.48, -1.94, 0.00]

  旋转 (x, y, z, w): [0.00, 0.00, 0.12, 0.99]

7.3运行结果

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐