首页 关于
树枝想去撕裂天空 / 却只戳了几个微小的窟窿 / 它透出天外的光亮 / 人们把它叫做月亮和星星
目录

GMapping的ROS封装——初始化

通过分析例程的launch文件, 我们知道例程gmapping_dema.launch运行的是gmapping包中的节点slam_gmapping。 这个launch文件位于包turtlebot_gazebo中, 加载了在turtlebot_navigation包中的gmapping.launch.xml文件, 在该文件中最终加载运行了slam_gmapping的节点。

实际上slam_gmapping只是对GMapping的ROS封装。OpenSLAM给出了GMapping的源码, 只是这份代码相当古老,最近的一次提交还是2012年的时候,源码编译起来都比较费劲。好在人们对它进行了修改, 在ROS的环境下以openslam_gmapping包的形式进行维护。

本文中,详细解读GMapping的ROS封装。后面一些章节中再来拆解openslam_gmapping。

1. 运行过程分析

在分析源码之前,让我们来分析一下这个Demo都做了些什么工作。首先我们打开四个终端,分别在各个终端中运行如下的四条指令,它们依次打开Gazebo仿真环境,用于控制机器人的keyboard_teleop,GMapping demo, 以及查看构建地图用的rviz。详细请参考Turtlebot你好!一文。

        $ roslaunch turtlebot_gazebo turtlebot_world.launch
        $ roslaunch turtlebot_teleop keyboard_teleop.launch
        $ roslaunch turtlebot_gazebo gmapping_demo.launch
        $ roslaunch turtlebot_rviz_launchers view_navigation.launch

我们可以通过工具rqt_graph来查看当前ROS系统的运行时图,如下图所示,其中"slam_gmapping"就是我们所关注的GMapping demo。在下图的最左边, 我们可以看到节点turtlebot_teleop_keyboard发布了主题/cmd_vel_mux/input/teleop。这就是下发的机器人控制指令,它被节点mobile_base_nodelet_manager订阅。

mobile_base_nodelet_manager是turtlebot_world.launch文件开启的ROS节点,它是机器人的控制器, 运行的是yocs_cmd_vel_mux中的CmdVelMuxNodelet。这个节点可以接收来自多个节点的控制指令, 它根据一些预设的优先级来决定机器人的运动,其输出就是主题"/mobile_base/commands/velocity"。

主题"/mobile_base/commands/velocity"被Gazebo的底盘控制插件所订阅,该插件将根据这一主题的消息计算机器人的里程计信息, 并将之以tf的形式发布。于此同时Gazebo插件还负责计算机器人身上各个关节的运动情况,并以"joint_states"的形式发布。节点robot_state_publisher订阅这一主题,并维护机器人上各个关节之间的变换关系, 以tf的形式发布。

此外,Gazebo的插件还发布机器人上的双目摄像头数据,它们被laserscan_nodelte_manager订阅并将其深度信息转换为激光扫描数据,以主题"/scan"的形式发布。 本文的主角slam_gmapping收集激光扫描数据,结合机器人里程计,实时计算地图信息,并以主题"/map"的形式发布。

$ rosnode info /slam_gmapping 
----------------------------------------------------
Node [/slam_gmapping]
Publications: 
 * /map_metadata [nav_msgs/MapMetaData]
 * /tf [tf2_msgs/TFMessage]
 * /map [nav_msgs/OccupancyGrid]
 * /rosout [rosgraph_msgs/Log]
 * /slam_gmapping/entropy [std_msgs/Float64]

Subscriptions: 
 * /tf [tf2_msgs/TFMessage]
 * /scan [sensor_msgs/LaserScan]
 * /tf_static [tf2_msgs/TFMessage]
 * /clock [rosgraph_msgs/Clock]

Services: 
 * /slam_gmapping/set_logger_level
 * /slam_gmapping/get_loggers
 * /dynamic_map

如右边的代码所示,在终端中运行rosnode info /slam_gmapping就可以查看其详细的运行信息。从指令的运行结果来看,slam_gmapping发布了5个主题。

其中,[/map_metadata]是关于地图的一些配置数据,其消息类型为[nav_msgs/MapMetaData],它描述了地图的分辨率、尺寸、原点等信息。实际上这一信息是[/map]中描述的占用栅格地图数据的一部分。

[/map]使用消息[nav_msgs/OccupancyGrid]来描述占用栅格地图,它由如下的三个字段构成。

[/slam_gmapping/entropy]描述了粒子滤波器中关于机器人位姿的不确定性。[/tf]是ROS系统的tf坐标变换库订阅和发布的主题, tf库坐标变换进行了封装,很多时候我们不太关心主题[/tf]中的消息内容,而是通过tf库中的接口来直接获取坐标变换关系。slam_gmapping发布的这个tf主要是描述地图到里程计的坐标变换。

此外,[/slam_gmapping]还订阅了四个主题。[/tf]来自于机器人里程计的坐标变换,[/scan]则是激光雷达的扫描信息。[/tf_static]是tf2中添加的新特性,目的是提供一个不变的在任何时刻都成立的变换矩阵。 [/clock]则是来自Gazebo的主题,与仿真时间有关。

我们看到,[/slam_gmapping]还提供了三个服务,其中set_logger_level和get_loggers与日志相关,而dynamic_map则返回当前构建的地图。

2. 源码结构

$ git clone https://github.com/ros-perception/slam_gmapping.git
$ cd slam_gmapping
$ tree .
.
├── gmapping
│   ├── CHANGELOG.rst
│   ├── CMakeLists.txt
│   ├── launch
│   │   └── slam_gmapping_pr2.launch
│   ├── nodelet_plugins.xml
│   ├── package.xml
│   ├── src
│   │   ├── main.cpp
│   │   ├── nodelet.cpp
│   │   ├── replay.cpp
│   │   ├── slam_gmapping.cpp
│   │   └── slam_gmapping.h
│   └── test
│       ├── basic_localization_laser_different_beamcount.test
│       ├── basic_localization_stage.launch
│       ├── basic_localization_stage_replay2.launch
│       ├── basic_localization_stage_replay.launch
│       ├── basic_localization_symmetry.launch
│       ├── basic_localization_upside_down.launch
│       ├── rtest.cpp
│       └── test_map.py
└── slam_gmapping
    ├── CHANGELOG.rst
    ├── CMakeLists.txt
    └── package.xml

5 directories, 21 files

如右侧代码所示,我们通过Git工具把slam_gmapping从github上拉下来,并查看其目录结构。

我们会看到有两个包:gmapping和slam_gmapping。其中slam_gmapping是一个所谓的mata-package,这是ROS系统早期的一个概念,现在已经不再使用了,所以不用管它。 gmapping才是对GMapping算法的ROS封装。

查看编译指导文件CMakeLists.txt,一共添加了两个可执行文件slam_mapping和slam_mapping_replay,以及一个链接库文件slam_nodelet。例程中运行的是slam_mapping, 它涉及到slam_gmapping.cpp, main.cpp, 和slam_gmapping.h三个源文件。

add_executable(slam_gmapping src/slam_gmapping.cpp src/main.cpp)
add_library(slam_gmapping_nodelet src/slam_gmapping.cpp
                                  src/nodelet.cpp)
add_executable(slam_gmapping_replay src/slam_gmapping.cpp
                                    src/replay.cpp)

它的main.cpp很简短,在main函数的一开始先初始化ROS的工作环境,接着构建了一个SlamGMapping的对象gn,并通过成员函数startLiveSlam开始建图,最后调用ros::spin()开启消息循环。

        #include <ros/ros.h>
        #include <slam_gmapping.h>
        
        int main(int argc, char** argv) {
          ros::init(argc, argv, "slam_gmapping");
        
          SlamGMapping gn;
          gn.startLiveSlam();
          ros::spin();
        
          return(0);
        }

3. SlamGMapping的初始化过程和参数

SlamGMapping对象gn用ROS的接口完成了对GMapping的封装。在它的默认构造函数中,初始化了地图到里程计的坐标变换map_to_odom_,雷达计数器laser_count_。 使用私有的命名空间构建了ROS的节点句柄private_nh_,用NULL为一些指针赋予初值。在函数体中,调用系统函数time获取当前的系统时间,这是一种常用的初始化随机数种子的方法。 然后就调用成员函数init()完成初始化过程。

        SlamGMapping::SlamGMapping():
            map_to_odom_(tf::Transform(tf::createQuaternionFromRPY( 0, 0, 0 ), tf::Point(0, 0, 0 ))),
            laser_count_(0), private_nh_("~"), scan_filter_sub_(NULL), scan_filter_(NULL), transform_thread_(NULL) {
            seed_ = time(NULL);
            init();
        }

表 1. GMapping参数配置

GMapping封装
~throttle_scansint每当接收到n个激光扫描数据,就进行一次更新。
~base_frame string tf框架下的机器人坐标系名称。
~map_frame string tf框架下的地图坐标系名称。
~odom_frame string tf框架下里程计坐标系名称。
~map_update_intervaldouble地图的更新周期,既连续两次地图计算的时间间隔,时间单位为s。
关于激光传感器的配置参数
~/maxRangedouble传感器的最大测量距离,如果采集到比该值更大的数值将抛弃之。在缺省的情况下, 以第一次扫描中的最大值减去1cm作为maxRange的值。
~/maxUrangedouble用于建图的传感器最大值,默认情况下与maxRange相同。
~/sigmadouble扫描匹配过程中cell的标准差。
~/kernelSizeint扫描匹配过程的搜索窗口大小。
~/lstepdouble扫描匹配的初始距离步长。
~/astepdouble扫描匹配的初始角度步长。
~/iterationsint扫描匹配的迭代步长的细分数量,最终距离和角度的匹配精度分别为(lstep * 2 ^ (-iterations))和(astep * 2 ^ (-iterations))
~/lsigmadouble扫描匹配过程中单个激光扫描束的标准差。
~/ogaindouble似然度平滑的增益。
~/lskipint每个n+1个扫描进行一次扫描匹配,取值为0时表示每次扫描之后都进行一次匹配。
~/minimumScoredouble判定扫描匹配质量足够好的最小值。默认值为0, 最大值可以达到600+,如果遇到了"跳跃"现象可以尝试提高该值。
运动模型中高斯噪声的标准差
~/srrdouble位置的噪声项。
~/sttdouble方位角的噪声项。
~/srtdouble位置到方位角的协方差项。
~/strdouble方位角到位置的协方差项。
扫描匹配所用的似然度采样特性
~/llsamplerangedouble距离采样范围。
~/llsamplestepdouble距离采样步长。
~/lasamplerangedouble角度采样范围。
~/lasamplestepdouble角度采样步长。
~/occ_threshdouble占用概率阈值。
初始地图的尺度和分辨率
~/xmindouble地图的最小x值。
~/xmaxdouble地图的最大x值。
~/ymindouble地图的最小y值。
~/ymaxdouble地图的最大y值。
~/deltadouble地图中一个像素的尺度。
粒子滤波器的配置
~/linearUpdatedouble只有当机器人至少运动了linearUpdate的距离之后才进行一次新的测量。
~/angularUpdatedouble只有当机器人至少转动了angularUpdate的角度之后才进行一次新的测量。
~/resampleThresholddouble粒子重采样的阈值。只有当评价粒子相似度的指标\(N_{eff}\)小于该阈值时才进行重采样,所以降低该值意味着提高重采样的频率。
~/particlesint粒子的数量,每个粒子都表示一条机器人的可能路径。

在init()函数中,首先构建了一个GMapping::GridSlamProcessor对象gsp_。 这个类就是基本的GridFastSLAM算法, 它用Rao-Blackwellized粒子滤波器完成SLAM工作,每个粒子都有一个关于地图和机器人位姿的估计。

        void SlamGMapping::init() {
            gsp_ = new GMapping::GridSlamProcessor();
            ROS_ASSERT(gsp_);

接着创建了一个tf广播器tfB_,用于发布从地图到里程计的坐标变换map_to_odom_。

            tfB_ = new tf::TransformBroadcaster();
            ROS_ASSERT(tfB_);

下面暂时将栅格SLAM算法中用到的激光传感器和里程计对象置为空,它们将在获得第一次扫描数据时构建对象。变量got_first_scan_标识着是否接收到了第一次的扫描数据, got_map_则表示已经生成了地图。在初始化时这两个变量都应该为false。

            gsp_laser_ = NULL;
            gsp_odom_ = NULL;
            got_first_scan_ = false;
            got_map_ = false;

接下来就是各种参数的配置。需要配置的参数有很多,大体上都是以类似下面的形式进行初始化的。首先通过私有句柄private_nh_从参数服务器上请求相关的参数,如果查询发现没有该参数的配置, 就赋予一个缺省值。右边的表1中详细列出了各种参数的数据类型和功能。

            // Parameters used by our GMapping wrapper
            if(!private_nh_.getParam("throttle_scans",
                                      throttle_scans_))
                throttle_scans_ = 1;
            if(!private_nh_.getParam("base_frame",
                                     base_frame_))
                base_frame_ = "base_link";
            if(!private_nh_.getParam("map_frame",
                                      map_frame_))
                map_frame_ = "map";
            if(!private_nh_.getParam("odom_frame",
                                      odom_frame_))
                odom_frame_ = "odom";

            // 省略剩余的配置,详细参考源码
        }

4. initMapper

虽然我们已经构建了GridSlamProcessor对象gsp_,并且从ROS的参数服务器获得了各种配置参数。但是这些参数配置并没有在对象gsp_上生效。 我们还需要调用函数initMapper完成建图引擎的配置。在下文中的地图更新封装介绍中, 我们会看到这一操作将被推迟到第一次获得激光扫描数据之时。

在该函数中,根据输入的传感器测量数据,我们可以获得激光传感器的坐标系名称"scan.header.frame_id",以及获得传感器测量值时刻的时间戳。根据这两个数据, 我们构建了一个打戳的单元向量ident。将该参数和ident一起提供给tf的库函数transformPose,就可以从其输出参数laser_pose中获得激光传感器相对于机器人基座的坐标变换。

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

        bool SlamGMapping::initMapper(const sensor_msgs::LaserScan& scan) {
            laser_frame_ = scan.header.frame_id;
            tf::Stamped<tf::Pose> ident;
            tf::Stamped<tf::Transform> laser_pose;
            ident.setIdentity();
            ident.frame_id_ = laser_frame_;
            ident.stamp_ = scan.header.stamp;
            try {
                tf_.transformPose(base_frame_,
                            ident, laser_pose);
            } catch(tf::TransformException e) {
                return false;
            }

接下来在机器人坐标系下,在雷达传感器的正上方1m处构建一个点,然后将它转换到激光雷达的坐标系下。

            tf::Vector3 v;
            v.setValue(0, 0, 1 + laser_pose.getOrigin().z());
            tf::Stamped<tf::Vector3> up(v, scan.header.stamp, base_frame_);
            try {
                tf_.transformPoint(laser_frame_, up, up);
            } catch(tf::TransformException& e) {
                return false;
            }

构建的这个点将被用来判定传感器的安装方式。GMapping要求激光雷达一定是水平安装的,也就是说雷达的Z轴要么朝上,要么朝下。如果传感器是水平安装的, 那么从机器人坐标系下转换到传感器坐标系下,该点的z轴数据应当接近为1或者-1。

            if (fabs(fabs(up.z()) - 1) > 0.001)
                return false;

从传感器消息中获取激光扫描数据量和中间角度。

            gsp_laser_beam_count_ = scan.ranges.size();
            double angle_center = (scan.angle_min + scan.angle_max)/2;

接下来我们根据刚刚判定的传感器安装方式以及中间角度,来初始化传感器的中心位置centered_laser_pose_。另外扫描的最小角度和最大角度了说明扫描方向, 如果扫描反向,那么在以后的数据处理过程中,将以相反的顺序处理扫描数据。这里用变量do_reverse_range_来记录这一特性。

            if (up.z() > 0) {
                do_reverse_range_ = scan.angle_min > scan.angle_max;
                centered_laser_pose_ = tf::Stamped(tf::Transform(tf::createQuaternionFromRPY(0,0,angle_center),
                                                               tf::Vector3(0,0,0)), ros::Time::now(), laser_frame_);
            } else {
                // 传感器倒着安装,代码略
            }

然后,我们根据扫描的数据长度和增长步长初始化雷达的扫描角度表laser_angles_,该表中的角度都是从小到大增长的。

            laser_angles_.resize(scan.ranges.size());
            double theta = -std::fabs(scan.angle_min - scan.angle_max)/2;
            for(unsigned int i = 0; i < scan.ranges.size(); ++i) {
                laser_angles_[i] = theta;
                theta += std::fabs(scan.angle_increment);
            }

接下来配置参数maxRange和maxUrange。默认情况下,maxRange的取值为第一次扫描最大值减去1cm,maxUrange默认与maxRange取值相同。

            if(!private_nh_.getParam("maxRange", maxRange_))
                maxRange_ = scan.range_max - 0.01;
            if(!private_nh_.getParam("maxUrange", maxUrange_))
                maxUrange_ = maxRange_;

接着构建距离传感器对象gsp_laser_。该对象的名称一定是"FLASER"。此外传递的扫描角度增量是绝对值,如果实际的传感器扫描方向与之相反, 我们的封装会在传递给建图引擎的时候就将之反转过来。

            GMapping::OrientedPoint gmap_pose(0, 0, 0);
            gsp_laser_ = new GMapping::RangeSensor("FLASER", gsp_laser_beam_count_,
                                fabs(scan.angle_increment), gmap_pose, 0.0, maxRange_);

然后将新建的距离传感器对象添加到建图引擎中。这里的类型GMapping::SensorMap实际上就是标准库中的map,在openslam_gmapping的头文件 sensor.h中定义为 typedef std::map<std::string, Sensor*> SensorMap;,在字符串与传感器对象之间建立映射关系。

            GMapping::SensorMap smap;
            smap.insert(make_pair(gsp_laser_->getName(), gsp_laser_));
            gsp_->setSensorMap(smap);

接着创建里程计对象,获取初始位置。并根据这一初始位置完成建图引擎的初始化操作。

            gsp_odom_ = new GMapping::OdometrySensor(odom_frame_);
            GMapping::OrientedPoint initialPose;
            if(!getOdomPose(initialPose, scan.header.stamp))
                initialPose = GMapping::OrientedPoint(0.0, 0.0, 0.0);
            // 省略了对建图引擎的参数配置语句
            gsp_->GridSlamProcessor::init(particles_, xmin_, ymin_, xmax_, ymax_, delta_, initialPose);
        }

5. 总结

在本文中,我们解读了slam_gmapping对GMapping建图引擎的封装的初始化过程。GMapping使用激光传感器和里程计数据进行SLAM建图,它是一种Rao-Blackwellized粒子滤波器。 我们还详细介绍了GMapping相关的各种参数配置。




Copyright @ 高乙超. All Rights Reserved. 京ICP备16033081号-1