AddAccumulatedRangeData
现在进入AddAccumulatedRangeData
函数,返回类型为 std::unique_ptr<LocalTrajectoryBuilder2D::MatchingResult>
所用参数为
const common::Time time
当前同步时间const sensor::RangeData& gravity_aligned_range_data
经过重力修正后的点云returns和missconst transform::Rigid3d& gravity_alignment
, 重力向量const absl::optional<common::Duration>& sensor_duration
先看开头,我交换了部分代码顺序,并不影响。这里实际是获得scanMatch
的两个参数1
2
3
4
5
6
7
8
9
10
11if (gravity_aligned_range_data.returns.empty())
{
LOG(WARNING) << "Dropped empty horizontal range data.";
return nullptr;
}
const sensor::PointCloud& filtered_gravity_aligned_point_cloud =
sensor::AdaptiveVoxelFilter(options_.adaptive_voxel_filter_options())
.Filter(gravity_aligned_range_data.returns);
if (filtered_gravity_aligned_point_cloud.empty())
return nullptr;
这一段就是流程图的Adaptive Voxel Filter环节,解释参考(九) Voxel Filter和Adaptive Voxel Filter
然后是估计位姿部分1
2
3
4
5const transform::Rigid3d non_gravity_aligned_pose_prediction =
extrapolator_->ExtrapolatePose(time);
// 经过重力方向计算投影后的2d位置
const transform::Rigid2d pose_prediction = transform::Project2D(
non_gravity_aligned_pose_prediction * gravity_alignment.inverse() );
返回的pose_prediction
和上面的filtered_gravity_aligned_point_cloud
一起进入 Scan Mathching 环节,这个在流程图上很明显。
表面上只是重力对齐以后再处理,其实这里面还为后续的匹配与查找减小了查找范围,因为* gravity_alignment.inverse()
这一乘使得与上一帧数据的角度变化缩小了(基本是同一角度),所以 为后面的ScanMatch减小了角度的查找范围。
在后面再把角度乘回来:1
2
3
4
5pose_estimate_2d =
ScanMatch(time, pose_prediction, filtered_gravity_aligned_point_cloud);
transform::Rigid3d pose_estimate =
transform::Embed3D(*pose_estimate_2d) * gravity_alignment;
这样就实现了角度的估计,gravity_alignment上下是同一数值,只是为了减少计算量,点云数据也按这个gravity_alignment做了处理
scan match
1 | // local map frame <--- gravity-aligned frame |
这里的ScanMatch
包含了real time correlative scan matcher 和 ceres scan match两部分,前者为后者提供好的初值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// 输出是对该帧传感器数据的最优pose
std::unique_ptr<transform::Rigid2d> LocalTrajectoryBuilder2D::ScanMatch(
const common::Time time, const transform::Rigid2d& pose_prediction,
const sensor::PointCloud& filtered_gravity_aligned_point_cloud)
{
// 如果还没有子图,返回 PoseExtrapolator 的预测值
if (active_submaps_.submaps().empty() )
return absl::make_unique<transform::Rigid2d>(pose_prediction);
// 永远是头部的子图做匹配 SCAN-to-MAP
std::shared_ptr<const Submap2D> matching_submap =
active_submaps_.submaps().front();
transform::Rigid2d initial_ceres_pose = pose_prediction;
// 函数最终更新 initial_ceres_pose,返回匹配的最高分数
if (options_.use_online_correlative_scan_matching())
{
const double score = real_time_correlative_scan_matcher_.Match(
pose_prediction, filtered_gravity_aligned_point_cloud,
// Grid2D类型
*matching_submap->grid(), &initial_ceres_pose );
kRealTimeCorrelativeScanMatcherScoreMetric->Observe(score);
}
// ceres scan matcher 部分
}
Real Time Correlative Scan Matcher
这里借用了karto的做法,即本质是暴力匹配,而hector是用了高斯牛顿法和多分辨率地图。但是karto还是用了多分辨率地图,而cartographer这里没有。
它使用类似于在回环检测中激光与子地图匹配的方式,然后最好的匹配用作Ceres Scan Matcher的先验。这个scan matcher非常消耗资源,但它在特征丰富的环境中是很鲁棒。
我们要找到机器人位姿的后验概率分布,使用贝叶斯公式得到
运动模型是已知的多元高斯分布, 算法只研究观测模型。观测模型难以计算,一般具有多极值。 雷达观测数据 里包含了很多激光点数据,假设他们是完全独立的
将一帧观测数据的概率拆分成了当前帧的每一个点的概率,在计算这个概率之前,我们需要对数据帧以及地图进行栅格化。
位姿估计器传来的预测值不适合做初值,真正初值是在它附近。
实时相关性scan match 以预测值为中心,在搜索窗口内枚举出所有候选位姿,并生成每个位姿对应的点云,也就是说按照 把当前观测 投影到栅格地图 m ( t 时刻的激光数据与 t-1 时刻的地图匹配, scan to map),然后分别计算每个点云中hit
点在栅格地图的CellIndex
,并计算所有hit点的占据概率之和,概率最大值对应的位姿就是我们需要的。代表着在这个位姿下,激光点得映射到占据栅格中的可能性最大,当前观测与已知环境最为一致,前后帧匹配成功。 scan match非常重要,如果结果位姿很差,建图就会失败,后端怎么调整也没用
搜索的精度是栅格地图的分辨率
这个scan matcher可以修改做重定位,不过搜索窗口扩大很多暴力搜索
我们要计算 ,这就需要一个三层for循环 计算每一个位姿,还有第四个循环,也就是遍历点云中每个点。 计算量巨大,因为每一个位姿都要重新投影,需要计算sin和cos函数计算2D切片
把三层循环交换顺序,最外层遍历 ,这样(x, y)的循环只有加法。多分辨率搜索
首先选择一个相对低的分辨率对 map 和 scan 进行栅格化,每个栅格上保留高分辨率情况下的该区域的最高概率,这样在匹配时就不会错过正确的位姿估计
低分辨率找到最佳匹配后再在该位置进行高分辨率的栅格化,并设定相对较小的 search window 进行计算。
1 | // 更新 pose_estimate, 返回最高的分数 |
- 获取位姿估计器传来的初值的旋转分量,将激光点云左乘旋转分量,即先做旋转变换
- 根据搜索窗口x、y、yaw的大小计算 num_scans和LinearBounds, x y搜索的num_linear_perturbations
- 切片旋转:GenerateRotatedScans函数,其实就是theta循环,将角度进行序列化(初始角度跟search_window的大小相关,按步长增加),点云依次左乘角度对应的旋转向量(旋转轴为z轴),即可生成
num_scans
段点云 - 把点云中所有点做变换:左乘初值的平移部分,根据变换后的坐标,找出它在栅格地图中的
CellIndex
并保存 - 根据搜索范围,生成候选者,数量是
25*num_scans
- 根据保存的索引队列,从栅格地图中获取最大置信度,这就用到了ProbabilityGrid类,根据每个点的
CellIndex
获取占据概率,最终对分数进行加权处理 - 求得每个候选位置的score,score最高的为最佳位置。
候选者是针对scan_index
(0~num_scans
)所做的x y方向的offset,生成所有候选者的过程其实就是num_scans
和x y三个循环
Match 函数的计算量很大 。 试验发现最终的分数,大部分在0.6~0.9
SearchParameters
根据lua参数,确定后面的切片步长angular_perturbation_step_size
,切片数num_scans
,搜索区域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
41SearchParameters::SearchParameters(
const double linear_search_window,
const double angular_search_window,
const sensor::PointCloud& point_cloud,
const double resolution)
: resolution(resolution)
{
// We set this value to something on the order of
// resolution to make sure that the std::acos() below is defined.
float max_scan_range = 3.f * resolution;
for (const sensor::RangefinderPoint& point : point_cloud) {
const float range = point.position.head<2>().norm();
max_scan_range = std::max(range, max_scan_range);
}
const double kSafetyMargin = 1. - 1e-3;
angular_perturbation_step_size =
kSafetyMargin * std::acos(1. - common::Pow2(resolution) /
(2. * common::Pow2(max_scan_range)));
// 这两者的计算不同
num_angular_perturbations =
std::ceil(angular_search_window / angular_perturbation_step_size);
const int num_linear_perturbations =
std::ceil(linear_search_window / resolution);
// 切片数目,由于resolution不修改,实际取决于 max_scan_range 和 angular_search_window
num_scans = 2 * num_angular_perturbations + 1;
/* Linear search window in pixel offsets; bounds are inclusive.
struct LinearBounds {
int min_x;
int max_x;
int min_y;
int max_y;
};
std::vector<LinearBounds> linear_bounds; Per rotated scans */
linear_bounds.reserve(num_scans);
// 对同一帧scan, linear_bounds的元素都相同,不同帧就不同了
for (int i = 0; i != num_scans; ++i) {
linear_bounds.push_back(
LinearBounds{-num_linear_perturbations, num_linear_perturbations,
-num_linear_perturbations, num_linear_perturbations} );
}
}
GenerateRotatedScans 切片旋转
生成序列化旋转的多段点云1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21std::vector<sensor::PointCloud> GenerateRotatedScans(
const sensor::PointCloud& point_cloud,
const SearchParameters& search_parameters)
{
std::vector<sensor::PointCloud> rotated_scans;
rotated_scans.reserve(search_parameters.num_scans);
// 旋转角度步长
double delta_theta = -search_parameters.num_angular_perturbations *
search_parameters.angular_perturbation_step_size;
// 个数 num_scans
for (int scan_index = 0; scan_index < search_parameters.num_scans;
++scan_index,
delta_theta += search_parameters.angular_perturbation_step_size)
{
// 左乘旋转向量,旋转轴为z轴
rotated_scans.push_back(sensor::TransformPointCloud(
point_cloud, transform::Rigid3f::Rotation(Eigen::AngleAxisf(
delta_theta, Eigen::Vector3f::UnitZ()) ) ) );
}
return rotated_scans;
}
ScoreCandidates
1 | void ScoreCandidates( |