tf(一) 概念和基本使用

tf包处理的是任一个点在所有坐标系之间的坐标变换问题,它把各种转换关系建立在一个树结构上,树的每个节点是坐标系,每个坐标系可以有多个child,但只能有一个parent,转换只能是从parent向child。比如Tb-a表示坐标系a向b转换,也就是说a是parent,b是child,这个变换描述的就是child坐标系中的点在parent坐标系下的姿态。要实现这个变换,就是用child坐标系在parent坐标系下的描述(一个矩阵)去描述(乘以)这个点在child坐标系下的描述(坐标)。world参考系是tf树最顶端的父参考系

如果打算用tf解决你的坐标变换问题,请一定要先清晰的画出这棵树的结构,再开始写程序。比较重要的类是tf::TransformBroadcaster, tf::TransformListener, tf::Transform, tf::StampedTransform

在tf的运行机制中,由于tf会把监听到的内容放到一个缓存中。我们通过transformPose获取变换关系,是通过查询这个缓存来实现的。获取的数据不能保证实时性,会有一定的延迟。也有可能无法获得,因此这个函数在运行过程中会抛出异常,所以这里使用try-catch语句捕获这个异常并返回。

tf::TransformBroadcaster类

sendtransform接口可以建立tf树,发布一个从已有的父坐标系到新的子坐标系的变换时,这棵树就会添加一个树枝,之后就是维护。TransformBroadcaster类就是一个publisher, 如果两个frame之间发生了相对运动,TransformBroadcaster类就会发布TransformStamped消息到tf话题,当多个节点向tf话题发消息时,就形成了tf树。

tf::Transform

建立坐标系之间的位移和旋转的关系,最后用于sendTransform函数。

它是一个坐标转换。成员有:Matrix3x3 m_basis,用3*3的矩阵表示旋转; Vector3 m_origin,用3*1的向量表示平移。

tf::Transform支持乘法运算符,实际的计算是先把旋转矩阵和平移量组合为变换矩阵,变换矩阵相乘后,再转换为tf::Transform类型

tf::Transform类的重要函数如下:

1
2
3
4
5
Matrix3x3 &   getBasis ()    //Return the basis matrix for the rotation
const Vector3 & getOrigin () //Return the origin vector translation
Quaternion getRotation () //Return a quaternion representing the rotation
Transform operator* (const Transform &t) const //Return the product of this transform and the other.
Transform inverse () //Return the inverse of this transform

inverse()函数很有用,我们可以把上面程序中的transform.getOrigin().x()改成transform.inverse().getOrigin().x()就可以求出乌龟1在乌龟2坐标系中的坐标了。

tf::StampedTransform类继承自tf::Transform,它多了两个重要变量就是child_frame_id_frame_id_

tf::TransformListener

监听一个父坐标系到子坐标系的变换,waitForTransform是监听转换关系,可以指定监听的时间或一直阻塞;lookupTransform紧随其后,获取 tf::Transform

使用前需要#include <tf/transform_listener.h>

TransformListener构造函数有两个,常用的是

1
2
3
4
TransformListener::TransformListener(
ros::Duration max_cache_time = ros::Duration(DEFAULT_CACHE_TIME),
bool spin_thread = true
)

平时用的是无参构造函数,其实是默认构造函数,如果指定缓存时间,就用tf::TransformListener tf_(ros::Duration(15) );Costmap2DROS中使用的tf缓存,根源是move_base_node.cpp中的tf::TransformListener tf(ros::Duration(10) );

参考我写的程序test_costmap。开始,如果没有map—->base_link的TF转换,则报错No Transform available Error。此时发布TF变换,则不再报错。然后再关闭TF变换,test_costmap还能正常运行10s,然后报错 Extrapolation Error

transformPose

原型是void transformPose(const std::string &target_frame, const geometry_msgs::PoseStamped &stamped_in, geometry_msgs::PoseStamped &stamped_out) const

target_frame就是你要把源pose转换成哪个frame上的pose。假如你的源pose的frame_id是”odom”,你想转到”map”上,那么target_frame写成“map”就可以了。stamped_in就是源pose,而stamped_out就是目标数据了,也就是转换完成的数据。需要注意的是,从参数上来看,转换时是不需要指定源frame_id的,这是因为它已经包含在了stamped_in中,换句话说,就是这个函数一个隐含的使用条件是,stamped_in中必须指明源pose属于哪个frame

把odom坐标系的数据转换到map坐标系下

1
2
3
4
5
6
7
8
9
10
11
12
13
geometry_msgs::PoseStamped pose_odom;
pose_odom.header = odom->header;
pose_odom.pose = odom->pose.pose;

geometry_msgs::PoseStamped pose_map;
try{
listener.transformPose("map", pose_odom, pose_map);
}
catch( tf::TransformException ex)
{
ROS_WARN("transfrom exception : %s",ex.what());
return;
}

有时会出现这样的报错: transfrom exception : “map” passed to lookupTransform argument target_frame does not exist ,但是使用tf_echo发现是正常的。需要检查代码是不是在回调函数里运行了, 不需要在回调函数里创建TransformListener对象, 将它作为类成员变量或者全局变量。

全局变量是在main函数之前完成构造函数的,如果用到的类构造函数用到NodeHandle,就会报错。比如tf::TransformListener,解决方法是用全局指针,比如boost::shared_ptr<T>,然后在main函数的ros::init()之后指向一个对象。

参考:tf::TransformListener::transformPose [exception] target_frame does not exist