lego-loam代码分析(4)-mapping流程

run主流程

整个mapping为一个独立线程完成,通过输入的传感器信息:特征点云和odometry两个信息,进行mapping;其程序入口为

void MapOptimization::run() {}

其整个slam流程如下:

  • 当前帧根据odom转换世界坐标系;
  • 提取submap用于点云匹配;
  • 对当前帧激光点云进行降采样;
  • 当前帧对submap进行匹配优化;
  • 存储当前帧pose信息和cloud;
  • 闭环优化后结果更新;

transformAssociateToMap

基于获取的里程计信息,估算mapping时位姿。源码中采用基本的坐标转换,代码难以理解,需要仔细推导。其基本思想为,根据当前odometry信息和上时刻的odometry,可获取运动增量。根据上刻的在mapping中的位姿和运动增量从而获取mapping当前时刻的估计位置。代码中变量解释如下:

// transformSum为当前里程计信息
// transformBefMapped 上次里程计信息
// transformAftMapped 上次map中的位置
// transformTobeMapped 为里程计估计的当前时刻的位置

如果采用Eigen库代替,即采用矩阵运算,则代码形式如下:

transformTobeMapped = transformAftMapped * transformBefMapped.transpose() * T_w_laserOdom;

提取submap(extractSurroundingKeyFrames)

此函数主要对历史存储的pose及其对应点云cloud进行提取,根据是否闭环需求分成两种提取方法;lego-loam相对于loam为改进版,最主要的改进有两点:1.scan-to-scan改为scan-to-map降低帧间匹配的累计误差;2.增加后端闭环优化; 而提取的submap则为其中关键的一步,即用于帧间匹配的map。
注:scan-to-map和后端闭环思想在许多slam中都已经应用,如cartographer,katro slam等,且原作者的代码很少使用库,如优化库和矩阵运算库等,其数学功底非常深厚。

非闭环处理

由于不做闭环处理,因此闭环检测和优化的时间可省略,为提高每一步增量匹配更加精准,采用历史缓存中与当前位置距离在一定范围内所有keypose的集合,根据keypos对应的所有点云形成submap;

    kdtreeSurroundingKeyPoses.setInputCloud(cloudKeyPoses3D);
    kdtreeSurroundingKeyPoses.radiusSearch(
        currentRobotPosPoint, (double)_surrounding_keyframe_search_radius,    // 没有闭环时,仅搜索与当前位置在50m内的位置,用于构建submap
        pointSearchInd, pointSearchSqDis);

    for (int i = 0; i < pointSearchInd.size(); ++i){                                                    // 将50m内所有帧对应位置放入队列中
      surroundingKeyPoses->points.push_back(
          cloudKeyPoses3D->points[pointSearchInd[i]]);
    }
    for (int i = 0; i < surroundingExistingKeyPosesID.size(); ++i) {        // 将附近搜索范围内的n组点云形成一个整点云
      *laserCloudCornerFromMap += *surroundingCornerCloudKeyFrames[i];
      *laserCloudSurfFromMap += *surroundingSurfCloudKeyFrames[i];
      *laserCloudSurfFromMap += *surroundingOutlierCloudKeyFrames[i];
    }

注:代码在维护这个集合时,集合中采用增加和删减的keypose(满足和不满足条件),而非简单的每次重新构建

闭环处理

如果需要做闭环处理,则应提高匹配性能,且做简单的增量submap即可,即采用当前位置索引提取历史中前方按顺序索引的N个位置。即采用滑窗方法仅使用最近的N个点云数据形成submap;其目的是暂时无需考虑其他点云带来的影响,仅是增量mapping,如此可提高相邻帧的匹配精度,而整体mapping的累计误差则需要回环修正。
闭环处理获取的submap,仅是整个点云中的一部分,其大小即为关键帧的个数,采用滑窗方法,仅保留最新的设置个数的keypose形成的点云集合;

   if (recentCornerCloudKeyFrames.size() <
        _surrounding_keyframe_search_num) {  // queue is not full (the beginning
                                         // of mapping or a loop is just
                                         // closed)
                                         // clear recent key frames queue
      recentCornerCloudKeyFrames.clear();
      recentSurfCloudKeyFrames.clear();
      recentOutlierCloudKeyFrames.clear();
      int numPoses = cloudKeyPoses3D->points.size();             // 按照最近遍历每一个轨迹点 location pose放入滑窗队列中
      for (int i = numPoses - 1; i >= 0; --i) {
        int thisKeyInd = (int)cloudKeyPoses3D->points[i].intensity;
        PointTypePose thisTransformation = cloudKeyPoses6D->points[thisKeyInd];
        updateTransformPointCloudSinCos(&thisTransformation);
        // extract surrounding map
        recentCornerCloudKeyFrames.push_front(                       // 将历史帧按最新数据的顺序放入
            transformPointCloud(cornerCloudKeyFrames[thisKeyInd]));
        recentSurfCloudKeyFrames.push_front(
            transformPointCloud(surfCloudKeyFrames[thisKeyInd]));
        recentOutlierCloudKeyFrames.push_front(
            transformPointCloud(outlierCloudKeyFrames[thisKeyInd]));
        if (recentCornerCloudKeyFrames.size() >= _surrounding_keyframe_search_num)
          break;
      }
    } else {  // queue is full, pop the oldest key frame and push the latest  如果最近帧数据队列已满,
              // key frame
      if (latestFrameID != cloudKeyPoses3D->points.size()-1) {       // 如果机器人移动,需要剔除原来的数据,加入新的帧数据
        // if the robot is not moving, no need to
        // update recent frames

        recentCornerCloudKeyFrames.pop_front();
        recentSurfCloudKeyFrames.pop_front();
        recentOutlierCloudKeyFrames.pop_front();
        // push latest scan to the end of queue
        latestFrameID = cloudKeyPoses3D->points.size() - 1;
        PointTypePose thisTransformation =
            cloudKeyPoses6D->points[latestFrameID];
        updateTransformPointCloudSinCos(&thisTransformation);
        recentCornerCloudKeyFrames.push_back(
            transformPointCloud(cornerCloudKeyFrames[latestFrameID]));
        recentSurfCloudKeyFrames.push_back(
            transformPointCloud(surfCloudKeyFrames[latestFrameID]));
        recentOutlierCloudKeyFrames.push_back(
            transformPointCloud(outlierCloudKeyFrames[latestFrameID]));
      }
    }

    for (int i = 0; i < recentCornerCloudKeyFrames.size(); ++i) {     // 构建submap的点云
      *laserCloudCornerFromMap += *recentCornerCloudKeyFrames[i];
      *laserCloudSurfFromMap += *recentSurfCloudKeyFrames[i];
      *laserCloudSurfFromMap += *recentOutlierCloudKeyFrames[i];
    }

最后提取出来的submap经过降采样进行存储。

当前帧降采样(downsampleCurrentScan)

由于构建的submap都采用了稀疏点云存储,因此输入传感器点云(包括角点、平面点、非特征点)也应降按照相同参数进行降采样;

scanTOmap优化匹配(scan2MapOptimization)

当目标点云数目足够多的时候,调用LM优化算法进行scanTOmap匹配。获取匹配后位姿;

  if (laserCloudCornerFromMapDSNum > 10 && laserCloudSurfFromMapDSNum > 100) {   // 如果当前激光帧特征点个数达到一定个数时,才进行匹配优化
    kdtreeCornerFromMap.setInputCloud(laserCloudCornerFromMapDS);
    kdtreeSurfFromMap.setInputCloud(laserCloudSurfFromMapDS);

    for (int iterCount = 0; iterCount < 10; iterCount++) {                       // 优化匹配迭代10次,
      laserCloudOri->clear();
      coeffSel->clear();

      cornerOptimization(iterCount);
      surfOptimization(iterCount);

      if (LMOptimization(iterCount) == true) break;
    }

    transformUpdate();                                                           // 获取匹配后当前帧的位姿 

cornerOptimization

将当前点云中每个点在submap中寻找的最近5个点,作为对应点。求出五个点的协方差矩阵,进行QR分解,获取直线方程(需判断是否满足直线特征)。

      float cx = 0, cy = 0, cz = 0;
      for (int j = 0; j < 5; j++) {                                        // 
        cx += laserCloudCornerFromMapDS->points[pointSearchInd[j]].x;
        cy += laserCloudCornerFromMapDS->points[pointSearchInd[j]].y;
        cz += laserCloudCornerFromMapDS->points[pointSearchInd[j]].z;
      }
      cx /= 5;
      cy /= 5;
      cz /= 5;                                                             // 获取5个点坐标均值 

      float a11 = 0, a12 = 0, a13 = 0, a22 = 0, a23 = 0, a33 = 0;
      for (int j = 0; j < 5; j++) {
        float ax = laserCloudCornerFromMapDS->points[pointSearchInd[j]].x - cx;
        float ay = laserCloudCornerFromMapDS->points[pointSearchInd[j]].y - cy;
        float az = laserCloudCornerFromMapDS->points[pointSearchInd[j]].z - cz;

        a11 += ax * ax;
        a12 += ax * ay;
        a13 += ax * az;
        a22 += ay * ay;
        a23 += ay * az;
        a33 += az * az;
      }
      a11 /= 5;
      a12 /= 5;
      a13 /= 5;
      a22 /= 5;
      a23 /= 5;
      a33 /= 5;                                                           // 获取5个点的方差

      matA1(0, 0) = a11;
      matA1(0, 1) = a12;
      matA1(0, 2) = a13;
      matA1(1, 0) = a12;
      matA1(1, 1) = a22;
      matA1(1, 2) = a23;
      matA1(2, 0) = a13;
      matA1(2, 1) = a23;
      matA1(2, 2) = a33;                                                 // 获取协方差矩阵,即分布状况

      Eigen::SelfAdjointEigenSolver<Eigen::Matrix3f> esolver(matA1);

      matD1 = esolver.eigenvalues().real();                              // 获取特征值和特征向量
      matV1 = esolver.eigenvectors().real();                             // 特征向量

根据直线方程,先求出直线上的两个点PointA和PointB。与与里程计中scan-to-scan方法一致,求出对应的点到线的距离;
注意:获取的5个点不一定在一条直线上,需要判断是直线,才认为有效匹配,即(matD1[2] > 3 * matD1[1])

surfOptimization

将当前点云中每个点在submap中寻找的最近5个点,作为对应点。根据5个点构建平面方程,进行QR分解,获取平面方程解。

     // 创建Ax=B,并求解
      for (int j = 0; j < 5; j++) {
        matA0(j, 0) =
            laserCloudSurfFromMapDS->points[pointSearchInd[j]].x;
        matA0(j, 1) =
            laserCloudSurfFromMapDS->points[pointSearchInd[j]].y;
        matA0(j, 2) =
            laserCloudSurfFromMapDS->points[pointSearchInd[j]].z;
      }
      // QR分解求出平面方程
      matX0 = matA0.colPivHouseholderQr().solve(matB0);

      float pa = matX0(0, 0);
      float pb = matX0(1, 0);
      float pc = matX0(2, 0);
      float pd = 1;

已知平面方程,与里程计中scan-to-scan方法一致,求出对应的点到面的距离;
注意:获取的5个点不一定在同一平面上,需要判断是平面,才认为有效匹配

      // 平面是否满足条件,即5个点是否在平面阈值范围内
      bool planeValid = true;
      for (int j = 0; j < 5; j++) {
        if (fabs(pa * laserCloudSurfFromMapDS->points[pointSearchInd[j]].x +
                 pb * laserCloudSurfFromMapDS->points[pointSearchInd[j]].y +
                 pc * laserCloudSurfFromMapDS->points[pointSearchInd[j]].z +
                 pd) > 0.2) {
          planeValid = false;
          break;
        }
      }

LMOptimization

里程计中scan-to-scan中的LM优化方法一致。

其实本内容与里程计采用的方法基本一致,仅是将上一阵激光点云改为了submap(即多帧点云构建),其目的是降低因匹配带来的累计误差。LM优化方法完全一致,而寻找对应点的方法则是采用最小二乘方法,求解平面与直线方程。

存储当前帧pose信息和cloud(saveKeyFramesAndFactor)

采用最基本的图优化理论,存储关键轨迹,即稀疏的keypose。
(cartographer,基本图优化架构slam基本结构)

闭环优化

1.基于距离最近的特点,寻找可能闭环的关键点。前面已经经过,如果开启闭环操作时,其用于匹配的submap仅是相邻的几帧点云组建完成,因此具有较高的相邻匹配度,但是会存在一定的累计误差。但当前位置距离历史位置较近时,表明可能存在闭环。

  // find the closest history key frame
  std::vector<int> pointSearchIndLoop;
  std::vector<float> pointSearchSqDisLoop;
  kdtreeHistoryKeyPoses.setInputCloud(cloudKeyPoses3D);                            // 当前机器人所在位置附近,搜索历史所有范围在闭环搜索范围内(默认为7m) 的key location,
  kdtreeHistoryKeyPoses.radiusSearch(
      currentRobotPosPoint, _history_keyframe_search_radius, pointSearchIndLoop,
      pointSearchSqDisLoop);

  closestHistoryFrameID = -1;
  for (int i = 0; i < pointSearchIndLoop.size(); ++i) {                           // 所有在闭环搜索范围的位置ID
    int id = pointSearchIndLoop[i];
    if (abs(cloudKeyPoses6D->points[id].time - timeLaserOdometry) > 30.0) {       // 所有范围内location的时刻与现在时刻需在30s以上,否则无需闭环
      closestHistoryFrameID = id;                                                 // 仅需找到一帧曾经的位置与当前位置在 搜索范围内,且时间满足30s以上
      break;
    }
  }
  1. 找到最近历史中pose后,将最近pose索引前后5帧激光数据,即共11帧数据构建loop_submap,用于闭环匹配;
  // save history near key frames
  for (int j = - _history_keyframe_search_num; j <= _history_keyframe_search_num; ++j) {   // 将历史中找到的一帧closestHistoryFrameID的前后_history_keyframe_search_num个作为闭环的submap
    if (closestHistoryFrameID + j < 0 ||
        closestHistoryFrameID + j > latestFrameIDLoopCloure)
      continue;
    *nearHistorySurfKeyFrameCloud += *transformPointCloud(                                 // 包括 corner 和 surf两种特征点云
        cornerCloudKeyFrames[closestHistoryFrameID + j],
        &cloudKeyPoses6D->points[closestHistoryFrameID + j]);
    *nearHistorySurfKeyFrameCloud += *transformPointCloud(
        surfCloudKeyFrames[closestHistoryFrameID + j],
        &cloudKeyPoses6D->points[closestHistoryFrameID + j]);
  }
  1. 利用当前cloud点云信息和loop_submap采用ICP进行匹配;
  // ICP Settings
  pcl::IterativeClosestPoint<PointType, PointType> icp;
  icp.setMaxCorrespondenceDistance(100);
  icp.setMaximumIterations(100);
  icp.setTransformationEpsilon(1e-6);
  icp.setEuclideanFitnessEpsilon(1e-6);
  icp.setRANSACIterations(0);
  // Align clouds
  icp.setInputSource(latestSurfKeyFrameCloud);                        // 历史帧队列中最新的一帧的点云
  icp.setInputTarget(nearHistorySurfKeyFrameCloudDS);                 // 闭环检测提取的submap
  pcl::PointCloud<PointType>::Ptr unused_result(
      new pcl::PointCloud<PointType>());
  icp.align(*unused_result);                                          // icp 匹配

将可能闭环的keypos前后相邻几帧构建submap,并与当前帧采用ICP进行匹配,如果匹配度满足一定阈值时,表明存在闭环,并获取闭环匹配的位姿转移矩阵。

闭环优化

采用gtsam库,对位姿进行优化,与g2o相类似,以位姿pose为节点,两个位姿差为约束条件。当闭环存在时,即出现闭环约束时,进行优化后,更新位姿;

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
LeGO-LOAM 是一个激光雷达 SLAM(Simultaneous Localization and Mapping)算法,用于在移动机器人中实现实时的定位和建图。它是基于特征的方法,结合了激光雷达的数据和惯性测量单元(IMU)的数据来进行定位和建图。 如果你想在 Ubuntu 18.04 上使用 LeGO-LOAM,你可以按照以下步骤进行: 1. 首先,确保你的系统已经安装了 ROS(Robot Operating System)。你可以按照 ROS 官方网站的指导安装 ROS Kinetic 版本:http://wiki.ros.org/kinetic/Installation/Ubuntu 2. 创建一个 ROS 工作空间,可以使用以下命令: ``` mkdir -p ~/catkin_ws/src cd ~/catkin_ws/ catkin_make ``` 3. 进入到 src 目录下,下载 LeGO-LOAM 的源代码: ``` cd ~/catkin_ws/src git clone https://github.com/RobustFieldAutonomyLab/LeGO-LOAM.git ``` 4. 回到 catkin_ws 目录下,编译源代码: ``` cd ~/catkin_ws catkin_make ``` 5. 设置 ROS 环境变量,可以将以下命令添加到你的 `.bashrc` 文件中: ``` source ~/catkin_ws/devel/setup.bash ``` 6. 启动 LeGO-LOAM 节点,可以使用以下命令: ``` roslaunch lego_loam run.launch ``` 这样,LeGO-LOAM 就会开始运行,并通过激光雷达和 IMU 数据进行定位和建图。你可以根据你的具体需求进行参数配置和数据处理。 请注意,以上步骤仅适用于在 Ubuntu 18.04 上使用 ROS Kinetic 版本。如果你使用其他版本的 ROS 或其他操作系统,请参考 LeGO-LOAM 的官方文档进行相应的配置。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值