摘自:https://blog.csdn.net/dieju8330/article/details/96770964

2D激光SLAM::AMCL发布的odom----map坐标TF变换解读

dieju8330 2019-07-24 21:56:08 2896 收藏 41

分类专栏: 2D激光SLAM ROS

版权

AMCL发布的odom----map坐标TF变换解读


一、背景

1、AMCL的作用是什么?

答:AMCL是基于蒙特卡洛定位方法的一种定位方法实现,集成于ROS操作系统的navigation导航包里面,简单来说,AMCL就是得到一个相对准确的机器人的世界坐标。

2、为什么需要AMCL?机器人地盘不是都有里程计和陀螺仪吗?

答:

(1)里程计会有累计误差,即跑远了之后,这个误差会一直叠加,另外,还会出现车轮打滑的现象,这时候的里程计信息就不准确了。

(2)陀螺仪是基于积分型的,也会出现误差。

3、AMCL获取到机器人的世界坐标之后,如何把这个信息传达给navigation导航包的move_base路径规划模块?

答:这个就涉及到本文要说的,amcl获取到机器人的世界坐标之后,经过一系列变换,最后发布一个tf变换,关于odom-----map两个坐标系的TF变换


二、关于odom和map坐标系

1、odom

odom指里程计的坐标系,这个坐标系在小车启动底盘的时候建立(即以小车的起点为原点),以小车前进方向为X轴(在ROS中,坐标系是右手,拇指、食指和中指),张开这三个手指,相互垂直,然后食指指向正前方,此时,中指的方向就是Y轴,拇指则是Z轴。

2、map

map我个人理解为全局地图也就是全局坐标系---global map

global map同样也是在机器人启动的时候建立,坐标系的方向跟odom一致。

到这里就会有疑惑,odom和map两个坐标系不应该就重合了吗?

答:在机器人刚启动的时候,这两个坐标系的确是重合的,但是跑远了之后,由于里程计的误差,会使得odom坐标系与global map坐标系的原点不重合,(个人理解是,按照里程计给出的机器人当前位姿,然后根据这个数据返回,最后机器人返回的地方并非一开始启动时的原点)

刚启动时,

跑远之后,或者车轮打滑了【可以看到,map没有动,这是机器人真正的起点,而odom坐标系的原点已经偏移了,就是说,依照里程计给出的机器人位姿信息,逆着反推回去,只能回到下图红色的odom‘,而不是最初的起点】


三、如何修正?

发布一个odom----map的TF坐标变换即可

1、如何发布?

首先需要明确一些必须的信息,要发布这个TF坐标变换,需要得到一个重要的参数---【机器人真实的位姿(世界坐标系下)】但是如何获取,这是AMCL主要解决的事情,这里不过多的关注

1)假定我们得到了base_link在世界坐标系global_map的坐标变换tmp_tf (即base_link在global_map下的坐标)

2)那么tmp_tf.inverse()则表示世界坐标系global_map到base_link的坐标变换(即global_map在base_link下的坐标)

3)由于base_link到odom坐标系的坐标变换是可以直接知道的(即base_link在odom下的坐标)

4)因此,使用tf.transformPose可以获得global_map到odom的变换tmp1_tf,(即global_map原点在odom坐标系下的坐标)

5)最后,对tmp1_tf求逆,得到odom-->map的变换,(即odom在global_map坐标系下的坐标)

6)发布odom-->map即可实现修正

四、AMCL模块关于此TF变换的源码(带注释,可结合上述过程来看)

  1. //发布最大权重的集群的pose统计值

  2. pose_pub_.publish(p);

  3. last_published_pose = p;

  4. ROS_DEBUG("New pose: %6.3f %6.3f %6.3f",

  5. hyps[max_weight_hyp].pf_pose_mean.v[0],

  6. hyps[max_weight_hyp].pf_pose_mean.v[1],

  7. hyps[max_weight_hyp].pf_pose_mean.v[2]);

  8. // subtracting base to odom from map to base and send map to odom instead

  9. // map->odom = map->base_link - base_link->odom

  10. geometry_msgs::PoseStamped odom_to_map;

  11. try

  12. {

  13. tf2::Quaternion q;

  14. q.setRPY(0, 0, hyps[max_weight_hyp].pf_pose_mean.v[2]);

  15. //tmp_tf是base_link在global map下的坐标,即base-->map

  16. tf2::Transform tmp_tf(q, tf2::Vector3(hyps[max_weight_hyp].pf_pose_mean.v[0],

  17. hyps[max_weight_hyp].pf_pose_mean.v[1],

  18. 0.0));

  19. geometry_msgs::PoseStamped tmp_tf_stamped;

  20. tmp_tf_stamped.header.frame_id = base_frame_id_;

  21. tmp_tf_stamped.header.stamp = laser_scan->header.stamp;

  22. //tmp_tf.inverse()是输入,tmp_tf_stamped.pose是输出

  23. //tmp_tf_stamped是global map原点在base_link下的坐标,即map-->base

  24. tf2::toMsg(tmp_tf.inverse(), tmp_tf_stamped.pose);

  25. //odom_frame_id_ default value is "odom"

  26. //将global map原点在base_link下的坐标变换成global map原点在odom下的坐标

  27. //即map-->odom,相当于在odom原点看map原点的位置

  28. //这里的odom_to_map并非真的odom-->map,而是反过来map-->odom

  29. this->tf_->transform(tmp_tf_stamped, odom_to_map, odom_frame_id_);

  30. }

  31. catch(tf2::TransformException)

  32. {

  33. ROS_DEBUG("Failed to subtract base to odom transform");

  34. return;

  35. }

  36. //转换odom_to_map.pose为latest_tf_

  37. tf2::convert(odom_to_map.pose, latest_tf_);

  38. latest_tf_valid_ = true;

  39. if (tf_broadcast_ == true)

  40. {

  41. // We want to send a transform that is good up until a

  42. // tolerance time so that odom can be used

  43. ros::Time transform_expiration = (laser_scan->header.stamp +

  44. transform_tolerance_);

  45. //设置tmp_tf_stamped头部信息

  46. geometry_msgs::TransformStamped tmp_tf_stamped;

  47. tmp_tf_stamped.header.frame_id = global_frame_id_;

  48. tmp_tf_stamped.header.stamp = transform_expiration;

  49. //这个变换就是child_frame_id在parent_frame_id下的坐标

  50. tmp_tf_stamped.child_frame_id = odom_frame_id_;

  51. //tmp_tf_stamped这个变换是odom原点在map坐标系的坐标,即odom-->map

  52. tf2::convert(latest_tf_.inverse(), tmp_tf_stamped.transform);

  53. //发布

  54. this->tfb_->sendTransform(tmp_tf_stamped);

  55. sent_first_transform_ = true;

  56. }

  57. }


五、个人仿照AMCL发布的TF变换源码(使用tf包替代了AMCL使用的tf2,感觉tf2比较小众?)

为了方便测试,代码中的真实位姿使用了odom的信息来替代了

  1. #include <ros/ros.h>

  2. #include <iostream>

  3. #include <nav_msgs/Odometry.h>

  4. #include "ros/ros.h"

  5. #include "sensor_msgs/LaserScan.h"

  6. #include "message_filters/subscriber.h"

  7. #include "tf/message_filter.h"

  8. #include "tf/transform_datatypes.h"

  9. #include "tf/transform_listener.h"

  10. #include "tf/transform_broadcaster.h"

  11. #include "nav_msgs/OccupancyGrid.h"

  12. //#include "tf2/utils.h"

  13. #include "math.h"

  14. //#include "tf2/convert.h"

  15. //#include "tf2_ros/message_filter.h"

  16. using namespace std;

  17. tf::TransformListener *tf_;

  18. tf::TransformBroadcaster *tfb_;

  19. nav_msgs::Odometry::ConstPtr oppp;

  20. // The basic vector

  21. typedef struct

  22. {

  23. double v[3]={0};

  24. } pose_vector_t;

  25. static double normalize(double z)

  26. {

  27. return atan2(sin(z),cos(z));

  28. }

  29. static double angle_diff(double a, double b)

  30. {

  31. double d1, d2;

  32. a = normalize(a);

  33. b = normalize(b);

  34. d1 = a-b;

  35. d2 = 2*M_PI - fabs(d1);

  36. if(d1 > 0)

  37. d2 *= -1.0;

  38. if(fabs(d1) < fabs(d2))

  39. return(d1);

  40. else

  41. return(d2);

  42. }

  43. // Return a zero vector

  44. pose_vector_t pose_vector_zero()

  45. {

  46. pose_vector_t c;

  47. c.v[0] = 0.0;

  48. c.v[1] = 0.0;

  49. c.v[2] = 0.0;

  50. return c;

  51. }

  52. void pose_vector_setValue(pose_vector_t * c,double x,double y,double yaw)

  53. {

  54. double *v;

  55. v=c->v;

  56. *v=x;

  57. v++;

  58. *v=y;

  59. v++;

  60. *v=yaw;

  61. //c->(v+1)=y;

  62. //*c->(v=yaw;

  63. }

  64. pose_vector_t lastPose_v;

  65. tf::Transform lastTransfrom_map_in_odom;

  66. void odomMsgCallback(const nav_msgs::Odometry::ConstPtr &odomMsg){

  67. static bool init=false;

  68. static double delta_x=0;

  69. static bool forward=true;

  70. ros::Duration transform_tolerance_;

  71. transform_tolerance_.fromSec(0.1);

  72. //位姿偏移量初始化

  73. pose_vector_t delta = pose_vector_zero();

  74. lastTransfrom_map_in_odom=tf::Transform(tf::createQuaternionFromRPY(0, 0, 0),

  75. tf::Vector3(0, 0, 0));

  76. if(!init){

  77. init=true;

  78. pose_vector_setValue(&lastPose_v,

  79. odomMsg->pose.pose.position.x,

  80. odomMsg->pose.pose.position.y,

  81. tf::getYaw(odomMsg->pose.pose.orientation));

  82. }else{

  83. delta.v[0] = odomMsg->pose.pose.position.x - lastPose_v.v[0];

  84. delta.v[1] = odomMsg->pose.pose.position.y - lastPose_v.v[1];

  85. delta.v[2] = angle_diff(tf::getYaw(odomMsg->pose.pose.orientation), lastPose_v.v[2]);

  86. //判断位移偏移量是否大于阈值

  87. if(true/*sqrt(pow(delta.v[0],2)+pow(delta.v[1],2))>=0*/){

  88. /******************发布坐标变换**********************************/

  89. //取odom获取的位姿作为真实位姿

  90. pose_vector_t truePose_v;

  91. truePose_v.v[0]=odomMsg->pose.pose.position.x+delta_x;

  92. truePose_v.v[1]=odomMsg->pose.pose.position.y;

  93. truePose_v.v[2]=tf::getYaw(odomMsg->pose.pose.orientation);

  94. tf::Stamped<tf::Transform> map_in_odom;

  95. tf::Stamped<tf::Transform> odom_in_map;

  96. //tf::Transform map_in_odom;

  97. try{

  98. //创建一个基于global map的坐标

  99. tf::Stamped<tf::Pose> ident(tf::Transform(tf::createQuaternionFromRPY(0, 0, truePose_v.v[2]),

  100. tf::Vector3(truePose_v.v[0], truePose_v.v[1], 0)),odomMsg->header.stamp, "map");

  101. //创建上面坐标的逆,即global map原点在Base_link坐标系下的坐标

  102. tf::Stamped<tf::Pose> tmp(ident.inverse(),odomMsg->header.stamp, "base_link");

  103. //然后将该坐标转换到odom坐标系下

  104. //得到的map_in_odom是 global map原点在odom坐标系下的坐标

  105. tf_->transformPose("odom", tmp, map_in_odom);

  106. //求逆变换

  107. //得到的odom_in_map是 odom坐标原点在global map坐标系下的坐标

  108. //即得到从odom坐标系到global map坐标系的变换矩阵

  109. odom_in_map.setData(map_in_odom.inverse());

  110. odom_in_map.frame_id_="map";

  111. odom_in_map.stamp_=odomMsg->header.stamp+transform_tolerance_;

  112. ROS_INFO("calculate odom in map success");

  113. ROS_INFO("odom in map x:[%f] y:[%f] yaw:[%f]",

  114. odom_in_map.getOrigin().x(),

  115. odom_in_map.getOrigin().y(),

  116. odom_in_map.getRotation().getAngle());

  117. ROS_INFO("now send the TF Broadcast odom_in_map");

  118. tfb_->sendTransform(tf::StampedTransform(odom_in_map,odom_in_map.stamp_,"map","odom"));

  119. lastTransfrom_map_in_odom=odom_in_map;

  120. }catch(tf::TransformException e){

  121. ROS_WARN("Failed to compute odom pose, skipping scan (%s)", e.what());

  122. return ;

  123. }

  124. // ROS_INFO("now send the TF Broadcast odom_in_map");

  125. // odom_in_map.setData(tf::Transform(tf::createQuaternionFromRPY(0, 0, 0),

  126. // tf::Vector3(0, 0, 0)));

  127. // odom_in_map.frame_id_="map";

  128. // odom_in_map.stamp_=odomMsg->header.stamp+transform_tolerance_;

  129. // tfb_->sendTransform(tf::StampedTransform(odom_in_map,odom_in_map.stamp_,"map","odom"));

  130. if(delta_x<=0){

  131. forward=true;

  132. }

  133. if(delta_x>=10){

  134. forward=false;

  135. }

  136. if(forward)

  137. delta_x+=0.02;

  138. else

  139. delta_x-=0.02;

  140. // pose_vector_setValue(&lastPose_v,

  141. // odomMsg->pose.pose.position.x,

  142. // odomMsg->pose.pose.position.y,

  143. // tf::getYaw(odomMsg->pose.pose.orientation));

  144. }else{

  145. //位移偏移量没有达到阈值

  146. //发布之前的变换

  147. ROS_INFO("now send the TF Broadcast odom_in_map");

  148. tfb_->sendTransform(tf::StampedTransform(lastTransfrom_map_in_odom,odomMsg->header.stamp+transform_tolerance_,"map","odom"));

  149. }

  150. }

  151. // tf::Stamped<tf::Transform> odom_in_map;

  152. // tfb_->sendTransform(tf::StampedTransform(odom_in_map,odom_in_map.stamp_,"map","odom"));

  153. }

  154. int main(int argc, char **argv)

  155. {

  156. ros::init(argc, argv, "publish_tf_node");

  157. ros::NodeHandle nh;

  158. tf_=new tf::TransformListener;

  159. tfb_=new tf::TransformBroadcaster;

  160. message_filters::Subscriber<nav_msgs::Odometry>* odom_sub_;

  161. //message_filters::Subscriber<sensor_msgs::LaserScan>* scan_sub_;

  162. tf::MessageFilter<nav_msgs::Odometry>* odom_filter_;

  163. // Subscribers

  164. //订阅"odom"

  165. ROS_INFO("subscribed the topic \"odom\" ");

  166. odom_sub_ = new message_filters::Subscriber<nav_msgs::Odometry>(nh, "odom", 100);

  167. //odom_sub_->registerCallback(odomMsgCallback/*boost::bind(odomMsgCallback, this, _1)*/);

  168. odom_filter_ =new tf::MessageFilter<nav_msgs::Odometry>(*odom_sub_, *tf_, "base_link", 100);

  169. odom_filter_->registerCallback(odomMsgCallback);

  170. ros::spin();

  171. }

2D激光SLAM::AMCL发布的odom----map坐标TF变换解读相关推荐

  1. 2D激光SLAM算法汇总

    点击上方"3D视觉工坊",选择"星标" 干货第一时间送达 作者:小小酥 | 来源:知乎 https://zhuanlan.zhihu.com/p/7833413 ...

  2. 激光slam课程学习笔记--第2课:2D激光slam

    前言:这系列笔记是学习曾书格老师的激光slam课程所得,这里分享只是个人理解,有误之处,望大佬们赐教.这节课介绍的是2D激光slam. 1. 2d激光slam的介绍 激光slam的输入:IMU数据,里 ...

  3. 2d激光slam学习

    ----------------------------------------------参考资料来源---深蓝学院----------------------------------------- ...

  4. SLAM学习--2D激光SLAM--入门学习

    一.学习心得记录(不一定对):转载了很多博主的网站,若侵权,告知必删 1. 激光slam 和视觉slam 实际在实现的时候完全是两码事,可能根本思想差不多,特别是非线性优化方面,但是实现方案差距较大, ...

  5. 机器人学习--网友资料系列 激光SLAM建图、粒子滤波定位和位姿图优化

    一.移动机器人自主导航的前提是在未知环境中先构建地图 (目前市内很多用的2D激光雷达,构建栅格地图,相当于立体空间中的某个水平面高度的切面) 一般用的是2D 激光SLAM算法 构建概率栅格占用地图: ...

  6. 激光SLAM入门学习笔记

    激光SLAM入门学习笔记 激光SLAM入门学习笔记 一.推荐阅读书籍 二.推荐公众号.知乎.博客 1.公众号 2.知乎 3.博客 三.推荐阅读论文&代码(参考泡泡机器人) 2D激光SLAM 3 ...

  7. 二维激光SLAM( 使用Laser Scan Matcher )

    目录 一.Laser Scan Matcher安装配置 二.二维激光定位 一.Laser Scan Matcher安装配置 ROS自带的laser_scan_matcher库,使用的是CSM(Cano ...

  8. 激光SLAM基础(1) —— 激光SLAM框架和基本数学理论

    激光SLAM笔记(1)--激光SLAM框架和基本数学理论 1.SLAM分类 1.1.基于传感器的分类 1.2.基于后端的分类 13.基于图的SLAM 2.激光SLAM算法(基于优化的算法) 2.1.激 ...

  9. [SLAM]激光SLAM初学者代码及论文推荐【转】

    目录 2D激光SLAM Gmapping Hector_slam Karto Cartographer 3D激光SLAM LOAM A-LOAM LeGO-LOAM Lio-mapping hdl_g ...

最新文章

  1. Android MTK平台最完备的开机动画修改教程
  2. faster rcnn源码解读总结
  3. 腾讯AI大战王者荣耀!504场1v1仅输1场,5v5达电竞职业水平
  4. arduino控制直流电机_Arduino的高电流直流电机控制板
  5. Linux镜像下载及虚拟机中安装
  6. Snort IPS入侵防御系统模式
  7. 计算bed区间gc含量,碱基深度等
  8. 方舟服务器显示mod不符,方舟生存进化mod不符怎么办
  9. Arm 公司推出了 Mbed linux OS
  10. linux 蓝牙 iphone,Linux On iPhone 7 现在可运行 Wayland
  11. 用CSS添加鼠标样式-箭头、小手、十字 CSS实现虚线之dotted边框-点虚线、dashed边框-破折号虚线
  12. 从零玩转Docker(一):什么是Docker?
  13. Qt之绘制折线图:图标以及坐标轴设置
  14. C++实现前向欧拉法Forward Euler解决偏微分方程
  15. JAVA-读取excel转成html 将excel表格转换为HTML文件格式 转成前端表格样式
  16. 从0开始实现目标检测——原理篇
  17. PM常用语看这篇就够了
  18. 【转摘】著名画家毕建勋 学画笔记
  19. python自动化生成请假条
  20. Face ID,iPhone X的杀手锏

热门文章

  1. OSChina Maven使用说明
  2. mark制图软件_VectorWorks(三维设计制图软件) V2019 Mac版
  3. 网站运营中同时把握好网站推广和用户体验度
  4. TCP/UDP常见端口
  5. 一起来做NES开发(1)
  6. HEVC-I帧中CU,TU,PU之间的关系
  7. Flink China Meetup 资料整理
  8. 【15W字长文】主从复制高可用Redis集群,完整包含Redis所有知识点
  9. iPhone排水功能如何使用?如何利用快捷指令实现iPhone排水功能?
  10. 使用openfeign调用报错java.io.IOException: too many bytes written,以及调用过程中参数传递为空等问题