A星算法

地图和起点,终点

算法的伪代码.png

1.jpg

2.jpg

3.jpg

公式表示为:f(n)=g(n)+h(n),其中f(n)是经过节点n从初始点到目标点的代价函数,g(n)表示从初始节点到节点n的代价,h(n)表示从节点n到目标点的启发式代价

  • Dijkstra是无目的的扩展,A星是启发式,选择离目标点最近的方向扩展,但不一定是最短路径。
  • Dijkstra算法的实质是广度优先搜索,是一种发散式的搜索,所以空间复杂度和时间复杂度都比较高。
  • 对路径上的当前点,A*算法不但记录其到源点的代价,还计算当前点到目标点的期望代价,是一种启发式算法,也可以认为是一种深度优先的算法。

rviz里查看nav_msgs/Path

有一次需要写一个简单的Publisher程序,发布一段路径,不过是没有在机器人程序的环境下。消息的类型是nav_msgs/Path

1
2
std_msgs/Header header
geometry_msgs/PoseStamped[] poses

注意poses是个带header的vector,也就是说消息里有两个相同的header,这是不太好的地方。

rviz里的global frame是map,如果fixed frame选项设置不正确,那么就会无法显示显示相应的数据信息,并提示一下错误: For frame [XX]: Fixed Frame [map] does not exist

如果rviz显示不了topic,报frame 、transform之类错误,可以这样解决:

  1. 把global fixed frame设成topic所在的坐标系,按照上图的显示,应该改为my_frame
  2. 发布global fixed frame到topic所在坐标系的tf关系,比如使用static_transform_publisher
1
<node args="-5 -5 0 0 0 0 1 map my_frame  10" pkg="tf" name="map_to_my_frame" type="static_transform_publisher"/>

如果上面的nav_msgs/Path少设置了header,rviz里会报错:
frame[] does not exists.png

经过以上设置,就可以在rviz查看nav_msgs/Path了

参考:
RVIZ中的fixed frame选项
rviz无法显示的问题


Matlab中使用ROS

把Matlab编写的路径规划算法用于ROS中的自主导航,matlab也有ros的接口,可以通过话题直接把算法结果发给ROS环境,控制机器人移动

实现Matlab和ROS的通信

master可以在Matlab上,但这种需求很少,一般在Ubuntu上,下面演示从Matlab连接ROS的过程

执行setenv('ROS_MASTER_URI','http://192.168.1.7:11311'),IP为master

然后执行rosinit,正常的话会出现下面提示:

1
2
The value of the ROS_MASTER_URI environment variable, http://192.168.1.7:11311, will be used to connect to the ROS master.
Initializing global node /matlab_global_node_37037 with NodeURI http://192.168.1.8:20811/

现在二者通信成功了,可以使用rosnodel list等命令了。但是与标准的ROS命令有所不同,以下列出常用的几个:
常用命令.png

rosshutdown用于退出ROS网络,在此之后Matlab和ROS不再通信。
rosshutdown.png

参考:Matlab中的ROS


Matlab常用操作

matlab执行dos命令

dos函数: dos('ping 192.168.0.109')

for循环

1
2
3
4
5
6
7
% ii  ---循环变量,也就是循环次数
clc;clear;

for ii = 1:10
fprintf('value of a: %d\n', ii);
end
fprintf('跳出循环后,value of a: %d\n', ii);

控制表达式产生了一个1ⅹ10数组,所以语句1到n将会被重复执行10次。注意在循环体在最后一次执行后,循环系数将会一直为10。

randn 和 rand 函数

randn:产生正态分布的随机数或矩阵的函数

randn:产生均值为0,方差σ^2 = 1,标准差σ = 1的正态分布的随机数或矩阵的函数。

  • Y = randn(n):返回一个n*n的随机项的矩阵。如果n不是个数量,将返回错误信息。 n可以是0,此时为空矩阵

  • Y = randn(m,n) 或 Y = randn([m n]): 返回一个m*n的随机项矩阵。

产生一个随机分布的指定均值和方差的矩阵:将randn产生的结果乘以标准差,然后加上期望均值即可。例如:产生均值为0.6,方差为0.1的一个5*5的随机数方式如下:

1
x = .6 + sqrt(0.1) * randn(5)


rand函数产生由在(0, 1)之间均匀分布的随机数组成的数组

  • Y = rand(n): 返回一个n*n的随机矩阵如果n不是数量,则返回错误信息

  • Y = rand(m,n) 或 Y = rand([m n]): 返回一个m x n的随机矩阵


laser_filters

scan_tools提供了一系列用于激光SLAM的工具,在github的分支只到indigo,所以无法从ros直接安装,但可以编译源码安装. 其中重要的有:

  • laser_scan_matcher: an incremental laser scan matcher, using Andrea Censi’s Canonical Scan Matcher implementation. It downloads and installs Andrea Censi’s Canonical Scan Matcher [1] locally.

  • scan_to_cloud_converter: converts LaserScan to PointCloud messages.

laser_filters 过滤雷达数据

过滤器的机制和代价地图是类似的,节点scan_to_scan_filter_chain相当于代价地图,每一个filter相当于地图的每一层,通过加载yaml而加载filter。

以range过滤器为例,修改range_filter.yaml如下:

1
2
3
4
5
6
7
8
9
scan_filter_chain:
- name: box_filter
type: laser_filters/LaserScanRangeFilter
params:
# use_message_range_limits: false # if not specified defaults to false
lower_threshold: 0.18 # 默认0
upper_threshold: 0.22 # 默认100000.0
lower_replacement_value: 0 # 默认 NaN
upper_replacement_value: 999 # 默认 NaN

运行launch: roslaunch laser_filters range_filter_example.launch,里面就是节点scan_to_scan_filter_chain和yaml文件

结果查看scan_filtered话题,只显示出0.18~0.22距离的数据,太小的显示为0,太大的显示为999.

又比如使用LaserScanAngularBoundsFilter,只取-45°~45°内的scan,结果如下,要把laser坐标系的x轴放到水平向右的方向观察

-45°至45°.png

参考:
scan_filtered的使用
laser_filters


csm包的源码解读 (一)

sm_params sm_result

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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140

struct sm_params
{
/** First scan (参考帧) */
LDP laser_ref;
/** Second scan ("目标帧) */
LDP laser_sens;
double first_guess[3];

/** Maximum angular displacement between scans (deg)*/
double max_angular_correction_deg;
/** Maximum translation between scans (m) */
double max_linear_correction;

int max_iterations;
/** A threshold for stopping. */
double epsilon_xy;
/** A threshold for stopping. */
double epsilon_theta;
/** Maximum distance for a correspondence to be valid */
double max_correspondence_dist;

/** 使用 smart tricks 匹配. 只影响匹配速度,不影响收敛结果 */
int use_corr_tricks;

/** Restart if error under threshold (0 or 1)*/
int restart;
/** Threshold for restarting */
double restart_threshold_mean_error;
/** Displacement for restarting */
double restart_dt;
/** Displacement for restarting */
double restart_dtheta;

/* Functions concerning discarding correspondences.
THESE ARE MAGIC NUMBERS -- and they need to be tuned. */

/** Percentage of correspondences to consider: if 0.9,
always discard the top 10% of correspondences with more error */
double outliers_maxPerc;

/** Parameters describing a simple adaptive algorithm for discarding.
1) Order the errors.
2) Choose the percentile according to outliers_adaptive_order.
(if it is 0.7, get the 70% percentile)
3) Define an adaptive threshold multiplying outliers_adaptive_mult
with the value of the error at the chosen percentile.
4) Discard correspondences over the threshold.

This is useful to be conservative; yet remove the biggest errors.
*/
double outliers_adaptive_order; /* 0.7 */
double outliers_adaptive_mult; /* 2 */

/** Do not allow two different correspondences to share a point */
int outliers_remove_doubles;

/* Functions that compute and use point orientation for defining matches. */
/** For now, a very simple max-distance clustering algorithm is used */
double clustering_threshold;
/** Number of neighbour rays used to estimate the orientation.*/
int orientation_neighbourhood;
/** Discard correspondences based on the angles */
int do_alpha_test;
double do_alpha_test_thresholdDeg;

/** I believe this trick is documented in one of the papers by Guttman (but I can't find
the reference). Or perhaps I was told by him directly.

If you already have a guess of the solution, you can compute the polar angle
of the points of one scan in the new position. If the polar angle is not a monotone
function of the readings index, it means that the surface is not visible in the
next position. If it is not visible, then we don't use it for matching.

This is confusing without a picture! To understand what's going on, make a drawing
in which a surface is not visible in one of the poses.
Implemented in the function visibilityTest().
*/
int do_visibility_test;

/** If 1, use PlICP; if 0, use vanilla ICP. */
int use_point_to_line_distance;

/** If 1, the field "true_alpha" is used to compute the incidence
beta, and the factor (1/cos^2(beta)) used to weight the impact
of each correspondence. This works fabolously if doing localization,
that is the first scan has no noise.
If "true_alpha" is not available, it uses "alpha".
*/
int use_ml_weights;

/* If 1, the field "readings_sigma" is used to weight the correspondence by 1/sigma^2 */
int use_sigma_weights;

/** Use the method in http://purl.org/censi/2006/icpcov to compute
the matching covariance. */
int do_compute_covariance;

/** Checks that find_correspondences_tricks give the right answer */
int debug_verify_tricks;

/** Pose of sensor with respect to robot: used for computing
the first estimate given the odometry. */
double laser[3];

/** Noise in the scan */
double sigma;

/** mark as invalid ( = don't use ) rays outside of this interval */
double min_reading, max_reading;

/* Parameters specific to GPM (unfinished :-/ ) */
double gpm_theta_bin_size_deg;
double gpm_extend_range_deg;
int gpm_interval;
/* Parameter specific to HSM (unfinished :-/ ) */
struct hsm_params hsm;
};

struct sm_result
{
/** 1 if the result is valid */
int valid;
/** Scan matching result (x,y,theta) */
double x[3];

/** Number of iterations done */
int iterations;
/** Number of valid correspondence in the end */
int nvalid;
/** Total correspondence error */
double error;

/** Fields used for covariance computation */
#ifndef RUBY
gsl_matrix *cov_x_m;
gsl_matrix *dx_dy1_m;
gsl_matrix *dx_dy2_m;
#endif
};
1
2
3
4
5
6
7
8
9
10
11
12
13
struct correspondence
{
/** 1 if this correspondence is valid */
int valid;
/** Closest point in the other scan. */
int j1;
/** Second closest point in the other scan. */
int j2;
/** Type of correspondence (point to point, or point to line) */
enum { corr_pp = 0, corr_pl = 1} type;
/** Squared distance from p(i) to point j1 */
double dist2_j1;
};

向量gsl_vector的使用

1
2
3
4
5
6
7
8
int n=10;
gsl_vector *v=gsl_vector_alloc(n);
int i;
/* read and write vectors */
for (i=0;i<n;i++)
gsl_vector_set(v,i,pow(i+1,2));
for (i=0;i<n;i++)
printf("The %d element of v is %f\n",i, gsl_vector_get(v,i));

首先建立一个10维的vector,然后对vector赋值:gsl_vector_set(),然后再读取vector的值:gsl_vector_get()

论文中的所有算法步骤完整的体现在了icp_loop.c文件中的icp_loop函数里。

雷达rays的合格和不合格数目:
int num_valid = count_equal(ld->valid, ld->nrays, 1);
int num_invalid = count_equal(ld->valid, ld->nrays, 0);

PLICP标定结果.png


ICP系列算法

SLAM前端匹配,是构建方程的过程。后端优化是求解方程的过程。一个好的SLAM算法重点在前端。

做帧间的匹配是为了得到机器人前后的局部位姿变换关系,可以认为是一种里程计。其实标定了轮子直径跟间距我们就有了一个里程计了,但是因为轮子打滑等各种因素存在误差会比较大。再用雷达的数据做一次配准可以提高定位精度。

  • ICP:迭代最近点
  • PL-ICP:点到线的ICP
  • IMLS-ICP:implicit moving least square,隐式移动最小二乘
  • Generalized ICP (GICP),综合考虑 point-to-point、point-to-plane 和 plane-to-plane 策略,精度、鲁棒性都有所提高;
    Normal Iterative Closest Point (NICP)
  • NICP:考虑法向量和局部曲率,更进一步利用了点云的局部结构信息,其论文中实验结果比 GICP 的性能更好。

ICP

R,t就是相邻两帧激光对应的机器人位置的欧氏变换,以此类推,根据很多帧的点云,就能求出一系列的机器人位姿变换关系。
ICP有二维和三维。
由于两个点云对应同一个实体,那么理论上配准后的距离应该为0,由于误差的存在,我们要求距离的最小值。

对于已知对应点的情况(已知R),我们可以求出闭式解,而不必用迭代方法:

实际中不知道对应点匹配,不能直接算出R和t,要进行迭代计算:寻找对应点,根据对应点计算R,t;对点云转换再计算误差;不断迭代直至误差足够小。ICP算法里不能有nan的雷达数据

ICP方法存在以下缺点,所以不在实际SLAM中使用:

  1. 依赖初始值,初始值不好时,迭代次数增加;对于较大的初始误差,可能会出现错误的迭代结果
  2. ICP是一阶收敛,收敛速度慢。(所以会用kd-tree来加快搜索效率)例如我使用A2雷达,频率12.7Hz,两帧间隔0.079s。ICP时间在0.01s~0.02s,而超过0.015s的有一部分,这就了两帧scan间隔的%19。甚至有些雷达,ICP计算时间大于两帧间隔,这样算出来的位置始终是延迟的.
  3. 两帧激光点云中的点不可能表示的是空间中相同的位置,所以用点到点的距离作为误差方程势必会引入随机误差。
  4. 会有离群点及噪声

所以原始的ICP太粗糙,不足以应用实践。

PL-ICP

深蓝学院这一段又讲的不好,对censi论文中的PL-ICP示意图讲解不到位,对红色和蓝色的点,同心圆没有说明,而且数学公式怀疑有问题,看的云里雾里。

Point to Line-ICP修改的是误差尺度,思想和之后的ICP类似。

雷达的激光点是对实际环境中曲面的离散采样。重要的不是激光点,而是曲面。最好的误差尺度为当前激光点到实际
曲面的距离;所以关键的问题在于如何恢复曲面

PL-ICP:用分段线性的方法(折线)来对实际曲面进行近似,从而定义当前帧激光点到曲面的距离

  1. ICP对点对点的距离作为误差,PL-ICP为点到线的距离作为误差;PL-ICP的误差形式更符号实际情况。
  2. 收敛速度不同,ICP为一阶收敛,PL-ICP为二阶收敛。
  3. PL-ICP的求解精度高于ICP,特别是在结构化环境中,但不适合室外
  4. PL-ICP对初始值更敏感。不单独使用,其容易陷入局部循环。与里程计、CSM等一起使用,通常用里程计得到一个初始转换矩阵q0给到PL-ICP算法

经我测试,换成PL-ICP后,配准时间降了一个数量级,耗时比较长的是0.00274035s。但是静止时输出的帧间匹配结果的精度,没有明显提升,可能是雷达问题。


ros的csm包实现了ICP和PL-ICP算法。作者给出了一个该功能包的操作说明文件(csm_manual.pdf)。里面详细描述了各项配置参数的含义。其中sm/app文件夹中的sm0.c sm1.c sm2.c sm3.c 相当于是几个使用示例。 主要的算法实现是在csm/icp文件夹中的几个文件里。论文中的所有算法步骤完整的体现在了icp_loop.c文件中的icp_loop函数里。


AMCL的缺陷和遗留问题

缺陷

  1. 如果AMCL算法无法快速解决绑架问题,添加随机位姿的粒子可能会导致粒子集扩散。失效恢复机制有时能重定位成功(可能花十几秒时间),有时失败

  2. AMCL依赖里程计,所以误差不能太大,否则影响粒子的位姿;依赖扫描匹配,所以环境中缺乏明显的特征或有物体遮挡或者环境有明显变化时,扫描匹配结果有较大偏差,最终影响粒子权重。

  3. AMCL收敛速度不够快,在初值比较好的情况下,如果设置min_particles为300,max_particles为5000,重采样需要14~20次才能收敛到300个粒子。如果设置得更小,收敛速度就更慢。 重采样几次时,有的粒子权重还是另一些粒子权重的三四倍。

问题

  1. AMCL的运动模型是旋转——平移——旋转,但我的里程计实际是按两个时刻机器人走了一小段直线

  2. 似然域模型的高斯模糊图有什么意义,跟激光点到最近障碍的距离有什么关系

  3. AMCL出现定位抖动,很可能是里程计误差

  4. 如果出现机器人行走一段时间后,粒子集稀疏甚至彻底分散的情况。这可能是环境和地图不匹配,雷达扫描也就不匹配,测量模型出问题,结果w_diff比较大,AMCL向地图注入随机粒子

连接的master错误.png


源码分析(七)重采样

laserReceived 中的实现

以下内容在 if(lasers_update_[laser_index]) 的括号内

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
// 粒子重采样
if(!(++resample_count_ % resample_interval_))
{
pf_update_resample(pf_);
resampled = true;
}

pf_sample_set_t* set = pf_->sets + pf_->current_set;
ROS_DEBUG("Num samples: %d\n", set->sample_count);

// 发布话题particlecloud, 发布的是最终结果cloud,在机器人移动后执行
if (!m_force_update) //在运动模型那里赋值false
{
geometry_msgs::PoseArray cloud_msg;
cloud_msg.header.stamp = ros::Time::now();
cloud_msg.header.frame_id = global_frame_id_;
cloud_msg.poses.resize(set->sample_count);
for(int i=0;i<set->sample_count;i++) // 粒子个数
{
// tf::Pose转为geometry_msgs::Pose,就是用于后面发布消息
tf::poseTFToMsg(tf::Pose(tf::createQuaternionFromYaw(set->samples[i].pose.v[2]),
tf::Vector3(set->samples[i].pose.v[0],
set->samples[i].pose.v[1], 0) ),
cloud_msg.poses[i]);
}
// 构造函数中注册了话题
particlecloud_pub_.publish(cloud_msg);
}


以下内容紧接上面内容,不在if内,但还在laserReceived

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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
// 刚启动amcl时不执行,机器人移动后执行
if(resampled || force_publication)
{
// Read out the current hypotheses
double max_weight = 0.0;
int max_weight_hyp = -1;
std::vector<amcl_hyp_t> hyps;
hyps.resize(pf_->sets[pf_->current_set].cluster_count);
for(int hyp_count = 0; hyp_count < pf_->sets[pf_->current_set].cluster_count; hyp_count++)
{
double weight;
pf_vector_t pose_mean;
pf_matrix_t pose_cov;
if (!pf_get_cluster_stats(pf_, hyp_count, &weight, &pose_mean, &pose_cov))
{
ROS_ERROR("Couldn't get stats on cluster %d", hyp_count);
break;
}

hyps[hyp_count].weight = weight;
hyps[hyp_count].pf_pose_mean = pose_mean;
hyps[hyp_count].pf_pose_cov = pose_cov;

if(hyps[hyp_count].weight > max_weight)
{
max_weight = hyps[hyp_count].weight;
max_weight_hyp = hyp_count;
}
}

if(max_weight > 0.0)
{
ROS_DEBUG("Max weight pose: %.3f %.3f %.3f",
hyps[max_weight_hyp].pf_pose_mean.v[0],
hyps[max_weight_hyp].pf_pose_mean.v[1],
hyps[max_weight_hyp].pf_pose_mean.v[2] );
geometry_msgs::PoseWithCovarianceStamped p;
// Fill in the header
p.header.frame_id = global_frame_id_;
p.header.stamp = laser_scan->header.stamp;
// Copy in the pose
p.pose.pose.position.x = hyps[max_weight_hyp].pf_pose_mean.v[0];
p.pose.pose.position.y = hyps[max_weight_hyp].pf_pose_mean.v[1];
tf::quaternionTFToMsg(tf::createQuaternionFromYaw(hyps[max_weight_hyp].pf_pose_mean.v[2]),
p.pose.pose.orientation);
// Copy in the covariance, converting from 3-D to 6-D
pf_sample_set_t* set = pf_->sets + pf_->current_set;
for(int i=0; i<2; i++)
{
for(int j=0; j<2; j++)
{
// Report the overall filter covariance, rather than the
// covariance for the highest-weight cluster
//p.covariance[6*i+j] = hyps[max_weight_hyp].pf_pose_cov.m[i][j];
p.pose.covariance[6*i+j] = set->cov.m[i][j];
}
}
// Report the overall filter covariance, rather than the
// covariance for the highest-weight cluster
//p.covariance[6*5+5] = hyps[max_weight_hyp].pf_pose_cov.m[2][2];
p.pose.covariance[6*5+5] = set->cov.m[2][2];

pose_pub_.publish(p);
last_published_pose = p;

ROS_DEBUG("New pose: %6.3f %6.3f %6.3f",
hyps[max_weight_hyp].pf_pose_mean.v[0],
hyps[max_weight_hyp].pf_pose_mean.v[1],
hyps[max_weight_hyp].pf_pose_mean.v[2]);

// subtracting base to odom from map to base and send map to odom instead
tf::Stamped<tf::Pose> odom_to_map;
try
{
tf::Transform tmp_tf(tf::createQuaternionFromYaw(hyps[max_weight_hyp].pf_pose_mean.v[2]),
tf::Vector3(hyps[max_weight_hyp].pf_pose_mean.v[0],
hyps[max_weight_hyp].pf_pose_mean.v[1],
0.0));
tf::Stamped<tf::Pose> tmp_tf_stamped (tmp_tf.inverse(),
laser_scan->header.stamp,
base_frame_id_);
this->tf_->transformPose(odom_frame_id_,
tmp_tf_stamped,
odom_to_map);
}
catch(tf::TransformException)
{
ROS_DEBUG("Failed to subtract base to odom transform");
return;
}
latest_tf_ = tf::Transform(tf::Quaternion(odom_to_map.getRotation()),
tf::Point(odom_to_map.getOrigin()));
latest_tf_valid_ = true;
if (tf_broadcast_ == true)
{
// We want to send a transform that is good up until a
// tolerance time so that odom can be used
ros::Time transform_expiration = (laser_scan->header.stamp +
transform_tolerance_);
tf::StampedTransform tmp_tf_stamped(latest_tf_.inverse(),
transform_expiration,
global_frame_id_, odom_frame_id_);
this->tfb_->sendTransform(tmp_tf_stamped);
sent_first_transform_ = true;
}
}
else
{
ROS_ERROR("No pose!");
}
}

laserReceived到这里只剩一个else if(latest_tf_valid_)

pf_update_resample

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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
// Resample 粒子分布
void pf_update_resample(pf_t *pf)
{
int i;
double total;
pf_sample_set_t *set_a, *set_b;
pf_sample_t *sample_a, *sample_b;

double* c;
double w_diff;
// 用来接收当前粒子集的信息
set_a = pf->sets + pf->current_set;
// 保存重采样后的粒子
set_b = pf->sets + (pf->current_set + 1) % 2;

// Build up cumulative probability table for resampling.
// TODO: Replace this with a more efficient procedure (e.g., GeneralDiscreteDistributions.html)
// 对粒子的权重进行积分,获得分布函数,后面用于重采样
c = (double*)malloc(sizeof(double)*(set_a->sample_count+1));
c[0] = 0.0;
for(i=0;i<set_a->sample_count;i++)
c[i+1] = c[i] + set_a->samples[i].weight;
// c[1] = 第一个粒子权重
// c[2] = 第一和第二粒子权重之和,以此类推 c[i] 是粒子集的 前i个粒子的权重之和
// Create the kd tree for adaptive sampling
pf_kdtree_clear(set_b->kdtree);

// Draw samples from set a to create set b.
total = 0;
set_b->sample_count = 0;
// 表示注入粒子的概率
w_diff = 1.0 - pf->w_fast / pf->w_slow;
if(w_diff < 0.0)
w_diff = 0.0;

// Can't (easily) combine low-variance sampler with KLD adaptive
// sampling, so we'll take the more traditional route.
/*
// Low-variance resampler, taken from Probabilistic Robotics, p110
count_inv = 1.0/set_a->sample_count;
r = drand48() * count_inv;
c = set_a->samples[0].weight;
i = 0;
m = 0;
*/
// 确保重采样生成的粒子集(set_b)的粒子数不超过规定的最大的粒子数
while(set_b->sample_count < pf->max_samples)
{
sample_b = set_b->samples + set_b->sample_count++;
// 产生的随机数小于w_diff时,将往set_b中随机注入粒子
if(drand48() < w_diff)
// pf->random_pose_fn 为一个函数指针,其返回一个随机位姿
sample_b->pose = (pf->random_pose_fn)(pf->random_pose_data);
else
{
// Can't (easily) combine low-variance sampler with KLD adaptive
// sampling, so we'll take the more traditional route.
/*
// Low-variance resampler, taken from Probabilistic Robotics, p110
U = r + m * count_inv;
while(U>c)
{
i++;
// Handle wrap-around by resetting counters and picking a new random
// number
if(i >= set_a->sample_count)
{
r = drand48() * count_inv;
c = set_a->samples[0].weight;
i = 0;
m = 0;
U = r + m * count_inv;
continue;
}
c += set_a->samples[i].weight;
}
m++;
*/

// Naive discrete event sampler
double r = drand48(); // drand48 返回服从均匀分布的[0.0, 1.0)之间的 double 随机数
// c[i]相当于把粒子权重以数轴距离的形式表现,查看r容易落在数轴哪个位置。
// 当set_a的第i个粒子权重很大时,r落在c[i]与c[i+1]之间的概率就很高,找符合条件的i
// 虽然这里不能保证每次都能从set_a中提取权重最大的粒子,因为r是一个随机数。但是由于粒子数量较大,
// 因此提取的大多数粒子权重还是高的,这是一个统计学的方法
for(i=0; i<set_a->sample_count; i++)
{
if((c[i] <= r) && (r < c[i+1]))
break;
}
assert(i<set_a->sample_count);

sample_a = set_a->samples + i;
assert(sample_a->weight > 0);
// 从set_a中挑选粒子赋给一个粒子,之后放进set_b
sample_b->pose = sample_a->pose;
}
sample_b->weight = 1.0;
// total之前是0
total += sample_b->weight;
// Add sample to histogram
pf_kdtree_insert(set_b->kdtree, sample_b->pose, sample_b->weight);
// 大的粒子数在定位初期能提高定位精度,但是当粒子集开始聚合后,程序便不再需要这么多的粒子
// 因此这里需要引入一个新的控制条件来节省计算资源
// pf_resample_limit根据时间与粒子集测量更新的结果,返回粒子集所需要的最佳粒子数量,
// 从而实现了粒子数量随着时间的变化而自我调整
if (set_b->sample_count > pf_resample_limit(pf, set_b->kdtree->leaf_count))
break;
}
// 重置,避免w_diff越来越大,导致大量插入随机位姿的粒子
if(w_diff > 0.0)
pf->w_slow = pf->w_fast = 0.0;

// 将set_b的新粒子插入Kd-Tree中并且对粒子的权重归一化,为了之后的聚类分析做准备
for (i = 0; i < set_b->sample_count; i++)
{
sample_b = set_b->samples + i;
sample_b->weight /= total; // 粒子集b的粒子权重都是 1/total
}
// 重新聚类分析
pf_cluster_stats(pf, set_b);
// 将set_b设为当前粒子集,参与下一次的更新循环
// 此时的set_b就对应rviz显示的粒子云
pf->current_set = (pf->current_set + 1) % 2;

pf_update_converged(pf);
free(c);
return;
}

如果w_diff在某次计算的比较大,那么就容易注入随机粒子,比如:

1
2
3
4
5
w_diff >0:  0.032770
random pose num: 160

w_diff >0: 0.040729
random pose num: 214

最后收敛后,set_b中的粒子都来自set_a,有些对应set_a中的相同的粒子;有些粒子对应set_a中权重相同的,但不是同一个的粒子


KLD采样

根据KLD采样计算定位所需的粒子数量,不需要重采样则对粒子集进行排序,选出权重最大的cluster,其位姿为该时刻机器人的最优估计位姿。

// Compute the required number of samples, given that there are k bins
// with samples in them. This is taken directly from Fox et al.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int pf_resample_limit(pf_t *pf, int k)
{
double a, b, c, x;
int n;
if (k <= 1)
return pf->max_samples;
a = 1;
b = 2 / (9 * ((double) k - 1));
// pop_z 为3
c = sqrt(2 / (9 * ((double) k - 1))) * pf->pop_z;
x = a - b + c;
// pop_err 为0.01
n = (int) ceil( (k - 1) / (2 * pf->pop_err) * x * x * x );

if (n < pf->min_samples)
return pf->min_samples;
if (n > pf->max_samples)
return pf->max_samples;

return n;
}

粒子收敛,我们在rviz上看到的particlecloud话题,它的元素个数,粒子数其实是重采样之后的粒子集的粒子数set_b

1
2
3
4
5
6
7
if(  !(++resample_count_ % resample_interval_)  )
{
pf_update_resample(pf_);
resampled = true;
}
pf_sample_set_t* set = pf_->sets + pf_->current_set;
ROS_DEBUG("Num samples: %d\n", set->sample_count);

开始为max_particles,开始滤波后,虽然rviz上粒子看上去收敛到一起,但是粒子数不一定只有那么少。

如果min_particles大于pf_resample_limit计算得到的粒子数,那么particlecloud对应得粒子数是min_particles;如果小于,就是后者。也就是取二者的最大值。 但这个函数返回值并不是单调增或单调