一些不熟悉的C++知识
std::move()
std::move
右值引用
左值是指表达式结束后依然存在的持久化对象,右值是指表达式结束时就不再存在的临时对象
在C++11之前,右值是不能引的,如
1 | int &a = 1; // error: 非常量的引用必须为左值 |
在C++11中我们可以引用右值,使用&&实现:
1 | int &&a = 1; |
右值引用的目的
1. 消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。
2. 能够更简洁明确地定义泛型函数。
例子
1 |
|
输出
1 | After copy, str is "Hello" |
std::unique_ptr
无法复制,只能移动的智能指针
函数重载和重写虚函数
在C++中同名函数有三种关系:
- 重载 (overlode):相同作用域;函数名相同;参数列表不同;返回类型随意。
- 覆盖
(override):不同作用域下(分别在父类和子类中);函数名相同;参数列表列表相同;返回类型相同(协变除外);基类函数必须有
virtual
修饰;父类和子类的访问限定可以不同。 - 隐藏 (overhide):不同作用域下(分别在父类和子类中);函数名相同;除过覆盖的同名函数都是隐藏关系。
强制类型转换
c++除了能使用c语言的强制类型转换外,还新增了四种强制类型转换:static_cast、dynamic_cast、const_cast、reinterpret_cast,主要运用于继承关系类间的强制转化,语法为:
1 | static_cast<new_type> (expression) |
备注:new_type为目标数据类型,expression为原始数据类型变量或者表达式。
1 | char a = 'a'; |
using
除了命名变量空间以外,using
还可以作为类型重命名,例如这两个写法是等价的:
1 | typedef std::vector<int> intvec; |
智能for
格式
1 | for(auto iter:itered){ |
global_trajectory_builder.cc
void AddSensorData()
1 | void AddSensorData(const std::string& sensor_id,const sensor::TimedPointCloudData& timed_point_cloud_data) override |
在这个GlobalTrajectoryBuilder
类的AddSensorData()
函数里面首先调用类的私有成员变量local_trajectory_builder_
的AddRangeData()
成员函数,注意这个AddRangeData
已经不是GlobalTrajectoryBuilder
类中的AddSensorData()
函数了。
local_trajectory_builder_2d.cc
本文件重点 帧间匹配
filtered_gravity_aligned_point_cloud
点云数据的重力矫正
室内场景中地板是水平的, 墙壁是竖直的, 所以点云数据的重力方向应该尽可能与场景中的法线方向垂直或平行。而人工拍摄得到的深度图像因为各种因素影响很可能重力方向是倾斜的, 所以本文对点云进行重力矫正, 将其进行旋转使之与场景坐标系对准。
ScanMatch()
real_time_correlative_scan_matcher_
得到一个位置的解,用这个作为初始解用ceres_scan_matcher_
优化的方法优化解。
correlative_scan_matcher_
的方法是基于搜索的方法。
ceres_scan_matcher_
的方法是基于优化的方法,优化的过程中采用梯度下降的方法,采用三次样条差值的方法使得离散栅格连续化。这种局部优化的方法很容易陷入到局部极小值当中,因此这个方法能正常工作的前提是初始值离全局最优值比较近。
DiscreteScan2D
1 | typedef std::vector<Eigen::Array2i> DiscreteScan2D |
TSDF
参考:https://www.jianshu.com/p/462fe75753f7
ceres库中的一些术语
优化目标 \[ \begin{split}\min_{\mathbf{x}} &\quad \frac{1}{2}\sum_{i} \rho_i\left(\left\|f_i\left(x_{i_1}, ... ,x_{i_k}\right)\right\|^2\right) \\ \text{s.t.} &\quad l_j \le x_j \le u_j\end{split} \] \(\rho()\) :loss function
\(f(x)\) :cost function
求和符号中的每一项构成一个residual block
map
cartographer中的点云表达
cartographer中的点是自己封装对Eigen库的3*1的matrix
做了一层结构体封装,原型是
1 | //不带时间信息的 |
使用的时候进行了重命名
1 | using PointType = RangefinderPoint; |
点云类PointCloud
的核心就是其私有的成员变量,points_
,如下:
1 | class PointCloud { |
PointCloud
,此外还封装了一些诸如添加点,大小等函数。
一帧激光的点云数据就是在此基础上再进行一次封装。
1 | struct RangeData { |
注意这个结构体里面的misses
点指得是发射出的激光里面那些没有收到反射值(测距超过量程了)的点,不是一般所指的那些激光线束所经过而代表的空闲点。
cartographer中的地图的一些表达
地图存的是一维数据,查询时先将二维xy转换为一维索引,然后查询。
重要基类 Grid2D
,私有成员变量情况如下
1 | class Grid2D : public GridInterface { |
概率栅格地图继承自Grid2D
1 | class ProbabilityGrid : public Grid2D { |
其中重要的函数,初始化栅格地图的函数如下
1 | void ProbabilityGrid::SetProbability(const Eigen::Array2i& cell_index,const float probability) { |
一组概率
cartographer中一般叫Probability
的都是占有概率,叫CorrespondenceCost
的都是空闲概率。
关于向地图中插入一组新的激光数据
核心函数
1 | void CastRays(const sensor::RangeData& range_data, //待插入的扫描数据 |
hit_table
和miss_table
提前计算好的一个occupy栅格再次被击中后应当更新的栅格值,即:
\[ hit\_table\_[Grid_{value\_old}] = Grid_{value\_new} \] miss_table同理。
核心步骤
记录击中点在栅格中的位置(代码中用
end
来记录,意思应该是指代表极光线束的末端),更新地图1
2
3
4for (const sensor::RangefinderPoint& hit : range_data.returns) {
ends.push_back(superscaled_limits.GetCellIndex(hit.position.head<2>()));
probability_grid->ApplyLookupTable(ends.back()/kSubpixelScale, hit_table);
}记录原点到每一个end点之间的miss点,然后用这些点更新地图
1
2
3
4
5
6for (const Eigen::Array2i& end : ends) {
std::vector<Eigen::Array2i> ray = RayToPixelMask(begin, end, kSubpixelScale);
for (const Eigen::Array2i& cell_index : ray) {
probability_grid->ApplyLookupTable(cell_index, miss_table);
}
}上面两步骤中的跟新地图都通过调运
probability_grid->ApplyLookupTable()
函数完成。函数中最关键的一步即为1
*cell = table[*cell];