(七) 源码分析5 updateMap函数

第1部分

这里的更新地图,只是为了可视化。因为真正的地图存储在粒子里面,这里会拿一个权值最大的粒子的地图发布出来.

得到权值最大的粒子,然后遍历这个粒子的整个轨迹,根据轨迹上记录的信息来进行建图,然后把得到的地图发布出去。

每次addScan()成功了,就会调用这个函数来生成地图,并发布出去。

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
ROS_DEBUG("Update map");
// 更新地图的时候,加了一把锁
boost::mutex::scoped_lock map_lock (map_mutex_);
// 构造函数的解释在下一篇
GMapping::ScanMatcher matcher;

/*设置scanmatcher的各个参数*/
matcher.setLaserParameters(scan.ranges.size(), &(laser_angles_[0]),
gsp_laser_->getPose() );
matcher.setlaserMaxRange(maxRange_);
matcher.setusableRange(maxUrange_);
matcher.setgenerateMap(true);

// 得到累计权重(weightSum)最大的粒子,不是当前的最大权重的粒子
// 累计权重即该粒子在各个时刻的权重之和(轨迹上的各个节点的权重之和)
GMapping::GridSlamProcessor::Particle best =
gsp_->getParticles()[gsp_->getBestParticleIndex()];

std_msgs::Float64 entropy;
// computePoseEntropy 遍历粒子集合计算熵
entropy.data = computePoseEntropy();
//发布位姿的熵
if(entropy.data > 0.0)
entropy_publisher_.publish(entropy);

//如果没有地图,则初始化一个地图
if(!got_map_)
{
// nav_msgs::GetMap::Response 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;
}

计算位姿的信息熵

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
double SlamGMapping::computePoseEntropy()
{
double weight_total=0.0;
for( std::vector<GMapping::GridSlamProcessor::Particle>::const_iterator it = gsp_->getParticles().begin();
it != gsp_->getParticles().end();
++it)
{
weight_total += it->weight;
}
double entropy = 0.0;
for( std::vector<GMapping::GridSlamProcessor::Particle>::const_iterator it = gsp_->getParticles().begin();
it != gsp_->getParticles().end();
++it)
{
if(it->weight/weight_total > 0.0)
entropy += it->weight/weight_total * log(it->weight/weight_total);
}
return -entropy;
}

第2部分

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
/*地图的中点*/
GMapping::Point center;
center.x=(xmin_ + xmax_) / 2.0;
center.y=(ymin_ + ymax_) / 2.0;

/* 初始化一个scanmatcherMap 创建一个地图 */
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]));
matcher.registerScan(smap, n->pose, &(n->reading->m_dists[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);
}

//根据地图的信息计算出来各个点的情况:occ、free、noinformation
//这样对地图进行标记主要是方便用RVIZ显示出来
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
/// 得到.xy被占用的概率
GMapping::IntPoint p(x, y);
double occ=smap.cell(p);
assert(occ <= 1.0);

//unknown
if(occ < 0)
map_.map.data[MAP_IDX(map_.map.info.width, x, y)] = GMAPPING_UNKNOWN;

//占用
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)] = GMAPPING_OCC;
}

//freespace
else
map_.map.data[MAP_IDX(map_.map.info.width, x, y)] = GMAPPING_FREE;
}
}

//到了这一步,肯定是有地图了。
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);

OctoMap安装配置

ROS环境下的安装:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 也可用与非ROS环境
sudo apt-get install ros-kinetic-octomap
# octomap的查看工具
sudo apt-get install ros-kinetic-octovis
````
`octomap_ros`和`octomap_msgs`提供了消息文件,封装和转换方法。`octomap_server`提供建图和服务

## 编译

在`package.xml`中添加:
```sh
<build_depend>octomap</build_depend>
<run_depend>octomap</run_depend>

因为库提供了CMake的config文件,所以直接这样用:

1
2
3
find_package(octomap REQUIRED)
include_directories(${OCTOMAP_INCLUDE_DIRS})
target_link_libraries(${OCTOMAP_LIBRARIES})


点云滤波

地图分很多种:稀疏的,稠密的,还有半稀疏的。稀疏的地图放大了看就是一个个离散的空间点,不过我们可以把它变成连续的稠密的网格,这个过程也叫点云的网格化。点云网格化需要对点云进行一系列处理

一般下面这几种情况需要进行点云滤波处理:

  1. 点云数据密度不规则需要平滑

  2. 因为遮挡等问题造成离群点需要去除

  3. 大量数据需要降采样

  4. 噪声数据需要去除

滤波程序

package.xml中要添加:

1
2
<build_depend>libpcl-all-dev</build_depend>
<exec_depend>libpcl-all</exec_depend>

CMake按照第一篇里进行配置

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
#include <ros/ros.h>
#include <sensor_msgs/PointCloud2.h>
#include <pcl_conversions/pcl_conversions.h>
#include <pcl/point_cloud.h>
#include <pcl/point_types.h>
//滤波的头文件
#include <pcl/filters/voxel_grid.h>

ros::Publisher pub;

void cloud_cb (const sensor_msgs::PointCloud2ConstPtr& cloud_msg)
{
// 声明存储原始数据与滤波后的数据的点云的格式
pcl::PCLPointCloud2* cloud = new pcl::PCLPointCloud2; //原始的点云的数据格式
pcl::PCLPointCloud2ConstPtr cloudPtr(cloud);

// ROS消息转化为PCL中的点云数据格式
pcl_conversions::toPCL(*cloud_msg, *cloud);

pcl::PCLPointCloud2 cloud_filtered; //存储滤波后的数据格式
// 进行一个滤波处理
pcl::VoxelGrid<pcl::PCLPointCloud2> sor; //创建滤波对象
sor.setInputCloud (cloudPtr); //设置输入的滤波,将需要过滤的点云给滤波对象
sor.setLeafSize (0.1, 0.1, 0.1); //设置滤波时创建的体素大小为1cm立方体
sor.filter (cloud_filtered);//执行滤波处理,存储输出cloud_filtered

// 再将滤波后的点云的数据格式转换为ROS下的数据格式发布
sensor_msgs::PointCloud2 output;
pcl_conversions::moveFromPCL(cloud_filtered, output);//第一个参数是输入,后面的是输出
pub.publish (output);
}

int main (int argc, char** argv)
{
ros::init (argc, argv, "filter_cloud");//声明节点的名称
ros::NodeHandle nh;

ros::Subscriber sub = nh.subscribe<sensor_msgs::PointCloud2> ("/camera/depth/points", 1, cloud_cb);
pub = nh.advertise<sensor_msgs::PointCloud2> ("filtered_cloud", 1);

ros::spin ();
}

点云合并

把相邻的点合并成一个点,一方面可以减少图像的数据量,另一方面可以增加点云的稳定度。个人感觉贴近滤波
点云合并

直通滤波

多线雷达点云大部分的点都集中的中间区域,距离越远点云越稀疏,信息量也越小,应该筛选掉一些较远的点。这就用到了直通滤波,它是最为简单、粗暴的一种滤波方式,就是直接对点云的X、Y、Z轴的点云坐标约束来进行滤波。

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
#include <iostream>
#include <pcl/io/pcd_io.h>
#include <pcl/point_types.h>
#include <pcl/filters/passthrough.h>

int main(int argc, char** argv)
{

pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);

// Fill in the cloud data
pcl::PCDReader reader;
reader.read("16line.pcd", *cloud);

std::cerr << "Cloud before filtering: " << cloud->points.size() << std::endl;

pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_filtered2(new pcl::PointCloud<pcl::PointXYZ>);
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_filtered3(new pcl::PointCloud<pcl::PointXYZ>);

// Create the filtering object 直通滤波
pcl::PassThrough<pcl::PointXYZ> pass;
pass.setInputCloud(cloud);
pass.setFilterFieldName("x"); // 设置操作的坐标轴
pass.setFilterLimits(-5.0, 5.0);
pass.filter(*cloud_filtered2);
// filter range Y-axis
// 注意多次滤波需要再次输入点云和调用filter函数
pass.setInputCloud(cloud_filtered2);
pass.setFilterFieldName("y");
pass.setFilterLimits(-5.0, 5.0);
pass.filter(*cloud_filtered3);

std::cerr << "Cloud after filtering: " << cloud_filtered3->points.size() << std::endl;

// 读取pcd点云数据进行操作,最后保存为pcd文件
pcl::PCDWriter writer;
writer.write("16line_filtered.pcd", *cloud_filtered3, false);

return 0;
}

pass.setFilterLimitsNegative(true);: 是否保存滤波的限制范围内的点云,默认为false,保存限制范围点云,true时候是相反。

条件滤波法

在某些情况下,我们也会需要去除给定区域内部的点,比如激光雷达扫描近处的点(可能就是装载雷达的车),这些点通常对建图和感知不会有作用。

设定滤波条件进行滤波,点在范围内保留,不在范围内丢弃。

getFilterLimitsNegative 可以对选定范围取反,将其设为 true 时可以进行给定只保留给定范围内的点的功能。

体素滤波

实现降采样,既减少点云的数量,并同时保持点云的形状特征。创建一个三维体素,用体素内所有点的重心近似表示体素中的其他点,这样体素内所有点就用一个重心点最终表示。

使用小程序进行体素滤波,输入input.pcd,输出output.pcd

1
pcl_voxel_grid input.pcd output.pcd -leaf 0.03,0.03,0.03

对于体素下采样接口功能函数pcl提供了两种方式 pcl::ApproximateVoxelGridpcl::VoxelGrid。两种区别主要在于第一种是用每个体素栅格的中心点来近似该体素内的点,提升了速度,但是也损失了原始点云的局部形态精细度。 ApproximateVoxelGrid是依据每一个体素的中心点来获取点云的,并不是依赖每个体素里面是否存在点云。所以会导致两种方式的处理结果又偏差,VoxelGrid的方式更加精确但是耗时相对于ApproximateVoxelGrid更高。

半径滤波

对整个输入迭代一次,对于每个点进行半径R的邻域搜索(球体),如果邻域点的个数低于某一阈值,则该点将被视为噪声点并被移除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);      //待滤波点云
if (pcl::io::loadPCDFile("test.pcd", *cloud) < 0)
{
PCL_ERROR("\a点云文件不存在!\n");
system("pause");
return -1;
}

pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_filtered(new pcl::PointCloud<pcl::PointXYZ>); //滤波后点云
pcl::RadiusOutlierRemoval<pcl::PointXYZ> ror; //创建滤波器对象
ror.setInputCloud(cloud); //设置待滤波点云
ror.setRadiusSearch(0.02); //设置查询点的半径邻域范围
ror.setMinNeighborsInRadius(5); //设置判断是否为离群点的阈值,即半径内至少包括的点数
//ror.setNegative(true); //默认false,保存内点;true,保存滤掉的外点
ror.filter(*cloud_filtered); //执行滤波,保存滤波结果于cloud_filtered

实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void radiusremoval(pcl::PointCloud<pcl::PointXYZ>::Ptr& cloud, pcl::PointCloud<pcl::PointXYZ>::Ptr& cloud_filtered, double radius, int min_pts)
{
pcl::KdTreeFLANN<pcl::PointXYZ> tree;
tree.setInputCloud(cloud);
vector<float> avg;
for (int i = 0; i < cloud->points.size(); ++i)
{
vector<int> id(min_pts);
vector<float> dist(min_pts);
tree.nearestKSearch(cloud->points[i], min_pts, id, dist);
// 是否是平方
if (dist[dist.size() - 1] < radius)
{
cloud_filtered->push_back(cloud->points[i]);
}
}
}

构建一个 KD 树,对每个点进行范围搜索(最近邻搜索 nearestKSearch),最后判断邻近点数(最大距离)是否满足要求即可。

统计滤波


看过源码后得知, 计算均值 时除以的是点云的点数,不是邻域的点数。 对滤波结果有影响的是标准差阈值系数k和邻域的点数。 使用的也是 nearestKSearch

缺点:对较远的激光线也筛除掉了。首先对激光雷达点云数据中的每个点而言,由于每个激光线束本身扫描频率是一致的, 显然越远的扫描线点会越稀疏,因此其平均距离会越远(即便其本身可能不一定是噪点)。因此更合理的方法可能是对每个点周围小范围内(同一条线内)的点进行高斯拟合,然后判断该点是否满足该高斯分布,但是如果需要得到比较准确的高斯分布参数,点数需要达到一定数量,否则和上述基于点云密度进行去噪差别不大。

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
#include <pcl/io/pcd_io.h>  //文件输入输出
#include <pcl/point_types.h> //点类型相关定义
#include <pcl/visualization/cloud_viewer.h> //点云可视化相关定义
#include <pcl/filters/statistical_outlier_removal.h> //滤波相关
#include <pcl/common/common.h>

// 1.读取点云
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
pcl::PCDReader r;
r.read<pcl::PointXYZ>("table_scene_lms400.pcd", *cloud);
cout << "there are " << cloud->points.size() << " points before filtering." << endl;

// 2.统计滤波
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_filter(new pcl::PointCloud<pcl::PointXYZ>);
pcl::StatisticalOutlierRemoval<pcl::PointXYZ> sor;
sor.setInputCloud(cloud);
sor.setMeanK(50); // K近邻搜索点个数
sor.setStddevMulThresh(1.0); // 标准差倍数
sor.setNegative(false); // 保留未滤波点(内点)
sor.filter(*cloud_filter); // 保存滤波结果到cloud_filter

// 3.滤波结果保存
pcl::PCDWriter w;
w.writeASCII<pcl::PointXYZ>("table_scene_lms400_filter.pcd", *cloud_filter);
cout << "there are " << cloud_filter->points.size() << " points after filtering." << endl;

system("pause");
return 0;

常用滤波顺序: 直通 —-> 体素 —-> 统计

参考:统计滤波的源码


(六) 源码分析4 processScan函数

当前位姿与上一次位姿做差,计算做累计角度偏差和位移偏差。利用激光雷达测得距离做得分处理。非首帧调用scanMatch,upDateTreeWeight,resample。首帧则调用invalidActiveArea,computeActiveArea,registerScan。

第1部分

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
bool GridSlamProcessor::processScan(const RangeReading & reading, int adaptParticles)
{
/* retireve the position from the reading, and compute the odometry */
/*当前雷达在里程计坐标系的位置 addScan最后的setPose */
OrientedPoint relPose = reading.getPose();

/*m_count表示这个函数被调用的次数,开始为0,如果是第0次调用,则所有的位姿都是一样的*/
if (!m_count)
m_lastPartPose = m_odoPose = relPose;

/*对于每一个粒子,都从里程计运动模型中采样,得到车子的初步估计位置 这一步对应里程计的更新 */
int tmp_size = m_particles.size();

//这个for循环显然可以用 OpenMP 进行并行化
for (ParticleVector::iterator it=m_particles.begin(); it!=m_particles.end(); it++)
{
OrientedPoint& pose(it->pose); // 上一时刻粒子的位姿
// relPose是里程计记录的最新位姿, m_odoPose是建图引擎记录的上一时刻的位姿
// pose就是it->pose加了噪声,由于上面是引用,所以m_particles容器的成员pose全更新为pose
pose=m_motionModel.drawFromMotion(it->pose, relPose, m_odoPose);
}
onOdometryUpdate(); /*回调函数,什么都没做,可以自己修改*/

// 根据两次里程计的数据,计算机器人的线性位移和角度位移的累积值
// m_odoPose表示上一次的里程计位姿 relPose表示新的里程计的位姿
OrientedPoint move = relPose - m_odoPose;
move.theta=atan2(sin(move.theta), cos(move.theta));

// 统计机器人在进行激光雷达更新之前, 走了多远的距离 以及 平移了多少的角度
// 这两个变量最后还是要清零
m_linearDistance+=sqrt(move*move); // x²+y²的开方
m_angularDistance+=fabs(move.theta);

// if the robot jumps throw a warning
/*
如果机器人在走了m_distanceThresholdCheck这么远的距离都没有进行激光雷达的更新
则需要进行报警。这个误差很可能是里程计或者激光雷达的BUG造成的。
例如里程计数据出错 或者 激光雷达很久没有数据等等
每次进行激光雷达的更新之后 m_linearDistance这个参数就会清零
m_distanceThresholdCheck在开头定义为5 */
if (m_linearDistance > m_distanceThresholdCheck)
{
/*------- 一堆报警内容 -------*/
}
//更新:把当前的位置赋值给旧的位置
m_odoPose = relPose;
//先声明为false,最后如果成功就赋值 true
bool processed=false;
// 只有当机器人走过一定的距离或者旋转过一定的角度,或者过一段指定的时间才处理激光数据
// 否则太低效了,period_被构造函数写死成5秒,可以考虑修改
if ( ! m_count || m_linearDistance >=m_linearThresholdDistance
|| m_angularDistance >=m_angularThresholdDistance
|| (period_ >= 0.0 && (reading.getTime() - last_update_time_) > period_) )
{
/*第2部分*/
}
m_readingCount++;
return processed;
}

ScanMatcher 构造函数

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
ScanMatcher::ScanMatcher(): m_laserPose(0,0,0)
{
line_angle = 0.0;
m_laserBeams=0;
m_optRecursiveIterations=9;
m_activeAreaComputed=false;

// 5cm解析度的默认参数
// 这个参数是计算似然位姿的时候使用的,实际的gmapping中没有用到
m_llsamplerange=0.01;
m_llsamplestep=0.01;
m_lasamplerange=0.005;
m_lasamplestep=0.005;

//地图进行拓展的大小
m_enlargeStep=10.;
m_fullnessThreshold=0.1;

//指示里程计和陀螺仪是否可靠
//如果可靠的话,那么进行score计算的时候,就需要对离里程计数据比较远的位姿增加惩罚
//对于我们的应用来说,陀螺仪在短期内还是很可靠的。
m_angularOdometryReliability=0.;
m_linearOdometryReliability=0.;

//理论上的离激光点的空闲距离 也就是说沿着激光束方向离激光点这么远距离的栅格一定是空闲的。
m_freeCellRatio=sqrt(2.);

//跳过一帧激光数据的开始几束激光
m_initialBeamsSkip=0;
m_linePoints = new IntPoint[20000];
}

全是参数赋值,但是全写死,不能通过ROS修改

drawFromMotion 里程计运动模型

  • p 表示粒子估计的最优位置(机器人上一个时刻的最优位置)
  • pnew 表示里程计算出来的新的位置
  • pold 表示建图引擎记录的上一时刻的位姿(即上一个里程计的位置)

不是执行圆加运算,pnew和pold只是用于产生噪声控制量,然后加到p上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
OrientedPoint 
MotionModel::drawFromMotion(const OrientedPoint& p, const OrientedPoint& pnew, const OrientedPoint& pold) const
{
double sxy=0.3*srr;
// 计算出pnew 相对于 pold走了多少距离,得到控制量
// 这里的距离表达是相对于车身坐标系来说的
OrientedPoint delta=absoluteDifference(pnew, pold);

/*初始化一个点*/
OrientedPoint noisypoint(delta);

/*走过的三个方向的距离,为控制量添加上噪声项*/
noisypoint.x+=sampleGaussian(srr*fabs(delta.x)+str*fabs(delta.theta)+sxy*fabs(delta.y));
noisypoint.y+=sampleGaussian(srr*fabs(delta.y)+str*fabs(delta.theta)+sxy*fabs(delta.x));
noisypoint.theta+=sampleGaussian(stt*fabs(delta.theta)+srt*sqrt(delta.x*delta.x+delta.y*delta.y));

/*限制角度的范围为 -π~π */
noisypoint.theta=fmod(noisypoint.theta, 2*M_PI);
if (noisypoint.theta>M_PI)
noisypoint.theta-=2*M_PI;

/*把加入了噪声的控制量 加到粒子估计的最优的位置上,得到新的粒子位姿的预估(根据运动模型推算)*/
return absoluteSum(p,noisypoint);
}

这里的MotionModel是一个十分粗糙的运动模型,只是简单的矢量加减运算。 相比于《Probabilistic Robotics》中提到的速度模型和里程计模型而言,有很多方面都没有考虑,精度上可能有折扣。

第2部分 if中的处理激光数据

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
last_update_time_ = reading.getTime();
/*复制一帧数据 把激光数据转换为scan-match需要的格式*/
int beam_number = reading.getSize();
double * plainReading = new double[beam_number];
for(unsigned int i=0; i<beam_number; i++)
{
plainReading[i]=reading.m_dists[i];
}
/*如果不是第一帧数据*/
if (m_count>0)
{
/*
为每个粒子进行scanMatch,计算出来每个粒子的最优位姿,同时计算该最优位姿的得分和似然 对应于gmapping论文中的用最近的一次测量计算proposal的算法
除了进行scanMatch之外,还对粒子进行了权重的计算,并计算了粒子的有效区域,但不进行内存分配 内存分配在resample()函数中
这个函数在gridslamprocessor.hxx里
*/
scanMatch(plainReading);

//至此 关于proposal的更新完毕了,接下来是计算权重
onScanmatchUpdate();
/*
由于scanMatch中对粒子的权重进行了更新,那么这个时候各个粒子的轨迹上的累计权重都需要重新计算
这个函数即更新各个粒子的轨迹上的累计权重是更新
GridSlamProcessor::updateTreeWeights(bool weightsAlreadyNormalized) 函数在gridslamprocessor_tree.cpp里面实现
*/
updateTreeWeights(false);
/*
粒子重采样 根据neff的大小来进行重采样 不但进行了重采样,也对地图进行更新
GridSlamProcessor::resample 函数在gridslamprocessor.hxx里面实现
*/
std::cerr<<"plainReading:"<<m_beams<<std::endl;
resample(plainReading, adaptParticles, reading_copy);
}
/*如果是第一帧激光数据*/
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);

//为每个粒子创建路径的第一个节点。该节点的权重为0,父节点为it->node(这个时候为NULL)。
//因为第一个节点就是轨迹的根,所以没有父节点
TNode* node=new TNode(it->pose, 0., it->node, 0);
node->reading = reading_copy;
it->node=node;
}
}
// "Tree: normalizing, resetting and propagating weights at the end..." ;
//进行重采样之后,粒子的权重又会发生变化,因此需要再次更新粒子轨迹的累计权重
//GridSlamProcessor::updateTreeWeights(bool weightsAlreadyNormalized) 函数在gridslamprocessor_tree.cpp里面实现
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;
}

(四) 源码分析2 initMapper 第2部分

initMapper 第2部分

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
// 激光的名称必须是"FLASER"
// 传入计算的angle increment绝对值, 因为gmapping接受正的angle increment
// 根据上面得到的激光雷达的数据信息, 初始化一个激光传感器
gsp_laser_ = new GMapping::RangeSensor("FLASER",
gsp_laser_beam_count_,
fabs(scan.angle_increment),
gmap_pose, // (0,0,0)
0.0,
maxRange_);
ROS_ASSERT(gsp_laser_);
// std::map<std::string, Sensor*>
GMapping::SensorMap smap;
// getName()就是 FLASER
smap.insert(make_pair(gsp_laser_->getName(), gsp_laser_));
gsp_->setSensorMap(smap); // GMapping::GridSlamProcessor*
// 构造函数里只有变量的赋值
gsp_odom_ = new GMapping::OdometrySensor(odom_frame_);
ROS_ASSERT(gsp_odom_);

///得到里程计的初始位姿,如果没有,则把初始位姿设置为(0,0,0)
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_);
gsp_->setlasamplerange(lasamplerange_);
gsp_->setlasamplestep(lasamplestep_);
gsp_->setminimumScore(minimum_score_);

// 生成1作为方差,均值为0的高斯分布
GMapping::sampleGaussian(1, seed_);

ROS_INFO("Mapper Initialization complete");
return true;

getOdomPose

获得centered_laser_pose_ (laser坐标系的原点, 方向(0,0,angle_center)) 在odom坐标系中的坐标gmap_pose,即scan消息的时间戳时,激光雷达在里程计坐标系中的位姿

调用: getOdomPose(initialPose, scan.header.stamp)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
bool
SlamGMapping::getOdomPose(GMapping::OrientedPoint& gmap_pose, const ros::Time& t)
{
// Get the pose of the centered laser at the right time
centered_laser_pose_.stamp_ = t;

tf::Stamped<tf::Transform> odom_pose;
// 得到centered_laser_pose_ 在odom坐标系中的坐标 odom_pose
try
{
tf_.transformPose(odom_frame_, centered_laser_pose_, odom_pose);
}
catch(tf::TransformException e)
{
ROS_WARN("Failed to compute odom pose, skipping scan (%s)", e.what());
return false;
}
double yaw = tf::getYaw(odom_pose.getRotation());
ROS_INFO("get centered_laser_pose_'s odom pose: yaw: %f", yaw);
ROS_INFO("centered_laser_pose_'s odom pose:: %f %f %f",odom_pose.getOrigin().x(), odom_pose.getOrigin().y(), odom_pose.getOrigin().z());
gmap_pose = GMapping::OrientedPoint(odom_pose.getOrigin().x(),
odom_pose.getOrigin().y(), yaw);
return true;
}


sampleGaussian

S是随机数种子,保证每次的高斯分布不同。AMCL源码中是pf_pdf_gaussian_sample函数中调用pf_ran_gaussian

1
2
3
4
5
6
7
8
9
10
double sampleGaussian(double sigma, unsigned int S)
{
if (S!=0)
{
srand(S);
}
if (sigma==0)
return 0;
return pf_ran_gaussian (sigma);
}


GridSlamProcessor::init

GridFastSLAM初始化主要是用来初始化各个粒子的一些信息。

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
void GridSlamProcessor::init(unsigned int size, double xmin, double ymin, double xmax, double ymax, double delta, OrientedPoint initialPose)
{
  //设置地图大小和分辨率
 m_xmin=xmin;
m_ymin=ymin;
m_xmax=xmax;
m_ymax=ymax;
m_delta=delta;

/*设置每个粒子的初始值*/
m_particles.clear(); // vector<Particle>
TNode* node=new TNode(initialPose, 0, 0, 0);

//粒子对应的地图进行初始化 用两个地图来进行初始化 一个高分辨率地图 一个低分辨率地图
//高分辨率地图由自己指定 低分辨率地图固定为0.1m
//定义最终使用的地图数据类型 cell类型为 PointAccumulator 存储数据类型为HierarchicalArray2D<PointAccumulator>
// typedef Map<PointAccumulator,HierarchicalArray2D<PointAccumulator> > ScanMatcherMap;
ScanMatcherMap lmap(Point(xmin+xmax, ymin+ymax)*.5, xmax-xmin, ymax-ymin, delta);
ScanMatcherMap lowMap(Point(xmin+xmax,ymin+ymax)*0.5,xmax-xmin,ymax-ymin, 0.1);
for (unsigned int i=0; i<size; i++) // size是粒子数
{
m_particles.push_back(Particle(lmap,lowMap));
//m_particles.push_back(Particle(lmap));
m_particles.back().pose = initialPose;
// 开始时,前一帧的粒子位姿也是 initialPose
m_particles.back().previousPose = initialPose;
m_particles.back().setWeight(0);
m_particles.back().previousIndex=0;

// we use the root directly
m_particles.back().node= node;
}
m_neff=(double)size;
m_count=0;
m_readingCount=0;
m_linearDistance=m_angularDistance=0;
}


静态层

静态层加载过程.png

静态层所用的参数:static map params


(五) 源码分析3 laserCallback第2部分及addScan, updateMap

laserCallback 第2部分

initMapper

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
GMapping::OrientedPoint odom_pose;
// addScan这个函数要转到pf的核心代码了,将调用processScan
if(addScan(*scan, odom_pose))
{
ROS_DEBUG("scan processed done !");
// 从粒子集合中挑选最优的粒子, 以其地图坐标为基准, 计算从激光雷达到地图之间的坐标变换
GMapping::OrientedPoint mpose = gsp_->getParticles()[gsp_->getBestParticleIndex()].pose;
ROS_INFO("new best pose: %.3f %.3f %.3f", mpose.x, mpose.y, mpose.theta);
tf::Transform laser_to_map = tf::Transform(tf::createQuaternionFromRPY(0, 0, mpose.theta), tf::Vector3(mpose.x, mpose.y, 0.0)).inverse();

ROS_INFO("odom pose: %.3f %.3f %.3f", odom_pose.x, odom_pose.y, odom_pose.theta);
tf::Transform odom_to_laser = tf::Transform(tf::createQuaternionFromRPY(0, 0, odom_pose.theta), tf::Vector3(odom_pose.x, odom_pose.y, 0.0));

ROS_WARN("correction: %.3f %.3f %.3f", mpose.x - odom_pose.x, mpose.y - odom_pose.y, mpose.theta - odom_pose.theta);

// 另一处的lock在 SlamGMapping::publishTransform(), 这里是获得map_to_odom_,前者是发布
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_WARN("cannot process scan");

这里如果addScan返回false,就是未向地图添加scan,原因在processScan里,可能是机器人走过的线距离太短或者旋转过角度太少,未达到门槛;或者未达到指定的时间(period_,写死为5秒),毕竟我们不希望机器人停着不动就更新地图,那太消耗资源了。

updateMap()得到最优的粒子的地图数据,利用占据栅格地图算法,更新地图。

addScan

把新的测量值和里程计坐标报告给建图引擎gsp_,让其更新粒子集。 根据激光雷达的安装方式,对角度进行修改。

然后将ROS的激光雷达采集的信息转换成gmapping能看懂的格式。设置和激光数据时间戳匹配的机器人的位姿。调用processscan函数

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
// 加入一个激光雷达的数据, 里面会调用 processScan()函数,这个函数被laserCallback()函数调用
// gmap_pose为刚定义的里程计的累积位姿
bool SlamGMapping::addScan(const sensor_msgs::LaserScan& scan, GMapping::OrientedPoint& gmap_pose)
{
//得到与激光的时间戳相对应的机器人的里程计的位姿
if(!getOdomPose(gmap_pose, scan.header.stamp))
return false;

//检测是否所有帧的数据都是相等的,如果不相等就return
if(scan.ranges.size() != gsp_laser_beam_count_)
return false;

double* ranges_double = new double[scan.ranges.size()];

// 如果激光是反着装的,这激光的顺序需要反过来,这段代码省略
if (do_reverse_range_)
{
......
}
else
{
for(unsigned int i=0; i < scan.ranges.size(); i++)
{
// mapper那里无法过滤掉
// 排除掉所有激光距离小于range_min的值,它们很可能是噪声数据
if(scan.ranges[i] < scan.range_min)
ranges_double[i] = (double)scan.range_max;
else
ranges_double[i] = (double)scan.ranges[i];
}
}
// 把ROS的激光雷达数据信息 转换为 GMapping算法看得懂的形式
// 激光传感器gsp_laser_ 在initMapper里定义,ranges数据赋值给m_dists,size给m_beams
GMapping::RangeReading reading(scan.ranges.size(),
ranges_double,
gsp_laser_,
scan.header.stamp.toSec());
// 上面的初始化是进行深拷贝,因此申请的内存可以直接释放。
delete[] ranges_double;

//设置和激光数据的时间戳匹配的机器人的位姿 不是雷达位姿吗???
reading.setPose(gmap_pose);

//调用gmapping算法进行处理
return gsp_->processScan(reading);
}

gmap_pose应当是不同时间戳时,雷达扫描扇形的中心(包含角度)在odom坐标系中的坐标。

问题:

  • reading.setPose(gmap_pose); 解释存疑

最后就是核心算法所在的processScan


tf MessageFilter

tf::MessageFilter可以订阅任何的ROS消息,然后将其缓存,直到这些消息可以转换到目标坐标系,然后进行相应的处理(进入回调函数)

tf::MessageFilter的初始化需要message_filters::Subscriber,tf转换,目标坐标系,等待时间。当message_filters::Subscriber的消息能够由tf转换到目标坐标系时,调用回调函数,回调函数由tf::MessageFilter::registerCallback()进行注册。

步骤:

  1. 定义数据:TransformListener、message_filters::Subscriber、tf::MessageFilter
  2. 用话题的名称来初始化message_filters::Subscriber
  3. tf message_filters::Subscriber 目标坐标系来初始化tf::MessageFilter
  4. 给tf::MessageFilter注册callback
    ,编写callback,并在回调中完成坐标转换,至此完成消息订阅+坐标转换

(三) 源码分析1 总体和laserCallback 第1部分

gmapping包里,查看CMakeLists可知,gmapping所用的可执行文件主要是slam_gmapping,对应slam_gmapping.cpp

用到了头文件gridslamprocessor.h,其位于/opt/ros/kinetic/include/gmapping/gridfastslam/

gmapping用到的很重要的动态库有/opt/ros/kinetic/lib/libgridfastslam.so,它的来源是:

1
2
3
4
5
6
7
8
add_library(gridfastslam
gfsreader.cpp
gridslamprocessor.cpp
gridslamprocessor_tree.cpp
motionmodel.cpp
)
# 又链接库 scanmatcher sensor_range
target_link_libraries(gridfastslam scanmatcher sensor_range)

scanmatcher库的来源:
add_library(scanmatcher eig3.cpp scanmatcher.cpp scanmatcherprocessor.cpp smmap.cpp)


main函数如下,显然就看SlamGMapping了:

1
2
3
4
ros::init(argc, argv, "slam_gmapping");
SlamGMapping gn;
gn.startLiveSlam();
ros::spin();

gmapping流程图.png

构造函数

构造函数只有两行,定义了随机数种子,供之后的高斯采样使用。然后是init函数,它也很简单,先是下面两行,看来主要是类GridSlamProcessor

1
2
gsp_ = new GMapping::GridSlamProcessor();
tfB_ = new tf::TransformBroadcaster();

GridSlamProcessor构造函数其实有用的就一句period_ = 5.0;,其他是参数赋值,但在initMapper里又会重新赋值,其实是多余的。 然后声明两个指针和布尔量,剩下的代码全是读取和配置参数了

startLiveSlam

1
2
3
4
5
6
7
8
9
10
11
12
//注册entryopy  map  map_metadata话题   dynamic_map服务
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));

// 以成员函数publishLoop作为线程启动,参数为 transform_publish_period,默认 0.05
transform_thread_ = new boost::thread(boost::bind(&SlamGMapping::publishLoop, this, transform_publish_period_));

mapCallback是service的回调函数,留给使用者定义客户端,这样一来重点是laserCallback,稍后再分析。


线程transform_thread_也很简单:

1
2
3
4
5
6
7
8
9
if(transform_publish_period == 0)
return;

ros::Rate r(1.0 / transform_publish_period);
while(ros::ok())
{
publishTransform();
r.sleep();
}


publishTransform如下:

1
2
3
4
5
map_to_odom_mutex_.lock();
ros::Time tf_expiration = ros::Time::now() + ros::Duration(tf_delay_);
// 构造函数里定义的广播, 发布map和odom之间的变换
tfB_->sendTransform( tf::StampedTransform (map_to_odom_, tf_expiration, map_frame_, odom_frame_));
map_to_odom_mutex_.unlock();

laserCallback 第1部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
laser_count_++;
/*每隔throttle_scans_ (默认值 1)帧数据计算一次,限流作用*/
if ((laser_count_ % throttle_scans_) != 0)
return;

static ros::Time last_map_update(0,0);

if(!got_first_scan_)
{
/*重要参数的初始化,将slam里的参数传递到 openslam 里 ,设定坐标系,坐标原点,以及
采样函数随机种子的初始化,还调用了GridSlamProcessor::init,初始化了粒子数,子地图大小*/
if(!initMapper(*scan))
return;
got_first_scan_ = true;
}

下面分析函数 bool SlamGMapping::initMapper(const sensor_msgs::LaserScan& scan)

  • 这个函数在收到的第一帧激光雷达数据的时候会被调用一次,之后就再也不会被调用了。
  • 这个函数的功能主要就是对gmapping算法中需要的一些参数进行赋值,即对gmapping算法进行初始化
  1. 判断激光雷达是否是水平放置的,如果不是 则报错。
  2. 假设激光雷达数据的角度是对称的 & 递增的 为每个激光束分配角度。
  3. 为gmapping算法设置各种需要的参数。

initMapper 第1部分

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
laser_frame_ = scan.header.frame_id;
// 雷达坐标系原点在base_link中的位置
tf::Stamped<tf::Pose> ident;
tf::Stamped<tf::Transform> laser_pose;
ident.setIdentity();
ident.frame_id_ = laser_frame_; // "laser"
ident.stamp_ = scan.header.stamp;
ROS_INFO("ident : %f %f %f",ident.getOrigin().x(), ident.getOrigin().y(), ident.getOrigin().z());
try
{
// 输出laser_pose,雷达坐标系原点在base_link中的位置
tf_.transformPose(base_frame_, ident, laser_pose);
}
catch(tf::TransformException e)
{
ROS_WARN("Failed to compute laser pose, aborting initialization (%s)",
e.what());
return false;
}
ROS_INFO("laser_pose: %f %f %f",laser_pose.getOrigin().x(), laser_pose.getOrigin().y(), laser_pose.getOrigin().z());
// base_link坐标系中,创建laser_pose1米之上的点,再把它转到laser坐标系
tf::Vector3 v;
v.setValue(0, 0, 1 + laser_pose.getOrigin().z());
tf::Stamped<tf::Vector3> up(v, scan.header.stamp, base_frame_);
try
{
tf_.transformPoint(laser_frame_, up, up);
ROS_DEBUG("Z-Axis in sensor frame: %.3f", up.z());
}
catch(tf::TransformException& e)
{
ROS_WARN("Unable to determine orientation of laser: %s",
e.what());
return false;
}

// gmapping doesn't take roll or pitch into account.
// 这里是判断雷达是否倾斜
if (fabs(fabs(up.z()) - 1) > 0.001)
{
ROS_WARN("Laser has to be mounted planar! Z-coordinate has to be 1 or -1, but gave: %.5f",
up.z());
return false;
}
gsp_laser_beam_count_ = scan.ranges.size();
// 扇形扫描面的中心
double angle_center = (scan.angle_min + scan.angle_max)/2;
// 雷达是否正装
if (up.z() > 0)
{
do_reverse_range_ = scan.angle_min > scan.angle_max;
// laser坐标系中的点(0,0,0), 方向(0,0,angle_center)
centered_laser_pose_ = tf::Stamped<tf::Pose>(
tf::Transform(tf::createQuaternionFromRPY(0,0,angle_center),
tf::Vector3(0,0,0)), ros::Time::now(), laser_frame_);
ROS_INFO("Laser is mounted upwards.");
}
else
{
do_reverse_range_ = scan.angle_min < scan.angle_max;
centered_laser_pose_ = tf::Stamped<tf::Pose>(tf::Transform(tf::createQuaternionFromRPY(M_PI,0,-angle_center),
tf::Vector3(0,0,0)), ros::Time::now(), laser_frame_);
ROS_INFO("Laser is mounted upside down.");
}
// Compute the angles of the laser from -x to x, basically symmetric and in increasing order
laser_angles_.resize(scan.ranges.size());
// 把角度调成对称,也就是扇形要关于z轴对称,扫描角度从-3.124~3.142变为-3.133~3.133
double theta = - std::fabs(scan.angle_min - scan.angle_max)/2;
for(unsigned int i=0; i<scan.ranges.size(); ++i)
{
laser_angles_[i]=theta;
theta += std::fabs(scan.angle_increment);
}

ROS_INFO("Laser angles in laser-frame: min: %.3f max: %.3f inc: %.3f", scan.angle_min, scan.angle_max,
scan.angle_increment);
ROS_INFO("Laser angles in top-down centered laser-frame: min: %.3f max: %.3f inc: %.3f", laser_angles_.front(), laser_angles_.back(), std::fabs(scan.angle_increment));

// 这个是Gmapping算法接受的点类型
GMapping::OrientedPoint gmap_pose(0, 0, 0);
// 设置激光雷达的最大观测距离和最大使用距离
ros::NodeHandle private_nh_("~");
if(!private_nh_.getParam("maxRange", maxRange_))
maxRange_ = scan.range_max-0.01;
if(!private_nh_.getParam("maxUrange", maxUrange_))
maxUrange_=maxRange_;

GMapping::OrientedPoint

这个是Gmapping算法接受的点类型

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
template <class T, class A>
struct orientedpoint: public point<T>{
inline orientedpoint() : point<T>(0,0), theta(0) {};
inline orientedpoint(const point<T>& p);
inline orientedpoint(T x, T y, A _theta): point<T>(x,y), theta(_theta){}
inline void normalize();
inline orientedpoint<T,A> rotate(A alpha)
{
T s=sin(alpha), c=cos(alpha);
A a=alpha+theta;
a=atan2(sin(a),cos(a));
return orientedpoint(
c*this->x-s*this->y,
s*this->x+c*this->y,
a);
}
A theta;
};

template <class T, class A>
void orientedpoint<T,A>::normalize()
{
if (theta >= -M_PI && theta < M_PI)
return;

int multiplier = (int)(theta / (2*M_PI));
theta = theta - multiplier*2*M_PI;
if (theta >= M_PI)
theta -= 2*M_PI;
if (theta < -M_PI)
theta += 2*M_PI;
}


常用数据集

我看到很多篇论文使用了log格式的数据集进行建图算法的仿真,比如这里的,发现这输入carmen框架,已经比较古老

  1. 进入informatik.uni-freiburg选择download log file下载数据集
  2. 看到一个文本网页,另存下载或者新建一個文件如data.clf保存.注意是.clf格式
  3. 这个clf文件显然不是我们想要的,可以转换为bag文件. 使用clf_to_bag.py


MIT data center的数据集页面
数据集2011-04-11-07-34-27.bag所用odom坐标系是odom_combined


俄勒冈大学的数据集

数据集hallway_slow_2011-03-04-21-41-33.bag所用的雷达话题是base_laser1_scan

参考:各类数据集