前言
这节还是围绕tf2来进行,只不过针对调试相关,把之前有过一面之缘的问题再次拿出来重点说明一下,此过程中我们会碰到之前几期中认识但还不怎么熟络的朋友比如tf2_echo、tf2_monitor、view_frames。
动动手
我们会利用一个有不少问题的例子来开展演练,通过分析问题解决问题的思路熟悉下tf2调试的大体流程。
修改例程
我们继续利用learning_tf2_cpp包,拷贝一份turtle_tf2_listener.cpp为turtle_tf2_listener_debug.cpp,将原句:
std::string toFrameRel = "turtle2";
改为
std::string toFrameRel = "turtle3";
将原句
try {
t = tf_buffer_->lookupTransform(
toFrameRel, fromFrameRel,
tf2::TimePointZero);
} catch (const tf2::TransformException & ex) {
改为
try {
t = tf_buffer_->lookupTransform(
toFrameRel, fromFrameRel,
this->now());
} catch (const tf2::TransformException & ex) {
我们再来编写一个启动文件start_tf2_debug_demo_launch.py到launch文件夹中:
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
DeclareLaunchArgument(
'target_frame', default_value='turtle1',
description='Target frame name.'
),
Node(
package='turtlesim',
executable='turtlesim_node',
name='sim',
output='screen'
),
Node(
package='learning_tf2_cpp',
executable='turtle_tf2_broadcaster',
name='broadcaster1',
parameters=[
{'turtlename': 'turtle1'}
]
),
Node(
package='learning_tf2_cpp',
executable='turtle_tf2_broadcaster',
name='broadcaster2',
parameters=[
{'turtlename': 'turtle2'}
]
),
Node(
package='learning_tf2_cpp',
executable='turtle_tf2_listener_debug',
name='listener_debug',
parameters=[
{'target_frame': LaunchConfiguration('target_frame')}
]
),
])
将turtle_tf2_listener_debug可执行文件相关内容添加到CMakeLists.txt中:
add_executable(turtle_tf2_listener_debug src/turtle_tf2_listener_debug.cpp)
ament_target_dependencies(
turtle_tf2_listener_debug
geometry_msgs
rclcpp
tf2
tf2_ros
turtlesim
)
install(TARGETS
turtle_tf2_listener_debug
DESTINATION lib/${PROJECT_NAME})
再构建包,我们运行下这个debug例子看看。
$ros2 launch learning_tf2_cpp start_tf2_debug_demo_launch.py
这会启动一只小海龟turtle1,同时在窗口的左下方,也会出现第二只小海龟turtle3,我们再在另外一个终端启动turtle_teleop_key控制turtle1的游动。
$ros2 run turtlesim turtle_teleop_key
正常情况下,turtle3是会随着turtle1的步伐节奏游动的,但是实际情况是没有,并且报出如下的信息(Could not transform turtle3 to turtle1,目标帧turtle3不存在)。
此处有个很容易搞懵逼的概念需要解释清楚,不知道大家注意到没有,我们明明是指定从源帧turtle1到目标帧turtle3的转换,怎么提示成了turle3->turtle1,turtle3到turtle1的转换呢?其实输出的提示并没有错,提示的原意是:turtle3转换到turtle1帧的视角(坐标系)。
lookupTransform(target_frame, source_frame, ...),target_frame = turtle3,source_frame = turtle1,target_frame意为我们需要转换的坐标系(坐标框架或帧),source_frame意为数据来源的坐标系,也就是我们最终要落地的坐标系,target_frame统一到source_frame。
首先,turtle1是第一只小海龟的坐标系,它游动时产生了位姿数据(turtle1坐标系中),其次,我们希望第二只小海龟(turtle3坐标系中)能追随第一只小海龟的运动,也就是如何将turtle3的数据转换体现到turtle1坐标系中。跟随的前提是这俩海龟得统一到同一个坐标系下才有意义,既然让turtle3跟随turtle1,那么就得turtle3转换到turtle1中,所以我们就得需要获取turtle3到turtle1的转换,才能让turtle3跑到turtle1的坐标系中一起遨游哇。
再举个例子。
假设你有一个移动机器人,它有一个激光雷达(LiDAR)传感器,该传感器安装在机器人的顶部,并且有一个固定的偏移量。激光雷达的数据是在它自己的坐标系(我们称之为
lidar_frame
)中获取的,但你可能想要将这些数据转换到机器人的基座坐标系(我们称之为base_link
)中,以便进行导航或其他处理。在这个例子中,
base_link
就是source_frame
,而lidar_frame
就是target_frame
。为了查看从lidar_frame
到base_link
的变换(即如何将LiDAR数据从LiDAR的坐标系转换到机器人的基座坐标系),可以使用以下命令:ros2 run tf2_ros tf2_echo base_link lidar_frame这个命令会不断地输出从
lidar_frame
到base_link
的变换,包括平移(translation)和旋转(rotation)。这个变换告诉你如何将LiDAR数据从lidar_frame
的坐标系转换到base_link
的坐标系。注意,虽然我们说“从
lidar_frame
到base_link
的变换”,但实际上这个变换是描述了如何将base_link
中的数据(或坐标)转换到lidar_frame
的视角,但这并不意味着你不能使用它来转换lidar_frame
中的数据到base_link
。在tf
和tf2
中,变换总是从一个父坐标系(通常是固定的或全局的坐标系)到一个子坐标系(通常是移动的或局部的坐标系),但你可以使用这个变换的逆来执行相反的操作。
确认我们对tf2的请求
先看看我们的请求是否合适,打开turtle_tf2_listener_debug.cpp源文件,找到如下几行内容:
std::string to_frame_rel = "turtle3";
try {
t = tf_buffer_->lookupTransform(
toFrameRel, fromFrameRel,
this->now());
} catch (const tf2::TransformException & ex) {
从上面可以看出,我们提供给了lookupTransform函数3个参数,其中源坐标系为turtle1,目标坐标系为turtle3,特定时间为现在,意思是向tf2请求获取从turtle3到turtle1的当前的位姿转换数据(解释见上面的块引用),我们不就是要turtle3实时追随turtle1的步伐吗。看起来都挺正常,那问题出在哪呢?
抓帧检查
同网络编程调试的抓包分析一样,我们也需要对帧进行捕获分析。
首先利用tf2_echo工具看看turtle3与turtle1之间的转换情况。
$ros2 run tf2_ros tf2_echo turtle3 turtle1
提示turtle3不存在(在我们之前的一篇博文中首次碰到这个问题,当时不清楚原因),明明代码里面都设置好了啊,怎么会不存在turtle3,搞笑呢。我们利用view_frames工具来瞅瞅情况。
$ros2 run tf2_tools view_frames
在当前路径下找到刚生成的frames_2024-04-29_21.29.12.pdf(一般文件名带日期),打开,情况如下:
从上面可以很清楚的看到,我们的ROS中确实没有turtle3(可能你会奇怪,我们不是明明在代码里指定了turtle3吗,怎么就没有了呢,我们还是需要再看一遍learning_tf2_listener_debug.cpp的内容,里面确实没有turtle3的孵化生成),只有通过服务方式孵化的turtle2。而且这样才能解释的通上面的target_frame(turtle2)是有自己的数据进行转换的,turtle3可没有任何数据啊,如何转换。
我们需要再次修改下代码,将turtle3改为turtle2。重新构建后(记得source环境)再来启动看看。
$ros2 launch turtle_tf2 start_debug_demo.launch.py
提示我们熟悉的时间问题了,Could not transform turtle2 to turtle1:Lookup would require extrapolation into the future.
检查时间戳
现在我们解决了帧名字不存在的问题,是时候看看时间戳了。我们现在尝试获取当前turtle1与turtle2之间的转换数据,为了捕获实时的数据,我们需要使用tf2_monitor工具来监视对应的帧情况。
$ros2 run tf2_ros tf2_monitor turtle2 turtle1
(返回信息开头又提示target_frame turtle2不存在,说实话我也有点茫然了,再次通过view_frames工具查看turtle2是存在的,有了解的同学可以评论区告诉一下啊)
我们先来看看上面的其他信息。这里的关键部分是turtle2到turtle1的变换链的延迟(上篇有解释过)。输出显示平均延迟大约为3毫秒。这意味着tf2只能在过去3毫秒后才能在这两个海龟之间进行变换。所以,如果我们要求tf2提供3毫秒前的海龟之间的变换,而不是现在的变换,tf2有时候能够给我们一个答案。
我们来修改下时间来获取100ms之前(时间足够长了,实际情况不需要这么长)的转换,如下:
try {
t = tf_buffer_->lookupTransform(
toFrameRel, fromFrameRel,
this->now() - rclcpp::Duration::from_seconds(0.1));
} catch (const tf2::TransformException & ex) {
再次恢复正常。但我们修改时间的方法不是太推荐,往往我们会使用下面的写法:
try {
t = tf_buffer_->lookupTransform(
toFrameRel, fromFrameRel,
tf2::TimePointZero);
} catch (const tf2::TransformException & ex) {
或
try {
t = tf_buffer_->lookupTransform(
toFrameRel, fromFrameRel,
tf2::TimePoint());
} catch (const tf2::TransformException & ex) {
或者下面这种带超时参数写法(我们在使用时间参数里用过的):
try {
t = tf_buffer_->lookupTransform(
toFrameRel, fromFrameRel,
this->now(),
rclcpp::Duration::from_seconds(0.05));
} catch (const tf2::TransformException & ex) {
以上就是今天的主要内容,重点是对于source_frame、target_frame转换的理解以及如何通过tf2_echo、view_frames以及tf2_monitor工具来帮助我们分析问题所在。
本篇完。