(九) 总结

gmapping源码中不完善的部分:

  1. drawFromMotion函数是一个十分粗糙的运动模型,只是简单的矢量加减运算。相比于《概率机器人》中提到的速度模型和里程计模型,有很多方面都没有考虑,精度上可能有折扣。
  2. m_angularOdometryReliabilitym_linearOdometryReliability用于控制激光评分的增益odo_gain, 实际都是0,所以没有用到,而且无法用参数赋值, 要使用就得修改源码.
  3. 紧接上面的代码,localScore=odo_gain*score(map, localPose, readings);,localScore在之后的likelihoodAndScore函数里实际赋值为0,根本没有用。所以这里有好几行可以删除
  4. 在论文中,粒子的权重不是用最优位姿的似然值来表示的,而是用所有的似然值的和来表示的
  5. 重采样的粒子索引选取用的是轮盘赌算法,有些论文提出了一些更好的算法

OpenMP加速gmapping

在CMakeLists里做如下配置:

1
2
3
4
5
6
FIND_PACKAGE( OpenMP REQUIRED)
if(OPENMP_FOUND)
message("OPENMP FOUND")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${OpenMP_C_FLAGS}")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${OpenMP_CXX_FLAGS}")
endif()

注意: OpenMP并不适合需要复杂的线程间同步和互斥的场合,这种情况下花的时间可能更长

gmapping使用OpenMP加速的语句: #pragma omp parallel for

  1. for循环的drawFromMotion之前

    1
    2
    3
    4
    5
    //#pragma omp parallel for
    for (unsigned int i = 0; i < m_particles.size(); i++) {

    m_particles[i].pose = m_motionModel.drawFromMotion(m_particles[i].pose, relPose,m_odoPose);
    }
  2. invalidateActiveArea之前

    1
    2
    3
    //#pragma omp parallel for
    for (unsigned int i = 0; i < m_particles.size(); i++) {
    m_matcher.invalidateActiveArea();
  3. scanMatch函数开头,实际就是对整个scanMatch并行化

    1
    2
    3
    inline void scanMatch(double * plainReading) {
    #pragma omp parallel for
    for (unsigned int i = 0; i < m_particles.size(); i++) {
  4. updateMap函数中

1
2
3
4
5
#pragma omp parallel for
for(int x=0; x < smap.getMapSizeX(); x++)
{
for(int y=0; y < smap.getMapSizeY(); y++)
{
  1. 重采样resample函数

对于保留下来的粒子进行更新,在并行化操作里面m_particles.push_back()会报错,因此需要把push_back()提出来,在外面的的for循环进行

1
2
3
4
5
6
7
8
9
10
11
#pragma omp parallel for
for(int i = 0; i<tmp_size;i++)
{
//对保留下来的粒子数据进行更新
//每个粒子的权重都设置为相同的值
temp[i].setWeight(0);
//为每个粒子更新running_scans
//增加了一帧激光数据 因此需要更新地图
m_matcher.registerScan(temp[i].map,temp[i].pose,plainReading);
//m_matcher.registerScan(temp[i].lowResolutionMap,temp[i].pose,plainReading);
}

为每个粒子更新地图时,同样可以并行化


(八) 源码分析6 scan match

先接上篇,不是第一帧激光数据的情况,上来是scanMatch函数,也就是GridSlamProcessor::scanMatch(在文件gridslamprocessor.hxx)。

主要部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
inline void GridSlamProcessor::scanMatch(const double* plainReading)
{
// sample a new pose from each scan in the reference
/* 每个粒子都要进行 scan-match */
double sumScore=0;
int particle_number = m_particles.size();
//可以用openMP的方式来进行并行化,因此这里不能用迭代器,只能用下标的方式
//并行化之后,里面的循环会均匀分到各个不同的核里面
for (int i = 0; i < particle_number; i++)
{
OrientedPoint corrected;
double score, l, s;
/*进行scan-match 计算粒子的最优位姿,这是gmapping本来的做法*/
score = m_matcher.optimize(corrected, m_particles[i].map, m_particles[i].pose, plainReading);
/* 匹配成功则更新最优位姿 */
if (score>m_minimumScore)
{
m_particles[i].pose = corrected;
}
/* 扫描匹配不上,则使用里程计的数据,使用里程计数据不进行更新.
因为在进行扫描匹配之前 里程计已经更新过了*/
else
{
if (m_infoStream)
{
m_infoStream << "Scan Matching Failed, using odometry. Likelihood=" << l <<std::endl;
m_infoStream << "lp:" << m_lastPartPose.x << " " << m_lastPartPose.y << " "<< m_lastPartPose.theta <<std::endl;
m_infoStream << "op:" << m_odoPose.x << " " << m_odoPose.y << " "<< m_odoPose.theta;
}
}
// 粒子的最优位姿计算了之后,重新计算粒子的权重,相当于粒子滤波器中的观测步骤
/* 计算p(z|x,m),粒子的权重由粒子的似然来表示, 计算粒子的得分和权重(似然)
注意粒子的权重经过ScanMatch之后已经更新了
* 在论文中 粒子的权重不是用最优位姿的似然值来表示的
* 是用所有的似然值的和来表示的, s是得分 l是似然,也就是权重 */
m_matcher.likelihoodAndScore(s, l, m_particles[i].map, m_particles[i].pose, plainReading);
sumScore+=score;
m_particles[i].weight+= l;
m_particles[i].weightSum+= l;

//set up the selective copy of the active area
//by detaching the areas that will be updated
/*计算出来最优的位姿之后,进行地图的扩充 这里不会进行内存分配
*不进行内存分配的原因是这些粒子进行重采样之后有可能会消失掉,因此在后面进行重采样的时候统一进行内存分配。
*理论上来说,这里的操作是没有必要的,因为后面的重采样的时候还会进行一遍
*/
m_matcher.invalidateActiveArea();
m_matcher.computeActiveArea(m_particles[i].map, m_particles[i].pose, plainReading);
}
if (m_infoStream)
m_infoStream << "Average Scan Matching Score of all particles=" << sumScore/m_particles.size();
}

开始的都没意思,直接看ScanMatcher::optimize,在scanmatcher.cpp里,开头又是一个重要函数score,在scanmatcher.h里。它根据地图、激光数据、位姿迭代求解一个最优的新的位姿出来。

代码比较复杂,直接看整个scan函数(包括了optimize和score)的评分算法的理论:

对于栅格地图的环境地图模型,一般采用基于似然场模型(likelihood field range finder mode)的扫描匹配算法来进行激光雷达数据与地图的匹配。 该算法通过评估当前时刻位姿的激光雷达数据中每一个激光点与该时刻栅格地图的匹配程度,并将评估得到的每个激光点得分进行累加,得到当前时刻激光雷达数据与地图匹配的总分数,得分越高说明该时刻激光雷达信息与地图匹配程度越高,即机器人位姿越精确。由于该算法能够根据机器人当前时刻状态、 观测值和地图信息直接得到该时刻位姿的匹配程度,因而该算法常与基于粒子滤波的定位算法结合使用,用于选取各粒子中得分最高的位姿,并计算得分, 从而完成机器人位姿的确定,其原理如图:
基于似然场模型的扫描匹配示意图.png

黑色的格子代表前一时刻栅格地图中障碍物的位置, 圆形的点表示以里程计运动模型作为提议分布得到的机器人位姿估计为基础,将当前时刻激光雷达数据转换到栅格地图坐标系中的激光点的分布。把激光雷达的坐标转换到世界坐标系: 先旋转到机器人坐标系,然后再转换到世界坐标系。

该位姿下观测信息与地图匹配得分的具体算法步骤如下:对于当前激光雷达数据中任意一个激光点,设其端点在世界坐标系中坐标为 lp ,则其对应栅格地图中坐标为 phit ,对 phit周围八个栅格进行障碍物判定,并计算这些障碍物的平均坐标 pavr, 求出 pavr与 phit 的距离 dist 。 该激光点的评分可由下式表示
评分公式.png
ε为基于扫描匹配概率的激光方差,对该时刻所有激光点进行上式的计算,并将评分进行求和,分数越高说明该位姿的可信度越高。 对当前时刻所有粒子位姿进行扫描匹配运算, 并根据得分实现粒子的权重更新,最后求出粒子群的平均评分sumScore/m_particles.size()

如果设置的minimumScore参数过大,一些粒子的匹配会失败,因为要求太高了,gmapping会出现下列信息:
scan match失败.png
gmapping变成使用里程计进行位姿估计,这其实是不好的,因为从论文可知激光精度比里程计精确得多,但是注意粒子的权重计算还是调用likelihoodAndScore函数。

试验小强的scan match评分

直接修改gridslamprocessor.hxx的scanMatch函数。 小强的gmapping编译有问题,执行catkin_make --pkg gmapping之后,需要 cp /home/xiaoqiang/Documents/ros/devel/lib/gmapping/slam_gmapping /home/xiaoqiang/Documents/ros/src/gmapping/launch,否则roslaunch找不到节点文件

根据测试,激光评分多数在140~160,有时也会超过160.

for循环剩下的部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/*矫正成功则更新位姿*/
if (score>m_minimumScore)
{
m_particles[i].pose = corrected;
}
// 扫描匹配不上,则使用里程计的数据,使用里程计数据不进行更新,因为扫描匹配之前,里程计已经更新过了
else
{
//输出信息 这个在并行模式下可以会出现错位
if (m_infoStream)
{
m_infoStream << "Scan Matching Failed, using odometry. Likelihood=" << l <<endl;
m_infoStream << "lp:" << m_lastPartPose.x << " " << m_lastPartPose.y << " "<< m_lastPartPose.theta <<endl;
m_infoStream << "op:" << m_odoPose.x << " " << m_odoPose.y << " "<< m_odoPose.theta <<endl;
}
}
/* 粒子的最优位姿计算了之后,重新计算粒子的权重(相当于粒子滤波器中的观测步骤,
计算p(z|x,m)),粒子的权重由粒子的似然来表示。
* 计算粒子的得分和权重(似然) 注意粒子的权重经过ScanMatch之后已经更新了
* 在论文中,例子的权重不是用最有位姿的似然值来表示的。
* 是用所有的似然值的和来表示的。
*/
m_matcher.likelihoodAndScore(s, l, m_particles[i].map, m_particles[i].pose, plainReading);

sumScore+=score;
m_particles[i].weight+=l;
m_particles[i].weightSum+=l;
//set up the selective copy of the active area
//by detaching the areas that will be updated
/*计算出来最优的位姿之后,进行地图的扩充 这里不会进行内存分配
*不进行内存分配的原因是这些粒子进行重采样之后有可能会消失掉,因此在后面进行冲采样的时候统一进行内存分配。
*理论上来说,这里的操作是没有必要的,因为后面的重采样的时候还会进行一遍
*/
m_matcher.invalidateActiveArea();
m_matcher.computeActiveArea(m_particles[i].map, m_particles[i].pose, plainReading);

重采样

resample函数: 该函数在gridslamprocessor.hxx。首先是备份老的粒子的轨迹,即保留叶子的节点。然后是需要重采样还是不需要重采样,如果不需要重采样,则权值不变。只为轨迹创建一个新的节点,每个粒子更新地图。当有效值小于阈值的时候需要重采样,通过resampleIndexes提取到需要删除的粒子。删除粒子后,保留当前的粒子并在保存的粒子的节点里新增一个节点。删除要删除粒子的节点,保留的粒子进行数据更新,将每个粒子的设置同一个权重。最后更新一下地图。

resampleIndexes:该函数在particlefilter.h中,使用轮盘赌算法,决定哪些粒子会保留,保留的粒子会返回下标,里面的下标可能会重复,因为有些粒子会重复采样,而另外的一些粒子会消失掉。

首先计算总的权重,计算平均权重值(interval),根据权值进行采样,target是0-1分布随机选取的一数值,当总权重大于目标权重的,记录该粒子的索引,target在加上一个interval。如果某个粒子的权重比较大的话,就被采样多次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 简化声明
int resampleIndexes(const std::vector<Particle>& particles, int nparticles) const
{
Numeric cweight=0;
/*计算总的权重*/
unsigned int n=0;
for (typename std::vector<Particle>::const_iterator it=particles.begin(); it!=particles.end(); ++it)
{
cweight+=(Numeric)*it;
n++;
}
if (nparticles>0)
n=nparticles;
//compute the interval
Numeric interval=cweight/n;

// drand48 返回服从均匀分布的·[0.0, 1.0) 之间的double随机数
Numeric target=interval*::drand48();
// 如果某个粒子的区间为4*interval。那么它至少被采样3次。
cweight=0; n=0;
std::vector<unsigned int> indexes(n);
unsigned int i=0;
for (typename std::vector<Particle>::const_iterator it=particles.begin(); it!=particles.end(); ++it, ++i)
{
cweight+=(Numeric)* it;
while(cweight>target)
{
indexes[n++]=i;
target+=interval;
}
}
return indexes;
}


小强机器人的补充说明

多次测试,发现只能在室内平地建图,在地面不平的停车场运行carto,无论用机器人的odom还是carto的,odom坐标系会严重漂移,原因不明。

修改记录

2020.12.24 xqserial_no_odom.launch: 将base_link和base_footprint之间的变换的注释去掉

编译问题

/home/xiaoqiang/Documents/ros/src/ORB_SLAM2中的CMakeLists大部分内容都注释掉了,目前还用不到它.

注释xqserial_server/CMakeLists.txt的57行,否则出现下面情况:

nav_test不需要特别编译,它全是py文件,没有cpp。但是需要从网上下载rbx1工程,这是一本教程里用到的,只把其中的rbx1_nav放到当前工作空间里,编译通过。nav_squared.py需要它

startup.launch注释掉一部分,比如摄像头节点等等。

robot_pose_ekf包很奇怪,它应当是在/home/xiaoqiang/Documents/ros/src/navigation,但是修改相应源码,执行launch没有效果。最后发现实际使用的是R50_Release中的robot_pose_ekf。删掉后者就报错找不到了,可见xiaoqiang中的那个没有生效,不像个package,但是rospd又能找到,很奇怪。

运动部分

odomIMU话题的频率都是50Hz

小强有时不移动,可能是IMU在重启,因为此时的IMU数据除了四元数的 w 为1,其他全是0

/xqserial_server/Twist疑似无用,只有/motor_driver在一直发布它

xqserial_server/Pose2D的消息是(x,y,theta),是相对里程计坐标系原点的位姿

里程计模型也是速度积分的二轮差速,使用编码器读数获得极小时间内的位移,以左右轮的编码器增量的均值计算。

要开启红外功能,必须使用命令 rostopic pub -r 12 /barDetectFlag std_msgs/Bool "data: true",但红外检测距离太短了,目前被摄像头挡住了,实际不生效

control.py就是从键盘获取方向键,发布速度命令到cmd_vel话题,订阅了/xqserial_server/Odom话题,但是实际没用到。

xqserial_server命名空间包括StatusPublisherDiffDriverController两个类

xiaoqiang_log话题对应的LogRecored.msg如下,只有缺少串口设备时才发布,record是json形式

1
2
3
string collection_name
time stamp
string record

xiaoqiang_tts/text话题对应语音提示信息,也是在缺少串口设备时才发布

startup.launch

开机启动的launch文件是startup包中的startup.launch

1
2
3
4
5
6
7
8
9
10
11
12
13
<launch>
<node pkg="xqserial_server" type="xqserial_server" name="motor_driver" respawn="true" respawn_delay="10">
<param name="debug_flag" value="false"/>
<param name="wheel_separation" value="0.360"/>
<param name="wheel_radius" value="0.0625"/>
<param name="port" value="/dev/ttyUSB001"/>
<param name="r_min" value="1.0"/>
</node>

<node pkg="tf" type="static_transform_publisher" name="baselink_broadcaster" args="0 0 0.15 0 0 0 1 base_footprint base_link 50">
</node>
<node pkg="tf" type="static_transform_publisher" name="imulink_broadcaster" args="-0.1 -0.03 0 0 0 0 1 base_link imu 50"/>
</launch>

主要是xqserial_server,然后是静态发布tf变换: base_footprint --> base_link --> imu

urdf

rviz显示问题

尝试在远程机上同时启动xiaoqiang_udrf.launch和rviz,先尝试加载urdf.rviz,这种情况下的tf变换都是正常的
加载urdf并显示小强机器人.png

但是常用方式是远程机启动launch,本地机启动rviz,rviz里做好配置后,发现终端会报错:找不到所需要的所有STL文件,这几个文件在/home/xiaoqiang/Documents/ros/src/xiaoqiang_udrf/meshes。这是因为rviz在本机找这些文件,

launch文件里加载了xiaoqiang_udrf.URDF,这个文件又加载STL文件,语句是这样的:

1
<mesh  filename="package://xiaoqiang_udrf/meshes/base_link.STL" />

也就是说本地机需要一个名为xiaoqiang_udrf的package,自己新建一个。

这时再打开rviz就不会报错了,但还是有问题,整个模型透明发白,这是因为rivz中的全局坐标系“fixed frame”设置的不合适,将map改成base_link后即可正常显示。

结果机器人并不能动,rviz里仍显示几个wheel和odom没有tf变换,但是rosrun tf tf_echo有正常结果,结果发现就是显示的问题,重启rviz就好了

坐标系冲突

现在urdf中的坐标系叫做lidar_urdfstatic_transform_publisher定义的是base_link和laser的关系,在a2.launch里。
urdf中的base_link和laser的关系.png
static_transform_publisher中base_link和laser的关系.png
可以看到二者的坐标关系有所不同,如果把lidar_urdf也改成laser,那么在rviz可以看到laser坐标系会不断旋转跳动,这是存在两个tf变换造成的。由于urdf只是个显示功能,所以还是用两个坐标系。base_linklaser之间的关系可能不精确,这就需要标定了

tf变换

开机启动下的tf关系图.png

odom和base_foot坐标系低于地图

雷达扫描的轨迹和地图边缘有一定差距

LaserScan未正常显示,但tf正常

无地图状态下,以odom为固定坐标系,移动机器人时,base_link会相对移动。但是用rviz里的Odometry可以表达base_link在odom坐标系下的运动

使用伽利略客户端

使用最新版本,一开始总连接不成功,最后发现是公用网络的防火墙造成的,但是不知为什么,防火墙已经将它添加例外了,无法修改,只好把防火墙关了
防火墙.png
关闭串口.png

建图

gmapping建图: roslaunch gmapping a2.launch,对应rplidar_a2_test.rviz


(七) 源码分析5 updateMap函数

第1部分

这里的更新地图,只是为了可视化。因为真正的地图存储在粒子里面,这里会拿一个权值最大的粒子的地图发布出来.

得到权值最大的粒子,然后遍历这个粒子的整个轨迹,根据轨迹上记录的信息来进行建图,然后把得到的地图发布出去。

每次addScan()成功了,就会调用这个函数来生成地图,并发布出去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
ROS_DEBUG("Update map");
// 更新地图的时候,加了一把锁
boost::mutex::scoped_lock map_lock (map_mutex_);
// 构造函数的解释在下一篇
GMapping::ScanMatcher matcher;

/*设置scanmatcher的各个参数*/
matcher.setLaserParameters(scan.ranges.size(), &(laser_angles_[0]),
gsp_laser_->getPose() );
matcher.setlaserMaxRange(maxRange_);
matcher.setusableRange(maxUrange_);
matcher.setgenerateMap(true);

// 得到累计权重(weightSum)最大的粒子,不是当前的最大权重的粒子
// 累计权重即该粒子在各个时刻的权重之和(轨迹上的各个节点的权重之和)
GMapping::GridSlamProcessor::Particle best =
gsp_->getParticles()[gsp_->getBestParticleIndex()];

std_msgs::Float64 entropy;
// computePoseEntropy 遍历粒子集合计算熵
entropy.data = computePoseEntropy();
//发布位姿的熵
if(entropy.data > 0.0)
entropy_publisher_.publish(entropy);

//如果没有地图,则初始化一个地图
if(!got_map_)
{
// nav_msgs::GetMap::Response map_
map_.map.info.resolution = delta_;
map_.map.info.origin.position.x = 0.0;
map_.map.info.origin.position.y = 0.0;
map_.map.info.origin.position.z = 0.0;
map_.map.info.origin.orientation.x = 0.0;
map_.map.info.origin.orientation.y = 0.0;
map_.map.info.origin.orientation.z = 0.0;
map_.map.info.origin.orientation.w = 1.0;
}

计算位姿的信息熵

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
double SlamGMapping::computePoseEntropy()
{
double weight_total=0.0;
for( std::vector<GMapping::GridSlamProcessor::Particle>::const_iterator it = gsp_->getParticles().begin();
it != gsp_->getParticles().end();
++it)
{
weight_total += it->weight;
}
double entropy = 0.0;
for( std::vector<GMapping::GridSlamProcessor::Particle>::const_iterator it = gsp_->getParticles().begin();
it != gsp_->getParticles().end();
++it)
{
if(it->weight/weight_total > 0.0)
entropy += it->weight/weight_total * log(it->weight/weight_total);
}
return -entropy;
}

第2部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
/*地图的中点*/
GMapping::Point center;
center.x=(xmin_ + xmax_) / 2.0;
center.y=(ymin_ + ymax_) / 2.0;

/* 初始化一个scanmatcherMap 创建一个地图 */
GMapping::ScanMatcherMap smap(center, xmin_, ymin_, xmax_, ymax_,
delta_);

/*更新地图*/
//遍历最优粒子的整条轨迹树, 按照轨迹上各个节点存储的信息,计算激活区域更新地图
ROS_DEBUG("Trajectory tree:");
for(GMapping::GridSlamProcessor::TNode* n = best.node;n;n = n->parent)
{
ROS_DEBUG(" %.3f %.3f %.3f",
n->pose.x,
n->pose.y,
n->pose.theta);
if(!n->reading)
{
ROS_DEBUG("Reading is NULL");
continue;
}
//进行地图更新
//matcher.invalidateActiveArea();
//matcher.computeActiveArea(smap, n->pose, &((*n->reading)[0]));
//matcher.registerScan(smap, n->pose, &((*n->reading)[0]));
matcher.registerScan(smap, n->pose, &(n->reading->m_dists[0]));
}

// the map may have expanded, so resize ros message as well
// 扩充地图的大小
if(map_.map.info.width != (unsigned int) smap.getMapSizeX() || map_.map.info.height != (unsigned int) smap.getMapSizeY())
{

// NOTE: The results of ScanMatcherMap::getSize() are different from the parameters given to the constructor
// so we must obtain the bounding box in a different way
GMapping::Point wmin = smap.map2world(GMapping::IntPoint(0, 0));
GMapping::Point wmax = smap.map2world(GMapping::IntPoint(smap.getMapSizeX(), smap.getMapSizeY()));
xmin_ = wmin.x; ymin_ = wmin.y;
xmax_ = wmax.x; ymax_ = wmax.y;

ROS_DEBUG("map size is now %dx%d pixels (%f,%f)-(%f, %f)", smap.getMapSizeX(), smap.getMapSizeY(),
xmin_, ymin_, xmax_, ymax_);

map_.map.info.width = smap.getMapSizeX();
map_.map.info.height = smap.getMapSizeY();
map_.map.info.origin.position.x = xmin_;
map_.map.info.origin.position.y = ymin_;
map_.map.data.resize(map_.map.info.width * map_.map.info.height);

ROS_DEBUG("map origin: (%f, %f)", map_.map.info.origin.position.x, map_.map.info.origin.position.y);
}

//根据地图的信息计算出来各个点的情况:occ、free、noinformation
//这样对地图进行标记主要是方便用RVIZ显示出来
for(int x=0; x < smap.getMapSizeX(); x++)
{
for(int y=0; y < smap.getMapSizeY(); y++)
{
/// @todo Sort out the unknown vs. free vs. obstacle thresholding
/// 得到.xy被占用的概率
GMapping::IntPoint p(x, y);
double occ=smap.cell(p);
assert(occ <= 1.0);

//unknown
if(occ < 0)
map_.map.data[MAP_IDX(map_.map.info.width, x, y)] = GMAPPING_UNKNOWN;

//占用
else if(occ > occ_thresh_)
{
//map_.map.data[MAP_IDX(map_.map.info.width, x, y)] = (int)round(occ*100.0);
map_.map.data[MAP_IDX(map_.map.info.width, x, y)] = GMAPPING_OCC;
}

//freespace
else
map_.map.data[MAP_IDX(map_.map.info.width, x, y)] = GMAPPING_FREE;
}
}

//到了这一步,肯定是有地图了。
got_map_ = true;

//make sure to set the header information on the map
//把计算出来的地图发布出去
map_.map.header.stamp = ros::Time::now();
map_.map.header.frame_id = tf_.resolve( map_frame_ );

sst_.publish(map_.map);
sstm_.publish(map_.map.info);

OctoMap安装配置

ROS环境下的安装:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 也可用与非ROS环境
sudo apt-get install ros-kinetic-octomap
# octomap的查看工具
sudo apt-get install ros-kinetic-octovis
````
`octomap_ros`和`octomap_msgs`提供了消息文件,封装和转换方法。`octomap_server`提供建图和服务

## 编译

在`package.xml`中添加:
```sh
<build_depend>octomap</build_depend>
<run_depend>octomap</run_depend>

因为库提供了CMake的config文件,所以直接这样用:

1
2
3
find_package(octomap REQUIRED)
include_directories(${OCTOMAP_INCLUDE_DIRS})
target_link_libraries(${OCTOMAP_LIBRARIES})


点云滤波

地图分很多种:稀疏的,稠密的,还有半稀疏的。稀疏的地图放大了看就是一个个离散的空间点,不过我们可以把它变成连续的稠密的网格,这个过程也叫点云的网格化。点云网格化需要对点云进行一系列处理

一般下面这几种情况需要进行点云滤波处理:

  1. 点云数据密度不规则需要平滑

  2. 因为遮挡等问题造成离群点需要去除

  3. 大量数据需要降采样

  4. 噪声数据需要去除

滤波程序

package.xml中要添加:

1
2
<build_depend>libpcl-all-dev</build_depend>
<exec_depend>libpcl-all</exec_depend>

CMake按照第一篇里进行配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <ros/ros.h>
#include <sensor_msgs/PointCloud2.h>
#include <pcl_conversions/pcl_conversions.h>
#include <pcl/point_cloud.h>
#include <pcl/point_types.h>
//滤波的头文件
#include <pcl/filters/voxel_grid.h>

ros::Publisher pub;

void cloud_cb (const sensor_msgs::PointCloud2ConstPtr& cloud_msg)
{
// 声明存储原始数据与滤波后的数据的点云的格式
pcl::PCLPointCloud2* cloud = new pcl::PCLPointCloud2; //原始的点云的数据格式
pcl::PCLPointCloud2ConstPtr cloudPtr(cloud);

// ROS消息转化为PCL中的点云数据格式
pcl_conversions::toPCL(*cloud_msg, *cloud);

pcl::PCLPointCloud2 cloud_filtered; //存储滤波后的数据格式
// 进行一个滤波处理
pcl::VoxelGrid<pcl::PCLPointCloud2> sor; //创建滤波对象
sor.setInputCloud (cloudPtr); //设置输入的滤波,将需要过滤的点云给滤波对象
sor.setLeafSize (0.1, 0.1, 0.1); //设置滤波时创建的体素大小为1cm立方体
sor.filter (cloud_filtered);//执行滤波处理,存储输出cloud_filtered

// 再将滤波后的点云的数据格式转换为ROS下的数据格式发布
sensor_msgs::PointCloud2 output;
pcl_conversions::moveFromPCL(cloud_filtered, output);//第一个参数是输入,后面的是输出
pub.publish (output);
}

int main (int argc, char** argv)
{
ros::init (argc, argv, "filter_cloud");//声明节点的名称
ros::NodeHandle nh;

ros::Subscriber sub = nh.subscribe<sensor_msgs::PointCloud2> ("/camera/depth/points", 1, cloud_cb);
pub = nh.advertise<sensor_msgs::PointCloud2> ("filtered_cloud", 1);

ros::spin ();
}

点云合并

把相邻的点合并成一个点,一方面可以减少图像的数据量,另一方面可以增加点云的稳定度。个人感觉贴近滤波
点云合并

直通滤波

多线雷达点云大部分的点都集中的中间区域,距离越远点云越稀疏,信息量也越小,应该筛选掉一些较远的点。这就用到了直通滤波,它是最为简单、粗暴的一种滤波方式,就是直接对点云的X、Y、Z轴的点云坐标约束来进行滤波。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <iostream>
#include <pcl/io/pcd_io.h>
#include <pcl/point_types.h>
#include <pcl/filters/passthrough.h>

int main(int argc, char** argv)
{

pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);

// Fill in the cloud data
pcl::PCDReader reader;
reader.read("16line.pcd", *cloud);

std::cerr << "Cloud before filtering: " << cloud->points.size() << std::endl;

pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_filtered2(new pcl::PointCloud<pcl::PointXYZ>);
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_filtered3(new pcl::PointCloud<pcl::PointXYZ>);

// Create the filtering object 直通滤波
pcl::PassThrough<pcl::PointXYZ> pass;
pass.setInputCloud(cloud);
pass.setFilterFieldName("x"); // 设置操作的坐标轴
pass.setFilterLimits(-5.0, 5.0);
pass.filter(*cloud_filtered2);
// filter range Y-axis
// 注意多次滤波需要再次输入点云和调用filter函数
pass.setInputCloud(cloud_filtered2);
pass.setFilterFieldName("y");
pass.setFilterLimits(-5.0, 5.0);
pass.filter(*cloud_filtered3);

std::cerr << "Cloud after filtering: " << cloud_filtered3->points.size() << std::endl;

// 读取pcd点云数据进行操作,最后保存为pcd文件
pcl::PCDWriter writer;
writer.write("16line_filtered.pcd", *cloud_filtered3, false);

return 0;
}

pass.setFilterLimitsNegative(true);: 是否保存滤波的限制范围内的点云,默认为false,保存限制范围点云,true时候是相反。

条件滤波法

在某些情况下,我们也会需要去除给定区域内部的点,比如激光雷达扫描近处的点(可能就是装载雷达的车),这些点通常对建图和感知不会有作用。

设定滤波条件进行滤波,点在范围内保留,不在范围内丢弃。

getFilterLimitsNegative 可以对选定范围取反,将其设为 true 时可以进行给定只保留给定范围内的点的功能。

体素滤波

实现降采样,既减少点云的数量,并同时保持点云的形状特征。创建一个三维体素,用体素内所有点的重心近似表示体素中的其他点,这样体素内所有点就用一个重心点最终表示。

使用小程序进行体素滤波,输入input.pcd,输出output.pcd

1
pcl_voxel_grid input.pcd output.pcd -leaf 0.03,0.03,0.03

对于体素下采样接口功能函数pcl提供了两种方式 pcl::ApproximateVoxelGridpcl::VoxelGrid。两种区别主要在于第一种是用每个体素栅格的中心点来近似该体素内的点,提升了速度,但是也损失了原始点云的局部形态精细度。 ApproximateVoxelGrid是依据每一个体素的中心点来获取点云的,并不是依赖每个体素里面是否存在点云。所以会导致两种方式的处理结果又偏差,VoxelGrid的方式更加精确但是耗时相对于ApproximateVoxelGrid更高。

半径滤波

对整个输入迭代一次,对于每个点进行半径R的邻域搜索(球体),如果邻域点的个数低于某一阈值,则该点将被视为噪声点并被移除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);      //待滤波点云
if (pcl::io::loadPCDFile("test.pcd", *cloud) < 0)
{
PCL_ERROR("\a点云文件不存在!\n");
system("pause");
return -1;
}

pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_filtered(new pcl::PointCloud<pcl::PointXYZ>); //滤波后点云
pcl::RadiusOutlierRemoval<pcl::PointXYZ> ror; //创建滤波器对象
ror.setInputCloud(cloud); //设置待滤波点云
ror.setRadiusSearch(0.02); //设置查询点的半径邻域范围
ror.setMinNeighborsInRadius(5); //设置判断是否为离群点的阈值,即半径内至少包括的点数
//ror.setNegative(true); //默认false,保存内点;true,保存滤掉的外点
ror.filter(*cloud_filtered); //执行滤波,保存滤波结果于cloud_filtered

实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void radiusremoval(pcl::PointCloud<pcl::PointXYZ>::Ptr& cloud, pcl::PointCloud<pcl::PointXYZ>::Ptr& cloud_filtered, double radius, int min_pts)
{
pcl::KdTreeFLANN<pcl::PointXYZ> tree;
tree.setInputCloud(cloud);
vector<float> avg;
for (int i = 0; i < cloud->points.size(); ++i)
{
vector<int> id(min_pts);
vector<float> dist(min_pts);
tree.nearestKSearch(cloud->points[i], min_pts, id, dist);
// 是否是平方
if (dist[dist.size() - 1] < radius)
{
cloud_filtered->push_back(cloud->points[i]);
}
}
}

构建一个 KD 树,对每个点进行范围搜索(最近邻搜索 nearestKSearch),最后判断邻近点数(最大距离)是否满足要求即可。

统计滤波


看过源码后得知, 计算均值 时除以的是点云的点数,不是邻域的点数。 对滤波结果有影响的是标准差阈值系数k和邻域的点数。 使用的也是 nearestKSearch

缺点:对较远的激光线也筛除掉了。首先对激光雷达点云数据中的每个点而言,由于每个激光线束本身扫描频率是一致的, 显然越远的扫描线点会越稀疏,因此其平均距离会越远(即便其本身可能不一定是噪点)。因此更合理的方法可能是对每个点周围小范围内(同一条线内)的点进行高斯拟合,然后判断该点是否满足该高斯分布,但是如果需要得到比较准确的高斯分布参数,点数需要达到一定数量,否则和上述基于点云密度进行去噪差别不大。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <pcl/io/pcd_io.h>  //文件输入输出
#include <pcl/point_types.h> //点类型相关定义
#include <pcl/visualization/cloud_viewer.h> //点云可视化相关定义
#include <pcl/filters/statistical_outlier_removal.h> //滤波相关
#include <pcl/common/common.h>

// 1.读取点云
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
pcl::PCDReader r;
r.read<pcl::PointXYZ>("table_scene_lms400.pcd", *cloud);
cout << "there are " << cloud->points.size() << " points before filtering." << endl;

// 2.统计滤波
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_filter(new pcl::PointCloud<pcl::PointXYZ>);
pcl::StatisticalOutlierRemoval<pcl::PointXYZ> sor;
sor.setInputCloud(cloud);
sor.setMeanK(50); // K近邻搜索点个数
sor.setStddevMulThresh(1.0); // 标准差倍数
sor.setNegative(false); // 保留未滤波点(内点)
sor.filter(*cloud_filter); // 保存滤波结果到cloud_filter

// 3.滤波结果保存
pcl::PCDWriter w;
w.writeASCII<pcl::PointXYZ>("table_scene_lms400_filter.pcd", *cloud_filter);
cout << "there are " << cloud_filter->points.size() << " points after filtering." << endl;

system("pause");
return 0;

常用滤波顺序: 直通 —-> 体素 —-> 统计

参考:统计滤波的源码


laser_scan_matcher

laser_scan_matcher是一个增量式的scan配准工具. 在连续的sensor_msgs/LaserScan消息之间进行scan match, 把laser的估计位姿以geometry_msgs/Pose2D消息发布,产生tf变换。

激光里程计可以不需要轮式里程计和IMU,也可以使用二者加快配准速度。可接受sensor_msgs/LaserScan或者sensor_msgs/PointCloud2消息

当每次新的scan产生时(每次进入scan的回调函数时),如果为传感器当前的位姿提供初值(使用轮式里程计和IMU),可以加快PL-ICP的配准速度。如果没有初值,则使用零速度模型。 源码在getPrediction函数,在delta_t时间内的相对运动x, y用轮式里程计的delta_x和delta_y表示。 如果用了IMU,delta_theta用IMU的相对yaw表示,否则还用轮式里程计。

包里的demo.bag是手持一个Hokuyo雷达的移动结果,当然没有里程计。


常数速度模型: 假设机器人基于估计的速度运行,速度估计来自传感器或 derivating and filtering scan match的结果. 需要的话题是vel. 使用Alpha-beta滤波器 tracking scan-matching 的预测值

零速度模型: 不使用预测,假设机器人停在原地。

我们可以使用轮式里程计+IMU,或者 IMU+alpha-beta tracking。 使用的优先级是:IMU > Odometry > Constant Velocity > Zero Velocity. 源码在getPrediction函数

IMU和轮式里程计(尤其IMU)可以显著提高收敛速度, 强烈建议添加IMU

scan matcher性能稳定时, Alpha-beta tracking 也可以加速收敛,但可能在动态变化环境或缺少特征的环境下出现weird behavior, 一般建议开启,出现异常再关闭。

Keyframes 和 frame-to-frame scan matching

激光里程计一般是frame-to-frame,每一帧scan和上一帧配准. scan中的噪声是难免的,即使机器人不动, incremental 变换也不是零,机器人位姿会慢慢漂移。

可以使用keyframe-based matching. 位姿变换是在当前scan 和 keyframe scan之间计算. keyframe scan只有在机器人移动一定距离后才更新,如果机器人不动,keyframe scan不变,位姿就不会漂移。 参数kf_dist_linearkf_dist_angular(弧度制)作为阈值。 把两个阈值都改为0,就可以切换回frame-to-frame模式

话题和tf

订阅:

  • scan (sensor_msgs/LaserScan) Scans from a laser range-finder
  • cloud (sensor_msgs/PointCloud2) Scans in point cloud form
  • imu/data (sensor_msgs/Imu) 用于 theta prediction. 只有参数use_imu为true才有效
  • odom (nav_msgs/Odometry) 用于x-, y-的预测, 没有IMU时预测theta. 只有参数use_odom为true才有效

发布:

  • pose2D (geometry_msgs/Pose2D) The pose of the base frame, in some fixed (world) frame

  • 需要的tf: base_link——laser, the pose of the laser in the base frame.

  • 提供的tf: world——base_link, the pose of the robot base in the world frame. Only provided when publish_tf is enabled. world可以改为自定义的名称,一般为odom

参数

  • ~fixed_frame (string, default: “world”): the fixed frame,可以改为odom
  • ~base_frame (string, default: “base_link”): the base frame of the robot

  • ~use_imu (bool, default: true) Whether to use an imu for the theta prediction of the scan registration. Requires input on /imu/data topic.

  • ~use_odom (bool, default: true) Whether to use wheel odometry for the x-, y-, and theta prediction of the scan registration. Requires input on odom topic.
  • ~use_vel (bool, default: false) Whether to use constant velocity model for the x-, y-, and theta prediction of the scan registration. Requires input on vel topic.

官网的剩余ICP参数

在小强机器人的使用

注释StatusPublisher.cpp的部分代码,也就是屏蔽原来的odom:

1
2
3
4
5
mOdomPub = mNH.advertise<nav_msgs::Odometry>("xqserial_server/Odom", 1, true);

mOdomPub.publish(CarOdom);

br.sendTransform(tf::StampedTransform(transform, current_time.fromSec(base_time_), "odom", "base_footprint"));

注释xqserial.launch下面部分,因为我们的目的是odom-->base_link,目前base_footprintlink的parent了,base_link不能有两个parent:

1
<node pkg="tf" type="static_transform_publisher" name="baselink_broadcaster" args="0 0 0.15 0 0 0 1 base_footprint base_link 50">


编辑laser_scan_matcher中的rplidar.launch:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<launch>
<include file="$(find rplidar_ros)/launch/a2.launch" />
<node pkg="laser_scan_matcher" type="laser_scan_matcher_node"
name="laser_scan_matcher_node" output="screen">
<param name="publish_tf" value = "true" />
<param name="fixed_frame" value = "odom"/>
<param name="max_iterations" value="10"/>
<param name="publish_pose_with_covariance_stamped" value="true" />
<param name="publish_pose" value="false" />
<param name="do_compute_covariance" value="false" />

<param name="kf_dist_linear" value="0.08" />
<param name="kf_dist_angular" value="0.14" />
<remap from="/imu/data" to="/xqserial_server/IMU" />
<remap from="pose_with_covariance_stamped" to="plicp_pose" />
</node>
</launch>

启动startup, roslaunch laser_scan_matcher rplidar.launch. 现在原来的轮式坐标系odom坐标系换成了 激光坐标系 odom

现在tf变换是: odom —-> base_link —-> laser

问题

process scan失败.png


Kinect深度图

深度图不受阴影、光照、色度的影响。

深度图的各个像素点和实际中的对应坐标值通过标定矩阵匹配的方法获取环境信息的三维点云。每一帧深度图上的y坐标和z坐标代表深度图中的像素单位,不是实际世界坐标。

kinect深度在0.5~8米,随着距离的增加而精度下降,推荐深度范围1-4米,所以深度相机只能用于小场景。

Kinect最显著的缺点就是水平视场角太小。

深度图的z轴正向为相机前方

kinect——深度图点云数据——特征提取——特征匹配——建图

噪声和修复

深度图中有黑洞和边缘噪点。这就是测量噪声,而且深度相机的测量噪声随着测量距离的变大而变大。双边贝叶斯滤波


(六) 源码分析4 processScan函数

当前位姿与上一次位姿做差,计算做累计角度偏差和位移偏差。利用激光雷达测得距离做得分处理。非首帧调用scanMatch,upDateTreeWeight,resample。首帧则调用invalidActiveArea,computeActiveArea,registerScan。

第1部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
bool GridSlamProcessor::processScan(const RangeReading & reading, int adaptParticles)
{
/* retireve the position from the reading, and compute the odometry */
/*当前雷达在里程计坐标系的位置 addScan最后的setPose */
OrientedPoint relPose = reading.getPose();

/*m_count表示这个函数被调用的次数,开始为0,如果是第0次调用,则所有的位姿都是一样的*/
if (!m_count)
m_lastPartPose = m_odoPose = relPose;

/*对于每一个粒子,都从里程计运动模型中采样,得到车子的初步估计位置 这一步对应里程计的更新 */
int tmp_size = m_particles.size();

//这个for循环显然可以用 OpenMP 进行并行化
for (ParticleVector::iterator it=m_particles.begin(); it!=m_particles.end(); it++)
{
OrientedPoint& pose(it->pose); // 上一时刻粒子的位姿
// relPose是里程计记录的最新位姿, m_odoPose是建图引擎记录的上一时刻的位姿
// pose就是it->pose加了噪声,由于上面是引用,所以m_particles容器的成员pose全更新为pose
pose=m_motionModel.drawFromMotion(it->pose, relPose, m_odoPose);
}
onOdometryUpdate(); /*回调函数,什么都没做,可以自己修改*/

// 根据两次里程计的数据,计算机器人的线性位移和角度位移的累积值
// m_odoPose表示上一次的里程计位姿 relPose表示新的里程计的位姿
OrientedPoint move = relPose - m_odoPose;
move.theta=atan2(sin(move.theta), cos(move.theta));

// 统计机器人在进行激光雷达更新之前, 走了多远的距离 以及 平移了多少的角度
// 这两个变量最后还是要清零
m_linearDistance+=sqrt(move*move); // x²+y²的开方
m_angularDistance+=fabs(move.theta);

// if the robot jumps throw a warning
/*
如果机器人在走了m_distanceThresholdCheck这么远的距离都没有进行激光雷达的更新
则需要进行报警。这个误差很可能是里程计或者激光雷达的BUG造成的。
例如里程计数据出错 或者 激光雷达很久没有数据等等
每次进行激光雷达的更新之后 m_linearDistance这个参数就会清零
m_distanceThresholdCheck在开头定义为5 */
if (m_linearDistance > m_distanceThresholdCheck)
{
/*------- 一堆报警内容 -------*/
}
//更新:把当前的位置赋值给旧的位置
m_odoPose = relPose;
//先声明为false,最后如果成功就赋值 true
bool processed=false;
// 只有当机器人走过一定的距离或者旋转过一定的角度,或者过一段指定的时间才处理激光数据
// 否则太低效了,period_被构造函数写死成5秒,可以考虑修改
if ( ! m_count || m_linearDistance >=m_linearThresholdDistance
|| m_angularDistance >=m_angularThresholdDistance
|| (period_ >= 0.0 && (reading.getTime() - last_update_time_) > period_) )
{
/*第2部分*/
}
m_readingCount++;
return processed;
}

ScanMatcher 构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
ScanMatcher::ScanMatcher(): m_laserPose(0,0,0)
{
line_angle = 0.0;
m_laserBeams=0;
m_optRecursiveIterations=9;
m_activeAreaComputed=false;

// 5cm解析度的默认参数
// 这个参数是计算似然位姿的时候使用的,实际的gmapping中没有用到
m_llsamplerange=0.01;
m_llsamplestep=0.01;
m_lasamplerange=0.005;
m_lasamplestep=0.005;

//地图进行拓展的大小
m_enlargeStep=10.;
m_fullnessThreshold=0.1;

//指示里程计和陀螺仪是否可靠
//如果可靠的话,那么进行score计算的时候,就需要对离里程计数据比较远的位姿增加惩罚
//对于我们的应用来说,陀螺仪在短期内还是很可靠的。
m_angularOdometryReliability=0.;
m_linearOdometryReliability=0.;

//理论上的离激光点的空闲距离 也就是说沿着激光束方向离激光点这么远距离的栅格一定是空闲的。
m_freeCellRatio=sqrt(2.);

//跳过一帧激光数据的开始几束激光
m_initialBeamsSkip=0;
m_linePoints = new IntPoint[20000];
}

全是参数赋值,但是全写死,不能通过ROS修改

drawFromMotion 里程计运动模型

  • p 表示粒子估计的最优位置(机器人上一个时刻的最优位置)
  • pnew 表示里程计算出来的新的位置
  • pold 表示建图引擎记录的上一时刻的位姿(即上一个里程计的位置)

不是执行圆加运算,pnew和pold只是用于产生噪声控制量,然后加到p上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
OrientedPoint 
MotionModel::drawFromMotion(const OrientedPoint& p, const OrientedPoint& pnew, const OrientedPoint& pold) const
{
double sxy=0.3*srr;
// 计算出pnew 相对于 pold走了多少距离,得到控制量
// 这里的距离表达是相对于车身坐标系来说的
OrientedPoint delta=absoluteDifference(pnew, pold);

/*初始化一个点*/
OrientedPoint noisypoint(delta);

/*走过的三个方向的距离,为控制量添加上噪声项*/
noisypoint.x+=sampleGaussian(srr*fabs(delta.x)+str*fabs(delta.theta)+sxy*fabs(delta.y));
noisypoint.y+=sampleGaussian(srr*fabs(delta.y)+str*fabs(delta.theta)+sxy*fabs(delta.x));
noisypoint.theta+=sampleGaussian(stt*fabs(delta.theta)+srt*sqrt(delta.x*delta.x+delta.y*delta.y));

/*限制角度的范围为 -π~π */
noisypoint.theta=fmod(noisypoint.theta, 2*M_PI);
if (noisypoint.theta>M_PI)
noisypoint.theta-=2*M_PI;

/*把加入了噪声的控制量 加到粒子估计的最优的位置上,得到新的粒子位姿的预估(根据运动模型推算)*/
return absoluteSum(p,noisypoint);
}

这里的MotionModel是一个十分粗糙的运动模型,只是简单的矢量加减运算。 相比于《Probabilistic Robotics》中提到的速度模型和里程计模型而言,有很多方面都没有考虑,精度上可能有折扣。

第2部分 if中的处理激光数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
last_update_time_ = reading.getTime();
/*复制一帧数据 把激光数据转换为scan-match需要的格式*/
int beam_number = reading.getSize();
double * plainReading = new double[beam_number];
for(unsigned int i=0; i<beam_number; i++)
{
plainReading[i]=reading.m_dists[i];
}
/*如果不是第一帧数据*/
if (m_count>0)
{
/*
为每个粒子进行scanMatch,计算出来每个粒子的最优位姿,同时计算该最优位姿的得分和似然 对应于gmapping论文中的用最近的一次测量计算proposal的算法
除了进行scanMatch之外,还对粒子进行了权重的计算,并计算了粒子的有效区域,但不进行内存分配 内存分配在resample()函数中
这个函数在gridslamprocessor.hxx里
*/
scanMatch(plainReading);

//至此 关于proposal的更新完毕了,接下来是计算权重
onScanmatchUpdate();
/*
由于scanMatch中对粒子的权重进行了更新,那么这个时候各个粒子的轨迹上的累计权重都需要重新计算
这个函数即更新各个粒子的轨迹上的累计权重是更新
GridSlamProcessor::updateTreeWeights(bool weightsAlreadyNormalized) 函数在gridslamprocessor_tree.cpp里面实现
*/
updateTreeWeights(false);
/*
粒子重采样 根据neff的大小来进行重采样 不但进行了重采样,也对地图进行更新
GridSlamProcessor::resample 函数在gridslamprocessor.hxx里面实现
*/
std::cerr<<"plainReading:"<<m_beams<<std::endl;
resample(plainReading, adaptParticles, reading_copy);
}
/*如果是第一帧激光数据*/
else
{
//如果是第一帧数据,则可以直接计算activeArea。因为这个时候,对机器人的位置是非常确定的,就是(0,0,0)
for (ParticleVector::iterator it=m_particles.begin(); it!=m_particles.end(); it++)
{
m_matcher.invalidateActiveArea();
m_matcher.computeActiveArea(it->map, it->pose, plainReading);
m_matcher.registerScan(it->map, it->pose, plainReading);

//为每个粒子创建路径的第一个节点。该节点的权重为0,父节点为it->node(这个时候为NULL)。
//因为第一个节点就是轨迹的根,所以没有父节点
TNode* node=new TNode(it->pose, 0., it->node, 0);
node->reading = reading_copy;
it->node=node;
}
}
// "Tree: normalizing, resetting and propagating weights at the end..." ;
//进行重采样之后,粒子的权重又会发生变化,因此需要再次更新粒子轨迹的累计权重
//GridSlamProcessor::updateTreeWeights(bool weightsAlreadyNormalized) 函数在gridslamprocessor_tree.cpp里面实现
updateTreeWeights(false);

delete [] plainReading;
m_lastPartPose=m_odoPose; //update the past pose for the next iteration

//机器人累计行走的多远的路程没有进行里程计的更新 每次更新完毕之后都要把这个数值清零
m_linearDistance=0;
m_angularDistance=0;

m_count++;
processed=true;

//keep ready for the next step
for (ParticleVector::iterator it=m_particles.begin(); it!=m_particles.end(); it++)
{
it->previousPose=it->pose;
}