ROS 2边学边练(39)-- 调试tf2

前言

        这节还是围绕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_framebase_link的变换(即如何将LiDAR数据从LiDAR的坐标系转换到机器人的基座坐标系),可以使用以下命令:ros2 run tf2_ros tf2_echo base_link lidar_frame   

        这个命令会不断地输出从lidar_framebase_link的变换,包括平移(translation)和旋转(rotation)。这个变换告诉你如何将LiDAR数据从lidar_frame的坐标系转换到base_link的坐标系。

        注意,虽然我们说“从lidar_framebase_link的变换”,但实际上这个变换是描述了如何将base_link中的数据(或坐标)转换到lidar_frame的视角,但这并不意味着你不能使用它来转换lidar_frame中的数据到base_link。在tftf2中,变换总是从一个父坐标系(通常是固定的或全局的坐标系)到一个子坐标系(通常是移动的或局部的坐标系),但你可以使用这个变换的逆来执行相反的操作。

确认我们对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工具来帮助我们分析问题所在。

本篇完。

相关推荐

最近更新

  1. TCP协议是安全的吗?

    2024-04-30 00:30:04       16 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-04-30 00:30:04       16 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-04-30 00:30:04       15 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-04-30 00:30:04       18 阅读

热门阅读

  1. 第三部分 Vue讲解(22-25)(代码版)

    2024-04-30 00:30:04       11 阅读
  2. 启动前端项目

    2024-04-30 00:30:04       10 阅读
  3. 深度探索DreamFusion:AI和3D建模的革命

    2024-04-30 00:30:04       10 阅读
  4. SpringCloud Ribbon介绍

    2024-04-30 00:30:04       9 阅读
  5. Docker之安装部署

    2024-04-30 00:30:04       13 阅读
  6. 【置顶】前端工具库

    2024-04-30 00:30:04       8 阅读
  7. 【第27章】spring-spel进阶版

    2024-04-30 00:30:04       10 阅读
  8. 【Linux】SFTP定时下载文件

    2024-04-30 00:30:04       12 阅读
  9. 晨华一网统管综合信息平台

    2024-04-30 00:30:04       8 阅读
  10. 力扣练习4.29

    2024-04-30 00:30:04       10 阅读
  11. Git的基本概念和使用方式

    2024-04-30 00:30:04       11 阅读