cartographer中的扫描匹配

news2024/11/26 0:49:06

请添加图片描述

cartographer中的扫描匹配

cartographer中使用了两种扫描匹配方法:CSM(相关性扫描匹配方法(暴力匹配))、ceres优化匹配方法

CSM可以简单地理解为暴力搜索,即每一个激光数据与子图里的每一个位姿进行匹配,直到找到最优位姿,进行三层for循环,不过谷歌采用多分辨率搜索的方式,来降低计算量。基于优化的方法是谷歌的ceres库来实现,原理就是构造误差函数,使其对最小化,其精度高,但是对初始值敏感。

扫描匹配在cartographer中的调用

在cartographer的前端local_trajectory_builder_2d中会对传入的点云数据做各种处理,在对点云数据进行体素滤波之后传给ScanMatch进行扫描匹配。

// 扫描匹配, 进行点云与submap的匹配
std::unique_ptr<transform::Rigid2d> pose_estimate_2d =
   ScanMatch(time, pose_prediction, filtered_gravity_aligned_point_cloud);

ScanMatch函数中调用real_time_correlative_scan_matcher_.Match函数进行相关性扫描匹配

​ 调用ceres_scan_matcher_.Match函数使用ceres优化匹配

// 根据参数决定是否 使用correlative_scan_matching对先验位姿进行校准
  if (options_.use_online_correlative_scan_matching()) {
    const double score = real_time_correlative_scan_matcher_.Match(
        pose_prediction, filtered_gravity_aligned_point_cloud,
        *matching_submap->grid(), &initial_ceres_pose);
    kRealTimeCorrelativeScanMatcherScoreMetric->Observe(score);
  }

  auto pose_observation = absl::make_unique<transform::Rigid2d>();
  ceres::Solver::Summary summary;
  // 使用ceres进行扫描匹配
  ceres_scan_matcher_.Match(pose_prediction.translation(), initial_ceres_pose,
                            filtered_gravity_aligned_point_cloud,
                            *matching_submap->grid(), pose_observation.get(),
                            &summary);

ScanMatch函数

ScanMatch需要传入的参数:

time //点云时间
pose_prediction //先验位姿
filtered_gravity_aligned_point_cloud //匹配用的点云

ScanMatch返回值:

std::unique_ptr<transform::Rigid2d> //匹配后的二维位姿

首先判断submaps是否为空,为空的话直接返回传入的先验位姿pose_prediction

if (active_submaps_.submaps().empty()) {
  return absl::make_unique<transform::Rigid2d>(pose_prediction);
}

不为空,向下继续执行

将submaps的第一个子图赋值给matching_submap,用于匹配

std::shared_ptr<const Submap2D> matching_submap =
   active_submaps_.submaps().front();

将传入的先验位姿赋值给matching_submap,作为ceres优化匹配的初始位姿

std::shared_ptr<const Submap2D> matching_submap =
    active_submaps_.submaps().front();

根据配置文件的use_online_correlative_scan_matching参数,选择是否使用相关性匹配对先验位姿进行校准。

配置参数在src/cartographer/configuration_files/trajectory_builder_2d.lua文件中

-- 是否使用 real_time_correlative_scan_matcher 为ceres提供先验信息
  -- 计算复杂度高 , 但是很鲁棒 , 在odom或者imu不准时依然能达到很好的效果
  use_online_correlative_scan_matching = false,
  real_time_correlative_scan_matcher = {
    linear_search_window = 0.1,             -- 线性搜索窗口的大小
    angular_search_window = math.rad(20.),  -- 角度搜索窗口的大小
    -- 用于计算各部分score的权重
    translation_delta_cost_weight = 1e-1,   -- 平移
    rotation_delta_cost_weight = 1e-1,      -- 旋转
  },

若use_online_correlative_scan_matching参数选择为TRUE,就调用real_time_correlative_scan_matcher_.Match函数,进行相关性扫描匹配,校准先验位姿,返回的是匹配得分

if (options_.use_online_correlative_scan_matching()) {
  const double score = real_time_correlative_scan_matcher_.Match(
      pose_prediction, filtered_gravity_aligned_point_cloud,
      *matching_submap->grid(), &initial_ceres_pose);
  kRealTimeCorrelativeScanMatcherScoreMetric->Observe(score);
}

若use_online_correlative_scan_matching参数选择为FALSE,则直接使用传入的先验位姿pose_prediction作为ceres优化的初始位姿,调用ceres_scan_matcher_.Match函数进行ceres优化匹配

auto pose_observation = absl::make_unique<transform::Rigid2d>();
  ceres::Solver::Summary summary;
  // 使用ceres进行扫描匹配
  ceres_scan_matcher_.Match(pose_prediction.translation(), initial_ceres_pose,
                            filtered_gravity_aligned_point_cloud,
                            *matching_submap->grid(), pose_observation.get(),
                            &summary);

相关性扫描匹配

相关性扫描匹配方法实现在real_time_correlative_scan_matcher_.Match这个函数

传入参数:

initial_pose_estimate //预测出来的先验位姿
point_cloud //用于匹配的点云 点云的原点位于local坐标系原点
grid //用于匹配的栅格地图

输出的参数:

pose_estimate //校正后的位姿

返回值:

double  best_candidate.score  //匹配得分

代码步骤:

  1. 将点云旋转到预测的方向上
  2. 生成按照不同角度旋转后的点云集合
  3. 将旋转后的点云集合按照预测出的平移量进行平移,获取平移后的点在地图中的索引
  4. 生成所有的候选解
  5. 计算所有候选解的加权得分
  6. 获取最优解
  7. 将计算出的偏差量加上原始位姿获得校正后的位姿

step1. 将点云旋转到预测的方向上

传入的点云是经过变换到local坐标系原点的点云数据,需要将传入的点云数据进行旋转变换,变换到预测的方向上。

//获取初始位姿的角度即旋转量
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())));

根据配置参数初始化 SearchParameters数据结构,定义搜索的空间,包括平移的窗口和角度窗口

const SearchParameters search_parameters(
    options_.linear_search_window(), options_.angular_search_window(),
    rotated_point_cloud, grid.limits().resolution());

参数还是前面说的那个配置文件中

这里是调用了SearchParameters的构造函数,进入SearchParameters的构造函数

 // 求得 point_cloud 中雷达数据的 最大的值(最远点的距离)
  for (const sensor::RangefinderPoint& point : point_cloud) {
    const float range = point.position.head<2>().norm();
    max_scan_range = std::max(range, max_scan_range);
  }

计算论文中提到的角度分辨率
在这里插入图片描述

其中:d是雷达的最远距离,r是栅格的分辨率

求θ

根据余弦定理:
c o s θ = b 2 + c 2 − a 2 2 b c cosθ = \frac{b^2 + c^2 - a^2}{2bc} cosθ=2bcb2+c2a2
则有
$$
cosθ = \frac{d^2 + d^2 - r^2}{2dd}

 = 1 - \frac{r^2}{2d^2}

即 即
θ = arccos(1-\frac{r2}{2d2})
$$

// 根据论文里的公式 求得角度分辨率 angular_perturbation_step_size
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);

将划分得到的角度个数范围扩大2倍再加1,得到要生成的旋转点云的个数。划分得到的角度个数是一个正数,点云会从他的负的最小开始算,所以会乘以2倍,还有0所以会加1,就是点云的个数。

num_scans = 2 * num_angular_perturbations + 1;

用传入的线性搜索窗除以分辨率可以得到xy方向的搜索范围,单位是栅格

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});

step2. 生成按照不同角度旋转后的点云集合

const std::vector<sensor::PointCloud> rotated_scans =
    GenerateRotatedScans(rotated_point_cloud, search_parameters);

search_parameters存了需要旋转的点云的个数num_scans,乘以角分辨率之后就是不同的角度

进入GenerateRotatedScans函数

首先计算起始角度,划分得到的角度范围num_angular_perturbations是从负值到正值的,起始位置就从负的最小角开始,再乘以分辨率得到需要的角度

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) {
  // 将 point_cloud 绕Z轴旋转了delta_theta
  rotated_scans.push_back(sensor::TransformPointCloud(
      point_cloud, transform::Rigid3f::Rotation(Eigen::AngleAxisf(
                       delta_theta, Eigen::Vector3f::UnitZ()))));
}

step3. 将旋转后的点云集合按照预测出的平移量进行平移, 获取平移后的点在地图中的索引

const std::vector<DiscreteScan2D> discrete_scans = DiscretizeScans(
    grid.limits(), rotated_scans,
    Eigen::Translation2f(initial_pose_estimate.translation().x(),
                         initial_pose_estimate.translation().y()));

调用DiscretizeScans函数,将旋转后的点云集合按照预测出的平移量进行平移,获取平移后的点在地图中的索引

先定义一个DiscreteScan2D结构的变量 discrete_scans,用于存放平移后的点云在地图中的索引

然后申请相应点云数量的空间

std::vector<DiscreteScan2D> discrete_scans;
discrete_scans.reserve(scans.size());

进入第一层for循环,遍历每一个角度的点云

for (const sensor::PointCloud& scan : scans) {
    // discrete_scans中的每一个 DiscreteScan2D 的size设置为这一帧点云中所有点的个数
    discrete_scans.emplace_back();
    discrete_scans.back().reserve(scan.size());
    ...}

进入第二层for循环,对点云中的每个点进行平移,获取平移后的栅格索引

for (const sensor::RangefinderPoint& point : scan) {
    // 对scan中的每个点进行平移
    const Eigen::Vector2f translated_point =
        Eigen::Affine2f(initial_translation) * point.position.head<2>();

    // 将平移后的点 对应的栅格的索引放入discrete_scans
    discrete_scans.back().push_back(
        map_limits.GetCellIndex(translated_point));
    }

通过point.position.head<2>取出点云的x,y坐标,然后乘以一个平移量Affine2f(initial_translation),对这个点进行平移。

const Eigen::Vector2f translated_point =
      Eigen::Affine2f(initial_translation) * point.position.head<2>();

通过平移后的点的坐标传入GetCellIndex函数,获得物理坐标在栅格地图中的栅格的索引

map_limits.GetCellIndex(translated_point));

最后返回平移后的点在地图中的索引

return discrete_scans

step4. 生成所有的候选解

std::vector<Candidate2D> candidates =
    GenerateExhaustiveSearchCandidates(search_parameters);

进入GenerateExhaustiveSearchCandidates函数,首先获取所有候选解的个数

遍历经过旋转后的所有角度的点云,判断每一个角度上的点云会有多少种可能性,将其记录到num_candidates中。

首先计算x坐标上的点数,然后计算对应y坐标上的点数,将x乘以y得到总的可能性。

举例说明,假设栅格中的一个点在x方向有十个栅格的的距离,在y方向上有五个栅格的距离,那么他总共可平移的栅格就是50个,然后总共有8个点,那么所有的平移栅格就是400个。

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;
}

然后生成候选解,首先遍历所有角度的点云个数,然后遍历所有的点云的x坐标,最后遍历每个x偏移量,y偏移量,最后将点云索引,x坐标,y坐标,配置参数传给 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);
    }
  }
}

最后检查得到的候选解的size是否等于候选解的个数num_candidates,相等就返回所有候选解

CHECK_EQ(candidates.size(), num_candidates);
return candidates;

step5. 计算所有候选解得加权得分

ScoreCandidates(grid, discrete_scans, search_parameters, &candidates);

进入ScoreCandidates函数

遍历所有候选解,判断传入的地图类型(栅格地图(PROBABILITY_GRID)或三维点云地图(TSDF))

将点云在栅格地图中的索引,x、y的偏移量传给ComputeCandidateScore函数,计算每个栅格的得分。

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);
      break;

进入ComputeCandidateScore函数

遍历点云在栅格地图中的xy坐标,加上xy的偏移量得到点云的平移和旋转之后的在栅格地图中的坐标

for (const Eigen::Array2i& xy_index : discrete_scan) {
    // 对每个点都加上像素坐标的offset, 相当于对点云进行平移
    const Eigen::Array2i proposed_xy_index(xy_index.x() + x_index_offset,
                                           xy_index.y() + y_index_offset);

获取平移后的点云在栅格中占用的概率

const float probability =
      probability_grid.GetProbability(proposed_xy_index);

将获得的所有点的占用概率累加,得到此候选解得概率得分

  candidate_score += probability;
  }

将累加得分除以点的个数,获得此候选解的平均得分,并返回

candidate_score /= static_cast<float>(discrete_scan.size());
CHECK_GT(candidate_score, 0.f);
return candidate_score;

最后回到ScoreCandidates函数,对每个得分根据参数配置的平移和旋转的权重进行加权

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()));

step6. 获取最优解

使用std::max_element获取候选解中得分最高的一组最为最优解

const Candidate2D& best_candidate =
    *std::max_element(candidates.begin(), candidates.end());

step7. 将计算出的偏差量加上原始位姿获得校正后的位姿

*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;

基于ceres的扫描匹配

ceres基本原理

残差方程

基于ceres的扫描匹配(即非线性优化)的实现在ceres_scan_matcher_.Match函数,代码文件位置:src/cartographer/cartographer/mapping/internal/2d/scan_matching/ceres_scan_matcher_2d.cc、.h

CreateCeresScanMatcherOptions2D的构造函数主要是对配置文件中的参数进行读取,主要还是在CeresScanMatcher2D类中实现。类CeresScanMatcher2D只定义了两个成员变量。 其中,options_ 用于记录扫描匹配器的相关配置,而变量ceres_solver_options_则是优化过程的配置。

CeresScanMatcher2D类的核心就在于Match这个函数的实现,这个函数的目的就是扫描匹配

在给定机器人的初始位姿估计initial_pose_estimate的情况下,尽可能的将扫描的点云数据point_cloud放置到栅格地图grid上, 同时返回优化后的位姿估计pose_estimate和优化迭代信息summary。而参数target_translation主要用于约束位姿估计的xy坐标, 在Local SLAM的业务主线调用该函数的时候传递的是位姿估计器的估计值,Cartographer认为优化后的机器人位置应当与该估计值的偏差不大。

输入参数:

 * @param[in] target_translation 预测出来的先验位置, 只有xy
 * @param[in] initial_pose_estimate (校正后的)先验位姿, 有xy与theta
 * @param[in] point_cloud 用于匹配的点云 点云的原点位于local坐标系原点
 * @param[in] grid 用于匹配的栅格地图

输出参数:

 * @param[out] pose_estimate 优化之后的位姿
 * @param[out] summary 优化迭代信息

首先是创建一个double数组,用于存放输入的初始位姿x,y和θ,然后创建一个problem的对象用于描述将要求解的问题。

double ceres_pose_estimate[3] = {initial_pose_estimate.translation().x(),
                                 initial_pose_estimate.translation().y(),
                                 initial_pose_estimate.rotation().angle()};
ceres::Problem problem;

然后就是描述地图、平移和旋转的残差,在配置文件中为这三类来源定义了权重。

CHECK_GT(options_.occupied_space_weight(), 0.);
problem.AddResidualBlock(
    CreateOccupiedSpaceCostFunction2D(
        options_.occupied_space_weight() /
            std::sqrt(static_cast<double>(point_cloud.size())),
        point_cloud, grid),
    nullptr /* loss function */, ceres_pose_estimate);

CHECK_GT(options_.translation_weight(), 0.);
problem.AddResidualBlock(
    TranslationDeltaCostFunctor2D::CreateAutoDiffCostFunction(
        options_.translation_weight(), target_translation), // 平移的目标值, 没有使用校准后的平移(这里可能有点问题)
    nullptr /* loss function */, ceres_pose_estimate);      // 平移的初值

  // 旋转的残差, 固定了角度不变
CHECK_GT(options_.rotation_weight(), 0.);
problem.AddResidualBlock(
    RotationDeltaCostFunctor2D::CreateAutoDiffCostFunction(
        options_.rotation_weight(), ceres_pose_estimate[2]), // 角度的目标值
    nullptr /* loss function */, ceres_pose_estimate);       // 角度的初值

Cartographer为三类不同的残差源分别定义了一个类,并通过其静态函数CreateAutoDiffCostFunction提供使用Ceres原生的自动求导方法的代价函数计算。

通过其中RotationDeltaCostFunctor2D类来看看CreateAutoDiffCostFunction这个函数的实现

CreateAutoDiffCostFunction是类RotationDeltaCostFunctor2D公开的静态成员。有两个参数,其中scaling_factor是角度偏差的权重,可以通过配置文件指定; target_angle则是参考角度, 实际的调用传参是优化之前的角度估计。该函数最终构造和返回的是Ceres的AutoDiffCostFunction对象,从第2行中的模板列表中可以看到类RotationDeltaCostFunctor2D只提供一个残差项,其参与优化的参数有3个。

class RotationDeltaCostFunctor2D {
 public:
  // 静态成员函数, 返回CostFunction
  static ceres::CostFunction* CreateAutoDiffCostFunction(
      const double scaling_factor, const double target_angle) {
    return new ceres::AutoDiffCostFunction<
        RotationDeltaCostFunctor2D, 1 /* residuals */, 3 /* pose variables */>(
        new RotationDeltaCostFunctor2D(scaling_factor, target_angle));
  }
template <typename T>
bool operator()(const T* const pose, T* residual) const {
  residual[0] = scaling_factor_ * (pose[2] - angle_);
  return true; 
}

左边是类RotationDeltaCostFunctor2D对运算符"()"的重载,在第四行中计算角度偏差量并乘上权重得到残差项的代价。 这里的scaling_factor_和angle_是在类RotationDeltaCostFunctor2D中定义的两个私有成员变量分别记录了权重和参考角度。它们通过构造函数和静态函数CreateAutoDiffCostFunction的传参赋值。

类TranslationDeltaCostFunctor2D和OccupiedSpaceCostFunction2D也是以相同的套路实现的,把构造函数定义成私有的,防止用户直接构造对象。 在静态函数CreateAutoDiffCostFunction中以这两个类型为基础构建AutoDiffCostFunction对象。类TranslationDeltaCostFunctor2D定义了成员变量x_和y_用于记录位移偏差量的参考坐标。 类OccupiedSpaceCostFunction2D则使用成员变量point_cloud_和grid_分别记录待匹配的激光点云数据和栅格地图。

左边是类TranslationDeltaCostFunctor2D对运算符"()"的重载,它有两个残差项的代价需要计算,分别对应着x轴和y轴上的偏差量。在第4,5行中计算位置偏差量并乘上权重(尺度因子)来更新对应残差项的代价。

// 平移量残差的计算, (pose[0] - x_)的平方ceres会自动加上
template <typename T>
bool operator()(const T* const pose, T* residual) const {
  residual[0] = scaling_factor_ * (pose[0] - x_);
  residual[1] = scaling_factor_ * (pose[1] - y_);
  return true;
}

这里计算残差,都是基于使用的传感器预测的位姿很准的情况。

所以在调参的时候,如果传感器不是很好,位姿估计器不准确,可以适当的将地图的权重调的大于平移和旋转的权重。若是调参一直调不好,可以将计算平移和旋转的残差注释掉。

最后看看地图部分的残差

同样也是通过CreateAutoDiffCostFunction函数构建costFunction,他的构造是将地图的权重除以点云的数量,得到一个平均值,然后再进行传入,所以地图的权重就会被分散,当点云的数量越来越多时,地图的权重就会被分散的越来越小。

problem.AddResidualBlock(
        CreateOccupiedSpaceCostFunction2D(
            options_.occupied_space_weight() /
                std::sqrt(static_cast<double>(point_cloud.size())),
            point_cloud, grid),
        nullptr /* loss function */, ceres_pose_estimate);

下面是类OccupiedSpaceCostFunction2D对运算符"()"的重载。在函数的一开始,先把迭代查询的输入参数pose转换为坐标变换Tε用临时变量transform记录。

template <typename T>
bool operator()(const T* const pose, T* residual) const {
  Eigen::Matrix<T, 2, 1> translation(pose[0], pose[1]);
  Eigen::Rotation2D<T> rotation(pose[2]);
  Eigen::Matrix<T, 2, 2> rotation_matrix = rotation.toRotationMatrix();
  Eigen::Matrix<T, 3, 3> transform;
  transform << rotation_matrix, translation, T(0.), T(0.), T(1.);

然后使用Ceres库原生提供的双三次插值迭代器。

const GridArrayAdapter adapter(grid_);
ceres::BiCubicInterpolator<GridArrayAdapter> interpolator(adapter);
const MapLimits& limits = grid_.limits();

针对每个hit点计算对应的残差代价。

for (size_t i = 0; i < point_cloud_.size(); ++i) {
  // Note that this is a 2D point. The third component is a scaling factor.
  const Eigen::Matrix<T, 3, 1> point((T(point_cloud_[i].position.x())),
                                     (T(point_cloud_[i].position.y())),
                                     T(1.));
  // 根据预测位姿对单个点进行坐标变换
  const Eigen::Matrix<T, 3, 1> world = transform * point;
  // 获取三次插值之后的栅格free值, Evaluate函数内部调用了GetValue函数
  interpolator.Evaluate(
      (limits.max().x() - world[0]) / limits.resolution() - 0.5 +
          static_cast<double>(kPadding),
      (limits.max().y() - world[1]) / limits.resolution() - 0.5 +
          static_cast<double>(kPadding),
      &residual[i]);
  // free值越小, 表示占用的概率越大
  residual[i] = scaling_factor_ * residual[i];
}

参考:https://gaoyichao.com/Xiaotu/?book=Cartographer%E6%BA%90%E7%A0%81%E8%A7%A3%E8%AF%BB&title=%E5%9F%BA%E4%BA%8ECeres%E5%BA%93%E7%9A%84%E6%89%AB%E6%8F%8F%E5%8C%B9%E9%85%8D%E5%99%A8

使用ceres优化扫描匹配,除了要求hit点在占用栅格上出现的概率最大化之外, 还通过两个残差项约束了优化后的位姿估计在原始估计的附近。

总结

cartographer中使用了两种扫描匹配方法:CSM(相关性扫描匹配方法(暴力匹配))、ceres优化匹配方法

CSM可以简单地理解为暴力搜索,即每一个激光数据与子图里的每一个位姿进行匹配,直到找到最优位姿,进行三层for循环,不过谷歌采用多分辨率搜索的方式,来降低计算量。基于优化的方法是谷歌的ceres库来实现,原理就是构造误差函数,使其对最小化,其精度高,但是对初始值敏感。

在cartographer的前端local_trajectory_builder_2d中会对传入的点云数据做各种处理,在对点云数据进行体素滤波之后传给ScanMatch进行扫描匹配。

// 扫描匹配, 进行点云与submap的匹配
std::unique_ptr<transform::Rigid2d> pose_estimate_2d =
   ScanMatch(time, pose_prediction, filtered_gravity_aligned_point_cloud);

ScanMatch函数中调用real_time_correlative_scan_matcher_.Match函数进行相关性扫描匹配

eres%E5%BA%93%E7%9A%84%E6%89%AB%E6%8F%8F%E5%8C%B9%E9%85%8D%E5%99%A8

使用ceres优化扫描匹配,除了要求hit点在占用栅格上出现的概率最大化之外, 还通过两个残差项约束了优化后的位姿估计在原始估计的附近。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1093020.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数据结构和算法——树结构

又叫二叉排序树。 满二叉树&#xff1a;所有的叶子节点都在最后一层。 完全二叉树&#xff1a;如果所有叶子节点都在最后一层和倒数第二层&#xff0c;而且每个叶子节点都有左右子节点。 完全二叉树 前序遍历 1、先输出当前节点&#xff08;初始是root节点&#xff09;。 2、…

论文阅读:Image-to-Lidar Self-Supervised Distillation for Autonomous Driving Data

目录 摘要 Motivation 整体架构流程 技术细节 雷达和图像数据的同步 小结 论文地址: [2203.16258] Image-to-Lidar Self-Supervised Distillation for Autonomous Driving Data (arxiv.org) 论文代码&#xff1a;GitHub - valeoai/SLidR: Official PyTorch implementati…

股票买卖问题I、II、III、IV、V、VI

力控121、122、123、188、714、309。 买卖股票的第一题 121. 买卖股票的最佳时机 给定一个数组 prices &#xff0c;它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。 你只能选择 某一天 买入这只股票&#xff0c;并选择在 未来的某一个不同的日子 卖出该股票。设…

tomcat部署jenkins

tomcat部署jenkins 1.简介&#xff1a; Jenkins是一个开源的自动化服务器工具&#xff0c;用于持续集成和持续交付。它能够自动化构建、测试和部署软件项目&#xff0c;提高开发团队的效率和软件质量。 jenkins就是一个整合工具&#xff0c;把代码从git或者其他代码托管平台…

windows10 sockect tcp

1. 在vs下添加ws2_32.lib库 右键【项目】-【属性】-【链接器】-【输入】-【附加依赖项】&#xff0c;进行编辑&#xff0c;添加 ws2_32.lib库&#xff0c;去掉从父级或项目默认设置继承的勾选&#xff0c;如下图所示&#xff1a; 这是因为inet_addr是一个老函数&#xff0c;而…

【MySQL】索引的作用及知识储备

为什么要有索引 索引可以提高数据库的性能。不用加内存&#xff0c;不用改程序&#xff0c;不用调sql&#xff0c;只要执行正确的create indix&#xff0c;查询的速度就可能提高成百上千倍。但相应的代价是&#xff0c;插入&#xff0c;更新&#xff0c;删除的速度有所减弱。 …

【重新定义matlab强大系列十六】求解混合整数线性问题

&#x1f517; 运行环境&#xff1a;Matlab &#x1f6a9; 撰写作者&#xff1a;左手の明天 &#x1f947; 精选专栏&#xff1a;《python》 &#x1f525; 推荐专栏&#xff1a;《算法研究》 #### 防伪水印——左手の明天 #### &#x1f497; 大家好&#x1f917;&#x1f91…

【算法|动态规划No.18】leetcode718. 最长重复子数组

个人主页&#xff1a;兜里有颗棉花糖 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 兜里有颗棉花糖 原创 收录于专栏【手撕算法系列专栏】【LeetCode】 &#x1f354;本专栏旨在提高自己算法能力的同时&#xff0c;记录一下自己的学习过程&#xff0c;希望…

数据结构与算法-单链表小练习

&#x1f388;类定义 typedef int ElemType; typedef struct LNode {ElemType data;LNode* next; }LNode; class LinkList { private:LNode* head;public:LinkList();~LinkList();int deletelist(int x);void CreatList_h(int n);void print_evennode();void divide_LinkList(…

『GitHub Actions』部署静态博客指南

前言 之前博主是使用的 Jenkins 实现 vuepress 博客的自动部署与持续交付&#xff0c;但是因为现在迁移服务器到海外&#xff0c;并且服务器配置降低。现在经常出现服务器的 Jenkins 构建过程中 CPU 占用率过高&#xff0c;导致服务器卡死 然后我想的话既然只是部署静态博客&…

基于Java的博客管理系统设计与实现(源码+lw+部署文档+讲解等)

文章目录 前言具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序&#xff08;小蔡coding&#xff09;有保障的售后福利 代码参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作…

Puppeteer实现上下滚动、打开新Tab、用户数据保存(三)

Puppeteer实现上下滚动、打开新Tab、用户数据保存&#xff08;三&#xff09; Puppeteer实现上下滚动、打开新Tab、用户数据保存&#xff08;三&#xff09;一、实现上下滚动二、打开新Tab三、用户数据保存四、效果演示 一、实现上下滚动 在自动化测试中&#xff0c;我们需要能…

计算机毕业设计选题推荐-springboot 蛋糕甜品店管理系统

✍✍计算机编程指导师 ⭐⭐个人介绍&#xff1a;自己非常喜欢研究技术问题&#xff01;专业做Java、Python、微信小程序、安卓、大数据、爬虫、Golang、大屏等实战项目。 ⛽⛽实战项目&#xff1a;有源码或者技术上的问题欢迎在评论区一起讨论交流&#xff01; ⚡⚡ Java实战 |…

YOLO目标检测——跌倒摔倒数据集【含对应voc、coco和yolo三种格式标签】

实际项目应用&#xff1a;公共安全监控、智能家居、工业安全等活动区域无监管情况下的人员摔倒事故数据集说明&#xff1a;YOLO目标检测数据集&#xff0c;真实场景的高质量图片数据&#xff0c;数据场景丰富。使用lableimg标注软件标注&#xff0c;标注框质量高&#xff0c;含…

SpringBoot面试题1:什么是SpringBoot?为什么要用SpringBoot?

该文章专注于面试,面试只要回答关键点即可,不需要对框架有非常深入的回答,如果你想应付面试,是足够了,抓住关键点 面试官:什么是SpringBoot? Spring Boot 是一个用于快速开发独立的、基于 Spring 框架的应用程序的开源框架。它简化了 Spring 应用的配置和部署过程,使…

稚晖君项目复刻:L-ink门禁卡(1)——环境搭建与第一个项目创建

行文目录 前言其他文章正文开始STM32CubeMX安装STM32CubeMX安装L0的固件支持包Clion安装OpenOCD安装MinGw安装arm-none-eabi-gcc安装Clion配置 创建STM32工程创建STM32CubeMX工程更改芯片型号 参考文献 前言 其实关于稚晖君的L-ink门禁卡在我本科阶段就已经刷过好几次了&#x…

前端页面布局之【响应式布局】

目录 &#x1f31f;前言&#x1f31f;优点&#x1f31f;缺点&#x1f31f;media兼容性&#x1f31f;利用CSS3-Media Query实现响应式布局&#x1f31f;常见的媒体类型&#x1f31f;常见的操作符&#x1f31f;属性值&#x1f31f;设备检测&#x1f31f;响应式阈值选取&#x1f3…

使用vue3+element-ui plus 快速构建后台管理模板

一、安装 vue3 脚手架 npm create vuelatestcd vue-ui-template #切换到刚刚创建好的vue项目根目录中 npm install #下载项目所需要的依赖包 npm run dev #启动运行项目服务项目启动后&#xff0c;默认页面显示如下&#xff1a; 二、安装element-ui plus 官网链接&#xff1a;…

CDN到底有什么魅力,值得网站接入

当谈到提高网站性能和用户体验时&#xff0c;内容分发网络&#xff08;Content Delivery Network&#xff0c;CDN&#xff09;是一项不可忽视的技术。CDN加速已经成为许多在线企业的首选&#xff0c;用以减少加载时间、提高安全性和全球可访问性。本文将深入探讨CDN的原理、工作…