基于Mutual Information的Active SLAM《Information-Theoretic Exploration with Bayesian Optimization》

本文详细介绍了ROS系统中基于信息理论的主动SLAM算法,包括前沿点提取、候选移动点选择、互信息计算及贝叶斯优化过程。通过高斯过程回归进行最优候选点选取,实现机器人高效探索未知环境。该算法强调了距离和方向对决策的影响,并利用八叉树地图进行空间体积计算。
摘要由CSDN通过智能技术生成

ROS-based的源码来自国外大牛实验室:
https://github.com/RobustFieldAutonomyLab/turtlebot_exploration_3d/blob/master/include/exploration.h
本blog是对此源码的介绍
以下部分内容参考论文《Information-Theoretic Exploration with Bayesian Optimization》
感谢博主的引路https://blog.csdn.net/LOVE1055259415/article/details/80294714
我自己将其用python复现了,相对于C++版本更加简明易懂,欢迎点赞转载!:
https://github.com/BIT-MJY/Active_SLAM_Based_on_Information_Theory.git

开始正经介绍turtlebot_exploration_3d

关键步骤:

(1)提取前沿点

在turtlebot_exploration_3d.cpp的119行,函数实现位于include/exploration.h中

vector<vector<point3d>> frontier_groups=extractFrontierPoints(cur_tree);

extractFrontierPoints对已经扩展的栅格里面的,处于未占据状态的栅格进行前沿点提取。具体实现为:

首先判断这个点是否为未占据状态,若是,则观察其附近五个点是否存在为“未被八叉树地图扩展状态”,若是,则这个点将要被加入到前沿点group中。如果这个点距离此前加入到group中的前沿点距离较近(0.4m以内),则这个点归属于这个前沿点所在集合,否则,其自己单独成为一个集合,成为这一集合前沿点的中心(frontier centers),后续的点可能被加入到它引领的集合。

(2)提取候选移动点

在turtlebot_exploration_3d.cpp的160行,函数实现位于include/exploration.h中

vector<pair<point3d, point3d>> extractCandidateViewPoints(vector<vector<point3d>> frontier_groups, point3d sensor_orig, int n ) {

根据上一步提取的前沿点,在前沿点的各个中心点周围寻找候选点。以中心点为圆心,半径1m~5m的范围内枚举每一个点,进行四个条件的筛选:

  • 是否被octomap扩展过(即地图中是否存在该点)
  • 距离机器人当前位置不能太近(0.25米之外才行)
  • 不能距离其它前沿点太近(0.3米之外)这里代码是否有bug????感觉前沿点中心点自己把自己过滤掉了??是否可以直接将这一部分筛选条件去掉219-227行
  • 附近0.6m的矩形内不应有被占据状态的栅格
    注意候选点包含:x,y,z坐标以及yaw
    重点关注:
    在这里插入图片描述
    由上述截图也可以看出机器人位姿必须朝向前沿点的方向

(3)为每个候选点初始化mutual information

首先计算未被占据空间地体积:

double before = countFreeVolume(cur_tree);

然后对每个候选点进行raycasting:

octomap::Pointcloud hits = castSensorRays(cur_tree, c.first, c.second);

raycasting真正起作用的语句是include/exploration.h中的

    if(octree->castRay(position, RaysToCast.getPoint(i), end, true, Kinect_360.max_range)) {
            hits.push_back(end);
        } else {
            end = RaysToCast.getPoint(i) * Kinect_360.max_range;
            end += position;
            hits.push_back(end);
        }

octree->castRay的api官方文档存在于:
https://octomap.github.io/octomap/doc/classoctomap_1_1OccupancyOcTreeBase.html#afc368d6ff93f40ee33eca3df0b8d1967
可以发现此函数在raycasting击中占据栅格时返回true,击中未知栅格、或传感器达到最大范围依然没有击中占据栅格、或激光束达到八叉树边界的时候均返回false。

castSensorRays函数目的就是为了采集如果机器人位于候选点处,激光束射出去,那么激光束的endpoint是如何进行分布的。

接下来计算初始化的MI:

// Normalize the MI with distance
MIs[i] = calc_MI(cur_tree, c.first, hits, before) / 
    sqrt(pow(c.first.x()-kinect_orig.x(),2) + pow(c.first.y()-kinect_orig.y(),2));

那么具体如何计算MI呢?非常重要!所以贴在下面:

double calc_MI(const octomap::OcTree *octree, const point3d &sensor_orig, const octomap::Pointcloud &hits, const double before) {
    auto octree_copy = new octomap::OcTree(*octree);

    octree_copy->insertPointCloud(hits, sensor_orig, Kinect_360.max_range, true, true);
    double after = countFreeVolume(octree_copy);
    delete octree_copy;
    return after - before;
}

先用目前的地图octree复制一份,得到octree_copy,接着将(hits, sensor_orig)组成的当前观测插入octree,这个过程隐含了Bresenham画线算法。然后使用countFreeVolume计算空闲空间体积。用现在的体积 - 此前的体积,可得到互信息,对应论文中:
在这里插入图片描述
所以为什么不是before-after?因为论文中的H指的是占据空间的体积,而在代码中计算的是空闲空间的体积。
得到的MI需要除以sqrt(pow(c.first.x()-kinect_orig.x(),2) + pow(c.first.y()-kinect_orig.y(),2))即当前未知到候选点之间的距离。候选点越远,这个值越小,意思是我们更偏向于更小的移动。

最终,所有的互信息存储在MIs这个容器当中。

(4)进行贝叶斯优化

        // Bayesian Optimization for actively selecting candidate
        double train_time, test_time;
        GPRegressor g(100, 3, 0.01);
        for (int bay_itr = 0; bay_itr < num_of_bay; bay_itr++) {
            //Initialize gp regression
            
            MatrixXf gp_train_x(candidates.size(), 3), gp_train_label(candidates.size(), 1), gp_test_x(gp_test_poses.size(), 3);

            for (int i=0; i< candidates.size(); i++){
                gp_train_x(i,0) = candidates[i].first.x();
                gp_train_x(i,1) = candidates[i].first.y();
                gp_train_x(i,2) = candidates[i].second.z();
                gp_train_label(i) = MIs[i];
            }

            for (int i=0; i< gp_test_poses.size(); i++){
                gp_test_x(i,0) = gp_test_poses[i].first.x();
                gp_test_x(i,1) = gp_test_poses[i].first.y();
                gp_test_x(i,2) = gp_test_poses[i].second.z();
            }

            // Perform GP regression
            MatrixXf gp_mean_MI, gp_var_MI;
            train_time = ros::Time::now().toSec();
            g.train(gp_train_x, gp_train_label);
            train_time = ros::Time::now().toSec() - train_time;

            test_time = ros::Time::now().toSec();
            g.test(gp_test_x, gp_mean_MI, gp_var_MI);
            test_time = ros::Time::now().toSec() - test_time;

            // Get Acquisition function
            double beta = 2.4;
            vector<double>  bay_acq_fun(gp_test_poses.size());
            for (int i = 0; i < gp_test_poses.size(); i++) {
                bay_acq_fun[i] = gp_mean_MI(i) + beta*gp_var_MI(i);
            }
            vector<int> idx_acq = sort_MIs(bay_acq_fun);

            // evaluate MI, add to the candidate
            auto c = gp_test_poses[idx_acq[0]];
            octomap::Pointcloud hits = castSensorRays(cur_tree, c.first, c.second);
            candidates.push_back(c);
            MIs.push_back(calc_MI(cur_tree, c.first, hits, before));
            gp_test_poses.erase(gp_test_poses.begin()+idx_acq[0]);
        }
        
        end_mi_eva_secs = ros::Time::now().toSec();
        ROS_INFO("Mutual Infomation Eva took:  %3.3f Secs.", end_mi_eva_secs - begin_mi_eva_secs);

首先建立高斯过程回归器:GPRegressor g(100, 3, 0.01); 一共三个参数,分别为sf2、ell、noise。然后迭代三次完成贝叶斯优化。

gp_test_poses 来源于

vector<pair<point3d, point3d>> gp_test_poses = candidates;

gp_test_poses 是未经过下采样的候选点。而目前的candidates是被下采样的,来源于:

candidates.resize(min(num_of_samples_eva,temp_size));

gp_train_x存放了所有candidates,gp_train_label存放了train所有的互信息,gp_test_x存放了所有的gp_test_poses。

接下来进行训练

g.train(gp_train_x, gp_train_label);

训练后进行测试,获得均值和方差gp_mean_MI, gp_var_MI

g.test(gp_test_x, gp_mean_MI, gp_var_MI);

接下来计算获取函数Acquisition function相关,使用的是upper confidence bound(UCB)
在这里插入图片描述
然后对获取函数进行排序:

 vector<int> idx_acq = sort_MIs(bay_acq_fun);

找到拥有最大获取函数的候选点:

 auto c = gp_test_poses[idx_acq[0]];

然后计算他的MI,作为最佳选取点之一,然后在gp_test_poses将其抹去,避免下一次仍然选他。每次迭代都能在所有test候选点里面获取一个不同的最佳选取点。值得注意的是,这里的MI仍然加入MIs的容器中,符合论文中:
在这里插入图片描述
所以最后,对MIs进行排序:

vector<int> idx_MI = sort_MIs(MIs);

接下来用move base走过去:

arrived = goToDest(next_vp, Goal_heading);

这里之所以用到了move base就是因为用到了他的局部规划避障的功能。因为真实情况下确实有很多运动物体。但我们自己做仿真的时候可以考虑不使用move base

接下来更新八叉树地图,即使用回调函数回调一次。

// Update Octomap
ros::spinOnce();

即exploration.h中的

void kinectCallbacks( const sensor_msgs::PointCloud2ConstPtr& cloud2_msg ) {
....
    cur_tree->insertPointCloud(hits, kinect_orig, Kinect_360.max_range);
....

至此,基于互信息的active slam讲解到此结束。

介绍的较为粗糙,为了简(省)洁(事)一些细节没有介绍,欢迎感兴趣的读者在评论区进行交流。

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值