第 5 章 ROS 常用组件 1 —— TF 坐标变换_静态坐标变换 tf01_static / 动态坐标变换 tf02_dynamic

00 学习目标

在ROS中内置一些比较实用的工具,通过这些工具可以方便快捷的实现某个功能或调试程序,从而提高开发效率,本章主要介绍ROS中内置的如下组件:

  • TF坐标变换,实现不同类型的坐标系之间的转换;
  • rosbag 用于录制ROS节点的执行过程并可以重放该过程;
  • rqt 工具箱,集成了多款图形化的调试工具。

本章预期达成的学习目标:

  • 了解 TF 坐标变换的概念以及应用场景
  • 能够独立完成TF案例:小乌龟跟随
  • 可以使用 rosbag 命令或编码的形式实现录制与回放
  • 能够熟练使用rqt中的图形化工具

01 TF 坐标变换

机器人系统上,有多个传感器,如激光雷达、摄像头等,有的传感器是可以感知机器人周边的物体方位(或者称之为:坐标,横向、纵向、高度的距离信息)的,以协助机器人定位障碍物,可以直接将物体相对该传感器的方位信息,等价于物体相对于机器人系统或机器人其它组件的方位信息吗?显示是不行的,这中间需要一个转换过程。更具体描述如下:

场景1:雷达与小车
现有一移动式机器人底盘,在底盘上安装了一雷达,雷达相对于底盘的偏移量已知,现雷达检测到一障碍物信息,获取到坐标分别为(x,y,z),该坐标是以雷达为参考系的,如何将这个坐标转换成以小车为参考系的坐标呢?

在这里插入图片描述

场景2:现有一带机械臂的机器人(比如:PR2)需要夹取目标物,当前机器人头部摄像头可以探测到目标物的坐标(x,y,z),不过该坐标是以摄像头为参考系的,而实际操作目标物的是机械臂的夹具,当前我们需要将该坐标转换成相对于机械臂夹具的坐标,这个过程如何实现?

在这里插入图片描述

  • 窗口1:第2只乌龟会跟随第1只乌龟运动
roslaunch turtle_tf2 turtle_tf2_demo_cpp.launch(C++实现)
roslaunch turtle_tf2 turtle_tf2_demo.launch(python实现)

在这里插入图片描述

1.0 概念 - 作用 - 说明

概念

  • tf:TransForm Frame 坐标变换
  • 坐标系:ROS 中是通过坐标系统开标定物体的,确切的将是通过右手坐标系来标定的
    在这里插入图片描述
    作用
  • 在 ROS 中用于实现不同坐标系之间的点或向量的转换

说明
在ROS中坐标变换最初对应的是tf,不过在 hydro 版本开始, tf 被弃用,迁移到 tf2,后者更为简洁高效,tf2对应的常用功能包有:

  • tf2_geometry_msgs:可以将ROS消息转换成tf2消息
  • tf2:封装了坐标变换的常用消息
  • tf2_ros:为 tf2 提供了 roscpp 和 rospy 绑定,封装了坐标变换常用的 API
    官方参考

1.1 坐标 msg 消息

订阅发布模型中数据载体 msg 是一个重要实现,首先需要了解一下,在坐标转换实现中常用的 msg:

  • geometry_msgs/TransformStamped:用于传输坐标系相关位置信息
  • geometry_msgs/PointStamped:用于传输某个坐标系内坐标点的信息

在坐标变换中,频繁的需要使用到坐标系的相对关系以及坐标点信息。

1.1.1 geometry_msgs/TransformStamped 坐标系之间位置信息

  • 命令行键入:rosmsg info geometry_msgs/TransformStamped,传输坐标系相关位置信息

名词解释

  • string frame_id 被参考坐标系(基坐标系),string child_frame_id 另外坐标系
  • geometry_msgs/Quaternion rotation 坐标 A 相对于坐标 B 的欧拉角(三个角)
std_msgs/Header header                     # 头信息
  uint32 seq                                #|-- 序列号
  time stamp                                #|-- 时间戳
  string frame_id                           #|-- 坐标 ID
string child_frame_id                    # 子坐标系的 id
geometry_msgs/Transform transform        # 坐标信息
  geometry_msgs/Vector3 translation        # 偏移(平移)
    float64 x                                #|-- X 方向的偏移量
    float64 y                                #|-- Y 方向的偏移量
    float64 z                                #|-- Z 方向上的偏移量
  geometry_msgs/Quaternion rotation        # 四元数,旋转
    float64 x                              # 偏航,俯仰,翻滚角度上的变化
    float64 y                              #   
    float64 z                                
    float64 w
  • 四元数用于表示坐标的相对姿态

在这里插入图片描述

1.1.2 geometry_msgs/PointStamped 坐标系内坐标点

  • 命令行键入:rosmsg info geometry_msgs/PointStamped,传输某个坐标系内坐标点的信息
std_msgs/Header header                    # 头
  uint32 seq                                #|-- 序号
  time stamp                                #|-- 时间戳
  string frame_id                           #|-- 所属坐标系的 id
geometry_msgs/Point point                # 点坐标
  float64 x                                    #|-- x y z 坐标
  float64 y
  float64 z

在这里插入图片描述

1.2 静态坐标变换 —— 功能包 tf01_static

  • 静态坐标变换,是指两个坐标系之间的相对位置是固定的。

1.2.0 需求 - 分析 - 流程

需求

现有一机器人模型,核心构成包含主体与雷达,各对应一坐标系,坐标系的原点分别位于主体与雷达的物理中心,已知雷达原点相对于主体原点位移关系如下:x 0.2 y0.0 z0.5。当前雷达检测到一障碍物,在雷达坐标系中障碍物的坐标为 (2.0 3.0 5.0),请问,该障碍物相对于主体的坐标是多少?

分析

  • 坐标系相对关系,可以通过发布方发布
  • 订阅方,订阅到发布的坐标系相对关系,再传入坐标点信息(可以写死),然后借助于 tf 实现坐标变换,并将结果输出

流程:C++ 与 Python 实现流程一致

  • 新建功能包,添加依赖
  • 编写发布方实现
  • 编写订阅方实现
  • 执行并查看结果

1.2.1 新建功能包,添加依赖

  • 新建功能包
tf01_static
  • 添加依赖
roscpp rospy std_msgs tf2 tf2_ros tf2_geometry_msgs geometry_msgs

1.2.2 C++实现

1.2.2.1 发布方 demo01_static_pub.cpp
#include"ros/ros.h"
#include"tf2_ros/static_transform_broadcaster.h"
#include"geometry_msgs/TransformStamped.h"
#include"tf2/LinearMath/Quaternion.h"

/*
    需求: 发布两个坐标系的相对关系
    流程:
        1. 包含头文件
        2. 设置编码 节点初始化 NodeHandle
        3. 创建发布对象
        4. 组织被发布的消息
        5. 发布数据
        6. spin()
*/

int main(int argc, char *argv[])
{
    
    // 2. 设置编码 节点初始化 NodeHandle
    setlocale(LC_ALL,"");
    ros::init(argc,argv,"static_pub");
    ros::NodeHandle nh;(不是必须的)

    // 3. 创建发布对象 对应头文件2 —— tf2_ros/static_transform_broadcaster.h 静态坐标转换广播器
    tf2_ros::StaticTransformBroadcaster pub;

    // 4. 组织被发布的消息 对应头文件3 —— geometry_msgs/TransformStamped.h
    geometry_msgs::TransformStamped tfs;// 创建坐标系信息
    tfs.header.stamp = ros::Time::now();
    tfs.header.frame_id = "base_link";// 相对坐标系关系中被参考的那个,基坐标系
    tfs.child_frame_id = "laser";//雷达坐标系
    tfs.transform.translation.x = 0.2;//雷达x偏移值
    tfs.transform.translation.y = 0.0;//雷达y偏移值
    tfs.transform.translation.z = 0.5;//雷达z偏移值

    // 4.1 需要根据欧拉角转换 对应头文件4 —— tf2/LinearMath/Quaternion.h
    tf2::Quaternion qtn; // 创建四元数对象
    // 4.2 向该对象设置欧拉角,这个对象可以将欧拉角转换成四元数
    qtn.setRPY(0,0,0);// 设置偏航值,俯仰值,翻滚值,单位是弧度
    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. 发布数据
    pub.sendTransform(tfs);

    // 6. spin()
    ros::spin();

    return 0;
}
1.2.2.2 配置 CMakeLists.txt
add_executable(demo01_static_pub src/demo01_static_pub.cpp)

add_dependencies(demo01_static_pub ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})

target_link_libraries(demo01_static_pub
  ${catkin_LIBRARIES}
)
1.2.2.3 编译检验 demo01_static_pub.cpp
  • 编译
Ctrl + Shift + B

检验方式1

  • 窗口1:挂起
roscore
cd demo02_ws/
source ./devel/setup.bash
rosrun tf01_static demo01_static_pub
  • 窗口2
rostopic list

在这里插入图片描述

  • 打印该话题下的消息
rostopic echo /tf_static

在这里插入图片描述
检验方式2

  • 窗口1:图形化设置
rviz
  • Fixed Frame 选择 base_link

在这里插入图片描述

  • 点击左下角 Add,选择 TF
  • 红色 x 轴,绿色 y 轴,蓝色 z 轴
    在这里插入图片描述
1.2.2.4 订阅方 demo02_static_sub.cpp
  • 核心代码
 // 核心代码 ---- 将 ps 转换成相对于 base_link的坐标点 头文件5 —— tf2_geometry_msgs/tf2_geometry_msgs.h
        geometry_msgs::PointStamped ps_out; 
        /*
            调用buffer(数据存在缓存buffer里面)的转换函数 transform
            参数1: 被转换的坐标点
            参数2: 目标坐标系
            返回值: 输出的坐标点
            注意1: 调用时必须包含头文件 tf2_geometry_msgs/tf2_geometry_msgs.h
            注意2: 运行时存在的问题,抛出一个异常 base_link不存在
                  原因:
                     订阅数据是一个耗时操作,可能调用transform转换函数时,
                     坐标系相对关系还没有订阅到,因此出现异常
                  解决:
                     方案1: 在调用转换函数时,执行休眠
                     方案2: 进行异常处理
        */
        ps_out = buffer.transform(ps,"base_link"); // 目标点信息,基参考系,输出转换后的坐标
  • 订阅方程序
#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"

/*
    订阅方: 订阅发布的坐标系相对关系,传入一个坐标系,调用tf实现转换

    流程:
        1. 包含头文件
        2. 编码 初始化 NodeHandle(必须)
        3. 创建订阅对象: ---> 订阅坐标系相对关系
        4. 组织一个坐标点数据
        5. 转换算法,需要调用tf内置实现
        6. 最后输出
*/

int main(int argc, char *argv[])
{
    // 2. 编码 初始化 NodeHandle(必须)
    setlocale(LC_ALL,"");
    ros::init(argc,argv,"static_sub");
    ros::NodeHandle nh;

    // 3. 创建订阅对象: ---> 订阅坐标系相对关系 头文件2\3
    // 3.1 创建一个buffer缓存
    tf2_ros::Buffer buffer;
    // 3.2 再创建监听对象(监听对象可以将订阅的数据存入buffer)
    tf2_ros::TransformListener listener(buffer);

    // 4. 组织一个坐标点数据 头文件4
    geometry_msgs::PointStamped ps;
    ps.header.stamp = ros::Time::now();
    ps.header.frame_id = "laser";// 坐标系 障碍物位置相对于雷达坐标系而言
    ps.point.x = 2.0; // 障碍物相对于雷达坐标系的位置
    ps.point.y = 3.0;
    ps.point.z = 5.0;
    
    // 添加休眠 防止订阅方没有订阅到发布方消息
    ros::Duration(2).sleep();
    // 5. 转换算法,需要调用tf内置实现
    ros::Rate rate(1);
    while(ros::ok())
    {
        // 核心代码 ---- 将 ps 转换成相对于 base_link的坐标点 头文件5
        geometry_msgs::PointStamped ps_out; 
        /*
            调用buffer(数据存在缓存buffer里面)的转换函数 transform
            参数1: 被转换的坐标点
            参数2: 目标坐标系
            返回值: 输出的坐标点
            注意1: 调用时必须包含头文件 tf2_geometry_msgs/tf2_geometry_msgs.h
            注意2: 运行时存在的问题,抛出一个异常 base_link不存在
                  原因:
                     订阅数据是一个耗时操作,可能调用transform转换函数时,
                     坐标系相对关系还没有订阅到,因此出现异常
                  解决:
                     方案1: 在调用转换函数时,执行休眠
                     方案2: 进行异常处理
        */
        ps_out = buffer.transform(ps,"base_link"); // 目标点信息,基参考系,输出转换后的坐标

        // 6. 最后输出 
        ROS_INFO("转换后的坐标值:(%.2f,%.2f,%.2f),参考的坐标系:%s",
                    ps_out.point.x,
                    ps_out.point.y,
                    ps_out.point.z,
                    ps.header.frame_id.c_str()
                    );
        rate.sleep();
        ros::spinOnce();
    }
    return 0;
}
1.2.2.5 配置 CMakeLists.txt
add_executable(demo02_static_sub src/demo02_static_sub.cpp)

add_dependencies(demo02_static_sub ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})

target_link_libraries(demo02_static_sub
  ${catkin_LIBRARIES}
)
1.2.2.6 发布方与订阅方 编译执行
  • 注意:在订阅方程序中增加延时,保证先发布,再订阅,防止报错
  • 编译
Ctrl + Shift + B
  • 窗口1
roscore
  • 启动发布方 窗口2
cd demo02_ws/
source ./devel/setup.bash
rosrun tf01_static demo01_static_pub
  • 启动订阅方 窗口3
cd demo02_ws/
source ./devel/setup.bash
rosrun tf01_static demo02_static_sub

在这里插入图片描述

1.2.3 Python实现

1.2.3.1 发布方 demo01_static_pub_p.py
#! /usr/bin/env python

"""
    发布方: 发布两个坐标系的相对关系(车辆底盘 --- base_link 和 雷达 --- laser)
    流程:
        1. 导包
        2. 初始化节点
        3. 创建发布对象
        4. 组织被发布的数据
        5. 发布数据
        6. spin()
"""

import rospy
import tf2_ros
import tf
from geometry_msgs.msg import TransformStamped

if __name__ == "__main__":
     
    # 2. 初始化节点
    rospy.init_node("start_pub_p")
    # 3. 创建发布对象 头文件2
    pub = tf2_ros.StaticTransformBroadcaster()
    # 4. 组织被发布的数据 头文件4
    tfs = TransformStamped()
    # 4.1 header
    tfs.header.stamp = rospy.Time.now()
    tfs.header.frame_id = "base_link"
    # 4.2 child frame
    tfs.child_frame_id = "laser"
    # 4.3 相对关系(偏移 与 四元数)
    tfs.transform.translation.x = 0.2
    tfs.transform.translation.y = 0.0
    tfs.transform.translation.z = 0.5
    # 4.4 先从欧拉角转换成四元数 头文件3
    qtn = tf.transformations.quaternion_from_euler(0,0,0)
    # 4.5 再设置四元数
    tfs.transform.rotation.x = qtn[0]
    tfs.transform.rotation.y = qtn[1]
    tfs.transform.rotation.z = qtn[2]
    tfs.transform.rotation.w = qtn[3]

    # 5. 发布数据
    pub.sendTransform(tfs)
    # 6. spin()
    rospy.spin()
1.2.3.2 订阅方 demo02_static_sub_p.py
#! /usr/bin/env python

"""
    订阅方: 订阅坐标变换消息,传入被转换的坐标,调用转换算法

    流程:
        1. 导包
        2. 初始化
        3. 创建订阅对象
        4. 组织被转换的坐标点
        5. 转换逻辑实现,调用tf封装的算法
        6. 输出结果
"""

import rospy
import tf2_ros
from tf2_geometry_msgs import tf2_geometry_msgs

if __name__ == "__main__":

    # 2. 初始化
    rospy.init_node("static_sub_p")

    # 3. 创建订阅对象
    # 3.1 创建一个缓存对象 头文件2
    buffer = tf2_ros.Buffer()
    # 3.2 创建订阅对象(将缓存传入)
    sub = tf2_ros.TransformListener(buffer)

    # 4. 组织被转换的坐标点 头文件3
    ps = tf2_geometry_msgs.PointStamped()
    ps.header.stamp = rospy.Time.now()
    ps.header.frame_id = "laser"
    ps.point.x = 2.0
    ps.point.y = 3.0
    ps.point.z = 5.0

    # 5. 转换逻辑实现,调用tf封装的算法
    rate = rospy.Rate(1)
    while not rospy.is_shutdown():
        # 使用try防止
        try:
            # 转换实现
        """
            参数1: 被转换的坐标点
            参数2: 目标坐标系
            返回值: 转换后的坐标点

            PS:
            问题: 抛出异常 base_link 不存在
            原因: 转换函数调用时,可能还没有订阅到坐标系的相对关系
            解决: try 捕获异常,并处理
        """
            ps_out = buffer.transform(ps,"base_link")
            # 6. 输出结果
            rospy.loginfo("转换后的坐标:(%.2f,%.2f,%.2f),参考的坐标系: %s",
                        ps_out.point.x,
                        ps_out.point.y,
                        ps_out.point.z,
                        ps_out.header.frame_id)
        except Exception as e:
            rospy.logwarn("错误提示:%s",e) 
        
        rate.sleep()

1.2.4 x - 翻滚 y - 俯仰 z - 偏航(重要!!!)

  • 当坐标系之间的相对位置固定时,那么所需参数也是固定的: 父系坐标名称、子级坐标系名称、x偏移量、y偏移量、z偏移量、x 翻滚角度、y 俯仰角度、z 偏航角度,实现逻辑相同,参数不同,那么 ROS 系统就已经封装好了专门的节点,摄像头相对于基坐标系位置。
  • rosrun tf2_ros static_transform_publisher 0.1 0.0 0.3 0 0 0 /base_link /camera
  • rviz:依次是z - 偏航角(Yaw),y - 俯仰角(Pitch),x 翻滚角(Roll)
  • X轴:红色(翻滚角) 正值 —— 顺时针转动;负值 —— 逆时针转动
  • Y轴:绿色(俯仰角)
  • Z轴:蓝色(偏航角)
    在这里插入图片描述
    在这里插入图片描述

1.2.5 补充讲解

1.2.5.1 补充1

当坐标系之间的相对位置固定时,那么所需参数也是固定的: 父系坐标名称、子级坐标系名称、x偏移量、y偏移量、z偏移量、x翻滚角度、y俯仰角度、z偏航角度,实现逻辑相同,参数不同,那么 ROS 系统就已经封装好了专门的节点,使用方式如下:

  • rosrun tf2_ros static_transform_publisher x偏移量 y偏移量 z偏移量 z偏航角度 y俯仰角度
    x翻滚角度 父级坐标系 子级坐标系
  • 例如:rosrun tf2_ros static_transform_publisher 0.2 0 0.5 0 0 0 /baselink /laser

也建议使用该种方式直接实现静态坐标系相对信息发布。

1.2.5.2 补充2

可以借助于rviz显示坐标系关系,具体操作:

  • 新建窗口输入命令:rviz
  • 在启动的 rviz 中设置Fixed Frame 为 base_link
  • 点击左下的 add 按钮,在弹出的窗口中选择 TF 组件,即可显示坐标关系

1.3 动态坐标变换 —— 功能包 tf02_dynamic

1.3.0 需求 - 分析 - 流程

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

需求描述

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

实现分析

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

实现流程:C++ 与 Python 实现流程一致

  • 新建功能包,添加依赖
  • 创建坐标相对关系发布方(同时需要订阅乌龟位姿信息)
  • 创建坐标相对关系订阅方
  • 编译执行

1.3.1 新建功能包,添加依赖

  • 新建功能包
tf02_dynamic
  • 添加依赖
roscpp rospy std_msgs tf2 tf2_ros tf2_geometry_msgs geometry_msgs turtlesim

1.3.2 C++实现

1.3.2.0 预准备 - 获取话题和消息
  • 话题:/turtle1/pose 消息类型:turtlesim/Pose(类似于Person.msg)
  • 启动窗口1
roscore
  • 启动窗口2 —— 打开乌龟节点
rosrun turtlesim turtlesim_node
  • 启动窗口3 —— 获取话题 /turtle1/pose
rostopic list

在这里插入图片描述
启动窗口4 —— 获取消息格式 turtlesim/Pose

rostopic info /turtle1/pose

在这里插入图片描述

  • 启动窗口5 —— 获取消息的具体格式
rosmsg info turtlesim/Pose
float32 x
float32 y
float32 theta
float32 linear_velocity
float32 angular_velocity

在这里插入图片描述

1.3.2.1 发布方 demo01_dynamic_pub.cpp
#include"ros/ros.h"
#include"turtlesim/Pose.h"
// 创建发布对象
#include"tf2_ros/transform_broadcaster.h"
// 组织发布数据
#include"geometry_msgs/TransformStamped.h"
// 坐标系四元数
#include"tf2/LinearMath/Quaternion.h"
/*
    发布方: 需要订阅乌龟的位姿信息, 转换成相对于窗体的坐标关系,并发布
    准  备: 
        话题: /turtle1/pose
        消息: turtlesim/Pose

    流程:
        1. 包含头文件
        2. 设置编码,初始化, NodeHandle
        3. 创建订阅对象,订阅/turtle1/pose
        4. 回调函数处理订阅信息: 将位姿信息转换成坐标相对关系并发布(关注)
        5. spin()
*/

// 给传进回调函数的数据定义范型 头文件2
void doPose(const turtlesim::Pose::ConstPtr& pose){
    // 获取位姿信息,转换成坐标系相对关系(核心),并发布
    // a. 创建发布对象 头文件3
    static tf2_ros::TransformBroadcaster pub;

    // b. 组织被发布的数据 头文件4
    geometry_msgs::TransformStamped ts;
    ts.header.frame_id = "world"; // 相对于世界坐标系
    ts.header.stamp = ros::Time::now();
    ts.child_frame_id = "turtle1";
    // 坐标系偏移量
    ts.transform.translation.x = pose->x;
    ts.transform.translation.y = pose->y;
    ts.transform.translation.z = 0; // z一直是零(2D)
    // 坐标系四元数 头文件5
    /*
        位姿信息中没有四元数,但是有偏航角度,又已知乌龟时2D,没有翻滚和俯仰角,所以得出乌龟的
        欧拉角: 0 0 theta
    */
    tf2::Quaternion qtn;
    qtn.setRPY(0,0,pose->theta);
    ts.transform.rotation.x = qtn.getX();
    ts.transform.rotation.y = qtn.getY();
    ts.transform.rotation.z = qtn.getZ();
    ts.transform.rotation.w = qtn.getW();

    // c. 发布
    pub.sendTransform(ts);
}

int main(int argc, char *argv[])
{
    // 2. 设置编码,初始化, NodeHandle
    setlocale(LC_ALL,"");
    ros::init(argc,argv,"dynamic_pub");
    ros::NodeHandle nh;

    // 3. 创建订阅对象,订阅/turtle1/pose
    ros::Subscriber sub = nh.subscribe("/turtle1/pose",100,doPose);

    // 4. 回调函数处理订阅信息: 将位姿信息转换成坐标相对关系并发布(关注)
    // 5. spin()
    ros::spin();
    return 0;
}
1.3.2.2 配置 CMakeLists.txt
add_executable(demo01_dynamic_pub src/demo01_dynamic_pub.cpp)

add_dependencies(demo01_dynamic_pub ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})

target_link_libraries(demo01_dynamic_pub
  ${catkin_LIBRARIES}
)
1.3.2.3 订阅方 demo02_dynamic_sub.cpp(类似 demo02_static.sub)
#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"

/*
    订阅方: 订阅发布的坐标系相对关系,传入一个坐标系,调用tf实现转换

    流程:
        1. 包含头文件
        2. 编码 初始化 NodeHandle(必须)
        3. 创建订阅对象: ---> 订阅坐标系相对关系
        4. 组织一个坐标点数据
        5. 转换算法,需要调用tf内置实现
        6. 最后输出
*/

int main(int argc, char *argv[])
{
    // 2. 编码 初始化 NodeHandle(必须)
    setlocale(LC_ALL,"");
    ros::init(argc,argv,"dynamic_sub");
    ros::NodeHandle nh;

    // 3. 创建订阅对象: ---> 订阅坐标系相对关系 头文件2\3
    // 3.1 创建一个buffer缓存
    tf2_ros::Buffer buffer;
    // 3.2 再创建监听对象(监听对象可以将订阅的数据存入buffer)
    tf2_ros::TransformListener listener(buffer);

    // 4. 组织一个坐标点数据 头文件4
    geometry_msgs::PointStamped ps;
    // 参考的坐标系
    ps.header.frame_id = "turtle1";// 坐标系 物体位置相对于乌龟坐标系而言
    // 时间戳
    ps.header.stamp = ros::Time(0.0);
    
    ps.point.x = 2.0; // 物体相对于乌龟坐标系的位置
    ps.point.y = 3.0;
    ps.point.z = 5.0;
    
    // 添加休眠 防止订阅方没有订阅到发布方消息(这个方法不建议用,最好使用try方法
    // ros::Duration(2).sleep();
    // 5. 转换算法,需要调用tf内置实现
    ros::Rate rate(1);
    while(ros::ok())
    {
        // 核心代码 ---- 将 ps 转换成相对于 base_link的坐标点 头文件5
        geometry_msgs::PointStamped ps_out; 
        /*
            调用buffer(数据存在缓存buffer里面)的转换函数 transform
            参数1: 被转换的坐标点
            参数2: 目标坐标系
            返回值: 输出的坐标点
            注意1: 调用时必须包含头文件 tf2_geometry_msgs/tf2_geometry_msgs.h
            注意2: 运行时存在的问题,抛出一个异常 base_link不存在
                  原因:
                     订阅数据是一个耗时操作,可能调用transform转换函数时,
                     坐标系相对关系还没有订阅到,因此出现异常
                  解决:
                     方案1: 在调用转换函数时,执行休眠
                     方案2: 进行异常处理
        */
       try
       {
            ps_out = buffer.transform(ps,"world"); // 目标点信息,基参考系,输出转换后的坐标 头文件5r

            // 6. 最后输出 
            ROS_INFO("转换后的坐标值:(%.2f,%.2f,%.2f),参考的坐标系:%s",
                        ps_out.point.x,
                        ps_out.point.y,
                        ps_out.point.z,
                        ps.header.frame_id.c_str()
                        );
       }
       catch(const std::exception& e)
       {
           ROS_INFO("异常消息:%s",e.what());
       }
        
        rate.sleep();
        ros::spinOnce();
    }
    
    return 0;
}
1.3.2.4 配置 CMakeLists.txt
add_executable(demo02_dynamic_sub src/demo02_dynamic_sub.cpp)

add_dependencies(demo02_dynamic_sub ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})

target_link_libraries(demo02_dynamic_sub
  ${catkin_LIBRARIES}
)
1.3.2.5 编译运行
  • 编译
Ctrl + Shift + B
  • 启动窗口1
roscore
  • 启动窗口2
rosrun turtlesim turtlesim_node 
  • 启动发布方 窗口3
cd demo02_ws/
source ./devel/setup.bash
rosrun tf02_dynamic demo01_dynamic_pub
  • 启动订阅方 窗口4
cd demo02_ws/
source ./devel/setup.bash
rosrun tf02_dynamic demo02_dynamic_sub 
  • 启动窗口5:启动乌龟节点,使用键盘控制乌龟运动,对应转换的坐标值也在变化
cd demo02_ws/
source ./devel/setup.bash
rosrun turtlesim turtle_teleop_key

1.3.3 Python实现

1.3.3.1 发布方 demo01_dynamic_sub_p.py

roscore
rosrun turtlesim turtlesim_node
cd demo02_ws/
source ./devel/setup.bash
rosrun tf02_dynamic demo01_dynamic_pub_p.py

/rosout
/rosout_agg
/tf
/turtle1/cmd_vel
/turtle1/color_sensor
/turtle1/pose

rostopic echo /tf 查看具体信息

ransforms:
header:
seq: 0
stamp:
secs: 1658287751
nsecs: 917598485
frame_id: “world”
child_frame_id: “turtle1”
transform:
translation:
x: 5.544444561004639
y: 5.544444561004639
z: 0.0
rotation:
x: 0.0
y: 0.0
z: 0.0
w: 1.0

#! /usr/bin/env python

"""
    发布方: 需要订阅乌龟的位姿信息, 转换成相对于窗体的坐标关系,并发布
    准  备: 
        话题: /turtle1/pose
        消息: turtlesim/Pose

    流程:
        1. 包含头文件
        2. 初始化ROS节点
        3. 创建订阅对象,订阅/turtle1/pose
        4. 回调函数处理订阅信息: 将位姿信息转换成坐标相对关系并发布(关注)
        5. spin()
"""

import rospy
from turtlesim.msg import Pose
import tf2_ros
from geometry_msgs.msg import TransformStamped
import tf
def doPose(pose):
    # 1. 创建发布坐标系相对关系的对象 头文件3
    pub = tf2_ros.TransformBroadcaster()
    # 2. 将 pose 转换成 坐标系相对关系消息 头文件4
    ts = TransformStamped()
    ts.header.frame_id = "world"
    ts.header.stamp = rospy.Time.now()
    ts.child_frame_id = "turtle1"
    
    # 2.1 子级坐标系相对于父级坐标系的偏移量
    ts.transform.translation.x = pose.x
    ts.transform.translation.y = pose.y
    ts.transform.translation.z = 0

    # 2.2 四元数 头文件5
    # 从欧拉角转换四元数
    """
        乌龟是2D,不存在 x 上的翻滚和 y 上的俯仰,只有 z 上的偏航
        0 0 pose.theta
    """
    qtn = tf.transformations.quaternion_from_euler(0,0,pose.theta)
    ts.transform.rotation.x = qtn[0]
    ts.transform.rotation.y = qtn[1]
    ts.transform.rotation.z = qtn[2]
    ts.transform.rotation.w = qtn[3]

    # 3. 发布
    pub.sendTransform(ts)

if __name__ == "__main__":

    # 2. 初始化ROS节点
    rospy.init_node("dynamic_pub_p")
    # 3. 创建订阅对象 头文件2
    sub = rospy.Subscriber("/turtle1/pose",Pose,doPose,queue_size = 100)# /tur是话题名称

    # 4. 回调函数处理订阅信息
    # 5. spin()
    rospy.spin()
1.3.3.2 订阅方 demo02_dynamic_sub_p.py

roscore
cd demo02_ws/
rosrun turtlesim turtlesim_node
source ./devel/setup.bash
rosrun tf02_dynamic demo01_dynamic_pub_p.py
rosrun tf02_dynamic demo02_dynamic_sub_p.py
控制乌龟运动
rosrun turtlesim turtle_teleop_key

#! /usr/bin/env python

"""
    订阅方: 订阅坐标变换消息,传入被转换的坐标,调用转换算法

    流程:
        1. 导包
        2. 初始化
        3. 创建订阅对象
        4. 组织被转换的坐标点
        5. 转换逻辑实现,调用tf封装的算法
        6. 输出结果
"""

import rospy
import tf2_ros
from tf2_geometry_msgs import tf2_geometry_msgs

if __name__ == "__main__":

    # 2. 初始化
    rospy.init_node("dynamic_sub_p")

    # 3. 创建订阅对象
    # 3.1 创建一个缓存对象 头文件2
    buffer = tf2_ros.Buffer()
    # 3.2 创建订阅对象(将缓存传入)
    sub = tf2_ros.TransformListener(buffer)

    # 4. 组织被转换的坐标点 头文件3
    ps = tf2_geometry_msgs.PointStamped()
    # 时间戳--0
    ps.header.stamp = rospy.Time()
    # 坐标系
    ps.header.frame_id = "turtle1"
    ps.point.x = 2.0
    ps.point.y = 3.0
    ps.point.z = 5.0

    # 5. 转换逻辑实现,调用tf封装的算法
    rate = rospy.Rate(1)
    while not rospy.is_shutdown():
        # 使用try防止
        try:
            # 转换实现
        """
            参数1: 被转换的坐标点
            参数2: 目标坐标系
            返回值: 转换后的坐标点

            PS:
            问题: 抛出异常 base_link 不存在
            原因: 转换函数调用时,可能还没有订阅到坐标系的相对关系
            解决: try 捕获异常,并处理
        """
            ps_out = buffer.transform(ps,"world")
            # 6. 输出结果
            rospy.loginfo("转换后的坐标:(%.2f,%.2f,%.2f),参考的坐标系: %s",
                        ps_out.point.x,
                        ps_out.point.y,
                        ps_out.point.z,
                        ps_out.header.frame_id)
        except Exception as e:
            rospy.logwarn("错误提示:%s",e) 
        
        rate.sleep()
  • 2
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

2021 Nqq

你的鼓励是我学习的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值