ROS中的地图很好理解,就是一张普通的灰度图像,通常为pgm格式。这张图像上的黑色像素表示障碍物,白色像素表示可行区域,灰色是未探索的区域。如下图所示,
在SLAM建图的过程中,你可以在RViz里看到一张地图被逐渐建立起来的过程,类似于一块块拼图被拼接成一张完整的地图。这张地图对于我们定位、路径规划都是不可缺少的信息。事实上,地图在ROS中是以Topic的形式维护和呈现的,这个Topic名称就叫做/map
,它的消息类型是nav_msgs/OccupancyGrid
。
由于/map
中实际上存储的是一张图片,为了减少不必要的开销,这个Topic往往采用锁存(latched)的方式来发布。什么是锁存?其实就是:地图如果没有更新,就维持着上次发布的内容不变,此时如果有新的订阅者订阅消息,这时只会收到一个/map
的消息,也就是上次发布的消息;只有地图更新了(比如SLAM又建出来新的地图),这时/map
才会发布新的内容。 锁存器的作用就是,将发布者最后一次发布的消息保存下来,然后把它自动发送给后来的订阅者。这种方式非常适合变动较慢、相对固定的数据(例如地图),然后只发布一次,相比于同样的消息不定的发布,锁存的方式既可以减少通信中对带宽的占用,也可以减少消息资源维护的开销。
然后我们来看一下地图的OccupancyGrid类型是如何定义的,你可以通过rosmsg show nav_msgs/OccupancyGrid
来查看消息,或者直接rosed nav_msgs OccupancyGrid.msg
来查看srv文件。
std_msgs/Header header #消息的报头
uint32 seq
time stamp
string frame_id #地图消息绑定在TF的哪个frame上,一般为map
nav_msgs/MapMetaData info #地图相关信息
time map_load_time #加载时间
float32 resolution #分辨率 单位:m/pixel
uint32 width #宽 单位:pixel
uint32 height #高 单位:pixel
geometry_msgs/Pose origin #原点
geometry_msgs/Point position
float64 x
float64 y
float64 z
geometry_msgs/Quaternion orientation
float64 x
float64 y
float64 z
float64 w
int8[] data #地图具体信息
这个srv文件定义了/map话题的数据结构,包含了三个主要的部分:header, info和data。header是消息的报头,保存了序号、时间戳、frame等通用信息,info是地图的配置信息,它反映了地图的属性,data是真正存储这张地图数据的部分,它是一个可变长数组,int8
后面加了[]
,你可以理解为一个类似于vector的容器,它存储的内容有width*height个int8型的数据,也就是这张地图上每个像素。
Gmapping算法是目前基于激光雷达和里程计方案里面比较可靠和成熟的一个算法,它基于粒子滤波,采用RBPF的方法效果稳定,许多基于ROS的机器人都跑的是gmapping_slam。这个软件包位于ros-perception组织中的slam_gmapping仓库中。 其中的slam_gmapping
是一个metapackage,它依赖了gmapping
,而算法具体实现都在gmapping
软件包中,该软件包中的slam_gmapping
程序就是我们在ROS中运行的SLAM节点。如果你感兴趣,可以阅读一下gmapping
的源代码。
如果你的ROS安装的是desktop-full版本,应该默认会带gmapping。你可以用以下命令来检测gmapping是否安装
apt-cache search ros-$ROS_DISTRO-gmapping
如果提示没有,可以直接用apt安装
sudo apt-get install ros-$ROS_DISTRO-gmapping
gmapping在ROS上运行的方法很简单
rosrun gmapping slam_gmapping
但由于gmapping算法中需要设置的参数很多,这种启动单个节点的效率很低。所以往往我们会把gmapping的启动写到launch文件中,同时把gmapping需要的一些参数也提前设置好,写进launch文件或yaml文件。 具体可参考教学软包中的slam_sim_demo
中的gmapping_demo.launch
和robot_gmapping.launch.xml
文件。
gmapping的作用是根据激光雷达和里程计(Odometry)的信息,对环境地图进行构建,并且对自身状态进行估计。因此它得输入应当包括激光雷达和里程计的数据,而输出应当有自身位置和地图。 下面我们从计算图(消息的流向)的角度来看看gmapping算法的实际运行中的结构: 位于中心的是我们运行的slam_gmapping
节点,这个节点负责整个gmapping SLAM的工作。它的输入需要有两个:
输入
/tf
以及/tf_static
: 坐标变换,类型为第一代的tf/tfMessage
或第二代的tf2_msgs/TFMessage
其中一定得提供的有两个tf,一个是base_frame
与laser_frame
之间的tf,即机器人底盘和激光雷达之间的变换;一个是base_frame
与odom_frame
之间的tf,即底盘和里程计原点之间的坐标变换。odom_frame
可以理解为里程计原点所在的坐标系。
/scan
:激光雷达数据,类型为sensor_msgs/LaserScan
/scan
很好理解,Gmapping SLAM所必须的激光雷达数据,而/tf
是一个比较容易忽视的细节。尽管/tf
这个Topic听起来很简单,但它维护了整个ROS三维世界里的转换关系,而slam_gmapping
要从中读取的数据是base_frame
与laser_frame
之间的tf,只有这样才能够把周围障碍物变换到机器人坐标系下,更重要的是base_frame
与odom_frame
之间的tf,这个tf反映了里程计(电机的光电码盘、视觉里程计、IMU)的监测数据,也就是机器人里程计测得走了多少距离,它会把这段变换发布到odom_frame
和laser_frame
之间。
因此slam_gmapping
会从/tf
中获得机器人里程计的数据。
输出
/tf
: 主要是输出map_frame
和odom_frame
之间的变换/slam_gmapping/entropy
: std_msgs/Float64
类型,反映了机器人位姿估计的分散程度/map
: slam_gmapping
建立的地图/map_metadata
: 地图的相关信息输出的/tf
里又一个很重要的信息,就是map_frame
和odom_frame
之间的变换,这其实就是对机器人的定位。通过连通map_frame
和odom_frame
,这样map_frame
与base_frame
甚至与laser_frame
都连通了。这样便实现了机器人在地图上的定位。
同时,输出的Topic里还有/map
,在上一节我们介绍了地图的类型,在SLAM场景中,地图是作为SLAM的结果被不断地更新和发布。
目前ROS中常用的里程计广义上包括车轮上的光电码盘、惯性导航元件(IMU)、视觉里程计,你可以只用其中的一个作为odom,也可以选择多个进行数据融合,融合结果作为odom。通常来说,实际ROS项目中的里程计会发布两个Topic:
/odom
: 类型为nav_msgs/Odometry
,反映里程计估测的机器人位置、方向、线速度、角速度信息。/tf
: 主要是输出odom_frame
和base_frame
之间的tf。这段tf反映了机器人的位置和方向变换,数值与/odom
中的相同。由于以上三种里程计都是对机器人的位姿进行估计,存在着累计误差,因此当运动时间较长时,odom_frame
和base_frame
之间变换的真实值与估计值的误差会越来越大。你可能会想,能否用激光雷达数据来修正odom_frame
和base_frame
的tf。事实上gmapping不是这么做的,里程计估计的是多少,odom_frame
和base_frame
的tf就显示多少,永远不会去修正这段tf。gmapping的做法是把里程计误差的修正发布到map_frame
和odom_frame
之间的tf上,也就是把误差补偿在了地图坐标系和里程计原点坐标系之间。通过这种方式来修正定位。
这样map_frame
和base_frame
,甚至和laser_frame
之间就连通了,实现了机器人在地图上的定位。
/odom
slam_gmapping
也提供了一个服务:
/dynamic_map
: 其srv类型为nav_msgs/GetMap,用于获取当前的地图。该srv定义如下: nav_msgs/GetMap.srv
# Get the map as a nav_msgs/OccupancyGrid
---
nav_msgs/OccupancyGrid map
可见该服务的请求为空,即不需要传入参数,它会直接反馈当前地图。
slam_gmapping`需要的参数很多,这里以`slam_sim_demo`教学包中的`gmapping_demo`的参数为例,注释了一些比较重要的参数,具体请查看`ROS-Academy-for-Beginners/slam_sim_demo/launch/include/robot_gmapping.launch.xml
Karto SLAM和Gmapping SLAM在工作方式上非常类似,如下图所示
输入的Topic同样是/tf
和/scan
,其中/tf
里要连通odom_frame
与base_frame
,还有laser_frame
。这里和Gmapping完全一样。
唯一不同的地方是输出,slam_karto的输出少相比slam_gmapping了一个位姿估计的分散程度.
与Gmapping相同,提供/dynamic_map
服务
这里以ROS-Academy-for-Beginners
中的karto_slam
为例,选取了它的参数文件slam_sim_demo/param/karto_params.yaml
,关键位置做了注释:
# General Parameters
use_scan_matching: true
use_scan_barycenter: true
minimum_travel_distance: 0.2
minimum_travel_heading: 0.174 #in radians
scan_buffer_size: 70
scan_buffer_maximum_scan_distance: 20.0
link_match_minimum_response_fine: 0.8
link_scan_maximum_distance: 10.0
loop_search_maximum_distance: 4.0
do_loop_closing: true
loop_match_minimum_chain_size: 10
loop_match_maximum_variance_coarse: 0.4 # gets squared later
loop_match_minimum_response_coarse: 0.8
loop_match_minimum_response_fine: 0.8
# Correlation Parameters - Correlation Parameters
correlation_search_space_dimension: 0.3
correlation_search_space_resolution: 0.01
correlation_search_space_smear_deviation: 0.03
# Correlation Parameters - Loop Closure Parameters
loop_search_space_dimension: 8.0
loop_search_space_resolution: 0.05
loop_search_space_smear_deviation: 0.03
# Scan Matcher Parameters
distance_variance_penalty: 0.3 # gets squared later
angle_variance_penalty: 0.349 # in degrees (gets converted to radians then squared)
fine_search_angle_offset: 0.00349 # in degrees (gets converted to radians)
coarse_search_angle_offset: 0.349 # in degrees (gets converted to radians)
coarse_angle_resolution: 0.0349 # in degrees (gets converted to radians)
minimum_angle_penalty: 0.9
minimum_distance_penalty: 0.5
use_response_expansion: false
Hector SLAM算法不同于前面两种算法,Hector只需要激光雷达数据,而不需要里程计数据。这种算法比较适合手持式的激光雷达,并且对激光雷达的扫描频率有一定要求。
Hector算法的效果不如Gmapping、Karto,因为它仅用到激光雷达信息。这样建图与定位的依据就不如多传感器结合的效果好。但Hector适合手持移动或者本身就没有里程计的机器人使用。
Hector的计算图,如下所示
位于中心的节点叫作hector_mapping
,它的输入和其他SLAM框架类似,都包括了/tf
和/scan
,另外Hector还订阅一个/syscommand
Topic,这是一个字符串型的Topic,当接收到reset
消息时,地图和机器人的位置都会初始化到最初最初的位置。
在输出的Topic方面,hector多了一个/poseupdate
和/slam_out_pose
,前者是具有协方差的机器人位姿估计,后者是没有协方差的位姿估计。
与Gmapping相同,提供/dynamic_map
查询地图服务
以ROS-Academy-for-Beginners
中的hector_slam
为例,选取了它的launch文件slam_sim_demo/launch/hector_demo.launch
为例,关键位置做了注释:
<node pkg="hector_mapping" type="hector_mapping" name="hector_height_mapping" output="screen">
<param name="scan_topic" value="scan" />
<param name="base_frame" value="base_link" />
<param name="odom_frame" value="odom" />
<param name="output_timing" value="false"/>
<param name="advertise_map_service" value="true"/>
<param name="use_tf_scan_transformation" value="true"/>
<param name="use_tf_pose_start_estimate" value="false"/>
<param name="pub_map_odom_transform" value="true"/>
<param name="map_with_known_poses" value="false"/>
<param name="map_pub_period" value="1"/>
<param name="update_factor_free" value="0.45"/>
<param name="map_update_distance_thresh" value="0.1"/>
<param name="map_update_angle_thresh" value="0.05"/>
<param name="map_resolution" value="0.05"/>
<param name="map_size" value="1024"/>
<param name="map_start_x" value="0"/>
<param name="map_start_y" value="0"/>
node>