ROS高效进阶第一章 -- ROS高级组件之 TF坐标变换 其二

1 资料

本文是TF坐标变换系列的第二篇,我们分别使用cpp和python实现turtle_tf。本文的参考资料如下:
(1)TF坐标变换系列的第一篇,为本文提供了所有的背景知识:ROS高效进阶第一章 – ROS高级组件之 TF坐标变换 其一
(2)《ROS机器人开发实践》胡春旭 第4章第二节

2 正文

2.1 cpp版turtle_tf

(1)创建 learning_tf 软件包,以及相应的文件

cd ~/catkin_ws/src
catkin_create_pkg learning_tf roscpp rospy tf turtlesim

cd learning_tf 
mkdir launch scripts
touch launch/start_cpp.launch launch/start_py.launch
touch src/turtle_tf_broadcaster.cpp src/turtle_tf_listener.cpp
touch scripts/turtle_tf_broadcaster.py scripts/turtle_tf_listener.p

(2)turtle_tf_broadcaster.cpp
对于代码注释里提到的欧拉角和四元数,以及turtlesim::Pose的具体解释,见:ROS高效进阶第一章 – ROS高级组件之 TF坐标变换 其一

#include <ros/ros.h>
#include <tf/transform_broadcaster.h>
#include <turtlesim/Pose.h>
#include "cxxopts.hpp"
// poseCb()用于处理turtlesim::Pose消息,根据当前的pose消息内容,制作并广播指定turtle坐标系相对于world坐标系的tf数据
void poseCb(const turtlesim::PoseConstPtr& msg, tf::TransformBroadcaster& br, const std::string& turtle_name) {
    
    
    // 创建tf数据对象
    tf::Transform tf_data;
    // turtlesim::Pose的x,y值,z为0,就是turtle坐标系相对于world坐标系的平移向量
    tf_data.setOrigin(tf::Vector3(msg->x, msg->y, 0.0));
    // 创建四元数类,描述旋转
    tf::Quaternion q;
    // Quaternion提供设置欧拉角的接口,内部转换为四元数。
    // turtlesim::Pose的theta值,就是turtle坐标系相对于world坐标系的yaw(绕z轴)值,因为是平面,其余两个为0
    q.setRPY(0, 0, msg->theta);
    // 将四元数传给tf数据
    tf_data.setRotation(q);
	// 发送一个带时间戳的tf数据,其表示在world坐标系下,指定turtle坐标系的平移和旋转关系
	// 由于turtlesim::Pose的频率是50hz,因此这个tf广播器的频率也是50hz
    br.sendTransform(tf::StampedTransform(tf_data, ros::Time::now(), "world", turtle_name));
}

int main(int argc, char ** argv) {
    
    
    ros::init(argc, argv, "ycao_tf_broadcaster");

    // https://github.com/jarro2783/cxxopts
    cxxopts::Options options(argv[0], "specify turtle name");
    options.add_options()
        ("n,name", "Name of the turtle", cxxopts::value<std::string>())
        ("h,help", "show help");

    auto result = options.parse(argc, argv);
    if (result.count("help")) {
    
    
        ROS_INFO("%s", options.help().c_str());
        return 0;
    }
	// -n指定坐标系名,即turtle1或turtle2,该程序将广播指定坐标系相对于word坐标系的tf数据(平移和旋转)
    if (!result.count("name")) {
    
    
        ROS_ERROR("please specify turtle name using -n");
        return -1;
    }     

    std::string turtle_name = result["name"].as<std::string>();
    std::string pose_topic = turtle_name+"/pose";
    // 创建tf广播器,并传入turtlesim::Pose的回调函数,用来发送tf
    tf::TransformBroadcaster br;
    ros::NodeHandle nh;
    ros::Subscriber sub = nh.subscribe<turtlesim::Pose>(pose_topic, 10, std::bind(poseCb, std::placeholders::_1, std::ref(br), turtle_name));

	ros::spin();
    return 0;
}

(3)turtle_tf_listener.cpp
对于代码注释里提到的geometry_msgs::Twist的具体解释,见:ROS高效进阶第一章 – ROS高级组件之 TF坐标变换 其一

#include <ros/ros.h>
#include <tf/transform_listener.h>
#include <geometry_msgs/Twist.h>
#include <turtlesim/Spawn.h>

int main(int argc, char ** argv) {
    
    
    ros::init(argc, argv, "ycao_tf_listener");
    ros::NodeHandle nh;
    // 调用/spawn服务,生成turtle2
    ros::ServiceClient spawnClient = nh.serviceClient<turtlesim::Spawn>("/spawn");
    ros::service::waitForService("/spawn");
    turtlesim::Spawn::Request req;
    turtlesim::Spawn::Response resp;
    bool ok = spawnClient.call(req, resp);
    if (ok) {
    
    
        ROS_INFO("spawned a turtle named %s", resp.name.c_str());
    } else {
    
    
        ROS_ERROR("Failed to spawn");
    }
	// 创建turtle2的行动控制topic
    ros::Publisher cmd_pub = nh.advertise<geometry_msgs::Twist>("/turtle2/cmd_vel", 10);
	// 创建tf 监听器,以10hz的频率,监听相对于turtle2坐标系,turtle1坐标系的tf值(平移和旋转)
    tf::TransformListener listener;
    ros::Rate loop_rate(10);
    while (nh.ok()) {
    
    
        tf::StampedTransform tf_data;
        try {
    
    
			listener.waitForTransform("/turtle2", "/turtle1", ros::Time(0), ros::Duration(3.0));
			// 监听相对于turtle2坐标系,turtle1坐标系的tf值(平移和旋转)
			listener.lookupTransform("/turtle2", "/turtle1", ros::Time(0), tf_data);            
        } catch (tf::TransformException &ex) {
    
    
            ROS_ERROR("%s", ex.what());
            ros::Duration(1.0).sleep();
            continue;
        }
        
		// 根据turtle1相对于turtle2坐标系的tf值,设置turtle2的运动指令,让turtle2跟着turtle1跑
        geometry_msgs::Twist vel;
        
        // getOrigin()返回相对于turtle2坐标系,turtle1坐标原点的位置,也就是turtle1的位置
        // getRotation()返回相对于turtle2坐标系,turtle1坐标系的旋转角度,用四元数表示,也就是turtle1的朝向,这里用不到这个信息
        
        // atan2(y,x)一种反正切函数,返回turtle1与turtle2坐标原点连线与turtle2坐标系x轴正向之间的夹角,如果turtle1位于turtle2坐标系x轴上方,则为正值,反之为负值
        // 4.0是个经验值,当两者夹角比较大时,turtle2会更快的转向turtle1,随着夹角变小,转的越来越慢,直到夹角为0        
        vel.linear.x = 0.5 * sqrt(pow(tf_data.getOrigin().x(), 2) + pow(tf_data.getOrigin().y(), 2));
        
        // sqrt()是求turtle1与turtle两者的距离,也就是x平方加y平方,然后开根号。
        // 0.5是经验值,距离越大,turtle2的速度越大,随着距离减小,速度越来越慢,直到距离为0
        // 0.5可以控制turtle2的行进速度过快,避免出现“横冲直撞”,导致跟不住turtle1        
        vel.angular.z = 4.0 * atan2(tf_data.getOrigin().y(), tf_data.getOrigin().x());

        // 下面的改动,可以让turtle1控制turtle2的转向,即turtle2跟随turtle1转,但turtle2追不上turtle1,也很好玩
        /*
        double roll, pitch, yaw;
        tf::Quaternion qua = tf_data.getRotation();
        tf::Matrix3x3(qua).getRPY(roll, pitch, yaw);
        vel.angular.z = 4.0 * yaw;
		*/
		
        cmd_pub.publish(vel);
        loop_rate.sleep();
        ros::spinOnce();
    }
    return 0;
}

(4)start_cpp.launch

 <launch>
    <!-- Turtlesim Node-->
    <node pkg="turtlesim" type="turtlesim_node" name="sim"/>
    <node pkg="turtlesim" type="turtle_teleop_key" name="teleop" output="screen"/>

	<!-- 广播两个turtle坐标系相对于word坐标系的tf -->
    <node pkg="learning_tf" type="learning_tf_broadcaster" args="-n /turtle1" name="turtle1_tf_broadcaster" />
    <node pkg="learning_tf" type="learning_tf_broadcaster" args="-n /turtle2" name="turtle2_tf_broadcaster" />
	
	<!-- 创建turtle2,监听turtle1相对于turtle2坐标系的tf值,根据tf值设置turtle2的运动指令,让turtle2跟着turtle1跑 -->
    <node pkg="learning_tf" type="learning_tf_listener" name="listener" />
  </launch>

2.2 python版turtle_tf

(1)turtle_tf_broadcaster.py

#! /usr/bin/env python3
import roslib
import rospy
from functools import partial
import tf
from turtlesim.msg import Pose

def pose_cb(msg, tf_br, turtlename):
  # 设置指定turtle坐标系相对于work坐标系的平移数据
  translate_date = (msg.x, msg.y, 0)
  # quaternion_from_euler直译过来就是来自欧拉角的四元数
  # 设置指定turtle坐标系相对于world坐标系的旋转数据
  rotation_data = tf.transformations.quaternion_from_euler(0, 0, msg.theta)
  # 发送相对于world坐标系,指定turtle坐标系的tf(注意:这里的world写在后面,cpp里是写在前面)
  tf_br.sendTransform(translate_date, rotation_data, rospy.Time.now(), turtlename, "world")

def main():
  rospy.init_node("turtle_tf_broadcaster")
  
  # python版的tf广播器,利用全局参数传参
  # rospy.get_param()是python的参数获取接口,cpp里是ros::param::get()
  turtlename = rospy.get_param("~turtle")

  pose_topic = "/%s/pose" %turtlename
  tf_br = tf.TransformBroadcaster()
  bind_pose_cb = partial(pose_cb, tf_br=tf_br, turtlename=turtlename)
  rospy.Subscriber(pose_topic, Pose, bind_pose_cb)
  rospy.spin()

if __name__ == "__main__":
  main()

(2)turtle_tf_listener.py
实现的逻辑与cpp版,完全一样

#!/usr/bin/env python3
import roslib
import rospy
import math
import tf
from geometry_msgs.msg import Twist
from turtlesim.srv import Spawn

def main():
  rospy.init_node("turtle_tf_listener")
  
  rospy.wait_for_service("spawn")
  client = rospy.ServiceProxy("spawn", Spawn)
  try:
    req = SpawnRequest()
    resp = client(req)
    rospy.loginfo("spawned a turtle name %s" %resp.name)
  except rospy.ServiceException as e:
      rospy.loginfo("service call fail: %s" %e)

  cmd_pub= rospy.Publisher("/turtle2/cmd_vel", Twist, queue_size=10)
  
  tf_listener = tf.TransformListener()
  rate = rospy.Rate(10)
  while not rospy.is_shutdown():
    try:
      (translation, rotation) = tf_listener.lookupTransform("/turtle2", "turtle1", rospy.Time(0))
    except (tf.LookupException, tf.ConnectivityException, tf.ExtrapolationException):
      continue

    cmd_vel = Twist()
    cmd_vel.linear.x= 0.5 * math.sqrt(trans[0] ** 2 + trans[1] ** 2)
    cmd_vel.angular.z = 4 * math.atan2(translation[1], translation[0])

    # 下面的改动,可以让turtle1控制turtle2的转向,即turtle2跟随turtle1转,但turtle2追不上turtle1
    # euler = (roll, pitch, yaw)    
    '''
    quaternion = rotation
    euler = tf.transformations.euler_from_quaternion(quaternion)
    cmd_vel.angular.z = 4 * euler[2]
    '''
    
    cmd_pub.publish(cmd_vel)
    rate.sleep()

if __name__ == "__main__":
  main()

(3)start_py.launch
参考上面的start_cpp.launch就能看懂,唯一的区别就是使用全局参数给 tf 广播器传参

 <launch>
    <!-- Turtlesim Node-->
    <node pkg="turtlesim" type="turtlesim_node" name="sim"/>
    <node pkg="turtlesim" type="turtle_teleop_key" name="teleop" output="screen"/>

    <node pkg="learning_tf" type="turtle_tf_broadcaster.py" name="turtle1_tf_broadcaster" respawn="false" output="screen">
      <param name="turtle" type="string" value="turtle1"/>
    </node>
    <node pkg="learning_tf" type="turtle_tf_broadcaster.py" name="turtle2_tf_broadcaster" respawn="false" output="screen">
      <param name="turtle" type="string" value="turtle2"/>
    </node>

    <node pkg="learning_tf" type="learning_tf_listener" name="listener" />
  </launch>

2.3 编译和运行

(1)CMakeLists.txt

cmake_minimum_required(VERSION 3.0.2)
project(learning_tf)
find_package(catkin REQUIRED COMPONENTS
  roscpp
  rospy
  tf
  turtlesim
)
catkin_package()
include_directories(
  ${
    
    catkin_INCLUDE_DIRS}
  include/learning_tf
)
add_executable(${
    
    PROJECT_NAME}_broadcaster src/turtle_tf_broadcaster.cpp)
add_executable(${
    
    PROJECT_NAME}_listener src/turtle_tf_listener.cpp)
target_link_libraries(${
    
    PROJECT_NAME}_broadcaster
  ${
    
    catkin_LIBRARIES}
)
target_link_libraries(${
    
    PROJECT_NAME}_listener
  ${
    
    catkin_LIBRARIES}
)
catkin_install_python(PROGRAMS
  scripts/turtle_tf_broadcaster.py scripts/turtle_tf_listener.py
  DESTINATION ${
    
    CATKIN_PACKAGE_BIN_DESTINATION}
)

(2)编译运行

cd ~/catkin_ws/
catkin_make --source src/learning_tf
source devel/setup.bash
roslaunch learning_tf start_cpp.launch
roslaunch learning_tf start_py.launch

在这里插入图片描述

3 总结

本节代码托管在本人的github上:learning_tf

猜你喜欢

转载自blog.csdn.net/cy1641395022/article/details/131653825