Skip to content

TF2 广播器(Python)

目标: 学习如何将机器人的状态广播到 tf2。

教程级别: 中级

预计时间: 15 分钟

在接下来的两个教程中,我们将编写代码来重现 tf2 简介教程中的演示。后续教程将在此演示的基础上,介绍更高级的 tf2 功能,包括变换查找中的超时机制和时间旅行。

本教程假设你已经具备 ROS 2 基础知识,并且完成了 tf2 简介教程和 tf2 静态广播器教程(Python)。我们将复用上一个教程中的 learning_tf2_py 功能包(package)。

在之前的教程中,你已经学习了如何创建工作空间(workspace)和创建功能包(package)。

首先创建源文件。进入上一个教程中创建的 learning_tf2_py 功能包。在 src/learning_tf2_py/learning_tf2_py 目录中,输入以下命令下载示例广播器代码:

Linux/macOS:

Terminal window
wget https://raw.githubusercontent.com/ros/geometry_tutorials/{DISTRO}/turtle_tf2_py/turtle_tf2_py/turtle_tf2_broadcaster.py

Windows:

在 Windows 命令行提示符中:

Terminal window
curl -sk https://raw.githubusercontent.com/ros/geometry_tutorials/{DISTRO}/turtle_tf2_py/turtle_tf2_py/turtle_tf2_broadcaster.py -o turtle_tf2_broadcaster.py

或者在 PowerShell 中:

Terminal window
curl https://raw.githubusercontent.com/ros/geometry_tutorials/{DISTRO}/turtle_tf2_py/turtle_tf2_py/turtle_tf2_broadcaster.py -o turtle_tf2_broadcaster.py

用你喜欢的文本编辑器打开 turtle_tf2_broadcaster.py 文件。

import math
from geometry_msgs.msg import TransformStamped
import numpy as np
import rclpy
from rclpy.executors import ExternalShutdownException
from rclpy.node import Node
from tf2_ros import TransformBroadcaster
from turtlesim_msgs.msg import Pose
def quaternion_from_euler(ai, aj, ak):
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 = np.empty((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().__init__('turtle_tf2_frame_publisher')
# Declare and acquire `turtlename` parameter
self.turtlename = self.declare_parameter(
'turtlename', 'turtle').get_parameter_value().string_value
# Initialize the transform broadcaster
self.tf_broadcaster = TransformBroadcaster(self)
# Subscribe to a turtle{1}{2}/pose topic and call handle_turtle_pose
# callback function on each message
self.subscription = self.create_subscription(
Pose,
f'/{self.turtlename}/pose',
self.handle_turtle_pose,
1)
self.subscription # prevent unused variable warning
def handle_turtle_pose(self, msg):
t = TransformStamped()
# Read message content and assign it to
# corresponding tf variables
t.header.stamp = self.get_clock().now().to_msg()
t.header.frame_id = 'world'
t.child_frame_id = self.turtlename
# Turtle only exists in 2D, thus we get x and y translation
# coordinates from the message and set the z coordinate to 0
t.transform.translation.x = msg.x
t.transform.translation.y = msg.y
t.transform.translation.z = 0.0
# For the same reason, turtle can only rotate around one axis
# and this why we set rotation in x and y to 0 and obtain
# rotation in z axis from the message
q = quaternion_from_euler(0, 0, msg.theta)
t.transform.rotation.x = q[0]
t.transform.rotation.y = q[1]
t.transform.rotation.z = q[2]
t.transform.rotation.w = q[3]
# Send the transformation
self.tf_broadcaster.sendTransform(t)
def main():
try:
with rclpy.init():
node = FramePublisher()
rclpy.spin(node)
except (KeyboardInterrupt, ExternalShutdownException):
pass

下面来看看与将乌龟位姿发布到 tf2 相关的代码。

首先,声明并获取参数 turtlename,用于指定乌龟名称,例如 turtle1 或 turtle2。

self.turtlename = self.declare_parameter(
'turtlename', 'turtle').get_parameter_value().string_value

然后,节点订阅话题 {self.turtlename}/pose,每收到一条消息就调用 handle_turtle_pose 回调函数。

self .subscription = self.create_subscription(
Pose,
f'/{self.turtlename}/pose',
self.handle_turtle_pose,
1)

接下来创建一个 TransformStamped 对象,并设置相应的元数据。

  1. 首先为要发布的变换设置时间戳,通过调用 self.get_clock().now() 获取节点使用的当前时间。

  2. 然后设置所创建变换的父坐标系名称,这里是 world。

  3. 最后设置子坐标系名称,即乌龟本身的名称。

乌龟位姿消息的回调函数负责广播该乌龟的平移和旋转,将其作为从 world 坐标系到 turtleX 坐标系的变换发布出去。

t = TransformStamped()
# Read message content and assign it to
# corresponding tf variables
t.header.stamp = self.get_clock().now().to_msg()
t.header.frame_id = 'world'
t.child_frame_id = self.turtlename

这里将乌龟的位姿信息填入 3D 变换结构中。

# Turtle only exists in 2D, thus we get x and y translation
# coordinates from the message and set the z coordinate to 0
t.transform.translation.x = msg.x
t.transform.translation.y = msg.y
t.transform.translation.z = 0.0
# For the same reason, turtle can only rotate around one axis
# and this why we set rotation in x and y to 0 and obtain
# rotation in z axis from the message
q = quaternion_from_euler(0, 0, msg.theta)
t.transform.rotation.x = q[0]
t.transform.rotation.y = q[1]
t.transform.rotation.z = q[2]
t.transform.rotation.w = q[3]

最后,将构造好的变换传递给 TransformBroadcaster 的 sendTransform 方法,由它负责广播。

# Send the transformation
self.tf_broadcaster.sendTransform(t)

要让 ros2 run 命令能运行你的节点,必须在 setup.py(位于 src/learning_tf2_py 目录中)中添加入口点。

在 'console_scripts': 的方括号之间添加以下行:

'turtle_tf2_broadcaster = learning_tf2_py.turtle_tf2_broadcaster:main',

接下来为这个演示创建一个 launch 文件。在 src/learning_tf2_py 目录下创建一个 launch 文件夹。用文本编辑器在其中创建一个名为 turtle_tf2_demo_launch 的新文件,扩展名为 .py、.xml 或 .yaml,添加以下内容:

XML:

<?xml version="1.0" encoding="UTF-8"?>
<launch>
<node pkg="turtlesim" exec="turtlesim_node" name="sim" />
<node pkg="learning_tf2_py" exec="turtle_tf2_broadcaster" name="broadcaster1">
<param name="turtlename" value="turtle1" />
</node>
</launch>

YAML:

%YAML 1.2
---
launch:
- node:
pkg: "turtlesim"
exec: "turtlesim_node"
name: "sim"
- node:
pkg: "learning_tf2_py"
exec: "turtle_tf2_broadcaster"
name: "broadcaster1"
param:
- name: "turtlename"
value: "turtle1"

Python:

from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
Node(
package='turtlesim',
executable='turtlesim_node',
name='sim'
),
Node(
package='learning_tf2_py',
executable='turtle_tf2_broadcaster',
name='broadcaster1',
parameters=[
{'turtlename': 'turtle1'}
]
),
])

下面来分析 launch 文件的结构。每种格式都有各自的写法:

XML:

XML launch 文件以 XML 声明和根元素 <launch> 开头。

<?xml version="1.0" encoding="UTF-8"?>
<launch>

YAML:

YAML launch 文件以 YAML 版本声明和 launch: 键开头。

%YAML 1.2
---
launch:

Python:

在 Python launch 文件中,首先从 launch 和 launch_ros 包导入所需模块。launch 是一个通用的启动框架(并非 ROS 2 专用),而 launch_ros 包含了 ROS 2 特有的功能,比如这里导入的节点。

from launch import LaunchDescription
from launch_ros.actions import Node

接下来启动 turtlesim 仿真节点,并使用 turtle_tf2_broadcaster 节点将 turtle1 的状态广播到 tf2。

XML:

<node pkg="turtlesim" exec="turtlesim_node" name="sim" />
<node pkg="learning_tf2_py" exec="turtle_tf2_broadcaster" name="broadcaster1">
<param name="turtlename" value="turtle1" />
</node>

YAML:

- node:
pkg: "turtlesim"
exec: "turtlesim_node"
name: "sim"
- node:
pkg: "learning_tf2_py"
exec: "turtle_tf2_broadcaster"
name: "broadcaster1"
param:
- name: "turtlename"
value: "turtle1"

Python:

return LaunchDescription([
Node(
package='turtlesim',
executable='turtlesim_node',
name='sim'
),
Node(
package='learning_tf2_py',
executable='turtle_tf2_broadcaster',
name='broadcaster1',
parameters=[
{'turtlename': 'turtle1'}
]
),
])

返回上一级到 learning_tf2_py 目录,该目录下存放着 setup.py、setup.cfg 和 package.xml 文件。

用文本编辑器打开 package.xml,添加与 launch 文件导入语句对应的以下依赖:

<exec_depend>launch</exec_depend>
<exec_depend>launch_ros</exec_depend>

这样就添加了 launch 和 launch_ros 的执行依赖。

请确保保存文件。

重新打开 setup.py,添加以下内容以便安装 launch/ 文件夹中的 launch 文件。修改后的 data_files 字段如下:

data_files=[
...
(os.path.join('share', package_name, 'launch'), glob('launch/*')),
],

同时在文件顶部添加相应的导入:

import os
from glob import glob

关于创建 launch 文件的更多详情,请参阅相关教程。

在工作空间根目录运行 rosdep 检查缺失的依赖。

Linux:

Terminal window
rosdep install -i --from-path src --rosdistro {DISTRO} -y

macOS:

rosdep 仅在 Linux 上可用,你需要自行安装 geometry_msgs 和 turtlesim 依赖。

Windows:

rosdep 仅在 Linux 上可用,你需要自行安装 geometry_msgs 和 turtlesim 依赖。

仍在工作空间根目录下构建功能包:

Linux/macOS:

Terminal window
colcon build --packages-select learning_tf2_py

Windows:

Terminal window
colcon build --merge-install --packages-select learning_tf2_py

打开一个新终端,导航到工作空间根目录,并 source 环境配置文件:

Linux/macOS:

Terminal window
. install/setup.bash

Windows:

在 Windows 命令行提示符中:

Terminal window
call install\setup.bat

或者在 PowerShell 中:

Terminal window
.\install\setup.ps1

现在运行 launch 文件,它会启动 turtlesim 仿真节点和 turtle_tf2_broadcaster 节点:

XML:

Terminal window
ros2 launch learning_tf2_py turtle_tf2_demo_launch.xml

YAML:

Terminal window
ros2 launch learning_tf2_py turtle_tf2_demo_launch.yaml

Python:

Terminal window
ros2 launch learning_tf2_py turtle_tf2_demo_launch.py

在第二个终端窗口中输入以下命令:

Terminal window
ros2 run turtlesim turtle_teleop_key

你应该能看到 turtlesim 仿真已启动,其中有一只可以用键盘控制的乌龟。

turtlesim 广播

现在,用 tf2_echo 工具检查乌龟位姿是否确实被广播到了 tf2:

Terminal window
ros2 run tf2_ros tf2_echo world turtle1

这会显示第一只乌龟的位姿。用方向键驱动乌龟移动(确保当前活动窗口是 turtle_teleop_key 所在的终端,而不是仿真器窗口)。控制台输出类似如下:

Terminal window
At time 1714913843.708748879
- Translation: [4.541, 3.889, 0.000]
- Rotation: in Quaternion [0.000, 0.000, 0.999, -0.035]
- Rotation: in RPY (radian) [0.000, -0.000, -3.072]
- Rotation: in RPY (degree) [0.000, -0.000, -176.013]
- Matrix:
-0.998 0.070 0.000 4.541
-0.070 -0.998 0.000 3.889
0.000 0.000 1.000 0.000
0.000 0.000 0.000 1.000

如果你对 world 和 turtle2 之间的变换运行 tf2_echo,此时看不到任何输出,因为第二只乌龟尚不存在。不过,当我们在下一个教程中添加第二只乌龟后,turtle2 的位姿就会被广播到 tf2。

在本教程中,你学习了如何将机器人的位姿(即乌龟的位置和朝向)广播到 tf2,以及如何使用 tf2_echo 工具进行验证。要实际使用广播到 tf2 的变换,请继续学习下一篇关于创建 tf2 监听器的教程。