1. 程式人生 > >ROS之tf座標變換

ROS之tf座標變換

1、什麼是tf變換

        ROS中的很多軟體包都需要機器人釋出tf變換樹,那麼什麼是tf變換樹呢?抽象的來講,一棵tf變換樹定義了不同座標系之間的平移與旋轉變換關係。具體來說,我們假設有一個機器人,包括一個機器人移動平臺和一個安裝在平臺之上的鐳射雷達,以這個機器人為例,定義兩個座標系,一個座標系以機器人移動平臺的中心為原點,稱為base_link參考系,另一個座標系以鐳射雷達的中心為原點,稱為base_laser參考系。

image

假設在機器人執行過程中,鐳射雷達可以採集到距離前方障礙物的資料,這些資料當然是以鐳射雷達為原點的測量值,換句話說,也就是base_laser參考系下的測量值。現在,如果我們想使用這些資料幫助機器人完成避障功能,當然,由於鐳射雷達在機器人之上,直接使用這些資料不會產生太大的問題,但是鐳射雷達並不在機器人的中心之上,在極度要求較高的系統中,會始終存在一個雷達與機器人中心的偏差值。這個時候,如果我們採用一種座標變換,將及鐳射資料從

base_laser參考系變換到base_link參考下,問題不就解決了麼。這裡我們就需要定義這兩個座標系之間的變換關係。

為了定義這個變換關係,假設我們已知鐳射雷達安裝的位置在機器人的中心點上方20cm,前方10cm處。這就根據這些資料,就足以定義這兩個參考系之間的變換關係:當我們獲取鐳射資料後,採用(x: 0.1m, y: 0.0m, z: 0.2m)的座標變換,就可以將資料從base_laser參考系變換到base_link參考系了。當然,如果要方向變化,採用(x: -0.1m, y: 0.0m, z: -0.20m)的變換即可。

從上邊的示例看來,參考系之間的座標變換好像並不複雜,但是在複雜的系統中,存在的參考系可能遠遠大於兩個,如果我們都使用這種手動的方式進行變換,估計很快你就會被繁雜的座標關係搞蒙了。

ROS提供的tf變換就是為解決這個問題而生的,tf功能包提供了儲存、計算不同資料在不同參考系之間變換的功能,我們只需要告訴tf樹這些參考系之間的變換公式即可,這顆tf樹就可以樹的資料結構,管理我們所需要的參考系變換。

image

還是以上邊的示例為基礎,為了定義和儲存base_linkbase_laser兩個參考系之間的關係,我們需要將他們新增到tf樹中。從樹的概念上來講,tf樹中的每個節點都對應一個參考系,而節點之間的邊對應於參考系之間的變換關係。tf就是使用這樣的樹結構,保證每兩個參考系之間只有一種遍歷方式,而且所有變換關係,都是母節點到子節點的變換。

為了定義上邊示例中的參考系,我們需要定義兩個節點,一個對應於

base_link參考系,一個對應於base_laser參考系。為了建立兩個節點之間的邊,我們首先需要決定哪一個節點作為母節點,哪一個節點作為子節點,這一點在tf樹中是非常重要的。這裡我們選擇base_link作為母節點,這樣會方便後邊為機器人新增更多的感測器作為子節點。所以,根據之前的分析,從base_link節點到base_laser節點的變換關係為(x: 0.1m, y: 0.0m, z: 0.2m)。設定完畢後,我們就可以通過呼叫tf庫,直接完成base_laser參考系到base_link參考系的資料座標變換了。

2、程式碼流程

通過上邊的分析,應該可以從理論上幫助你理解什麼是tf,以及tf的功能了。在實際應用中,我們需要使用程式碼來完成這些理論,下邊我們就來看看如何使用程式碼來呼叫tf的變換。

程式碼的總體思路分為兩個部分:

1)編寫一個節點,廣播兩個參考系之間的tf變換關係

2)編寫另外一個節點,訂閱tf樹,然後從tf樹中遍歷到兩個參考系之間的變換公式,然後通過公式計算資料的變換。

我們先來完成第一步。首先我們來建立一個功能包,用於後邊程式的放置,這裡需要依賴roscpptfgeometry_msgs

  1. $ cd %TOP_DIR_YOUR_CATKIN_WS%/src
  2. $ catkin_create_pkg robot_setup_tf roscpp tf geometry_msgs

3、編寫廣播節點

進入功能包,建立src/tf_broadcaster.cpp檔案,來完成廣播節點的程式碼:

  1. #include <ros/ros.h>
  2. #include <tf/transform_broadcaster.h>
  3. int main(int argc, char** argv){
  4.   ros::init(argc, argv, "robot_tf_publisher");
  5.   ros::NodeHandle n;
  6.   ros::Rate r(100);
  7.   tf::TransformBroadcaster broadcaster;
  8.   while(n.ok()){
  9.     broadcaster.sendTransform(
  10.       tf::StampedTransform(
  11.         tf::Transform(tf::Quaternion(0, 0, 0, 1), tf::Vector3(0.1, 0.0, 0.2)),
  12.         ros::Time::now(),"base_link", "base_laser"));
  13.     r.sleep();
  14.   }
  15. }

逐行分析如上程式碼:

  1. #include <tf/transform_broadcaster.h>

因為後邊會使用到tf::TransformBroadcaster類的例項,來完成tf樹的廣播,所以需要先包含相關的標頭檔案。

  1. tf::TransformBroadcaster broadcaster;

建立一個tf::TransformBroadcaster類的例項,用來廣播base_link → base_laser的變換關係。

  1.    broadcaster.sendTransform(
  2.       tf::StampedTransform(
  3.         tf::Transform(tf::Quaternion(0, 0, 0, 1), tf::Vector3(0.1, 0.0, 0.2)),
  4.         ros::Time::now(),"base_link", "base_laser"));

這部分是程式碼的關鍵所在。通過TransformBroadcaster類來發布變換關係的介面,需要五個引數。首先是兩個參考系之間的旋轉變換,通過btQuaternion四元數來儲存旋轉變換的引數,因為我們用到的兩個參考系沒有發生旋轉變換,所以傾斜角、滾動角、偏航角都是0。第二個引數是座標的位移變換,我們用到的兩個參考系在X軸和Z軸發生了位置,根據位移值填入到btVector3向量中。第三個引數是時間戳,直接太難過ROSAPI完成。第四個引數是母節點儲存的參考系,即base_link,最後一個引數是子節點儲存的參考系,即base_laser

4、編寫訂閱節點

上一節講解了如何使用程式碼編寫一個廣播tf變換的節點,這一節,我們編寫一個訂閱tf廣播的節點,並且使用tf樹中base_linkbase_laser的變換關係,完成資料的座標變換。在robot_setup_tf功能包中建立src/tf_listener.cpp檔案,程式碼如下:

  1. #include <ros/ros.h>
  2. #include <geometry_msgs/PointStamped.h>
  3. #include <tf/transform_listener.h>
  4. void transformPoint(const tf::TransformListener& listener){
  5.   //we'll create a point in the base_laser frame that we'd like to transform to the base_link frame
  6.   geometry_msgs::PointStamped laser_point;
  7.   laser_point.header.frame_id = "base_laser";
  8.   //we'll just use the most recent transform available for our simple example
  9.   laser_point.header.stamp = ros::Time();
  10.   //just an arbitrary point in space
  11.   laser_point.point.x = 1.0;
  12.   laser_point.point.y = 0.2;
  13.   laser_point.point.z = 0.0;
  14.   try{
  15.     geometry_msgs::PointStamped base_point;
  16.     listener.transformPoint("base_link", laser_point, base_point);
  17.     ROS_INFO("base_laser: (%.2f, %.2f. %.2f) -----> base_link: (%.2f, %.2f, %.2f) at time %.2f",
  18.         laser_point.point.x, laser_point.point.y, laser_point.point.z,
  19.         base_point.point.x, base_point.point.y, base_point.point.z, base_point.header.stamp.toSec());
  20.   }
  21.   catch(tf::TransformException& ex){
  22.     ROS_ERROR("Received an exception trying to transform a point from \"base_laser\" to \"base_link\": %s", ex.what());
  23.   }
  24. }
  25. int main(int argc, char** argv){
  26.   ros::init(argc, argv, "robot_tf_listener");
  27.   ros::NodeHandle n;
  28.   tf::TransformListener listener(ros::Duration(10));
  29.   //we'll transform a point once every second
  30.   ros::Timer timer = n.createTimer(ros::Duration(1.0), boost::bind(&transformPoint, boost::ref(listener)));
  31.   ros::spin();
  32. }

進行詳細的分析:

  1. #include <tf/transform_listener.h>

在後邊的程式碼中,我們會使用到tf::TransformListener物件,該物件會自動訂閱ROS中的tf訊息,並且管理所有的變換關係資料。所以需要先包含相關的標頭檔案。

  1. void transformPoint(const tf::TransformListener& listener){

我們建立一個回撥函式,每次收到tf訊息時,都會自動呼叫該函式,上一節我們設定了釋出tf訊息的頻率是1Hz,所以回撥函式執行的頻率也是1Hz。在回撥函式中,我們需要完成資料從base_laserbase_link參考系的座標變換。

  1. //we'll create a point in the base_laser frame that we'd like to transform to the base_link frame
  2.   geometry_msgs::PointStamped laser_point;
  3.   laser_point.header.frame_id = "base_laser";
  4.   //we'll just use the most recent transform available for our simple example
  5.   laser_point.header.stamp = ros::Time();
  6.   //just an arbitrary point in space
  7.   laser_point.point.x = 1.0;
  8.   laser_point.point.y = 0.2;
  9.   laser_point.point.z = 0.0;

我們建立了一個geometry_msgs::PointStamped型別的虛擬點,該點的座標為(1.00.20.0)。該型別包含標準的header訊息結構,這樣,我們可以就可以在訊息中加入釋出資料的時間戳和參考系的id

  1. try{
  2.     geometry_msgs::PointStamped base_point;
  3.     listener.transformPoint("base_link", laser_point, base_point);
  4.     ROS_INFO("base_laser: (%.2f, %.2f. %.2f) -----> base_link: (%.2f, %.2f, %.2f) at time %.2f",
  5.         laser_point.point.x, laser_point.point.y, laser_point.point.z,
  6.         base_point.point.x, base_point.point.y, base_point.point.z, base_point.header.stamp.toSec());
  7.   }

這裡是程式碼的關鍵位置。我們已經在base_laser參考系下虛擬了一個數據點,那麼怎樣將該點的資料轉換到base_base參考系下呢?使用TransformListener物件中的transformPoint()函式即可,該函式包含三個引數:第一個引數是需要轉換到的參考系id,當然是base_link了;第二個引數是需要轉換的原始資料;第三個引數用來儲存轉換完成的資料。該函式執行完畢後,base_point就是我們轉換完成的點座標了!

  1. catch(tf::TransformException& ex){
  2.     ROS_ERROR("Received an exception trying to transform a point from \"base_laser\"