Gmapping是目前广泛运用的建图算法之一,通过操纵手柄或键盘移动机器人便可以快速地建立地图。它主要是利用粒子滤波原理进行实时定位再利用固定路径下的栅格地图建图方法建立占用栅格地图。
进行建图工作所开启的节点是slam_gmapping,而通过cmakelist我们看到slam_gmapping.cpp和main.cpp的源文件生成的该节点。因此我们从main.cpp首先开始看看。
int
main(int argc, char** argv)
{
ros::init(argc, argv, "slam_gmapping");
SlamGMapping gn;
gn.startLiveSlam();
ros::spin();
return(0);
}
好粗暴的主函数,我们转到slam_gmapping.cpp的SlamGMapping::startLiveSlam函数:
entropy_publisher_ = private_nh_.advertise<std_msgs::Float64>("entropy", 1, true);
sst_ = node_.advertise<nav_msgs::OccupancyGrid>("map", 1, true);
sstm_ = node_.advertise<nav_msgs::MapMetaData>("map_metadata", 1, true);
ss_ = node_.advertiseService("dynamic_map", &SlamGMapping::mapCallback, this);
scan_filter_sub_ = new message_filters::Subscriber<sensor_msgs::LaserScan>(node_, "scan", 5);
scan_filter_ = new tf::MessageFilter<sensor_msgs::LaserScan>(*scan_filter_sub_, tf_, odom_frame_, 5);
scan_filter_->registerCallback(boost::bind(&SlamGMapping::laserCallback, this, _1));
transform_thread_ = new boost::thread(boost::bind(&SlamGMapping::publishLoop, this, transform_publish_period_));
这个函数发布的话题有信息熵、地图以及地图的详细栅格信息(Metadata)。接收的话题有scan。另外,我们创建一个新的线程,在固定时刻干活,发布odom到map的tf变换,也是在不停地纠正里程计提供的位置。接着我们看看scan的回调函数。
一.SlamGMapping::laserCallback,这个函数依次初始化了一系列参数并生成了更新后的地图,可谓是整个程序的关键。
//开始就是一个“限流器”,为了使激光雷达数据每隔若干帧进行计算,不过默认参数是每一帧都计算。。。
laser_count_++;
if ((laser_count_ % throttle_scans_) != 0)
return;
static ros::Time last_map_update(0,0);
//这是在初始化一些重要参数,
// We can't initialize the mapper until we've got the first scan
if(!got_first_scan_)
{
if(!initMapper(*scan))
return;
got_first_scan_ = true;
}
GMapping::OrientedPoint odom_pose;
if(addScan(*scan, odom_pose))
{
ROS_DEBUG("scan processed");
GMapping::OrientedPoint mpose = gsp_->getParticles()[gsp_->getBestParticleIndex()].pose;
ROS_DEBUG("new best pose: %.3f %.3f %.3f", mpose.x, mpose.y, mpose.theta);
ROS_DEBUG("odom pose: %.3f %.3f %.3f", odom_pose.x, odom_pose.y, odom_pose.theta);
ROS_DEBUG("correction: %.3f %.3f %.3f", mpose.x - odom_pose.x, mpose.y - odom_pose.y, mpose.theta - odom_pose.theta);
tf::Transform laser_to_map = tf::Transform(tf::createQuaternionFromRPY(0, 0, mpose.theta), tf::Vector3(mpose.x, mpose.y, 0.0)).inverse();
tf::Transform odom_to_laser = tf::Transform(tf::createQuaternionFromRPY(0, 0, odom_pose.theta), tf::Vector3(odom_pose.x, odom_pose.y, 0.0));
map_to_odom_mutex_.lock();
map_to_odom_ = (odom_to_laser * laser_to_map).inverse();
map_to_odom_mutex_.unlock();
if(!got_map_ || (scan->header.stamp - last_map_update) > map_update_interval_)
{
updateMap(*scan);
last_map_update = scan->header.stamp;
ROS_DEBUG("Updated the map");
}
} else
ROS_DEBUG("cannot process scan");
我们依次看看它调用的函数:
1.SlamGMapping::initMapper函数,
gsp_laser_ = new GMapping::RangeSensor("FLASER",
gsp_laser_beam_count_,
fabs(scan.angle_increment),
gmap_pose,
0.0,
maxRange_);
ROS_ASSERT(gsp_laser_);
GMapping::SensorMap smap;
smap.insert(make_pair(gsp_laser_->getName(), gsp_laser_));
gsp_->setSensorMap(smap);
gsp_odom_ = new GMapping::OdometrySensor(odom_frame_);
ROS_ASSERT(gsp_odom_);
①RangeSensor它位于openslam_gmapping-master这个包里,绝大多数的算法都放在这里,而gmapping只是接口类。转到sensor_range包,如下是关于每个波束的定义:
struct Beam
{
OrientedPoint pose; //pose relative to the center of the sensor
double span; //spam=0 indicates a line-like beam
double maxRange; //maximum range of the sensor
double s,c; //sinus and cosinus of the beam (optimization);
};
而OrientedPoint的定义在util包里:
typedef orientedpoint<double, double> OrientedPoint;
因此RangeSensor接受了波束数量(激光束数量),最大距离,角度增量,实现了测距模型的初始化。
②SensorMap位于sensor.h:
class Sensor
{
public:
Sensor(const std::string& name="");
virtual ~Sensor();
inline std::string getName() const {return m_name;}
inline void setName(const std::string& name) {m_name=name;}
protected:
std::string m_name;
};
typedef std::map<std::string, Sensor*> SensorMap;
SensorMap是一个 map,调用GridSlamProcessor::setSensorMap的目的是构造波束角度的对应表,其中ScanMatcher初始化了默认为5cm的子地图。
GMapping::GridSlamProcessor函数:
GMapping::OrientedPoint initialPose;
if(!getOdomPose(initialPose, scan.header.stamp))
{
ROS_WARN("Unable to determine inital pose of laser! Starting point will be set to zero.");
initialPose = GMapping::OrientedPoint(0.0, 0.0, 0.0);
}
gsp_->setMatchingParameters(maxUrange_, maxRange_, sigma_,
kernelSize_, lstep_, astep_, iterations_,
lsigma_, ogain_, lskip_);
gsp_->setMotionModelParameters(srr_, srt_, str_, stt_);
gsp_->setUpdateDistances(linearUpdate_, angularUpdate_, resampleThreshold_);
gsp_->setUpdatePeriod(temporalUpdate_);
gsp_->setgenerateMap(false);
gsp_->GridSlamProcessor::init(particles_, xmin_, ymin_, xmax_, ymax_,
delta_, initialPose);
gsp_->setllsamplerange(llsamplerange_);
gsp_->setllsamplestep(llsamplestep_);
/// @todo Check these calls; in the gmapping gui, they use
/// llsamplestep and llsamplerange intead of lasamplestep and
/// lasamplerange. It was probably a typo, but who knows.
gsp_->setlasamplerange(lasamplerange_);
gsp_->setlasamplestep(lasamplestep_);
gsp_->setminimumScore(minimum_score_);
// Call the sampling function once to set the seed.
GMapping::sampleGaussian(1,seed_);
接着调用了GridSlamProcessor::init函数,初始化了最初始地图的大小、初始位置以及初始粒子数(30个)。
2.SlamGMapping::addScan函数,这一段转到粒子滤波算法部分,整个函数是在将激光数据导入processScan当中。
GridSlamProcessor::processScan函数首先调用了MotionModel::drawFromMotion,它构建了里程计的误差模型再叠加误差。
然后再调用了scanmatch函数,计算每个粒子的得分时调用了ScanMatcher::optimize函数,而optimize又调用了ScanMatcher::score函数,计算每个激光束的障碍物phit以及它的网格坐标iphit,再沿着激光束寻找空闲网格与障碍物的差值pfree、ipfree,再在障碍物四周8个网格(m_kernelSize)寻找障碍物栅格pr,并计算它们可能是障碍物的概率。如果pr大于阈值m_fullnessThreshold而pf小于阈值,则认为是最优的栅格。
接着回到optimize函数中,它对currentPose的上下左右的动态进行检测,找到最优的位姿,并赋予pnew,也就是说每当调用一次optimize函数,corrected就会更新为最优解。
inline void GridSlamProcessor::scanMatch(const double* plainReading){
// sample a new pose from each scan in the reference
double sumScore=0;
for (ParticleVector::iterator it=m_particles.begin(); it!=m_particles.end(); it++){
OrientedPoint corrected;
double score, l, s;
/* 开始计算每个粒子的得分 */
score=m_matcher.optimize(corrected, it->map, it->pose, plainReading);
// it->pose=corrected;
if (score>m_minimumScore){
it->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 <<std::endl;
}
}
/* 计算粒子的权重和 */
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();
/* 这是计算该粒子扫描到的地图上的自由空间 */
m_matcher.computeActiveArea(it->map, it->pose, plainReading);
}
if (m_infoStream)
m_infoStream << "Average Scan Matching Score=" << sumScore/m_particles.size() << std::endl;
}
接着我们看到,在重采样之前需要更新树节点的权重,应该是通过俄罗斯轮盘赌的遗传方法将粒子的权重依次更新。
void GridSlamProcessor::updateTreeWeights(bool weightsAlreadyNormalized){
if (!weightsAlreadyNormalized) {
normalize();
}
resetTree();
propagateWeights();
}
最后就是重采样了,其实就是按照未完全确定的权重,从之前产生的粒子集合中抽出m_indexes个粒子,组成更新后的最终粒子集,setWeight函数是根据目标分布与建议分布的商求解重采样的重要性系数。
inline bool GridSlamProcessor::resample(const double* plainReading, int adaptSize, const RangeReading* reading){
bool hasResampled = false;
TNodeVector oldGeneration;
for (unsigned int i=0; i<m_particles.size(); i++){
oldGeneration.push_back(m_particles[i].node);
}
if (m_neff<m_resampleThreshold*m_particles.size()){
if (m_infoStream)
m_infoStream << "*************RESAMPLE***************" << std::endl;
uniform_resampler<double, double> resampler;
m_indexes=resampler.resampleIndexes(m_weights, adaptSize);
if (m_outputStream.is_open()){
m_outputStream << "RESAMPLE "<< m_indexes.size() << " ";
for (std::vector<unsigned int>::const_iterator it=m_indexes.begin(); it!=m_indexes.end(); it++){
m_outputStream << *it << " ";
}
m_outputStream << std::endl;
}
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.
// cerr << "Existing Nodes:" ;
for (unsigned int i=0; i<m_indexes.size(); i++){
// cerr << " " << m_indexes[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]];
// cerr << i << "->" << m_indexes[i] << "B("<<oldNode->childs <<") ";
node=new TNode(p.pose, 0, oldNode, 0);
//node->reading=0;
node->reading=reading;
// cerr << "A("<<node->parent->childs <<") " <<endl;
temp.push_back(p);
temp.back().node=node;
temp.back().previousIndex=m_indexes[i];
}
while(j<m_indexes.size()){
deletedParticles.push_back(j);
j++;
}
// cerr << endl;
std::cerr << "Deleting Nodes:";
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;
}
std::cerr << " Done" <<std::endl;
//END: BUILDING TREE
std::cerr << "Deleting old particles..." ;
m_particles.clear();
std::cerr << "Done" << std::endl;
std::cerr << "Copying Particles and Registering scans...";
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);
}
std::cerr << " Done" <<std::endl;
hasResampled = true;
} else {
int index=0;
std::cerr << "Registering Scans:";
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;
node->reading=reading;
it->node=node;
//END: BUILDING TREE
m_matcher.invalidateActiveArea();
m_matcher.registerScan(it->map, it->pose, plainReading);
it->previousIndex=index;
index++;
node_it++;
}
std::cerr << "Done" <<std::endl;
}
//END: BUILDING TREE
return hasResampled;
}
3.SlamGMapping::updateMap函数,这个函数是更新地图的最后一步,依次选择了粒子集中的最优姿态,并对地图进行膨胀处理,再将地图数据发出。
void
SlamGMapping::updateMap(const sensor_msgs::LaserScan& scan)
{
ROS_DEBUG("Update map");
boost::mutex::scoped_lock map_lock (map_mutex_);
GMapping::ScanMatcher matcher;
matcher.setLaserParameters(scan.ranges.size(), &(laser_angles_[0]),
gsp_laser_->getPose());
matcher.setlaserMaxRange(maxRange_);
matcher.setusableRange(maxUrange_);
matcher.setgenerateMap(true);
/* 将权重最大的粒子作为里程计下一时刻的最优解 */
GMapping::GridSlamProcessor::Particle best =
gsp_->getParticles()[gsp_->getBestParticleIndex()];
/* 计算粒子集的熵 */
std_msgs::Float64 entropy;
entropy.data = computePoseEntropy();
if(entropy.data > 0.0)
entropy_publisher_.publish(entropy);
if(!got_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;
}
GMapping::Point center;
center.x=(xmin_ + xmax_) / 2.0;
center.y=(ymin_ + ymax_) / 2.0;
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]));
}
/* 将地图的尺寸膨胀 */
// 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);
}
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
GMapping::IntPoint p(x, y);
double occ=smap.cell(p);
assert(occ <= 1.0);
/* 如果是未知空间 */
if(occ < 0)
map_.map.data[MAP_IDX(map_.map.info.width, x, y)] = -1;
/* 如果是超过概率阈值那么确信是障碍物 */
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)] = 100;
}
/* 是自由空间 */
else
map_.map.data[MAP_IDX(map_.map.info.width, x, y)] = 0;
}
}
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);
}
这样就是LaserCallback函数的一次调用流程,在建图的过程中,上述的函数将反复调用。
因此,FastSLAM的流程是:
1.接收里程计信息,根据里程计误差模型产生若干粒子。
2.在每个时间周期对粒子的权值进行评估,评估的标准是与激光信息的契合程度,可以通过ICP方法来判定。
3.根据上一轮产生的带权值粒子进行权值更新与重采样,重采样是根据实际分布与建议分布之商来决定的。
4.取权值最大的粒子集合的平均值作为本轮的粒子先验,再利用激光信息更新地图。