赞
踩
在ROS中内置一些比较实用的工具,通过这些工具可以方便快捷的实现某个功能或调试程序,从而提高开发效率,本章主要介绍ROS中内置的如下组件:
本章预期达成的学习目标:
案例演示: 小乌龟跟随实现,该案例是ros中内置案例,终端下键入启动命令
roslaunch turtle_tf2 turtle_tf2_demo_cpp.launch`或`roslaunch turtle_tf2 turtle_tf2_demo.launch
键盘可以控制一只乌龟运动,另一只跟随运动。
机器人系统上,有多个传感器,如激光雷达、摄像头等,有的传感器是可以感知机器人周边的物体方位(或者称之为:坐标,横向、纵向、高度的距离信息)的,以协助机器人定位障碍物,可以直接将物体相对该传感器的方位信息,等价于物体相对于机器人系统或机器人其它组件的方位信息吗?显示是不行的,这中间需要一个转换过程。更具体描述如下:
场景1:雷达与小车
现有一移动式机器人底盘,在底盘上安装了一雷达,雷达相对于底盘的偏移量已知,现雷达检测到一障碍物信息,获取到坐标分别为(x,y,z),该坐标是以雷达为参考系的,如何将这个坐标转换成以小车为参考系的坐标呢?
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4NwagZqa-1679557675569)(image/10TF01.png)]](https://img-blog.csdnimg.cn/0159072a6ef54212923c75247183d207.png)
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TgVT659n-1679557675570)(image/11TF02.png)]](https://img-blog.csdnimg.cn/d2552d6b4f2e45dd90968d05af86e717.png)
场景2:现有一带机械臂的机器人(比如:PR2)需要夹取目标物,当前机器人头部摄像头可以探测到目标物的坐标(x,y,z),不过该坐标是以摄像头为参考系的,而实际操作目标物的是机械臂的夹具,当前我们需要将该坐标转换成相对于机械臂夹具的坐标,这个过程如何实现?
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AgXC3bJW-1679557675571)(image/PR2坐标变换.png)]](https://img-blog.csdnimg.cn/253f425fcc624ebea23a61a63b606f67.png)
当然,根据我们高中学习的知识,在明确了不同坐标系之间的的相对关系,就可以实现任何坐标点在不同坐标系之间的转换,但是该计算实现是较为常用的,且算法也有点复杂,因此在 ROS 中直接封装了相关的模块: 坐标变换(TF)。
**tf:**TransForm Frame,坐标变换
**坐标系:**ROS 中是通过坐标系统开标定物体的,确切的将是通过右手坐标系来标定的。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qWRz68t1-1679557675572)(image/右手坐标系.jpg)]](https://img-blog.csdnimg.cn/59386d80447c409ebad8accaeec8f288.png)
在 ROS 中用于实现不同坐标系之间的点或向量的转换。
**小乌龟跟随案例:**如本章引言部分演示。
在ROS中坐标变换最初对应的是tf,不过在 hydro 版本开始, tf 被弃用,迁移到 tf2,后者更为简洁高效,tf2对应的常用功能包有:
tf2_geometry_msgs:可以将ROS消息转换成tf2消息。
tf2: 封装了坐标变换的常用消息。
tf2_ros:为tf2提供了roscpp和rospy绑定,封装了坐标变换常用的API。
另请参考:
订阅发布模型中数据载体 msg 是一个重要实现,首先需要了解一下,在坐标转换实现中常用的 msg:geometry_msgs/TransformStamped和geometry_msgs/PointStamped
前者用于传输坐标系相关位置信息,后者用于传输某个坐标系内坐标点的信息。在坐标变换中,频繁的需要使用到坐标系的相对关系以及坐标点信息。
命令行键入:rosmsg info geometry_msgs/TransformStamped
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
四元数用于表示坐标的相对姿态
命令行键入: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
另请参考:
所谓静态坐标变换,是指两个坐标系之间的相对位置是固定的。
需求描述:
现有一机器人模型,核心构成包含主体与雷达,各对应一坐标系,坐标系的原点分别位于主体与雷达的物理中心,已知雷达原点相对于主体原点位移关系如下: x 0.2 y0.0 z0.5。当前雷达检测到一障碍物,在雷达坐标系中障碍物的坐标为 (2.0 3.0 5.0),请问,该障碍物相对于主体的坐标是多少?
结果演示:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mDIJcRdl-1679557675574)(image/静态坐标变换_坐标系关系.PNG)]](https://img-blog.csdnimg.cn/64e18d7b5d1c4fd0b9611bcf30715b71.png)
实现分析:
**实现流程:**C++ 与 Python 实现流程一致
方案A:C++实现
创建项目功能包依赖于 tf2、tf2_ros、tf2_geometry_msgs、roscpp rospy std_msgs geometry_msgs
#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.创建发布对象; tf2_ros::StaticTransformBroadcaster pub; // 创建静态坐标转换广播器 // 4.组织被发布的消息; 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; tfs.transform.translation.y = 0.0; tfs.transform.translation.z = 0.5; //需要根据欧拉角转换 tf2::Quaternion qtn; //创建 四元数 对象 //向该对象设置欧拉角,这个对象可以将欧拉角转换成四元数 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; }
#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.创建订阅对象; ---->订阅坐标系相对关系 // 3.1 创建一个buffer缓存 tf2_ros::Buffer buffer; // 3.2 再创建监听对象(监听对象可以将订阅的数据存入buffer) tf2_ros::TransformListener listener(buffer); // 4.组织一个(障碍)坐标点数据;(相对于子级坐标系) geometry_msgs::PointStamped ps; ps.header.frame_id = "laser"; ps.header.stamp = ros::Time::now(); ps.point.x = 2.0; ps.point.y = 3.0; ps.point.z = 5.0; // 添加休眠 // ros::Duration(2).sleep(); // 5.转换算法(相对于父级坐标系)。需要调用TF内置实现 ros::Rate rate(10); while (ros::ok()) { //核心代码 --- 将 ps 转换成相对于 base_link 的坐标点 geometry_msgs::PointStamped ps_out; //新建一个坐标点,用于接收转换结果 /* 调用了 buffer 的转换函数 transform 参数1:被转移的坐标点 参数2:目标坐标系 返回值:输出坐标点 ps:调用时必须包含头文件 tf2_geometry_msgs/tf2_geometry_msgs.h ps:运行时存在的问题:抛出一个异常 base_link 不存在 原因:订阅数据是个耗时操作,可能调用 transform 转换函数时,坐标系的相对关系和还没有订阅到,因此出现异常 解决: 方案1:在调用转换函数前,执行休眠 方案2:进行异常处理(建议) */ try { 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_out.header.frame_id.c_str() ); } catch(const std::exception& e) { // std::cerr << e.what() << '\n'; ROS_INFO("异常消息:%s",e.what()); } rate.sleep(); ros::spinOnce(); } return 0; }
可以使用命令行或launch文件的方式分别启动发布节点与订阅节点,如果程序无异常,控制台将输出,坐标转换后的结果。
roscore
cd demo04_ws/
source ./devel/setup.bash
rosrun tf01_static demo01_static_pub
source ./devel/setup.bash
rosrun tf01_static demo01_static_sub
当坐标系之间的相对位置固定时,那么所需参数也是固定的: 父系坐标名称、子级坐标系名称、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
也建议使用该种方式直接实现静态坐标系相对信息发布。
可以借助于rviz显示坐标系关系,具体操作:
另请参考:
所谓动态坐标变换,是指两个坐标系之间的相对位置是变化的。
需求描述:
启动 turtlesim_node,该节点中窗体有一个世界坐标系(左下角为坐标系原点),乌龟是另一个坐标系,键盘控制乌龟运动,将两个坐标系的相对位置动态发布。
结果演示:

实现分析:
**实现流程:**C++ 与 Python 实现流程一致
方案A:C++实现
创建项目功能包依赖于 tf2、tf2_ros、tf2_geometry_msgs、roscpp rospy std_msgs geometry_msgs、turtlesim
#include "ros/ros.h" #include "turtlesim/Pose.h" #include "tf2_ros/transform_broadcaster.h" //这个用来发布动态的 #include "geometry_msgs/TransformStamped.h" #include "tf2/LinearMath/Quaternion.h" //欧拉角相关的 /* 发布方:需要订阅乌龟的位姿信息,转换成相对于窗体的坐标关系,并发布 准备: 话题:/turtle/pose 消息:/turtlesim/Pose 流程: 1.包含头文件 2.设置编码 初始化 NodeHandle; 3.创建订阅对象,订阅 /turtle1/pose; 4.回调函数处理订阅的消息:将位姿信息转换为坐标相对关系并发布(关注) 5.spin() */ void doPose(const turtlesim::Pose::ConstPtr &pose){ //获取位姿信息,转换成坐标系相对关系(核心),并发布 //a. 创建发布对象; static tf2_ros::TransformBroadcaster pub; //采用静态,每次访问的都是这个对象 //b.组织被发布的数据; 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; //坐标系四元数 /* 位姿信息中没有四元数,但是有个偏航角度,又知乌龟是 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; }
配置文件此处略。
#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.创建订阅对象; ---->订阅坐标系相对关系 // 3.1 创建一个buffer缓存 tf2_ros::Buffer buffer; // 3.2 再创建监听对象(监听对象可以将订阅的数据存入buffer) tf2_ros::TransformListener listener(buffer); // 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; // 添加休眠 // ros::Duration(2).sleep(); // 5.转换算法(相对于父级坐标系)。需要调用TF内置实现 ros::Rate rate(10); while (ros::ok()) { //核心代码 --- 将 ps 转换成相对于 base_link 的坐标点 geometry_msgs::PointStamped ps_out; //新建一个坐标点,用于接收转换结果 /* 调用了 buffer 的转换函数 transform 参数1:被转移的坐标点 参数2:目标坐标系 返回值:输出坐标点 ps:调用时必须包含头文件 tf2_geometry_msgs/tf2_geometry_msgs.h ps:运行时存在的问题:抛出一个异常 base_link 不存在 原因:订阅数据是个耗时操作,可能调用 transform 转换函数时,坐标系的相对关系和还没有订阅到,因此出现异常 解决: 方案1:在调用转换函数前,执行休眠 方案2:进行异常处理(建议) */ try { ps_out = buffer.transform(ps,"world"); // 6.最后输出。 ROS_INFO("转换后的坐标值:(%.2f,%.2f,%.2f),参考的坐标系:%s", ps_out.point.x, ps_out.point.y, ps_out.point.z, ps_out.header.frame_id.c_str() ); } catch(const std::exception& e) { // std::cerr << e.what() << '\n'; ROS_INFO("异常消息:%s",e.what()); } rate.sleep(); ros::spinOnce(); } return 0; }
需求描述:
现有坐标系统,父级坐标系统 world,下有两子级系统 son1,son2,son1 相对于 world,以及 son2 相对于 world 的关系是已知的,求 son1原点在 son2中的坐标,又已知在 son1中一点的坐标,要求求出该点在 son2 中的坐标
实现分析:
**实现流程:**C++ 与 Python 实现流程一致
方案A:C++实现
创建项目功能包依赖于 tf2、tf2_ros、tf2_geometry_msgs、roscpp rospy std_msgs geometry_msgs、turtlesim
为了方便,使用静态坐标变换发布
建立一个launch文件
<launch>
<!-- 发布 son1 相对于 world 以及 son2 相对于 world 的坐标关系 -->
<node pkg="tf2_ros" type="static_transform_publisher" name="son1" args="5 0 0 0 0 0 /world /son1" output="screen" />
<node pkg="tf2_ros" type="static_transform_publisher" name="son2" args="3 0 0 0 0 0 /world /son2" output="screen" />
</launch>
/* 需求: 现有坐标系统,父级坐标系统 world,下有两子级系统 son1,son2, son1 相对于 world,以及 son2 相对于 world 的关系是已知的, 求 son1 与 son2中的坐标关系,又已知在 son1中一点的坐标,要求求出该点在 son2 中的坐标 */ #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" #include "geometry_msgs/TransformStamped.h" /* 订阅方实现:1.计算son1与son2的相对关系 2.计算son1的中某个坐标点在 son2 中的坐标值 流程: 1.包含头文件 2.编码 初始化 NodeHandle; 3.创建订阅对象; 4.编写解析逻辑; 5.spinOnce(); */ int main(int argc, char *argv[]) { // 2.编码 初始化 NodeHandle; setlocale(LC_ALL,""); ros::init(argc,argv,"tfs_sub"); ros::NodeHandle nh; // 3.创建订阅对象; tf2_ros::Buffer buffer; tf2_ros::TransformListener sub(buffer); // 4.编写解析逻辑; // 创建坐标点 geometry_msgs::PointStamped psAtSon1; psAtSon1.header.stamp = ros::Time::now(); psAtSon1.header.frame_id = "son1"; psAtSon1.point.x = 1.0; psAtSon1.point.y = 2.0; psAtSon1.point.z = 3.0; ros::Rate rate(10); while (ros::ok()) { //核心 try { //1.计算son1与son2的相对关系 /* A 相对于 B 的坐标系关系 参数1:目标坐标系 参数2:源坐标系 参数3:ros::Time(0) 取间隔最短的两个坐标关系帧计算相对关系 返回值:geometry_msgs::TransformStamped 源相对于目标坐标系的相对关系 */ geometry_msgs::TransformStamped son1ToSon2 = buffer.lookupTransform("son2","son1",ros::Time(0)); ROS_INFO("son1 相对于 son2的信息: 父级:%s,子级:%s 偏移量(%.2f,%.2f,%.2f)", son1ToSon2.header.frame_id.c_str(), //son2 son1ToSon2.child_frame_id.c_str(), //son1 son1ToSon2.transform.translation.x, son1ToSon2.transform.translation.y, son1ToSon2.transform.translation.z); //2.计算son1的中某个坐标点在 son2 中的坐标值 geometry_msgs::PointStamped psAtSon2 = buffer.transform(psAtSon1,"son2"); ROS_INFO("坐标点在 son2 中的值(%.2f,%.2f,%.2f)", psAtSon2.point.x, psAtSon2.point.y, psAtSon2.point.z); } catch(const std::exception& e) { ROS_INFO("错误提示:%s",e.what()); } rate.sleep(); ros::spinOnce(); } // 5.spinOnce(); return 0; }
可以使用命令行或launch文件的方式分别启动发布节点与订阅节点,如果程序无异常,将输出换算后的结果。
在机器人系统中,涉及的坐标系有多个,为了方便查看,ros 提供了专门的工具,可以用于生成显示坐标系关系的 pdf 文件,该文件包含树形结构的坐标系图谱。
首先调用rospack find tf2_tools查看是否包含该功能包,如果没有,请使用如下命令安装:
sudo apt install ros-noetic-tf2-tools
启动坐标系广播程序之后,运行如下命令:
rosrun tf2_tools view_frames.py
会产生类似于下面的日志信息:
[INFO] [1592920556.827549]: Listening to tf data during 5 seconds...
[INFO] [1592920561.841536]: Generating graph in frames.pdf file...
查看当前目录会生成一个 frames.pdf 文件
可以直接进入目录打开文件,或者调用命令查看文件:evince frames.pdf
需求描述:
程序启动之初: 产生两只乌龟,中间的乌龟(A) 和 左下乌龟(B), B 会自动运行至A的位置,并且键盘控制时,只是控制 A 的运动,但是 B 可以跟随 A 运行
结果演示:

实现分析:
乌龟跟随实现的核心,是乌龟A和B都要发布相对世界坐标系的坐标信息,然后,订阅到该信息需要转换获取A相对于B坐标系的信息,最后,再生成速度信息,并控制B运动。
**实现流程:**C++ 与 Python 实现流程一致
准备工作:
1.了解如何创建第二只乌龟,且不受键盘控制
创建第二只乌龟需要使用rosservice,话题使用的是 spawn
rosservice call /spawn "x: 1.0
y: 1.0
theta: 1.0
name: 'turtle_flow'"
name: "turtle_flow"
键盘是无法控制第二只乌龟运动的,因为使用的话题: /第二只乌龟名称/cmd_vel,对应的要控制乌龟运动必须发布对应的话题消息
2.了解如何获取两只乌龟的坐标
是通过话题 /乌龟名称/pose 来获取的
x: 1.0 //x坐标
y: 1.0 //y坐标
theta: -1.21437060833 //角度
linear_velocity: 0.0 //线速度
angular_velocity: 1.0 //角速度
方案A:C++实现
创建项目功能包依赖于 tf2、tf2_ros、tf2_geometry_msgs、roscpp rospy std_msgs geometry_msgs、turtlesim
#include "ros/ros.h" #include "turtlesim/Spawn.h" /* 需求:是向服务端发送请求,生成一只新的乌龟 话题:/spawn 消息:turtlesim/Spawn 1.包含头文件 2.初始化ROS节点 3.创建节点句柄 4.创建客户端 5.组织数据并发送 6.处理响应 */ int main(int argc, char *argv[]) { setlocale(LC_ALL,""); // 2.初始化ROS节点 ros::init(argc,argv,"service_call"); // 3.创建节点句柄 ros::NodeHandle nh; // 4.创建客户端 ros::ServiceClient client = nh.serviceClient<turtlesim::Spawn>("/spawn"); // 5.组织数据并发送 //5.1 组织请求数据 turtlesim::Spawn spawn; spawn.request.x = 1.0; spawn.request.y = 4.0; spawn.request.theta = 1.57; spawn.request.name = "turtle2"; //5.2 发送请求 //判断服务器状态 //ros::service::waitForService("/spawn"); client.waitForExistence(); bool flag = client.call(spawn); //flag 接收响应状态,响应结果也会被设置进spawn对象 // 6.处理响应 if (flag){ ROS_INFO("乌龟生成成功,新乌龟叫:%s",spawn.response.name.c_str()); }else{ ROS_INFO("请求失败!!!!"); } return 0; }
可以订阅乌龟的位姿信息,然后再转换成坐标信息,两只乌龟的实现逻辑相同,只是订阅的话题名称,生成的坐标信息等稍有差异,可以将差异部分通过参数传入:
/* 该文件实现:需要订阅 turtle1 和 turtle2 的 pose,然后广播相对 world 的坐标系信息 注意: 订阅的两只 turtle,除了命名空间(turtle1 和 turtle2)不同外, 其他的话题名称和实现逻辑都是一样的, 所以我们可以将所需的命名空间通过 args 动态传入 实现流程: 1.包含头文件 2.初始化 ros 节点 3.解析传入的命名空间 4.创建 ros 句柄 5.创建订阅对象 6.回调函数处理订阅的 pose 信息 6-1.创建 TF 广播器 6-2.将 pose 信息转换成 TransFormStamped 6-3.发布 7.spin */ #include "ros/ros.h" #include "turtlesim/Pose.h" #include "tf2_ros/transform_broadcaster.h" //这个用来发布动态的 #include "geometry_msgs/TransformStamped.h" #include "tf2/LinearMath/Quaternion.h" //欧拉角相关的 /* 发布方:需要订阅乌龟的位姿信息,转换成相对于窗体的坐标关系,并发布 准备: 话题:/turtle/pose 消息:/turtlesim/Pose 流程: 1.包含头文件 2.设置编码 初始化 NodeHandle; 3.创建订阅对象,订阅 /turtle1/pose; 4.回调函数处理订阅的消息:将位姿信息转换为坐标相对关系并发布(关注) 5.spin() */ //声明变量接收传递的参数 std::string turtle_name; void doPose(const turtlesim::Pose::ConstPtr &pose){ //获取位姿信息,转换成坐标系相对关系(核心),并发布 //a. 创建发布对象; static tf2_ros::TransformBroadcaster pub; //采用静态,每次访问的都是这个对象 //b.组织被发布的数据; geometry_msgs::TransformStamped ts; ts.header.frame_id = "world"; ts.header.stamp = ros::Time::now(); // 关键点2:动态传入 //ts.child_frame_id = "turtle1"; ts.child_frame_id = turtle_name; //坐标系偏移量设置 ts.transform.translation.x = pose->x; ts.transform.translation.y = pose->y; ts.transform.translation.z = 0; //坐标系四元数 /* 位姿信息中没有四元数,但是有个偏航角度,又知乌龟是 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; /* 解析 launch 文件通过 args 传入的参数 */ if (argc != 2) { ROS_ERROR("请传入一个参数"); return 1; }else{ turtle_name = argv[1]; } // 3.创建订阅对象,订阅 /turtle1/pose; // 关键点1:订阅的话题名称,turtle1 或 turtle2 动态传入 ros::Subscriber sub = nh.subscribe(turtle_name + "/pose",100,doPose); // 4.回调函数处理订阅的消息:将位姿信息转换为坐标相对关系并发布(关注) // 5.spin() ros::spin(); return 0; }
#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" #include "geometry_msgs/TransformStamped.h" #include "geometry_msgs/Twist.h" /* 需求1:换算出 turtle1 相对于 turtle2 的关系 需求2:计算角速度和线速度并发布 */ int main(int argc, char *argv[]) { // 2.编码 初始化 NodeHandle; setlocale(LC_ALL,""); ros::init(argc,argv,"tfs_sub"); ros::NodeHandle nh; // 3.创建订阅对象; tf2_ros::Buffer buffer; tf2_ros::TransformListener sub(buffer); //A.创建发布对象 ros::Publisher pub = nh.advertise<geometry_msgs::Twist>("/turtle2/cmd_vel",100); // 4.编写解析逻辑; ros::Rate rate(10); while (ros::ok()) { //核心 try { //1.计算son1与son2的相对关系 /* A 相对于 B 的坐标系关系 参数1:目标坐标系 参数2:源坐标系 参数3:ros::Time(0) 取间隔最短的两个坐标关系帧计算相对关系 返回值:geometry_msgs::TransformStamped 源相对于目标坐标系的相对关系 */ geometry_msgs::TransformStamped son1ToSon2 = buffer.lookupTransform("turtle2","turtle1",ros::Time(0)); // ROS_INFO("son1 相对于 son2的信息: 父级:%s,子级:%s 偏移量(%.2f,%.2f,%.2f)", // son1ToSon2.header.frame_id.c_str(), //turtle2 // son1ToSon2.child_frame_id.c_str(), //turtle1 // son1ToSon2.transform.translation.x, // son1ToSon2.transform.translation.y, // son1ToSon2.transform.translation.z); //B.根据相对计算并组织速度消息 geometry_msgs::Twist twist; /* 组织速度,只需要设置线速度的 x 与 角速度的 z x = 系数 * 开方(y^2 + x^2) z = 系数 * 反正切(对边,邻边) */ twist.linear.x = 0.5 * sqrt(pow(son1ToSon2.transform.translation.x,2) + pow(son1ToSon2.transform.translation.y,2)); twist.angular.z = 4 * atan2(son1ToSon2.transform.translation.y,son1ToSon2.transform.translation.x); //C.发布 pub.publish(twist); } catch(const std::exception& e) { ROS_INFO("错误提示:%s",e.what()); } rate.sleep(); ros::spinOnce(); } // 5.spinOnce(); return 0; }
使用 launch 文件组织需要运行的节点,内容示例如下:
<launch> <!-- 1.启动乌龟GUI节点 --> <node pkg="turtlesim" type="turtlesim_node" name="turtle1" output="screen" /> <node pkg="turtlesim" type="turtle_teleop_key" name="key" output="screen" /> <!-- 2.生成新的乌龟的节点 --> <node pkg="tf04_test" type="test01_new_turtle" name="turtle2" output="screen" /> <!-- 3.需要启动两个乌龟相对于世界的坐标关系的发布 --> <!-- 基本实现思路: 1.节点只编写一个 2.这个节点需要启动两次 3.节点启动时动态传参:第一次启动传递turtle1 第二次启动传递turtle2 --> <node pkg="tf04_test" type="test02_pub_turtle" name="pub1" args="turtle1" output="screen" /> <node pkg="tf04_test" type="test02_pub_turtle" name="pub2" args="turtle2" output="screen" /> <!-- 4. 需要订阅 turtle1 与 turtle2 相对于世界坐标系的坐标消息,并转换成 turtle1 相对于 turtle2 的坐标关系 再生成速度消息 --> <node pkg="tf04_test" type="test03_control_turtle2" name="control" args="turtle2" output="screen" /> </launch>
tf包,TF2 对应的是tf2和tf2_ros包,在 TF2 中不同类型的 API 实现做了分包处理。接下来,我们通过静态坐标变换来演示TF2的实现效率。
TF2 版静态坐标变换:rosrun tf2_ros static_transform_publisher 0 0 0 0 0 0 /base_link /laser
TF 版静态坐标变换:rosrun tf static_transform_publisher 0 0 0 0 0 0 /base_link /laser 100
会发现,TF 版本的启动中最后多一个参数,该参数是指定发布频率
使用rostopic查看话题,包含/tf与/tf_static, 前者是 TF 发布的话题,后者是 TF2 发布的话题,分别调用命令打印二者的话题消息
rostopic echo /tf: 当前会循环输出坐标系信息
rostopic echo /tf_static: 坐标系信息只有一次
如果是静态坐标转换,那么不同坐标系之间的相对状态是固定的,既然是固定的,那么没有必要重复发布坐标系的转换消息,很显然的,tf2 实现较之于 tf 更为高效
坐标变换在机器人系统中是一个极其重要的组成模块,在 ROS 中 TF2 组件是专门用于实现坐标变换的,TF2 实现具体内容又主要介绍了如下几部分:
1.静态坐标变换广播器,可以编码方式或调用内置功能包来实现(建议后者),适用于相对固定的坐标系关系
2.动态坐标变换广播器,以编码的方式广播坐标系之间的相对关系,适用于易变的坐标系关系
3.坐标变换监听器,用于监听广播器广播的坐标系消息,可以实现不同坐标系之间或同一点在不同坐标系之间的变换
4.机器人系统中的坐标系关系是较为复杂的,还可以通过 tf2_tools 工具包来生成 ros 中的坐标系关系图
5.当前 TF2 已经替换了 TF,官网建议直接学习 TF2,并且 TF 与 TF2 的使用流程与实现 API 比较类似,只要有任意一方的使用经验,另一方也可以做到触类旁通
机器人传感器获取到的信息,有时我们可能需要时时处理,有时可能只是采集数据,事后分析,比如:
机器人导航实现中,可能需要绘制导航所需的全局地图,地图绘制实现,有两种方式,方式1:可以控制机器人运动,将机器人传感器感知到的数据时时处理,生成地图信息。方式2:同样是控制机器人运动,将机器人传感器感知到的数据留存,事后,再重新读取数据,生成地图信息。两种方式比较,显然方式2使用上更为灵活方便。
在ROS中关于数据的留存以及读取实现,提供了专门的工具: rosbag。
是用于录制和回放 ROS 主题的一个工具集。
实现了数据的复用,方便调试、测试。
rosbag本质也是ros的节点,当录制时,rosbag是一个订阅节点,可以订阅话题消息并将订阅到的数据写入磁盘文件;当重放时,rosbag是一个发布节点,可以读取磁盘文件,发布文件中的话题消息。
另请参考:
需求:
ROS 内置的乌龟案例并操作,操作过程中使用 rosbag 录制,录制结束后,实现重放
实现:
1.准备
创建目录保存录制的文件
mkdir ./xxx
cd xxx
2.开始录制
rosbag record -a -O 目标文件
操作小乌龟一段时间,结束录制使用 ctrl + c,在创建的目录中会生成bag文件。
roscore
rosrun turtlesim turtlesim_node
rosrun turtlesim turtle_teleop_key
3.查看文件
rosbag info 文件名
4.回放文件
rosbag play 文件名
重启乌龟节点,会发现,乌龟按照录制时的轨迹运动。
另请参考:
命令实现不够灵活,可以使用编码的方式,增强录制与回放的灵活性,本节将通过简单的读写实现演示rosbag的编码实现。
方案A:C++实现
#include "ros/ros.h" #include "rosbag/bag.h" #include "std_msgs/String.h" /* 需求:使用 rosbag 向磁盘文件写出数据 (话题 + 消息) 流程: 1.导包 2.初始化操作; 3.创建rosbag对象; 4.打开文件流; 5.写数据; 6.关闭文件流。 */ int main(int argc, char *argv[]) { // 2.初始化操作; setlocale(LC_ALL,""); ros::init(argc,argv,"bag_write"); ros::NodeHandle nd; // 3.创建rosbag对象; rosbag::Bag bag; // 4.打开文件流; bag.open("hello.bag",rosbag::BagMode::Write); // 5.写数据; std_msgs::String msg; msg.data = "hello xxxx"; /* 参数1:话题 参数2:时间戳 参数3:消息 */ bag.write("/chatter",ros::Time::now(),msg); bag.write("/chatter",ros::Time::now(),msg); bag.write("/chatter",ros::Time::now(),msg); bag.write("/chatter",ros::Time::now(),msg); // 6.关闭文件流。 bag.close(); return 0; }
#include "ros/ros.h" #include "rosbag/bag.h" #include "rosbag/view.h" #include "std_msgs/String.h" /* 需求:使用 rosbag 向磁盘文件写出数据 (话题 + 消息) 流程: 1.导包 2.初始化操作; 3.创建rosbag对象; 4.打开文件流(以读的方式打开); 5.读数据; 6.关闭文件流。 */ int main(int argc, char *argv[]) { // 2.初始化操作; setlocale(LC_ALL,""); ros::init(argc,argv,"bag_read"); ros::NodeHandle nh; // 3.创建rosbag对象; rosbag::Bag bag; // 4.打开文件流(以读的方式打开); bag.open("hello.bag",rosbag::BagMode::Read); // 5.读数据;消息 // 取出话题,时间戳和消息内容 //可以先获取消息的集合,再迭代取出消息的字段 for (auto &&m : rosbag::View(bag)) { //解析 std::string topic = m.getTopic(); ros::Time time = m.getTime(); std_msgs::StringPtr p = m.instantiate<std_msgs::String>(); ROS_INFO("解析的内容,话题:%s,时间戳:%.2f,消息值:%s", topic.c_str(), time.toSec(), p->data.c_str() ); } // 6.关闭文件流。 bag.close(); return 0; }
另请参考:
之前,在 ROS 中使用了一些实用的工具,比如: ros_bag 用于录制与回放、tf2_tools 可以生成 TF 树 … 这些工具大大提高了开发的便利性,但是也存在一些问题: 这些工具的启动和使用过程中涉及到一些命令操作,应用起来不够方便,在ROS中,提供了rqt工具箱,在调用工具时以图形化操作代替了命令操作,应用更便利,提高了操作效率,优化了用户体验。
ROS基于 QT 框架,针对机器人开发提供了一系列可视化的工具,这些工具的集合就是rqt
可以方便的实现 ROS 可视化调试,并且在同一窗口中打开多个部件,提高开发效率,优化用户体验。
rqt 工具箱组成有三大部分
另请参考:
一般只要你安装的是desktop-full版本就会自带工具箱
如果需要安装可以以如下方式安装
$ sudo apt-get install ros-noetic-rqt
$ sudo apt-get install ros-noetic-rqt-common-plugins
rqt的启动方式有两种:
rqtrosrun rqt_gui rqt_gui启动 rqt 之后,可以通过 plugins 添加所需的插件
**简介:**可视化显示计算图
**启动:**可以在 rqt 的 plugins 中添加,或者使用rqt_graph启动
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y2XfBYmj-1679557675576)(image/02_rqt_graph插件.png)]](https://img-blog.csdnimg.cn/c5bbceb46a954d7ea5e849deee86bf58.png)
圆框是节点,方框是话题,箭头是一个数据的发布订阅方向
**简介:**rqt_console 是 ROS 中用于显示和过滤日志的图形化插件
**准备:**编写 Node 节点输出各个级别的日志信息
**简介:**rqt_console 是 ROS 中用于显示和过滤日志的图形化插件
**准备:**编写 Node 节点输出各个级别的日志信息
/* ROS 节点:输出各种级别的日志信息 */ #include "ros/ros.h" int main(int argc, char *argv[]) { ros::init(argc,argv,"log_demo"); ros::NodeHandle nh; ros::Rate r(0.3); while (ros::ok()) { ROS_DEBUG("Debug message d"); ROS_INFO("Info message oooooooooooooo"); ROS_WARN("Warn message wwwww"); ROS_ERROR("Erroe message EEEEEEEEEEEEEEEEEEEE"); ROS_FATAL("Fatal message FFFFFFFFFFFFFFFFFFFFFFFFFFFFF"); r.sleep(); } return 0; }
启动:
可以在 rqt 的 plugins 中添加,或者使用rqt_console启动![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZL1xG8H3-1679557675577)(image/01_rqt_console插件.png)]](https://img-blog.csdnimg.cn/f8e78cd1074f4f589271784e8b68174e.png)
**简介:**图形绘制插件,可以以 2D 绘图的方式绘制发布在 topic 上的数据
**准备:**启动 turtlesim 乌龟节点与键盘控制节点,通过 rqt_plot 获取乌龟位姿
**启动:**可以在 rqt 的 plugins 中添加,或者使用rqt_plot启动![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Rn0QOrug-1679557675578)(image/03_rqt_plot插件.png)]](https://img-blog.csdnimg.cn/e3631df694044a6a9a7387901fa992c6.png)
**简介:**录制和重放 bag 文件的图形化插件
**准备:**启动 turtlesim 乌龟节点与键盘控制节点
**启动:**可以在 rqt 的 plugins 中添加,或者使用rqt_bag启动
录制:![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5cjsvpdf-1679557675578)(image/14rqt_bag_录制.png)]](https://img-blog.csdnimg.cn/87d3fa6c684a47169536f6992f053bca.png)
重放:![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oV6PhTrp-1679557675579)(image/15rqt_bag_回放.png)]](https://img-blog.csdnimg.cn/e7d1f9e1390e4a36bcb4c1b64d16c2d4.png)
本章主要介绍了ROS中的常用组件,内容如下:
其中 TF坐标变换是重点,也是难点,需要大家熟练掌握坐标变换的应用场景以及代码实现。下一章开始将介绍机器人系统仿真,我们将在仿真环境下,创建机器人、控制机器人运动、搭建仿真环境,并以机器人的视角去感知世界。
参考和致谢:
https://www.bilibili.com/video/BV1Ci4y1L7ZZ?p=1&vd_source=45da538703ca980e21d6c2bff2a7c144
http://www.autolabor.com.cn/book/ROSTutorials/di-5-zhang-ji-qi-ren-dao-hang.html
万分感谢赵老师事无巨细的视频教学和清晰明了的笔记~
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。