cartographer估算出一个初始位姿(initial_pose_estimate)后,真正位姿往往并不是它,而是在它附近,实时相关扫描匹配器(RealTimeCorrelativeScanMatcher2D)就用于找到这个真正位姿。它根据初始位姿和点云,以初始位姿为中心,在指定范围内枚举出所有候选位姿,并生成每个位姿对应的点云。然后分别计算每个点云中hit点落在概率图上的位置,并计算这些位置处的占据概率之和,即点云和概率图的重合度。通过这种暴力匹配,认为占据概率之和最大的就是真正位姿。

图1的angular_search_window表示角度半径阀值,单位弧度,linear_search_window表示平移半径阀值,单位米,它们共同决定了暴力搜索范围。代码中,表示范围的数据结构是std::vector<Candidate2D>,假设变量名叫candidates。候选位姿数(candidates.size()) = 角度数 * 每个角度上的平移数。
- 角度数(num_scans)。先算出个角度步长angular_perturbation_step_size,然后角度半径阀值angular_search_window除以angular_perturbation_step_size得到num_angular_perturbations。由于角度范围是正、负各一半,再加一个0值,于是角度数num_scans等于“2 * num_angular_perturbations + 1”。
- 每个角度上的平移数。来自于linear_search_window,和角度类似,以一个步长去除linear_search_window。这个步长就是resolution(0.05m),当linear_search_window是0.1米时,x、y方向上各有5个位置,总合平移数就是5*5=25,图1的#0、#1到#24分别表示了这个25个平移位置。
以下是angular_search_window=0.35、linear_search_window=0.1,并且angular_perturbation_step_size=0.009时,那算出的候选项数:
num_scans(79) * 25 = 1975
针对1975中的某个位姿,可粗略这么认为:旋转部分是79个角度中的某一个,平移部分是雷达中心放在图1中25个位置中某一个
一、RealTimeCorrelativeScanMatcher2D::Match
Match执行实时相关扫描匹配,RealTimeCorrelativeScanMatcher2D其它成员可说都是它的辅助函数。
double RealTimeCorrelativeScanMatcher2D::Match( const transform::Rigid2d& initial_pose_estimate, const sensor::PointCloud& point_cloud, const Grid2D& grid, transform::Rigid2d* pose_estimate) const {
- initial_pose_estimate:初始位姿。CeresScanMatcher2D::Match中,就是odom坐标系下欲优化的local_pose。
- point_cloud:点云。CeresScanMatcher2D::Match中,就是base_footprint坐标系下,经过重力修正后的点云。
- pose_estimate:用于存储真正初始位姿。CeresScanMatcher2D::Match中,值就是&initial_pose_estimate,因为在最后赋值,不影响前面用它作为输入值。
CHECK(pose_estimate != nullptr); const Eigen::Rotation2Dd initial_rotation = initial_pose_estimate.rotation(); const sensor::PointCloud rotated_point_cloud = sensor::TransformPointCloud( point_cloud, transform::Rigid3f::Rotation(Eigen::AngleAxisf( initial_rotation.cast<float>().angle(), Eigen::Vector3f::UnitZ()))); const SearchParameters search_parameters( options_.linear_search_window(), options_.angular_search_window(), rotated_point_cloud, grid.limits().resolution());
首先把点云从base_footprint变换到odom坐标系,但只是修改旋转,没改平移,结果存放在rotated_point_cloud。然后根据范围参数linear_search_window和angular_search_window生成搜索参数,这些参数存储在search_parameters。
const std::vector<sensor::PointCloud> rotated_scans = GenerateRotatedScans(rotated_point_cloud, search_parameters);
根据搜索参数生成num_scans个旋转后的点云,即rotated_scans.size=num_scans。注意,这些点云只是经过了旋转变换,没经过平移。
const std::vector<DiscreteScan2D> discrete_scans = DiscretizeScans( grid.limits(), rotated_scans, Eigen::Translation2f(initial_pose_estimate.translation().x(), initial_pose_estimate.translation().y()));
将rotated_scan中的num_scans个点云进行平移变换。discrete_scans.size依旧是num_scans。
std::vector<Candidate2D> candidates = GenerateExhaustiveSearchCandidates(search_parameters);
根据搜索范围,生成candidates,candidates长度是num_scans*25。
ScoreCandidates(grid, discrete_scans, search_parameters, &candidates);
对candidates中各个位姿进行打分,分数最好的那个赋值给best_candidate。不论平移还是旋转,best_candidate存储的都是相对于initial_pose_estimate的偏移,所以真实位姿是“initial_pose_estimate + best_candidate”。
const Candidate2D& best_candidate = *std::max_element(candidates.begin(), candidates.end()); *pose_estimate = transform::Rigid2d( {initial_pose_estimate.translation().x() + best_candidate.x, initial_pose_estimate.translation().y() + best_candidate.y}, initial_rotation * Eigen::Rotation2Dd(best_candidate.orientation)); return best_candidate.score; }
二、SearchParameters::SearchParameters
SearchParameters::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); }
max_scan_range是点云中最远扫描点和雷达中心距离。for内变量range是某个点离雷达中心距离,即直角三角形中的。可以这么认为:以雷达中心为圆心、max_scan_range为半径的圆,恰好包含point_cloud中的所有点。
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); num_scans = 2 * num_angular_perturbations + 1;
如何推导角步长(angular_perturbation_step_size)公式?——由上所述,会出现个以雷达中心为圆心、max_scan_range为半径的圆,恰好包含point_cloud中的所有点。

图2中O是圆心,即雷达中心,OAB是等腰三角形,等边OA、OB长度是圆半径max_scan_range,底边AB长度是栅格分辨率resolution。由余弦定理:,对于本三角形有:
,代入公式有:
。
角度数(num_scans)。先算出个角度步长angular_perturbation_step_size,然后角度半径阀值angular_search_window除以angular_perturbation_step_size得到num_angular_perturbations。由于角度范围是正、负各一半,再加一个0值,于是角度数num_scans等于“2 * num_angular_perturbations + 1”。当中各变量值可参考图2。
const int num_linear_perturbations = std::ceil(linear_search_window / resolution); linear_bounds.reserve(num_scans); 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});

num_linear_perturbations表示在长度上,一侧有多少个搜索点。和角度类似,以一个步长去除linear_search_window,这个步长就是resolution(0.05m)。当linear_search_window是0.1米时,num_linear_perturbations就2,它表示在长度上,每一侧有两个搜索点。x、y的方向的初始值均为(-num_linear_perturbations, num_linear_perturbations),图2中(-2, 2),之所以正负范围,是因为要根据初始位姿四周进行搜索。由于搜索点是正、负各一半,再加一个0值,所以x、y方向上各有5个欲搜索位置,总合平移数就是5*5=25,图1的#0、#1到#24分别表示了这个25个平移位置。
} }
三、GenerateRotatedScans
根据搜索参数生成num_scans个“加”了旋转偏移后的点云,
std::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; for (int scan_index = 0; scan_index < search_parameters.num_scans; ++scan_index, delta_theta += search_parameters.angular_perturbation_step_size) { rotated_scans.push_back(sensor::TransformPointCloud( point_cloud, transform::Rigid3f::Rotation(Eigen::AngleAxisf( delta_theta, Eigen::Vector3f::UnitZ()))));
point_cloud是旋转初始位姿了的odom坐标系下点云。此处以它为基础按可能角度偏移生成num_scans个点云。
- [0/91]delta_theta: -0.35565
- [1/91]delta_theta: -0.34774
- [2/91]delta_theta: -0.33984
- ...
- [88/91]delta_theta: 0.33984
- [89/91]delta_theta: 0.34774
- [90/91]delta_theta: 0.35565
以上是num_scans中增加的角度偏移,依着索引升序,值由小变大,相邻角度间相差angular_perturbation_step_size,即角度步长。第一个偏移角度-angular_search_window,最后一个angular_search_window。
} return rotated_scans; }
四、DiscretizeScans
将rotated_scan中的num_scans个点云进行平移变换。返回值discrete_scans长度依旧是num_scans。
std::vector<DiscreteScan2D> DiscretizeScans( const MapLimits& map_limits, const std::vector<sensor::PointCloud>& scans, const Eigen::Translation2f& initial_translation) { std::vector<DiscreteScan2D> discrete_scans; discrete_scans.reserve(scans.size()); for (const sensor::PointCloud& scan : scans) { discrete_scans.emplace_back(); discrete_scans.back().reserve(scan.size()); for (const sensor::RangefinderPoint& point : scan) { const Eigen::Vector2f translated_point = Eigen::Affine2f(initial_translation) * point.position.head<2>(); }
Affine2f是仿射变化,但这里只有平移。以下是6个点的变换示例。(1,2)是初始位姿中平移部分。
- Affine2f(initial:(1.00000, 2.00000)) * point.position(-0.58136, -3.53174) => translated_point(0.41863, -1.53174)
- Affine2f(initial:(1.00000, 2.00000)) * point.position(-0.09588, -4.87782) => translated_point(0.90411, -2.87782)
- Affine2f(initial:(1.00000, 2.00000)) * point.position(-0.05497, -4.87491) => translated_point(0.94502, -2.87491)
- Affine2f(initial:(1.00000, 2.00000)) * point.position(0.10711, -5.20639) => translated_point(1.10711, -3.20639)
- Affine2f(initial:(1.00000, 2.00000)) * point.position(0.60323, -5.97015) => translated_point(1.60323, -3.97015)
- Affine2f(initial:(1.00000, 2.00000)) * point.position(0.65065, -5.94937) => translated_point(1.65065, -3.94937)
discrete_scans.back().push_back( map_limits.GetCellIndex(translated_point)); } } return discrete_scans; }
五、RealTimeCorrelativeScanMatcher2D::GenerateExhaustiveSearchCandidates
根据搜索范围,生成candidates,candidates长度是num_scans*25。
std::vector<Candidate2D> RealTimeCorrelativeScanMatcher2D::GenerateExhaustiveSearchCandidates( const SearchParameters& search_parameters) const { int num_candidates = 0; for (int scan_index = 0; scan_index != search_parameters.num_scans; ++scan_index) { const int num_linear_x_candidates = (search_parameters.linear_bounds[scan_index].max_x - search_parameters.linear_bounds[scan_index].min_x + 1); const int num_linear_y_candidates = (search_parameters.linear_bounds[scan_index].max_y - search_parameters.linear_bounds[scan_index].min_y + 1); num_candidates += num_linear_x_candidates * num_linear_y_candidates; }
预计算出candidates长度:num_candidates。
std::vector<Candidate2D> candidates; candidates.reserve(num_candidates); for (int scan_index = 0; scan_index != search_parameters.num_scans; ++scan_index) { for (int x_index_offset = search_parameters.linear_bounds[scan_index].min_x; x_index_offset <= search_parameters.linear_bounds[scan_index].max_x; ++x_index_offset) { for (int y_index_offset = search_parameters.linear_bounds[scan_index].min_y; y_index_offset <= search_parameters.linear_bounds[scan_index].max_y; ++y_index_offset) { candidates.emplace_back(scan_index, x_index_offset, y_index_offset, search_parameters); } } } CHECK_EQ(candidates.size(), num_candidates); return candidates; }

图3显示了GenerateExhaustiveSearchCandidates之后、ScoreCandidates之前的相关变量值。candidates中的长度是num_scans(91)*25=2275。当中以25个单元为一组,Candidate2D.scan_index是组索引,正如Watch4所示,每隔25个后增1。同组Candidate2D有着相同角度(orientation)。Watch4显示的candidates[2274]是最后一个单元,scan_index=90,x_index_offset、y_index_offset位在了图1的#24位置。
除了score字段,到现在已填充了Candidate2D其它字段,ScoreCandidates就是要填充score。
六、RealTimeCorrelativeScanMatcher2D::ScoreCandidates
对candidates中各个候选项进行打分,
6.1 RealTimeCorrelativeScanMatcher2D::ScoreCandidates
void RealTimeCorrelativeScanMatcher2D::ScoreCandidates( const Grid2D& grid, const std::vector<DiscreteScan2D>& discrete_scans, const SearchParameters& search_parameters, std::vector<Candidate2D>* const candidates) const { for (Candidate2D& candidate : *candidates) { switch (grid.GetGridType()) { case GridType::PROBABILITY_GRID: candidate.score = ComputeCandidateScore( static_cast<const ProbabilityGrid&>(grid), discrete_scans[candidate.scan_index], candidate.x_index_offset, candidate.y_index_offset);
默认使用概率图,launcher用的也是概率图。
break; case GridType::TSDF: candidate.score = ComputeCandidateScore( static_cast<const TSDF2D&>(grid), discrete_scans[candidate.scan_index], candidate.x_index_offset, candidate.y_index_offset); break; } candidate.score *= std::exp(-common::Pow2(std::hypot(candidate.x, candidate.y) * options_.translation_delta_cost_weight() + std::abs(candidate.orientation) * options_.rotation_delta_cost_weight())); } }
6.2 ComputeCandidateScore
对某个候选项打分。
float ComputeCandidateScore(const ProbabilityGrid& probability_grid, const DiscreteScan2D& discrete_scan, int x_index_offset, int y_index_offset) { float candidate_score = 0.f; for (const Eigen::Array2i& xy_index : discrete_scan) { const Eigen::Array2i proposed_xy_index(xy_index.x() + x_index_offset, xy_index.y() + y_index_offset);
xy_index存储着某个点以index表示的栅格坐标,所属点云的雷达中心栅格是初始位姿对应的那个栅格;(x_index_offset, y_index_offset)是偏移坐标系中的25个中的某一个,它们相加就是该候选项对应的雷达中心栅格。在旋转部分,这个候选项中的点云已经过“初始值+偏移角度”旋转,所以要调整的只是平移。
const float probability = probability_grid.GetProbability(proposed_xy_index); candidate_score += probability; } candidate_score /= static_cast<float>(discrete_scan.size());
candidate_score是累加点云所在栅格的占据概率,candidate_score是该点云中所有点在概率图占据概率的平均值。
CHECK_GT(candidate_score, 0.f); return candidate_score; }
七、SearchParameters::ShrinkToFit
参考“cartographer--后端match--减小搜索框策略(ShrinkToFit)[1] ”。
参考
- cartographer--后端match--减小搜索框策略(ShrinkToFit) https://blog.csdn.net/weixin_44684139/article/details/106161379