8.ROS的TF坐标变换(二):动态坐标变换、多坐标变换代码讲解(以LIO-SAM为例)

news2024/11/16 8:53:41

目录

1 ROS的动态坐标变换及代码解释

1.1 什么是ROS的动态坐标变换

1.2  CMakeLists.txt、package.xml基础配置

1.3 发布方代码实现

1.4 接收方代码实现  

2 ROS的多坐标变换及代码解释 

2.1 什么是ROS的多坐标变换

2.2 发布方代码实现

2.3 接收方代码实现       

3 LIOSAM中所有的TF坐标变换代码解析

3.1 后端每预测出一帧激光里程计发送TF信息

3.2 IMU前端获取lidar坐标系(/velodyne)到世界坐标系原点(/base_link)的坐标变化

3.3  IMU前端关联/map、/odom坐标系

3.4 IMU前端发布/odom /base_link的TF信息(里程计、 /base_link是原点相当于位姿)


1 ROS的动态坐标变换及代码解释

1.1 什么是ROS的动态坐标变换

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

        我们在这节中,启动 turtlesim_node,该节点中窗体有一个世界坐标系(左下角为坐标系原点),乌龟是另一个坐标系,键盘控制乌龟运动,将两个坐标系的相对位置动态发布。

        

实现分析:

  1. 乌龟本身不但可以看作坐标系,也是世界坐标系中的一个坐标点

  2. 订阅 turtle1/pose,可以获取乌龟在世界坐标系的 x坐标、y坐标、偏移量以及线速度和角速度

  3. 将 pose 信息转换成 坐标系相对信息并发布

实现流程:

  1. 新建功能包,添加依赖(CLION自己创建相关文件)

  2. 创建坐标相对关系发布方(同时需要订阅乌龟位姿信息)

  3. 创建坐标相对关系订阅方

  4. 执行

1.2  CMakeLists.txt、package.xml基础配置

        接上节,我们延续上节的CMake配置,不过我们要进行乌龟节点的演示,因此要加入小乌龟的功能包。

cmake_minimum_required(VERSION 2.8.3)
project(test)

######################
### Cmake flags
######################
set(CMAKE_BUILD_TYPE "Release")
set(CMAKE_CXX_FLAGS "-std=c++11")
set(CMAKE_CXX_FLAGS_RELEASE "-O3 -Wall -g -pthread")

find_package(catkin REQUIRED COMPONENTS
    roscpp
    rospy
    roslib
    # msg
    std_msgs
    sensor_msgs
    tf2
    tf2_ros
    tf2_geometry_msgs
    geometry_msgs
    turtlesim
)

catkin_package()

include_directories(${catkin_INCLUDE_DIRS})

add_executable(testing main.cpp)
add_executable(static_pub static_pub.cpp)
add_executable(static_sub static_sub.cpp)


target_link_libraries(testing ${catkin_LIBRARIES})
target_link_libraries(static_pub ${catkin_LIBRARIES})
target_link_libraries(static_sub ${catkin_LIBRARIES})

        package.xml配置也是一样,添加小乌龟节点:

<?xml version="1.0"?>
<package>
  <name>test</name>
  <version>0.0.0</version>
  <description>A test</description>
  <maintainer email="haha@nefu.com">haha</maintainer>
  <author>HITLHW</author>
  <license>BSD-3</license>


  <buildtool_depend>catkin</buildtool_depend>

  <build_depend>roscpp</build_depend>
  <run_depend>roscpp</run_depend>
  <build_depend>rospy</build_depend>
  <run_depend>rospy</run_depend>

  <build_depend>pcl_conversions</build_depend>
  <run_depend>pcl_conversions</run_depend>

  <build_depend>std_msgs</build_depend>
  <run_depend>std_msgs</run_depend>
  <build_depend>sensor_msgs</build_depend>
  <run_depend>sensor_msgs</run_depend>
  <build_depend>geometry_msgs</build_depend>
  <run_depend>geometry_msgs</run_depend>

  <build_depend>tf2</build_depend>
  <run_depend>tf2</run_depend>
  <build_depend>tf2_ros</build_depend>
  <run_depend>tf2_ros</run_depend>
  <build_depend>tf2_geometry_msgs</build_depend>
  <run_depend>tf2_geometry_msgs</run_depend>

  <build_depend>turtlesim</build_depend>
  <run_depend>turtlesim</run_depend>
</package>

1.3 发布方代码实现

        添加cpp文件,dyna_pub.cpp:

        修改CMakeLists.txt

add_executable(testing main.cpp)
add_executable(static_pub static_pub.cpp)
add_executable(static_sub static_sub.cpp)
add_executable(dyna_pub dyna_pub.cpp)


target_link_libraries(testing ${catkin_LIBRARIES})
target_link_libraries(static_pub ${catkin_LIBRARIES})
target_link_libraries(static_sub ${catkin_LIBRARIES})
target_link_libraries(dyna_pub ${catkin_LIBRARIES})
动态的坐标系相对姿态发布(一个坐标系相对于另一个坐标系的相对姿态是不断变动的)

    需求: 启动 turtlesim_node,该节点中窗体有一个世界坐标系(左下角为坐标系原点),乌龟是另一个坐标系,键盘
    控制乌龟运动,将两个坐标系的相对位置动态发布

    实现分析:
        1.乌龟本身不但可以看作坐标系,也是世界坐标系中的一个坐标点
        2.订阅 turtle1/pose,可以获取乌龟在世界坐标系的 x坐标、y坐标、偏移量以及线速度和角速度
        3.将 pose 信息转换成 坐标系相对信息并发布

    实现流程:
        1.包含头文件
        2.初始化 ROS 节点
        3.创建 ROS 句柄
        4.创建订阅对象
        5.回调函数处理订阅到的数据(实现TF广播)
            5-1.创建 TF 广播器
            5-2.创建 广播的数据(通过 pose 设置)
            5-3.广播器发布数据
        6.spin

        我们先来看看他们的信息格式吧!先打开乌龟的节点。

        rosrun turtlesim turtlesim_node

        查看话题:rostopic list

        查看乌龟位姿的信息格式:rosmsg info /turtle1/pose

liuhongwei@liuhongwei-Legion-Y9000P-IRX8H:~$ rostopic info /turtle1/pose
Type: turtlesim/Pose

Publishers: 
 * /turtlesim (http://liuhongwei-Legion-Y9000P-IRX8H:37701/)

Subscribers: None

        查看其位姿的信息格式:rosmsg info turtlesim/Pose

        OK,我们准备工作完成,准备写代码!

        我们先要明白订阅方要做什么,肯定是接收乌龟的位姿信息,因此需要一个回调函数。这个回调函数里面是处理乌龟的位姿将其转化为世界坐标系

        我们回顾上次课程,我们做静态坐标变换时引用的头文件是

        我们在做动态坐标变换时,需要引入头文件

        创建广播数据:

    geometry_msgs::TransformStamped tfs;
    //  |----头设置
    tfs.header.frame_id = "world";
    tfs.header.stamp = ros::Time::now();

    //  |----坐标系 ID
    tfs.child_frame_id = "turtle1";

    //  |----坐标系相对信息设置
    tfs.transform.translation.x = pose->x;
    tfs.transform.translation.y = pose->y;
    tfs.transform.translation.z = 0.0; // 二维实现,pose 中没有z,z 是 0
    //  |--------- 四元数设置
    tf2::Quaternion qtn;
    qtn.setRPY(0,0,pose->theta);
    tfs.transform.rotation.x = qtn.getX();
    tfs.transform.rotation.y = qtn.getY();
    tfs.transform.rotation.z = qtn.getZ();
    tfs.transform.rotation.w = qtn.getW();

        也就是说,乌龟的位姿是相当于世界坐标系的。

        发布广播数据:完整代码如下:

//
// Created by liuhongwei on 23-12-2.
//
// 1.包含头文件
#include "ros/ros.h"
#include "turtlesim/Pose.h"
#include "tf2_ros/transform_broadcaster.h"
#include "geometry_msgs/TransformStamped.h"
#include "tf2/LinearMath/Quaternion.h"

void doPose(const turtlesim::Pose::ConstPtr& pose){
    //  5-1.创建 TF 广播器
    static tf2_ros::TransformBroadcaster broadcaster;
    //  5-2.创建 广播的数据(通过 pose 设置)
    geometry_msgs::TransformStamped tfs;
    //  |----头设置
    tfs.header.frame_id = "world";
    tfs.header.stamp = ros::Time::now();

    //  |----坐标系 ID
    tfs.child_frame_id = "turtle1";

    //  |----坐标系相对信息设置
    tfs.transform.translation.x = pose->x;
    tfs.transform.translation.y = pose->y;
    tfs.transform.translation.z = 0.0; // 二维实现,pose 中没有z,z 是 0
    //  |--------- 四元数设置
    tf2::Quaternion qtn;
    qtn.setRPY(0,0,pose->theta);
    tfs.transform.rotation.x = qtn.getX();
    tfs.transform.rotation.y = qtn.getY();
    tfs.transform.rotation.z = qtn.getZ();
    tfs.transform.rotation.w = qtn.getW();


    //  5-3.广播器发布数据
    broadcaster.sendTransform(tfs);
}

int main(int argc, char *argv[])
{
    setlocale(LC_ALL,"");
    // 2.初始化 ROS 节点
    ros::init(argc,argv,"dynamic_tf_pub");
    // 3.创建 ROS 句柄
    ros::NodeHandle nh;
    // 4.创建订阅对象
    ros::Subscriber sub = nh.subscribe<turtlesim::Pose>("/turtle1/pose",1000,doPose);
    //     5.回调函数处理订阅到的数据(实现TF广播)
    //
    // 6.spin
    ros::spin();
    return 0;
}

        我们执行节点:

        看看现在所有的话题:

        查看话题里面包含什么信息吧! rostopic echo /tf

        可以看到它是一成不变的,我们控制小乌龟运动:

        现在可以看到tf里面是乌龟相对于world坐标系的变化。我们打开rviz查看:

        更改参考系为世界坐标系,订阅TF话题,可以看到他们的相对位姿变化。

1.4 接收方代码实现  

        建立cpp文件dyna_sub.cpp。

        我们来看要做什么,我们地图上有一个点,乌龟能看到,但是它是基于乌龟的,我们要这个点在世界坐标系下的观测。

//
// Created by liuhongwei on 23-12-2.
//
//1.包含头文件
#include "ros/ros.h"
#include "tf2_ros/transform_listener.h"
#include "tf2_ros/buffer.h"
#include "geometry_msgs/PointStamped.h"
#include "tf2_geometry_msgs/tf2_geometry_msgs.h" //注意: 调用 transform 必须包含该头文件

int main(int argc, char *argv[])
{
    setlocale(LC_ALL,"");
    // 2.初始化 ROS 节点
    ros::init(argc,argv,"dynamic_tf_sub");
    ros::NodeHandle nh;
    // 3.创建 TF 订阅节点
    tf2_ros::Buffer buffer;
    tf2_ros::TransformListener listener(buffer);

    ros::Rate r(1);
    while (ros::ok())
    {
        // 4.生成一个坐标点(相对于子级坐标系)
        geometry_msgs::PointStamped point_laser;
        point_laser.header.frame_id = "turtle1";
        point_laser.header.stamp = ros::Time();
        point_laser.point.x = 1;
        point_laser.point.y = 1;
        point_laser.point.z = 0;
        // 5.转换坐标点(相对于父级坐标系)
        //新建一个坐标点,用于接收转换结果
        //--------------使用 try 语句或休眠,否则可能由于缓存接收延迟而导致坐标转换失败------------------------
        try
        {
            geometry_msgs::PointStamped point_base;
            point_base = buffer.transform(point_laser,"world");
            ROS_INFO("坐标点相对于 world 的坐标为:(%.2f,%.2f,%.2f)",point_base.point.x,point_base.point.y,point_base.point.z);

        }
        catch(const std::exception& e)
        {
            // std::cerr << e.what() << '\n';
            ROS_INFO("程序异常:%s",e.what());
        }


        r.sleep();
        ros::spinOnce();
    }


    return 0;
}

        我们先执行发布节点,再执行接收节点。

        成功!!我们移动乌龟

        这里有个问题:

        如果我们把这个点的时间戳改成ros现在的时间,我们请求的TF变换一定是比这个点早的,这时会报错(两个时间戳差异较大无法进行TF变换)。

        我们可以设置一个等待时间,拿LIOSAM代码举例子:

                tfListener.waitForTransform(lidarFrame, baselinkFrame, ros::Time(0), ros::Duration(3.0));
                tfListener.lookupTransform(lidarFrame, baselinkFrame, ros::Time(0), lidar2Baselink);

        这段代码很好!它使用了 `waitForTransform()` 函数来等待指定的时间范围内获取从 `lidarFrame` 到 `baselinkFrame` 的变换信息。然后,使用 `lookupTransform()` 函数来获取这个变换信息并将结果存储在 `lidar2Baselink` 变量中。

        这种结合使用 `waitForTransform()` 和 `lookupTransform()` 的方式是一种良好的实践。它确保了在获取坐标系变换信息之前等待了一定时间,以避免时间推断错误,并在数据准备好后获取变换信息。

        我们加入我们的代码中:

        当你在使用ROS中的TF(Transform Library)时,你经常需要从一个坐标系转换到另一个坐标系。这段代码中的两个函数 `waitForTransform()` 和 `lookupTransform()` 正是用于这个目的的。

        1. `waitForTransform()` 函数:
   - 这个函数是用来等待系统中两个特定坐标系之间的转换关系被建立或者更新。
   - 参数 `lidarFrame` 和 `baselinkFrame` 表示你想要从 `lidarFrame` 到 `baselinkFrame` 进行坐标转换。
   - `ros::Time(0)` 表示你要获取最新的可用转换信息。这里使用 `ros::Time(0)` 表示获取最新的变换信息,即时的转换关系。
   - `ros::Duration(3.0)` 指定了最长等待的时间,这里设置为3秒。如果在这个时间段内没有找到有效的坐标系转换关系,程序会继续执行,但这可能意味着后续的坐标转换可能会出现问题或者失败。

        2. `lookupTransform()` 函数:
   - 一旦 `waitForTransform()` 等待到了需要的坐标系转换关系,`lookupTransform()` 函数就可以用来实际获取这个转换。
   - 它会把从 `lidarFrame` 到 `baselinkFrame` 的最新变换信息存储在 `lidar2Baselink` 变量中。

这两个函数一起使用,确保了在获取坐标系转换信息之前,等待了一定时间,以避免因为时间推断问题而导致的转换错误。

        代码如下:

//
// Created by liuhongwei on 23-12-2.
//
//1.包含头文件
#include "ros/ros.h"
#include "tf2_ros/transform_listener.h"
#include "tf2_ros/buffer.h"
#include "geometry_msgs/PointStamped.h"
#include "tf2_geometry_msgs/tf2_geometry_msgs.h" //注意: 调用 transform 必须包含该头文件

int main(int argc, char *argv[])
{
    setlocale(LC_ALL,"");
    // 2.初始化 ROS 节点
    ros::init(argc,argv,"dynamic_tf_sub");
    ros::NodeHandle nh;
    // 3.创建 TF 订阅节点
    tf2_ros::Buffer buffer;
    tf2_ros::TransformListener listener(buffer);

    ros::Rate r(1);
    while (ros::ok())
    {
        // 4.生成一个坐标点(相对于子级坐标系)
        geometry_msgs::PointStamped point_laser;
        point_laser.header.frame_id = "turtle1";
        point_laser.header.stamp = ros::Time::now();
        point_laser.point.x = 1;
        point_laser.point.y = 1;
        point_laser.point.z = 0;
        // 5.转换坐标点(相对于父级坐标系)
        //新建一个坐标点,用于接收转换结果
        //--------------使用 try 语句或休眠,否则可能由于缓存接收延迟而导致坐标转换失败------------------------
        try
        {
//            geometry_msgs::PointStamped point_base;
//            point_base = buffer.transform(point_laser,"world");
//            ROS_INFO("坐标点相对于 world 的坐标为:(%.2f,%.2f,%.2f)",point_base.point.x,point_base.point.y,point_base.point.z);

            geometry_msgs::TransformStamped transformStamped;
            transformStamped = buffer.lookupTransform("world", point_laser.header.frame_id, ros::Time(0));

            // 应用坐标变换到 point_laser 中
            geometry_msgs::PointStamped point_base;
            tf2::doTransform(point_laser, point_base, transformStamped);
            ROS_INFO("坐标点相对于 world 的坐标为:(%.2f,%.2f,%.2f)",point_base.point.x,point_base.point.y,point_base.point.z);
        }
        catch(const std::exception& e)
        {
            // std::cerr << e.what() << '\n';
            ROS_INFO("程序异常:%s",e.what());
        }


        r.sleep();
        ros::spinOnce();
    }


    return 0;
}

        正常啦!

2 ROS的多坐标变换及代码解释 

2.1 什么是ROS的多坐标变换

        现有坐标系统,父级坐标系统 world,下有两子级系统 son1,son2,son1 相对于 world,以及 son2 相对于 world 的关系是已知的,求 son1原点在 son2中的坐标,又已知在 son1中一点的坐标,要求求出该点在 son2 中的坐标。

        换言之,我们在做多传感器融合,有LIDAR、RADAR、CAM三个传感器,我们通过标定知道相机、雷达在机器人坐标系base_link下的关系(相机、雷达安装在机器人上),由我们从雷达看到一个点,怎么转换到相机坐标系下,怎么转换到机器人坐标系下。

实现分析:

  1. 首先,需要发布 son1 相对于 world,以及 son2 相对于 world 的坐标消息
  2. 然后,需要订阅坐标发布消息,并取出订阅的消息,借助于 tf2 实现 son1 和 son2 的转换
  3. 最后,还要实现坐标点的转换

实现流程:

  1. 新建功能包,添加依赖

  2. 创建坐标相对关系发布方(需要发布两个坐标相对关系)

  3. 创建坐标相对关系订阅方

  4. 执行

2.2 发布方代码实现

        创建一个launch文件发布 son1 相对于 world和son2相对于world的关系。

        (没有学过的可以看我上一篇博客)

<launch>
    <node pkg="tf2_ros" type="static_transform_publisher" name="son1" args="0.2 0.8 0.3 0 0 0 /world /son1" output="screen" />
    <node pkg="tf2_ros" type="static_transform_publisher" name="son2" args="0.5 0 0 0 0 0 /world /son2" output="screen" />
</launch>

        即创建一个静态发布器。发布坐标变换。

        启动节点:

        我们通过RVIZ来查看坐标变换关系:

2.3 接收方代码实现       

需求:
    现有坐标系统,父级坐标系统 world,下有两子级系统 son1,son2,
    son1 相对于 world,以及 son2 相对于 world 的关系是已知的,
    求 son1 与 son2中的坐标关系,又已知在 son1中一点的坐标,要求求出该点在 son2 中的坐标
实现流程:
    1.包含头文件
    2.初始化 ros 节点
    3.创建 ros 句柄
    4.创建 TF 订阅对象
    5.解析订阅信息中获取 son1 坐标系原点在 son2 中的坐标
      解析 son1 中的点相对于 son2 的坐标
    6.spin

        我们先看一个API:

tf2_ros::Buffer buffer;
tf2_ros::TransformListener listener(buffer);
geometry_msgs::TransformStamped tfs = buffer.lookupTransform("son2","son1",ros::Time(0));

        lookupTransform可以获取son2相对于son1的坐标变换,前提是他们已知的。

        我们设置一个在son1下的坐标点:

            geometry_msgs::PointStamped ps;
            ps.header.frame_id = "son1";
            ps.header.stamp = ros::Time::now();
            ps.point.x = 1.0;
            ps.point.y = 2.0;
            ps.point.z = 3.0;

        son1中一点的坐标,要求求出该点在 son2 中的坐标:这个用我们上篇博客的API既可:

            geometry_msgs::PointStamped psAtSon2;
            psAtSon2 = buffer.transform(ps,"son2");
            ROS_INFO("在 Son2 中的坐标:x=%.2f,y=%.2f,z=%.2f",
                     psAtSon2.point.x,
                     psAtSon2.point.y,
                     psAtSon2.point.z
            );

        运行:

        完整代码如下:

//
// Created by liuhongwei on 23-12-2.
//
/*

需求:
    现有坐标系统,父级坐标系统 world,下有两子级系统 son1,son2,
    son1 相对于 world,以及 son2 相对于 world 的关系是已知的,
    求 son1 与 son2中的坐标关系,又已知在 son1中一点的坐标,要求求出该点在 son2 中的坐标
实现流程:
    1.包含头文件
    2.初始化 ros 节点
    3.创建 ros 句柄
    4.创建 TF 订阅对象
    5.解析订阅信息中获取 son1 坐标系原点在 son2 中的坐标
      解析 son1 中的点相对于 son2 的坐标
    6.spin

*/
//1.包含头文件
#include "ros/ros.h"
#include "tf2_ros/transform_listener.h"
#include "tf2/LinearMath/Quaternion.h"
#include "tf2_geometry_msgs/tf2_geometry_msgs.h"
#include "geometry_msgs/TransformStamped.h"
#include "geometry_msgs/PointStamped.h"

int main(int argc, char *argv[])
{   setlocale(LC_ALL,"");
    // 2.初始化 ros 节点
    ros::init(argc,argv,"sub_frames");
    // 3.创建 ros 句柄
    ros::NodeHandle nh;
    // 4.创建 TF 订阅对象
    tf2_ros::Buffer buffer;
    tf2_ros::TransformListener listener(buffer);
    // 5.解析订阅信息中获取 son1 坐标系原点在 son2 中的坐标
    ros::Rate r(1);
    while (ros::ok())
    {
        try
        {
            //   解析 son1 中的点相对于 son2 的坐标
            geometry_msgs::TransformStamped tfs = buffer.lookupTransform("son2","son1",ros::Time(0));
            ROS_INFO("Son1 相对于 Son2 的坐标关系:父坐标系ID=%s",tfs.header.frame_id.c_str());
            ROS_INFO("Son1 相对于 Son2 的坐标关系:子坐标系ID=%s",tfs.child_frame_id.c_str());
            ROS_INFO("Son1 相对于 Son2 的坐标关系:x=%.2f,y=%.2f,z=%.2f",
                     tfs.transform.translation.x,
                     tfs.transform.translation.y,
                     tfs.transform.translation.z
            );

            // 坐标点解析
            geometry_msgs::PointStamped ps;
            ps.header.frame_id = "son1";
            ps.header.stamp = ros::Time::now();
            ps.point.x = 1.0;
            ps.point.y = 2.0;
            ps.point.z = 3.0;

            geometry_msgs::PointStamped psAtSon2;
            psAtSon2 = buffer.transform(ps,"son2");
            ROS_INFO("在 Son2 中的坐标:x=%.2f,y=%.2f,z=%.2f",
                     psAtSon2.point.x,
                     psAtSon2.point.y,
                     psAtSon2.point.z
            );
        }
        catch(const std::exception& e)
        {
            // std::cerr << e.what() << '\n';
            ROS_INFO("异常信息:%s",e.what());
        }


        r.sleep();
        // 6.spin
        ros::spinOnce();
    }
    return 0;
}

3 LIOSAM中所有的TF坐标变换代码解析

3.1 后端每预测出一帧激光里程计发送TF信息

        // Publish TF
        static tf::TransformBroadcaster br;
        tf::Transform t_odom_to_lidar = tf::Transform(tf::createQuaternionFromRPY(transformTobeMapped[0], transformTobeMapped[1], transformTobeMapped[2]),
                                                      tf::Vector3(transformTobeMapped[3], transformTobeMapped[4], transformTobeMapped[5]));
        tf::StampedTransform trans_odom_to_lidar = tf::StampedTransform(t_odom_to_lidar, timeLaserInfoStamp, odometryFrame, "lidar_link");
        br.sendTransform(trans_odom_to_lidar);

        transformTobeMapped是当前帧最佳的位姿估计。

1. `tf::TransformBroadcaster br;`:这行代码创建了一个名为`br`的静态变量,它是`TransformBroadcaster`类的一个实例。这个类用于发布坐标变换信息。

2. `tf::Transform t_odom_to_lidar = tf::Transform(tf::createQuaternionFromRPY(transformTobeMapped[0], transformTobeMapped[1], transformTobeMapped[2]), tf::Vector3(transformTobeMapped[3], transformTobeMapped[4], transformTobeMapped[5]));`:这一行代码创建了一个表示从odom坐标系到lidar_link坐标系的变换。它使用了`tf::Transform`类,其中`createQuaternionFromRPY()`函数根据给定的Roll、Pitch和Yaw创建了一个四元数(Quaternion),`tf::Vector3()`创建了一个3D向量,用来表示平移。

3. `tf::StampedTransform trans_odom_to_lidar = tf::StampedTransform(t_odom_to_lidar, timeLaserInfoStamp, odometryFrame, "lidar_link");`:这行代码创建了一个时间戳标记的变换 `trans_odom_to_lidar`。它基于刚刚创建的`tf::Transform`对象`t_odom_to_lidar`,并指定了时间戳`timeLaserInfoStamp`,坐标系为`odometryFrame(odom)`到`lidar_link`。

4. `br.sendTransform(trans_odom_to_lidar);`:这一行代码使用之前创建的`TransformBroadcaster`对象`br`,将刚刚创建的带有时间戳的坐标变换`trans_odom_to_lidar`发布出去,使其他节点可以接收到这个坐标变换信息。

        总体来说,这段代码的作用是创建一个从odom坐标系到lidar_link坐标系的坐标变换,并将它发布到ROS系统中,以便其他节点可以使用这个变换信息进行坐标转换。

3.2 IMU前端获取lidar坐标系(/velodyne)到世界坐标系原点(/base_link)的坐标变化

    tf::TransformListener tfListener;
    tf::StampedTransform lidar2Baselink;

........
            try
            {
                tfListener.waitForTransform(lidarFrame, baselinkFrame, ros::Time(0), ros::Duration(3.0));
                tfListener.lookupTransform(lidarFrame, baselinkFrame, ros::Time(0), lidar2Baselink);
            }
            catch (tf::TransformException ex)
            {
                ROS_ERROR("%s",ex.what());
            }

        这段代码涉及ROS中TF库的使用,主要用于监听和查询两个坐标系之间的变换关系。

1. `tfListener.waitForTransform(lidarFrame, baselinkFrame, ros::Time(0), ros::Duration(3.0));`:这一行代码等待系统中存在从`lidarFrame`到`baselinkFrame`之间的坐标变换。它会等待最多3秒钟,直到这个坐标变换可用或超时。

ros::Time(0)表示的是当前的时间,也就是就等3秒钟取三秒钟最好的。

2. `tfListener.lookupTransform(lidarFrame, baselinkFrame, ros::Time(0), lidar2Baselink);`:一旦变换可用,这行代码将查询`lidarFrame`到`baselinkFrame`之间的变换关系,并将结果存储在`lidar2Baselink`变量中。

3. `catch (tf::TransformException ex)`:如果在等待或查询过程中出现了异常,比如找不到变换关系,会触发`catch`块中的处理。`ex.what()`会输出异常的详细信息,通过`ROS_ERROR`打印错误消息。

总体来说,这段代码的目的是在ROS系统中等待并查询`lidarFrame`到`baselinkFrame`之间的坐标变换关系,并在获取失败时输出错误消息。

3.3  IMU前端关联/map、/odom坐标系

        static tf::TransformBroadcaster tfMap2Odom;
        static tf::Transform map_to_odom = tf::Transform(tf::createQuaternionFromRPY(0, 0, 0), tf::Vector3(0, 0, 0));
        tfMap2Odom.sendTransform(tf::StampedTransform(map_to_odom, odomMsg->header.stamp, mapFrame, odometryFrame));

        这段代码涉及使用ROS中的TF库来发布一个名为`map_to_odom`的静态坐标变换,将地图坐标系(`mapFrame`)与里程计坐标系(`odometryFrame`)进行关联。

1. `static tf::TransformBroadcaster tfMap2Odom;`:创建了一个名为`tfMap2Odom`的静态`TransformBroadcaster`对象,用于发布坐标变换。

2. `static tf::Transform map_to_odom = tf::Transform(tf::createQuaternionFromRPY(0, 0, 0), tf::Vector3(0, 0, 0));`:定义了一个静态的`tf::Transform`变量`map_to_odom`,表示一个从地图坐标系到里程计坐标系的变换。这个变换在这里被初始化为没有旋转(RPY为0)和没有平移(坐标为0,0,0)的初始变换。

3. `tfMap2Odom.sendTransform(tf::StampedTransform(map_to_odom, odomMsg->header.stamp, mapFrame, odometryFrame));`:通过`tfMap2Odom`的`sendTransform`函数,发布了一个时间戳标记的坐标变换。这个变换基于`map_to_odom`,时间戳为`odomMsg->header.stamp`(里程计消息的时间戳),并将这个变换从`mapFrame`(地图坐标系)发送到`odometryFrame`(里程计坐标系)。

        这段代码的作用是在ROS系统中发布一个固定的、从地图坐标系到里程计坐标系的初始坐标变换,可以为后续节点提供这两个坐标系之间的初始关联信息。

        可以看到/map坐标系和/odom是重合的。

3.4 IMU前端发布/odom /base_link的TF信息(里程计、 /base_link是原点相当于位姿)

        // static tf
        static tf::TransformBroadcaster tfMap2Odom;
        static tf::Transform map_to_odom = tf::Transform(tf::createQuaternionFromRPY(0, 0, 0), tf::Vector3(0, 0, 0));
        tfMap2Odom.sendTransform(tf::StampedTransform(map_to_odom, odomMsg->header.stamp, mapFrame, odometryFrame));

        std::lock_guard<std::mutex> lock(mtx);

        imuOdomQueue.push_back(*odomMsg);

        // get latest odometry (at current IMU stamp)
        if (lidarOdomTime == -1)
            return;
        while (!imuOdomQueue.empty())
        {
            if (imuOdomQueue.front().header.stamp.toSec() <= lidarOdomTime)
                imuOdomQueue.pop_front();
            else
                break;
        }
        Eigen::Affine3f imuOdomAffineFront = odom2affine(imuOdomQueue.front());
        Eigen::Affine3f imuOdomAffineBack = odom2affine(imuOdomQueue.back());
        Eigen::Affine3f imuOdomAffineIncre = imuOdomAffineFront.inverse() * imuOdomAffineBack;
        Eigen::Affine3f imuOdomAffineLast = lidarOdomAffine * imuOdomAffineIncre;
        float x, y, z, roll, pitch, yaw;
        pcl::getTranslationAndEulerAngles(imuOdomAffineLast, x, y, z, roll, pitch, yaw);
        
        // publish latest odometry
        nav_msgs::Odometry laserOdometry = imuOdomQueue.back();
        laserOdometry.pose.pose.position.x = x;
        laserOdometry.pose.pose.position.y = y;
        laserOdometry.pose.pose.position.z = z;
        laserOdometry.pose.pose.orientation = tf::createQuaternionMsgFromRollPitchYaw(roll, pitch, yaw);
        pubImuOdometry.publish(laserOdometry);

        // publish tf
        static tf::TransformBroadcaster tfOdom2BaseLink;
        tf::Transform tCur;
        tf::poseMsgToTF(laserOdometry.pose.pose, tCur);
        if(lidarFrame != baselinkFrame)
            tCur = tCur * lidar2Baselink;
        tf::StampedTransform odom_2_baselink = tf::StampedTransform(tCur, odomMsg->header.stamp, odometryFrame, baselinkFrame);
        tfOdom2BaseLink.sendTransform(odom_2_baselink);

        发布了base_link和odom之间的信息。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1277055.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java数据结构之《希尔排序》题目

一、前言&#xff1a; 这是怀化学院的&#xff1a;Java数据结构中的一道难度中等的一道编程题(此方法为博主自己研究&#xff0c;问题基本解决&#xff0c;若有bug欢迎下方评论提出意见&#xff0c;我会第一时间改进代码&#xff0c;谢谢&#xff01;) 后面其他编程题只要我写完…

OOM了?物理内存不够了?试试这个方法来提升内存容量,不花钱的

通过增加虚拟内存来提高内存使用 本文解决的实际问题&#xff1a; 当我们物理内存小的时候&#xff0c;会出现OOM&#xff0c;然后服务自动死掉的情况。因为物理内存大小是固定的&#xff0c;有没有其他好的办法来解决呢&#xff1f;这里我们可以适当调整Linux的虚拟内存来协作…

服务号可以升级订阅号吗

服务号和订阅号有什么区别&#xff1f;服务号转为订阅号有哪些作用&#xff1f;很多小伙伴想把服务号改为订阅号&#xff0c;但是不知道改了之后具体有什么作用&#xff0c;今天跟大家具体讲解一下。首先我们知道服务号一个月只能发四次文章&#xff0c;但是订阅号每天都可以发…

Hdoop学习笔记(HDP)-Part.18 安装Flink

目录 Part.01 关于HDP Part.02 核心组件原理 Part.03 资源规划 Part.04 基础环境配置 Part.05 Yum源配置 Part.06 安装OracleJDK Part.07 安装MySQL Part.08 部署Ambari集群 Part.09 安装OpenLDAP Part.10 创建集群 Part.11 安装Kerberos Part.12 安装HDFS Part.13 安装Ranger …

注意力机制及Transformer-3GPT版

#pic_center R 1 R_1 R1​ R 2 R^2 R2 目录 知识框架No.1 自注意力机制(self-attention)一、输入的种类以及表示1、输入是a vector2、输入是a set of vectors(一段文字)3、输入是a set of vectors(一段音频)4、输入是a set of vectors(一段图谱)5、输入是a set of vectors(一个…

什么是CAS, 什么是AQS

文章目录 什么是CAS, 什么是AQSCASAQS 什么是CAS, 什么是AQS CAS AQS AQS 全称是AbstractQueuedSynchronizer&#xff0c; 是juc 下一个核心的抽象类&#xff0c;用于构建各种同步器和锁 比如我们熟悉的 ReentrantLock、ReadWriteLock、CountDownLatch等等是基于AQS. 首先在…

知识蒸馏测试(使用ImageNet中的1000类dog数据,Resnet101和Resnet18分别做教师模型和学生模型)

当教师网络为resnet101,学生网络为resnet18时&#xff1a; 使用蒸馏方法训练的resnet18训练准确率都小于单独训练resnet18&#xff0c;使用蒸馏方法反而导致了下降。当hard_loss的alpha为0.7时&#xff0c;下降了1.1 当hard_loss的alpha为0.6时&#xff0c;下降了1.7说明当学生…

51单片机的智能加湿器控制系统【含proteus仿真+程序+报告+原理图】

1、主要功能 该系统由AT89C51单片机LCD1602显示模块DHT11湿度传感器模块继电器等模块构成。主要适用于智能自动加湿器、湿度保持、湿度控制等相似项目。 可实现基本功能: 1、LCD1602液晶屏实时显示湿度信息 2、DHT11采集湿度 3、按键可以调节适宜人体湿度的阈值范围&#xff0…

TCP_报文格式解读

报文格式 header部分字段含义解析 固定字段 对于header中固定部分字段含义&#xff0c;见之前的blog《TCP报文分析》&#xff1b; 对部分字段含义补充说明 Data Offset&#xff1a;4bit&#xff0c;tcp header的长度&#xff0c;单位&#xff1a;32bit&#xff08;4字节&…

正则表达式从放弃到入门(1):“正则表达式”是什么?

正则表达式从放弃到入门&#xff08;1&#xff09;&#xff1a;“正则表达式”是什么&#xff1f; 本博文转载自 这是一篇”正则表达式”扫盲贴&#xff0c;如果你还不理解什么是正则表达式&#xff0c;看这篇文章就对了。 如果你已经掌握了”正则表达式”&#xff0c;就不用再…

如何使用windows Terminal终端连接远程Linux服务器

近接触到了zsh这个shell&#xff0c;所以在ubuntu系统上反复折腾&#xff0c;终于在ubuntu-desktop系统上使用oh-my-zsh和powerlevel10k配置好了一个比较好看的终端&#xff08;个人认为挺好看&#xff0c;勿喷&#xff09;。 但是在从windwos的Mobaxterm登录ubuntu查看时&…

用Java写一个王者荣耀游戏

目录 sxt包 Background Bullet Champion ChampionDaji GameFrame GameObject Minion MinionBlue MinionRed Turret TurretBlue TurretRed beast包 Bear Beast Bird BlueBuff RedBuff Wolf Xiyi 打开Eclipse创建图片中的几个包 sxt包 Background package sxt;…

思维模型 系列位置效应

本系列文章 主要是 分享 思维模型&#xff0c;涉及各个领域&#xff0c;重在提升认知。重视首尾。 1 系列位置效应 1.1 系列位置效应在教育领域的应用 在一堂英语课上&#xff0c;老师先让学生学习了一篇英语文章&#xff0c;然后在文章的结尾部分强调了一些重要的单词和语法…

【数据结构】循环链表和双向链表

【循环链表】 (有头结点) pR1->next; R1->nextR2->next->next; free(R2->next); R2->nextp; 例&#xff1a;对于两个单循环链表a&#xff0c;b&#xff0c;将其连接起来&#xff0c;变成一个单循环链表 #include<stdio.h> #include<stdlib.h> …

nodejs的安装和验证

1.浏览器访问nodejs官网&#xff0c;根据操作系统选择对应版本的安装文件&#xff0c;如下图所示&#xff1a; 2.双击下载的安装文件&#xff0c;点击“Next”&#xff0c;如下图所示&#xff1a; 3.勾选“I accept the terms in the License Agreement”&#xff0c;然后点击“…

某公司前端笔试题(12.30)

1、对象数组去重&#xff1a; 数组去重&#xff1a; const a[{a:1,b:2},{a:2},{a:2},{a:1,c:3},{b:2,a:1}] 结果&#xff1a;[{a:1,b:2},{a:2},{a:1,c:3}] // 判断两个对象的属性值是否一致 const a [{ a: 1, b: 2 }, { a: 2 }, { a: 2 }, { a: 1, c: 3 }, { b: 2, a: 1 }] co…

ZooKeeper 如何保证数据一致性?

在分布式场景中&#xff0c;ZooKeeper 的应用非常广泛&#xff0c;比如数据发布和订阅、命名服务、配置中心、注册中心、分布式锁等。 ZooKeeper 提供了一个类似于 Linux 文件系统的数据模型&#xff0c;和基于 Watcher 机制的分布式事件通知&#xff0c;这些特性都依赖 ZooKee…

如果你想成为一名提示词工程师(Prompt Engineer),这款工具你不能错过

我的新书《Android App开发入门与实战》已于2020年8月由人民邮电出版社出版&#xff0c;欢迎购买。点击进入详情 前言 我们知道&#xff0c;如果想要通过AI得到更好更精确的答案&#xff0c;那么提示词Prompt的好坏至关重要。 因此&#xff0c;提示词工程师这个岗位应运而出。…

【PUSDN】WebStorm中报错Switch language version to React JSX

简述 WebStorm中报错Switch language version to React JSX 可能本页面的写法是其他语法。所以可以不用管。 测试项目&#xff1a;ant design vue pro 前情提示 系统&#xff1a; 一说 同步更新最新版、完整版请移步PUSDN Powered By PUSDN - 平行宇宙软件开发者网www.pusdn…

连接器信号完整性仿真教程 九

前面几篇博文介绍了用CST Studio Suite做连接器信号完整性仿真的基本操作步骤、方法、技巧。本文介绍用Ansys HFSS做连接器信号完整性仿真的基本操作布置。将以 B to B Connector为实例&#xff0c;Step By Step详细讲解Ansys HFSS连接器信号完整性仿真操作步骤。 打开ANSYS E…