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
5Matrix3x3 & 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 transforminverse()
函数很有用,我们可以把上面程序中的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
4TransformListener::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
13geometry_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