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

amcl的粒子滤波器

amcl全称自适应的蒙特卡洛定位(Adaptive Monte Carlo Localization),使用粒子滤波器(Particle Filter, PF)在已知地图的情况下定位机器人位姿。 它的自适应体现在KLD采样的应用上,通过Kullback-Leibler Divergence计算两个概率分布之间的差异,得到一个评价样本质量的统计边界, 并根据这一边界确定粒子数量,详细参见《P.R.》的第八章

在本系列的粒子滤波器一文中,我们提到典型的PF需要经过"递推样本"、“权重计算”、“重采样”三个阶段。 在分析amcl的总体业务逻辑时, 我们看到"递推样本"的工作是由里程计对象完成的, 激光雷达对象则完成了"权重计算"的工作。 在本文中,我们主要讨论amcl的粒子滤波器框架,以及重采样过程。

1. 数据结构的定义

amcl定义了如下代码所示的结构体pf_t来描述滤波器,我们在注释中说明了各个字段的含义。 值得注意的是,字段pop_err和pop_z是两个可以配置的参数,它们是KLD的体现。其中pop_err约束了是真实分布与估计分布之间的最大误差, pop_z则是一个z分位数限定了估计分布误差小于pop_err的概率。此外,amcl定义了两个样本集合,并使用current_set指示当前激活的集合索引。

参数alpha_slow和alpha_fast的配置,决定了字段w_slow和w_fast两个平均权重过滤值的衰减速率。这些字段用于触发recover机制,增加随机位姿, 用以削弱粒子滤波器的粒子匮乏问题的影响。

最后dist_threshold和converged是用来评定粒子滤波器是否收敛的两个字段。对于定位而言,只有当粒子集合收敛到了一个很小的区域,其不确定度才能保证比较小。

        typedef struct _pf_t {
            int min_samples, max_samples;        // 最少和最多样本数量
            double pop_err, pop_z;               // 样本集尺寸参数
            int current_set;                     // 当前激活态的样本集索引
            pf_sample_set_t sets[2];             // 样本集合,双缓存
            double w_slow, w_fast;               // running averages
            double alpha_slow, alpha_fast;       // decay rates for running averages
            pf_init_model_fn_t random_pose_fn;   // 绘制样本集合的函数指针
            void *random_pose_data;
            double dist_threshold;               // 判定粒子集合发散的阈值
            int converged;                       // 粒子集合收敛的标志
        } pf_t;

下面的左侧代码是粒子集合的数据结构定义。其中sample_count和samples分别记录了样本数量和样本,kdtree则用于描述样本的统计直方图。 字段mean和cov分别描述了当前粒子集合的平均位姿和协方差,它们是对当前机器人位姿和不确定度的估计。最后用字段converged来指示粒子集合是否收敛。

在介绍粒子滤波器的时候,我们就提到过,PF所用的蒙特卡洛估计方法,是一种非参数估计方法, 可以描述几乎任何概率分布。针对多峰的分布,这里定义了pf_cluster_t和字段clusters来显式的描述,我们把描述各个峰的子集称为粒子簇。 此外cluster_count和cluster_max_count分别描述了当前粒子簇的数量和最大粒子簇数量。

        typedef struct _pf_sample_set_t
        {
            int sample_count;
            pf_sample_t *samples;
            pf_kdtree_t *kdtree;
            int cluster_count, cluster_max_count;
            pf_cluster_t *clusters;
            pf_vector_t mean;
            pf_matrix_t cov;
            int converged; 
        } pf_sample_set_t;
        typedef struct {
            int count;
            double weight;
            pf_vector_t mean;
            pf_matrix_t cov;
            double m[4], c[2][2];
        } pf_cluster_t;
        typedef struct {
            pf_vector_t pose;
            double weight;
        } pf_sample_t;

上面的右边代码给出了粒子簇和粒子的数据结构。虽然粒子簇也是粒子的集合,它只是描述了簇的粒子数量、概率权重、均值方差等统计信息。而对于每个粒子,我们都有字段pose描述位姿, weight用于描述粒子的概率权重。

2. 粒子滤波器的初始化

回顾amcl的总体业务逻辑,可以看到我们需要调用函数pf_alloc来构建滤波器对象, 然后调用函数pf_init完成初始化。

2.1 pf_alloc

下面是函数pf_alloc的代码片段,我们省略了大量的初值语句,右图则是amcl_node.cpp文件中调用该函数的语句。该函数有6个输入参数,前四个是对滤波器的配置,约束了最少和最多的样本数量,权重过滤值衰减速率。 参数random_pose_fn是一个函数指针,提供了生成随机样本的函数,amcl中使用的是AmclNode::uniformPoseGenerator。对于参数random_pose_data,我的理解是粒子的样本空间, amcl实际使用的是地图对象。

        pf_t *pf_alloc(int min_samples, int max_samples, double alpha_slow, double alpha_fast,
                       pf_init_model_fn_t random_pose_fn, void *random_pose_data) {

在函数的一开始调用了C语言的库函数srand48完成随机数种子的初始化。并通过calloc申请滤波器对象的内存。

            srand48(time(NULL));
            pf = calloc(1, sizeof(pf_t));

接着通过一个for循环完成两个粒子集合的初始化工作。并在7到13行中的为每个粒子集申请内存,并赋予初值。在第14行中构建了三倍于样本集合尺寸的直方图对象kdtree。 最后完成粒子簇的内存申请和均值方差的初始化。

            for (j = 0; j < 2; j++) {
                set = pf->sets + j;
                set->sample_count = max_samples;
                set->samples = calloc(max_samples, sizeof(pf_sample_t));
                for (i = 0; i < set->sample_count; i++) {
                    sample = set->samples + i;
                    sample->pose.v[0] = 0.0; sample->pose.v[1] = 0.0; sample->pose.v[2] = 0.0;
                    sample->weight = 1.0 / max_samples;
                }
                set->kdtree = pf_kdtree_alloc(3 * max_samples);
                set->cluster_count = 0;
                set->cluster_max_count = max_samples;
                set->clusters = calloc(set->cluster_max_count, sizeof(pf_cluster_t));
                set->mean = pf_vector_zero();
                set->cov = pf_matrix_zero();
            }

该函数的最后,通过函数pf_init_converged设定滤波器未收敛,并返回滤波器对象。

            pf_init_converged(pf);
            return pf;
        }

2.2 pf_init

下面的代码片段是函数pf_init的实现。该函数有三个参数,其中pf是将要初始化的滤波器对象,mean和cov分别是机器人初始位姿和协方差描述。在函数中,首先通过索引current_set获取当前激活的粒子集合。 并根据输入的均值和方差构建一个高斯分布。

        void pf_init(pf_t *pf, pf_vector_t mean, pf_matrix_t cov) {
            pf_sample_set_t *set = pf->sets + pf->current_set;
            pf_pdf_gaussian_t *pdf = pf_pdf_gaussian_alloc(mean, cov);

首先清空直方图,并根据刚刚构建的正态分布采样,填充粒子集合更新统计直方图。

            pf_kdtree_clear(set->kdtree);
            set->sample_count = pf->max_samples;
            for (int i = 0; i < set->sample_count; i++) {
                pf_sample_t *sample = set->samples + i;
                sample->weight = 1.0 / pf->max_samples;
                sample->pose = pf_pdf_gaussian_sample(pdf);
                pf_kdtree_insert(set->kdtree, sample->pose, sample->weight);
            }
            pf_cluster_stats(pf, set);

最后释放掉临时申请的高斯分布对象pdf,并标记为未收敛。

            pf->w_slow = pf->w_fast = 0.0;
            pf_pdf_gaussian_free(pdf);
            pf_init_converged(pf);
            return;
        }

除了函数pf_init之外,amcl还提供了一个称为pf_init_model的函数,其实现如下所示。它完成的功能与pf_init基本是一样的,只是pf_init使用的是高斯模型,pf_init_model可以由用户提供模型。 它有三个参数,其中pf仍然是滤波器对象。init_fn是一个函数指针,用户需要提供一个生成随机样本的函数实现,该参数的作用就相当于pf_init中的高斯分布对象。参数init_data则是生成粒子的样本空间, 我们可以为之赋予地图数据。

        void pf_init_model(pf_t *pf, pf_init_model_fn_t init_fn, void *init_data) {
            // ...
            for (i = 0; i < set->sample_count; i++) {
                sample = set->samples + i;
                sample->weight = 1.0 / pf->max_samples;
                sample->pose = (*init_fn) (init_data);
                pf_kdtree_insert(set->kdtree, sample->pose, sample->weight);
            }
            // ...
        }

4. 重采样

根据《P.R.》的介绍, 我们知道如果我们按照粒子滤波器的常规操作,对完成"递推样本"、“权重计算”操作之后的粒子集合进行重采样, 就可以进行全局定位了,但是它还不能够解决机器人绑架的问题。《P.R.》提到了一种注入随机粒子的方法可以解决这一问题,并给出了 amcl所用的Augmented_MCL算法。概算法的核心思想就是, 通过对比短期和长期样本的均值估计来确定注入随机粒子的数量,并在重采样过程中注入新的随机粒子。

4.1 pf_update_sensor

amcl将这一核心思想差分为pf_update_sensor和pf_update_resample两个函数来实现了。下面是pf_update_sensor函数的代码片段,它完成的是短期和长期样本均值的估计, 也就是字段w_slow和w_fast的计算。该函数在测量更新的过程中被调用。 它有三个参数,其中pf是滤波器对象,sensor_fn则是一个函数指针实现了传感器的模型,用于计算各个粒子的概率权重。参数sensor_data则是用于更新的传感器数据。

        void pf_update_sensor(pf_t *pf, pf_sensor_model_fn_t sensor_fn, void *sensor_data) {

在该函数中,首先获取当前激活的样本集合,并通过函数指针sensor_fn计算各个样本的概率权重。

            pf_sample_set_t *set = pf->sets + pf->current_set;
            double total = (*sensor_fn) (sensor_data, set);

接下来首先完成样本权重的归一化。

            pf_sample_t *sample;
            double w_avg=0.0;
            for (i = 0; i < set->sample_count; i++) {
                sample = set->samples + i;
                w_avg += sample->weight;
                sample->weight /= total;
            }
            w_avg /= set->sample_count;

最后按照右边算法伪代码中的第10和11行的公式,计算统计量\(w_{slow}, w_{fast}\)。

            if(pf->w_slow == 0.0)
                pf->w_slow = w_avg;
            else
                pf->w_slow += pf->alpha_slow * (w_avg - pf->w_slow);
            if(pf->w_fast == 0.0)
                pf->w_fast = w_avg;
            else
                pf->w_fast += pf->alpha_fast * (w_avg - pf->w_fast);
        }

实际上,源码中还对上述代码第3行中的total等于0的情况进行了特殊的处理,这里省略之。

4.2 pf_update_resample

函数pf_update_resample具体实现了重采样操作,相比于传统的粒子滤波器的重采样操作,它多了一个根据统计量\(w_{slow}, w_{fast}\)插入随机样本的过程。下面是该函数的代码片段, 它只有一个参数pf用于指示进行重采样更新的滤波器对象。在函数的一开始用两个指针set_a和set_b分别获取了当前激活的粒子集合和待切换的集合。

        void pf_update_resample(pf_t *pf) {
            pf_sample_set_t *set_a = pf->sets + pf->current_set;
            pf_sample_set_t *set_b = pf->sets + (pf->current_set + 1) % 2;

然后对粒子的权重进行积分,获得分布函数,后面用于生成样本。

            double *c = (double*)malloc(sizeof(double)*(set_a->sample_count+1));
            c[0] = 0.0;
            for(i=0;isample_count;i++)
                c[i+1] = c[i]+set_a->samples[i].weight;

接着计算算法Augmented_MCL中第13行的判据,该判据是用于判定是否需要插入新粒子。

            w_diff = 1.0 - pf->w_fast / pf->w_slow;
            if(w_diff < 0.0)
                w_diff = 0.0;

下面,我们重置更新粒子集合set_b,它将保存重采样后的粒子。

            pf_kdtree_clear(set_b->kdtree);
            total = 0;
            set_b->sample_count = 0;

紧接着,我们就可以完成算法的第12到19行中的重采样过程。这里使用一个while循环进行实现,在循环中先生成一个随机数,并根据算法的第13行的判据插入新粒子。

            while(set_b->sample_count < pf->max_samples) {
                sample_b = set_b->samples + set_b->sample_count++;
                if (drand48() < w_diff)
                    pf_sample_t *sample_b->pose = (pf->random_pose_fn)(pf->random_pose_data);

如果判据不生效,那么我们就根据原粒子集合所描述的样本分布进行采样。

                else {
                    int i;
                    double r = drand48();
                    for (i = 0; i < set_a->sample_count; i++) {
                        if((c[i] <= r) && (r < c[i+1]))
                            break;
                    }
                    pf_sample_set_t * sample_a = set_a->samples + i;
                    sample_b->pose = sample_a->pose;
                }

为重采样后的粒子赋予相同的权重,并更新统计直方图。当满足样本限制的时候,就退出循环,结束重采样。

                sample_b->weight = 1.0;
                total += sample_b->weight;
                pf_kdtree_insert(set_b->kdtree, sample_b->pose, sample_b->weight);
                if (set_b->sample_count > pf_resample_limit(pf, set_b->kdtree->leaf_count))
                    break;
            }

在函数的最后,重置统计量\(w_{slow}, w_{fast}\),归一化样本权重,更新粒子簇,交换当前粒子集,检查粒子集是否收敛。

            if(w_diff > 0.0)
                pf->w_slow = pf->w_fast = 0.0;
            // 归一化样本权重
            for (int i = 0; i < set_b->sample_count; i++) {
                sample_b = set_b->samples + i;
                sample_b->weight /= total;
            }
            pf_cluster_stats(pf, set_b);
            pf->current_set = (pf->current_set + 1) % 2;
            pf_update_converged(pf);
            free(c);
            return;
        }

4.3 pf_resample_limit

在本文的一开始,我们就介绍说amcl利用Kullback-Leibler Divergence计算两个概率分布之间的差异,得到一个评价样本质量的统计边界,并根据这一边界确定粒子数量。 这就是《P.R.》中描述的的KLD采样。下面的函数根据直方图统计量k, 和误差边界参数\(\varepsilon\)和\(\sigma\)计算样本集合数量。 结合刚刚的重采样过程就是KLD_Sampling_MCL算法了。

        int pf_resample_limit(pf_t *pf, int k) {
            if (k <= 1)
                return pf->max_samples;
            double a = 1;
            double b = 2 / (9 * ((double) k - 1));
            double c = sqrt(2 / (9 * ((double) k - 1))) * pf->pop_z;
            double x = a - b + c;
            int 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;
        }

5. 完

本文中,我们介绍了amcl的粒子滤波器框架和重采样过程。相比于传统的粒子滤波器,amcl根据粒子集合的统计数据插入一定数量的新粒子,来解决全局定位失效或者说是机器人绑架的问题。 并根据KLD采样自适应的调节粒子数量,可以在定位精度和计算代价之间进行权衡。




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