概述
源碼位址:
https://github.com/ros-perception/openslam_gmapping
https://github.com/ros-perception/slam_gmapping
程式入口:main.cpp
int
main(int argc, char** argv)
{
ros::init(argc, argv, "slam_gmapping");
SlamGMapping gn;
gn.startLiveSlam();
ros::spin();
return(0);
}
定義生成gn時,運作了SlamGMapping::init()這個成員函數:
void SlamGMapping::init()
{
// 省略... ...
gsp_ = new GMapping::GridSlamProcessor(); //構造GridSlamProcessor類 利用粒子濾波
ROS_ASSERT(gsp_);
tfB_ = new tf::TransformBroadcaster(); //建立TransformBroadcaster
ROS_ASSERT(tfB_);
// 省略... ...
// 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";
private_nh_.param("transform_publish_period", transform_publish_period_, 0.05);
double tmp;
if(!private_nh_.getParam("map_update_interval", tmp))
tmp = 5.0;
map_update_interval_.fromSec(tmp);
// Parameters used by GMapping itself #GMapping 所用的各種參數
maxUrange_ = 0.0; maxRange_ = 0.0; // preliminary default, will be set in initMapper()
// 省略... ...
}
SlamGMapping::startLiveSlam()中
(1)先訂閱雷射雷達的資料
(2)通過boost::bind開啟兩個線程:laserCallback()和 publishLoop(),
在核心線程laserCallback中 :
laserCallback()
{
initMapper() //進行參數初始化
//gsp_->GridSlamProcessor::init()。
addScan() //調用openslam_gmappinig核心算法 gsp_->processScan()
updateMap() //對地圖進行更新
}
initMapper函數處理第一次收到雷射資料時候地圖的初始化問題,被laserCallback函數調用。
updateMap函數每超過map_update_interval_時間就要被laserCallback函數調用調用更新地圖。
在addScan函數中:
SlamGMapping::addScan(const sensor_msgs::LaserScan& scan, GMapping::OrientedPoint& gmap_pose)
{
//省略的對雷射資料的處理 ... ...
GMapping::RangeReading reading(scan.ranges.size(),
ranges_double,
gsp_laser_,
scan.header.stamp.toSec());
delete[] ranges_double;
reading.setPose(gmap_pose);
ROS_DEBUG("processing scan");
return gsp_->processScan(reading);
}
調用了openslam裡面的GridSlamProcessor::processScan函數,這個是gmapping核心的代碼。我們先來看看核心算法部分的代碼主要内容。
核心算法處理
bool GridSlamProcessor::processScan(const RangeReading & reading, int adaptParticles){
/**retireve the position from the reading, and compute the odometry*/
/*從reading中擷取odometry資料*/
OrientedPoint relPose=reading.getPose();
/*第0次是特殊處理,将目前裡程計值指派給上一次的值。relPose 表示裡程計算出來的目前位置*/
/*m_odoPose 表示裡程計上一次的位置 m_count 表示更新次數*/
if (!m_count){
m_lastPartPose=m_odoPose=relPose;
}
//write the state of the reading and update all the particles using the motion model
/*對于每一個粒子,都從裡程計運動模型中采樣*/
for (ParticleVector::iterator it=m_particles.begin(); it!=m_particles.end(); it++){
OrientedPoint& pose(it->pose);
// Step.1※※裡程計運動模型問題※※
pose=m_motionModel.drawFromMotion(it->pose, relPose, m_odoPose);
}
// update the output file
if (m_outputStream.is_open()){
//省略 。。。 。。。
}
//invoke the callback
onOdometryUpdate();
// accumulate the robot translation and rotation
// 算出裡程計在這個周期内跑了多遠
OrientedPoint move=relPose-m_odoPose;
move.theta=atan2(sin(move.theta), cos(move.theta));
m_linearDistance+=sqrt(move*move);
m_angularDistance+=fabs(move.theta);
// if the robot jumps throw a warning
// 如果算出跑的距離超過了門檻值,就報警
if (m_linearDistance>m_distanceThresholdCheck){
//省略 。。。 。。。
}
//下次疊代開始
m_odoPose=relPose;
bool processed=false;
// process a scan only if the robot has traveled a given distance
// 如果裡程計大于某個門檻值,就處理一批雷射資料。可參照視覺SLAM關鍵幀的内在含義
if (! m_count
|| m_linearDistance>m_linearThresholdDistance
|| m_angularDistance>m_angularThresholdDistance){
//省略 。。。 。。。
//this is for converting the reading in a scan-matcher feedable form
//把雷射雷達資料轉換成openslam_gmapping可以讀懂的格式
assert(reading.size()==m_beams);
double * plainReading = new double[m_beams];
for(unsigned int i=0; i<m_beams; i++){
plainReading[i]=reading[i];
}
m_infoStream << "m_count " << m_count << endl;
if (m_count>0){ //如果不是第一幀雷射資料
scanMatch(plainReading); //Step.2 為每個粒子進行scanMatch,計算出每個粒子的最優位姿
//省略 。。。 。。。
onScanmatchUpdate();
//Step.3 由于scanmatch之中對粒子的權重進行了更新,那麼這時候 各個粒子的軌迹上的累計權重都需要重新計算
updateTreeWeights(false);
//省略 。。。 。。。
// Step.4 重采樣
resample(plainReading, adaptParticles);
} 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);
// cyr: not needed anymore, particles refer to the root in the beginning!
// 這是軌迹的根,沒有父節點
TNode* node=new TNode(it->pose, 0., it->node, 0);
node->reading=0;
it->node=node;
}
}
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;
}
}
if (m_outputStream.is_open())
m_outputStream << flush;
m_readingCount++;
//bool 型函數,處理完畢processed=true。
return processed;
}
core.1 運動模型
先來看一下 在motionmodel.cpp檔案中的drawFromMotion這個函數,它實作了裡程計運動模型,采用如下圖的公式(取自中文版《機率機器人》第103頁)。
core.2 權重計算
接下來看一下GMapping::GridSlamProcessor::scanMatch這個函數。
scanMatch():實作通過掃描比對每一個粒子,采樣出新的位姿,進行權重計算。利用最近一次的觀測來提高Proposal distribution進行優化。
rbpf-gmapping直接使用的是運動模型作為提議分布
score():計算最優的粒子,原理就參考 《Probabilistic Robot》 一書的P143 頁 , likelihood_field_range_finder_mode。
在score 函數裡,首先計算障礙物的坐标phit,然後将phit轉換成網格坐标iPhit
計算光束上與障礙物相鄰的非障礙物網格坐标pfree,pfrree由phit沿雷射束方向移動一個網格的距離得到,将pfree轉換成網格坐标ipfree(增量,并不是實際值)
在iphit 及其附近8個(m_kernelSize:default=1)栅格(pr,對應自由栅格為pf)搜尋最優可能是障礙物的栅格。
最優準則: pr 大于某一門檻值,pf小于該門檻值,且pr栅格的phit的平均坐标與phit的距離bestMu最小。
得分計算: s +=exp(-1.0/m_gaussianSigmabestMubesMu) 參考NDT算法,距離越大,分數越小,分數的較大值集中在距離最小值處,符合正态分布模型
至此 score 函數結束并傳回粒子(currentPose)得分,然後回到optimize函數
optimize() 幹的事就是 currentPose 的位姿進行微調,前、後、左、右、左轉、右轉 共6次,然後選取得分最高的位姿,傳回最終的得分
likelihoodAndScore():重新計算權重。
/**Just scan match every single particle.
If the scan matching fails, the particle gets a default likelihood.*/
inline void GridSlamProcessor::scanMatch(const double* plainReading){
// sample a new pose from each scan in the reference
// 雷射資料以plainReading的封裝形式加入到函數當中來
double sumScore=0;
for (ParticleVector::iterator it=m_particles.begin(); it!=m_particles.end(); it++){
OrientedPoint corrected;
double score, l, s;
//optimize 函數傳回了bestScore,同時計算了corrected,即新位姿
score=m_matcher.optimize(corrected, it->map, it->pose, plainReading);
// it->pose=corrected;
// 當計算出的分數比較靠譜後,就用corrected值更新目前粒子的位姿
if (score>m_minimumScore){
it->pose=corrected;
} else {
//省略。。。 。。。
}
//likelihoodAndSocre 傳回了l的數值用于計算粒子的權重
m_matcher.likelihoodAndScore(s, l, it->map, it->pose, plainReading);
sumScore+=score;
it->weight+=l;
it->weightSum+=l;
//set up the selective copy of the active area
//by detaching the areas that will be updated
m_matcher.invalidateActiveArea(); // invalidateActiveArea是這個函數的開關
//建圖的過程,生成地圖的函數,使用Bresenham算法
// computeActiveArea函數的作用有兩個:一是調整地圖:把每個粒子所對應的掃描範圍給計算出來,然後調整地圖的大小使得上述掃描範圍不會超過地圖的大小。
// 二是計算活動區域:調用gridLine() 函數将所有粒子與其所對應的phit(雷射打到障礙物上的終點)連接配接起來,進而形成一片activeArea。
m_matcher.computeActiveArea(it->map, it->pose, plainReading);
}
}
core.3 權重更新
由于scanmatch之中對粒子的權重進行了更新,那麼這時候 各個粒子的軌迹上的累計權重都需要重新計算, 是以我們需要用updateTreeWeights子函數進行粒子權重的更新。
void GridSlamProcessor::updateTreeWeights(bool weightsAlreadyNormalized){
if (!weightsAlreadyNormalized) {
normalize();//歸一化粒子權重,并計算權重相似度Neff,用于判定是否需要進行重采樣
}
resetTree();//重置軌迹樹:周遊了所有的粒子,并沿着各個粒子的node節點向上追溯到根節點,
//并将周遊過程中各個節點的權重計數和通路計數器都設定為0。
propagateWeights();//更新軌迹樹權重:從葉子節點向上追溯到根節點,更新沿途經過的各個節點的權重和累積權重
}
詳細可參考:https://gaoyichao.com/Xiaotu
core.4 選擇性重采樣
processScan 這個函數調用了位于gridslamprocessor.hxx中的resample子函數,其代碼如下:
registerScan():配置設定記憶體,生成地圖
/**
頻繁的重采樣導緻粒子退化,曆史久遠的位姿喪失其自身的多樣性
plainReading 是雷射的原始資料;
adaptSize用于調節重采樣的粒子數目;
reading包含了帶時間戳的大量傳感器資訊
**/
inline bool GridSlamProcessor::resample(const double* plainReading, int adaptSize, const RangeReading* ){
bool hasResampled = false; //條件開關,兼職傳回值
TNodeVector oldGeneration; //軌迹樹向量的容器,記錄上一代粒子狀态
for (unsigned int i=0; i<m_particles.size(); i++){
oldGeneration.push_back(m_particles[i].node); //上一代粒子
}
/**
* 門檻值由這個積定義,當函數updateTreeWeights中定義的Neff 小于此門檻值,則開始重采樣
**/
if (m_neff<m_resampleThreshold*m_particles.size()){
//采取重采樣方法決定,哪些粒子會保留,保留的粒子會保留下标,有些粒子會重複采樣
//而另外一些粒子會消失掉
//uniform_resampler是結構體模闆定義的重采樣器,在particle.h中定義,包含resampleIndexes等成員
uniform_resampler<double, double> resampler;
m_indexes=resampler.resampleIndexes(m_weights, adaptSize);
//省略 。。。 。。。
onResampleUpdate(); //與算法無關
// 開始對對粒子的更新處理
//BEGIN: BUILDING TREE
ParticleVector temp; //重采樣後的粒子集合
unsigned int j=0;
std::vector<unsigned int> deletedParticles; //this is for deleteing the particles which have been resampled away.
// 不在選擇索引中的粒子被幹掉
for (unsigned int i=0; i<m_indexes.size(); i++){
while(j<m_indexes[i]){
deletedParticles.push_back(j);
j++;
}
if (j==m_indexes[i])
j++;
// 更新軌迹樹中粒子的最新狀态
Particle & p=m_particles[m_indexes[i]];
TNode* node=0;
TNode* oldNode=oldGeneration[m_indexes[i]];
node=new TNode(p.pose, 0, oldNode, 0);
node->reading=0;
// 記錄到temp粒子集合中
temp.push_back(p);
temp.back().node=node;
temp.back().previousIndex=m_indexes[i];
}
while(j<m_indexes.size()){ //釋放記憶體
deletedParticles.push_back(j);
j++;
}
for (unsigned int i=0; i<deletedParticles.size(); i++){
std::cerr <<" " << deletedParticles[i];
delete m_particles[deletedParticles[i]].node;
m_particles[deletedParticles[i]].node=0;
}
//END: BUILDING TREE
// 重采樣後的最新粒子資料去更新比對器中的各種資料,如地圖、位姿、雷射等
m_particles.clear();
for (ParticleVector::iterator it=temp.begin(); it!=temp.end(); it++){
it->setWeight(0);
m_matcher.invalidateActiveArea();
m_matcher.registerScan(it->map, it->pose, plainReading);
m_particles.push_back(*it);
}
hasResampled = true;
} else {
/**
如果不滿足重采樣條件,則依舊需要更新 1.各個粒子軌迹樹;2.比對器中的各種資料,如地圖、位姿、雷射、活動區域等
**/
int index=0;
TNodeVector::iterator node_it=oldGeneration.begin();
for (ParticleVector::iterator it=m_particles.begin(); it!=m_particles.end(); it++){
//create a new node in the particle tree and add it to the old tree
//BEGIN: BUILDING TREE
TNode* node=0;
node=new TNode(it->pose, 0.0, *node_it, 0);
node->reading=0;
it->node=node;
//END: BUILDING TREE
m_matcher.invalidateActiveArea();
m_matcher.registerScan(it->map, it->pose, plainReading);
it->previousIndex=index;
index++;
node_it++;
}
}
//END: BUILDING TREE
return hasResampled;
}
頻繁的重采樣導緻粒子退化,曆史久遠的位姿喪失其自身的多樣性。GMapping使用 [公式] 這個名額來限制重采樣次數。
其定義為: [公式]
當 [公式] 降低到某個門檻值以下,說明粒子的分布與真實分布差距很大,具體表現為某些粒子離真實值很近,而很多粒子離真實值較遠,重采樣就應該在此時進行。
重采樣的時間複雜度為 [公式] ,其中N為粒子數,M為地圖大小,是以采用用[公式] 這個名額來限制重采樣次數意義重大。
參考論文:
Improved Techniques for Grid Mapping with Rao-Blackwellized Particle Filters
參考文獻:
https://zhuanlan.zhihu.com
https://blog.csdn.net/
https://gaoyichao.com/Xiaotu/?
原理參考
http://blog.csdn.net/heyijia0327/article/details/40899819 pf原理講解
http://blog.csdn.net/u010545732/article/details/17462941 pf代碼實作
http://www.cnblogs.com/yhlx125/p/5634128.html gmapping分析
http://wenku.baidu.com/view/3a67461550e2524de4187e4d.html?from=search gmapping 分析
其他參考 :
http://ishare.iask.sina.com.cn/f/24615049.html