hector slam源码流程思路及相关参考整理

前言(原理简述)

在学习cartographer之前,先用这个项目练练手,因为cartographer相当于hector slam+karto slam结合的升级版。
hector slam论文题目:《A Flexible and Scalable SLAM System with Full 3D Motion Estimation
hector的论文和其他博客中,都有详细讲解原理,这里我说一下我看源码的目的:cartographer中的前端匹配使用了双三次线性插值+ceres库求解非线性优化问题(构造最小二乘,优化匹配),而hector slam中使用了双线性插值+高斯牛顿求解非线性优化问题,所以,先学习一下hector的思想,对cartographer是有帮助的,而且cartographer源码直接阅读难度有点大,所以暂且先学习这个。

代码梳理(不包含栅格地图构建部分)

安装源码之后,会看到有好几个文件夹:
源码包
程序的入口main函数在“hector_mapping文件夹的src文件下,找到main.cpp文件”。源码如下所示:

#include <ros/ros.h>

#include "HectorMappingRos.h"

int main(int argc, char** argv)
{
    //解析ros的初始化参数
  ros::init(argc, argv, "hector_slam");

  //创建HectorMappingRos类的对象,此处会调用该类的构造函数,因此我们转到该类的构造函数进行查看。
  HectorMappingRos sm;

  //ROS消息回调处理函数,主程序到这儿就不往下执行了,后面的return没有作用。一直调用前面的消息接收信息。
  ros::spin();

  return(0);
}

注意:构建类的对象的时候,会调用该类的构造函数,因此这是找到内部源码的关键(原谅我这么啰嗦,我只不过说出了我的顿悟啊)

接下来我们跳转到构造函数进行查看:

  //p_scan_subscriber_queue_size_默认为5,进入此scanCallback函数
  //告诉 master我们要订阅 p_scan_topic_ topic上的消息。当有消息到达topic时,ROS就会调用HectorMappingRos::scanCallback()函数。
  // 第二个参数是队列大小,以防我们处理消息的速度不够快,在缓存了5个消息后,再有新的消息到来就将开始丢弃先前接收的消息。
  // NodeHandle::subscribe()返回ros::Subscriber对象,你必须让它处于活动状态直到你不再想订阅该消息。
  // 当这个对象销毁时,它将自动退订上的消息。
  scanSubscriber_ = node_.subscribe(p_scan_topic_, p_scan_subscriber_queue_size_, &HectorMappingRos::scanCallback, this);
  sysMsgSubscriber_ = node_.subscribe(p_sys_msg_topic_, 2, &HectorMappingRos::sysMsgCallback, this);

  告诉节点管理器master我们将要在p_pose_update_topic_ topic上发布一个geometry_msgs::PoseWithCovarianceStamped的消息。
  /// 这样master就会告诉所有订阅了p_pose_update_topic_ topic的节点,将要有数据发布。第二个参数是发布序列的大小。
  /// 在这样的情况下,如果我们发布的消息太快,缓冲区中的消息在大于1个的时候就会开始丢弃先前发布的消息。
  /// NodeHandle::advertise() 返回一个 ros::Publisher对象,它有两个作用:
  /// 1) 它有一个publish()成员函数可以让你在topic上发布消息; 2) 如果消息类型不对,它会拒绝发布。
  poseUpdatePublisher_ = node_.advertise<geometry_msgs::PoseWithCovarianceStamped>(p_pose_update_topic_, 1, false);
  posePublisher_ = node_.advertise<geometry_msgs::PoseStamped>("slam_out_pose", 1, false);

构造函数的前面都是参数设置之类的,我们找到这个ros的订阅函数,通过注释我们大概理解一下,这个订阅函数,会一直回调一个函数scanCallback()函数进行话题的订阅,这个函数就是我们接下来的目标啊!点进去!

void HectorMappingRos::scanCallback(const sensor_msgs::LaserScan& scan)
{
  if (hectorDrawings)
  {
    hectorDrawings->setTime(scan.header.stamp);
  }

  ros::WallTime startTime = ros::WallTime::now();

  //如果p_use_tf_scan_transformation_为false,则进入循环
  if (!p_use_tf_scan_transformation_)
  {
      //此函数的作用为将激光点的距离数据转化为激光坐标系下的x,y坐标(应该是已经按照比例转换到地图的坐标中)存放于dataContainer类中。
    if (rosLaserScanToDataContainer(scan, laserScanContainer,slamProcessor->getScaleToMap()))
    {
        //updata()函数传入值为:dataContainer类、上一帧的位姿(x,y,theta)
      slamProcessor->update(laserScanContainer,slamProcessor->getLastScanMatchPose());
    }
  }
  else
  {
      //等待0.5秒
    ros::Duration dur (0.5);

因为我的目标是只看匹配优化部分,所以这里仅截取函数的前半部分源码。其中注释部分是我自己的理解,如果有错误,非常欢迎大家批评改正哈!(手动抱拳啦),可以看到,这里的主角是update()函数啦,所以,点进去!

void update(const DataContainer& dataContainer, const Eigen::Vector3f& poseHintWorld, bool map_without_matching = false)
  {
    //std::cout << "\nph:\n" << poseHintWorld << "\n";

    Eigen::Vector3f newPoseEstimateWorld;

    //若map_without_matching为false,则进行匹配。matchData()为匹配函数,重要!
    if (!map_without_matching){

        //matchData()函数传入数据为t-1时刻匹配的世界位姿,当前帧的激光数据类,t-1时刻匹配的协方差(或者为信息矩阵)
        //小技巧:点击matchData函数会进入到虚函数里面,此时点击左侧的小圆圈就会跳转到具体实现,同理,类也可以找到继承关系的类。
        newPoseEstimateWorld = (mapRep->matchData(poseHintWorld, dataContainer, lastScanMatchCov));
    }else{
        //若map_without_matching为true,则赋值为t-1时刻匹配位姿。
        newPoseEstimateWorld = poseHintWorld;
    }

    lastScanMatchPose = newPoseEstimateWorld;
    
    //判断位移增量与角度增量是否符合阈值要求
    if(util::poseDifferenceLargerThan(newPoseEstimateWorld, lastMapUpdatePose, paramMinDistanceDiffForMapUpdate, paramMinAngleDiffForMapUpdate) || map_without_matching)
    {

        //地图更新,也是重点!(还没有看)
      mapRep->updateByScan(dataContainer, newPoseEstimateWorld);

      mapRep->onMapUpdated();
      lastMapUpdatePose = newPoseEstimateWorld;
    }

同理,我还是值截取了相关的我想看的部分哈,见谅!同样,我们看到这里的主角是matchData()函数,点进去!我是先跳转到了虚函数声明部分,但我用的clion可以再点击虚函数左侧的小圆点跳转到具体实现的源码部分,如果你没有找到的话,那直接告诉你在:
hector_mapping/include/hector_slam_lib/slam_main/MapRepMultiMap.h文件里面奥。

virtual Eigen::Vector3f matchData(const Eigen::Vector3f& beginEstimateWorld, const DataContainer& dataContainer, Eigen::Matrix3f& covMatrix)
  {
    size_t size = mapContainer.size();

    Eigen::Vector3f tmp(beginEstimateWorld);

      /*默认三层地图,分辨率0.025m 0.05m 0.1m 从0.1m层开始处理
      论文中说,使用分层地图计算是为了避免陷入局部最小值,且其地图的分层,并不是通过高分辨率的地图降采样得到,
      而是使用不同的地图存储器,每种分辨率的地图单独极端,从低分辨率的地图开始进行扫描匹配,然后将方程的解迭代进入高精度的地图再解算,
       多次迭代后,得到当前时刻,机器人的位姿(更精确)。另一个优点是:使用多分辨率的地图,可是导航,路径规划更高效。
      */

    for (int index = size - 1; index >= 0; --index)
    {
      //std::cout << " m " << i;
      if (index == 0)
      {
        tmp  = (mapContainer[index].matchData(tmp, dataContainer, covMatrix, 5));
      }else{
          // pow(x,y)为求x的y次幂,将激光点数据缩小几倍???
        dataContainers[index-1].setFrom(dataContainer, static_cast<float>(1.0 / pow(2.0, static_cast<double>(index))));

        /*
        arg1 : t-1时刻机器人在世界坐标系下的位姿
        arg2 : 激光数据
        arg3 : t-1时刻扫描匹配的协方差
        arg4 : 最大迭代次数
        ret : t时刻机器人在世界坐标系下的位姿
        */
        tmp  = (mapContainer[index].matchData(tmp, dataContainers[index-1], covMatrix, 3));
      }
    }
    return tmp;
  }

这里继续,matchData()函数点进去!然后再点一下函数体内部的matchData()函数

if (dataContainer.getSize() != 0)
    {
        //将t-1时刻机器人在世界坐标系下的位姿转化为地图坐标系的坐标
      Eigen::Vector3f beginEstimateMap(gridMapUtil.getMapCoordsPose(beginEstimateWorld));

      //将beginEstimateMap(vector)复制到estimate(vector),这样保留了beginEstimateMap(vector)内容不变。
      Eigen::Vector3f estimate(beginEstimateMap);

      //关键函数!!!(返回值为求得增量之后的位移)
      estimateTransformationLogLh(estimate, gridMapUtil, dataContainer);
            int numIter = maxIterations;
      
      for (int i = 0; i < numIter; ++i)
      {

        estimateTransformationLogLh(estimate, gridMapUtil, dataContainer);
        //notConverged = estimateTransformationLogLh(estimate, gridMapUtil, dataContainer);

        if(drawInterface){
          float invNumIterf = 1.0f/static_cast<float> (numIter);
          drawInterface->setColor(static_cast<float>(i)*invNumIterf,0.0f, 0.0f);
          drawInterface->drawArrow(gridMapUtil.getWorldCoordsPose(estimate));
          //drawInterface->drawArrow(Eigen::Vector3f(0.0f, static_cast<float>(i)*0.05, 0.0f));
        }

        if(debugInterface)
        {
          debugInterface->addHessianMatrix(H);
        }
      }

      if (drawInterface)
      {
        drawInterface->setColor(0.0,0.0,1.0);
        drawScan(estimate, gridMapUtil, dataContainer);
      }
            estimate[2] = util::normalize_angle(estimate[2]);

      covMatrix = Eigen::Matrix3f::Zero();
      //covMatrix.block<2,2>(0,0) = (H.block<2,2>(0,0).inverse());
      //covMatrix.block<2,2>(0,0) = (H.block<2,2>(0,0));


      /*
      covMatrix(0,0) = 1.0/(0.1*0.1);
      covMatrix(1,1) = 1.0/(0.1*0.1);
      covMatrix(2,2) = 1.0/((M_PI / 18.0f) * (M_PI / 18.0f));
      */


      covMatrix = H;

      return gridMapUtil.getWorldCoordsPose(estimate);
    }

    return beginEstimateWorld;
  }

我对代码的理解已经写在了注释上,也有借鉴别人的,也有我自己的。点进estimateTransformationLogLh()函数

bool estimateTransformationLogLh(Eigen::Vector3f& estimate, ConcreteOccGridMapUtil& gridMapUtil, const DataContainer& dataPoints)
  {

      //核心函数,计算H矩阵和dTr向量
      //std::vector<Eigen::Vector2f> dataPoints;
    gridMapUtil.getCompleteHessianDerivs(estimate, dataPoints, H, dTr);


      //判断增量非0,避免无用计算
    if ((H(0, 0) != 0.0f) && (H(1, 1) != 0.0f)) {

        //计算优化方程的解,也就是论文中公式12
      //H += Eigen::Matrix3f::Identity() * 1.0f;
      Eigen::Vector3f searchDir (H.inverse() * dTr);

      //std::cout << "\nsearchdir\n" << searchDir  << "\n";

        //对增量进行限位处理
      if (searchDir[2] > 0.2f)
      {
        searchDir[2] = 0.2f;
        std::cout << "SearchDir angle change too large\n";
      } else if (searchDir[2] < -0.2f)
      {
        searchDir[2] = -0.2f;
        std::cout << "SearchDir angle change too large\n";
      }

        //更新估计值 estimate += searchDir
      updateEstimatedPose(estimate, searchDir);
      return true;
    }
    return false;
  }

点进getCompleteHessianDerivs()函数(hector_mapping/include/hector_slam_lib/map/OccGridMapUtil.h文件里)

 void getCompleteHessianDerivs(const Eigen::Vector3f& pose, const DataContainer& dataPoints, Eigen::Matrix3f& H, Eigen::Vector3f& dTr)
  {
      //当前帧激光点的数量
    int size = dataPoints.getSize();

    //输入为t-1时刻匹配激光传感器在地图坐标系下的位姿
      //由位姿算出转换二维转换矩阵
    // 定3*3的2D平移+旋转矩阵,即仿射变换矩阵,这矩阵是为了将激光坐标系下的激光点坐标变换到地图坐标系下
    //坐标变换参考链接:https://blog.csdn.net/jiajiading/article/details/102989118
    //https://blog.csdn.net/jiajiading/article/details/103369058
    Eigen::Affine2f transform(getTransformForState(pose));

    float sinRot = sin(pose[2]);
    float cosRot = cos(pose[2]);

    H = Eigen::Matrix3f::Zero();
    dTr = Eigen::Vector3f::Zero();

      //对一帧激光中的每个数据点进行处理
    for (int i = 0; i < size; ++i)
    {

        //获取一帧激光的中的第i个激光点在激光坐标系下的坐标(x,y)
      const Eigen::Vector2f& currPoint (dataPoints.getVecEntry(i));

        //通过双线性插值计算栅格概率,取地图的梯度,以及偏导数,对应论文中公式(4)、(5)、(6)、(14)
      //(transform * currPoint):先将激光坐标系下的坐标x,y转换为栅格地图坐标,然后进行地图插值,得到超越了分辨率的激光点的地图坐标值。
      Eigen::Vector3f transformedPointData(interpMapValueWithDerivatives(transform * currPoint));

        //击中点的栅格概率值应该是1,计算匹配误差(1-M(Pm))
      float funVal = 1.0f - transformedPointData[0];

        //更新x,y的位移增量, dTr 这个向量计算的是公式12 的求和符号后面的部分,没有乘H-1
      dTr[0] += transformedPointData[1] * funVal;
      dTr[1] += transformedPointData[2] * funVal;

        //计算旋转误差(由map->base_link),这个对应论文中公式 13,14 ,计算 M的梯度叉乘S对旋转角度的偏导数
      float rotDeriv = ((-sinRot * currPoint.x() - cosRot * currPoint.y()) * transformedPointData[1] + (cosRot * currPoint.x() - sinRot * currPoint.y()) * transformedPointData[2]);

        //更新角度增量
      dTr[2] += rotDeriv * funVal;

        //更新海塞矩阵,下面的对应于论文中公式13(这个公式有问题,掉了个求和符号,代码是正确的),计算hessian 矩阵
      H(0, 0) += util::sqr(transformedPointData[1]);
      H(1, 1) += util::sqr(transformedPointData[2]);
      H(2, 2) += util::sqr(rotDeriv);

      H(0, 1) += transformedPointData[1] * transformedPointData[2];
      H(0, 2) += transformedPointData[1] * rotDeriv;
      H(1, 2) += transformedPointData[2] * rotDeriv;
    }

    H(1, 0) = H(0, 1);
    H(2, 0) = H(0, 2);
    H(2, 1) = H(1, 2);

  }

找到interpMapValueWithDerivatives()函数,这就是我想看的双线性插值啦!点进去!

 Eigen::Vector3f interpMapValueWithDerivatives(const Eigen::Vector2f& coords)
  {
    //check if coords are within map limits.
    //检查激光点是否超出设置的地图匹配范围,pointOutOfMapBounds记得看一下???,如果超出,设置为000
    if (concreteGridMap->pointOutOfMapBounds(coords))
    {
      return Eigen::Vector3f(0.0f, 0.0f, 0.0f);
    }

      //对坐标进行向下取整,即得到坐标(x0,y0)
    //map coords are always positive, floor them by casting to int
    Eigen::Vector2i indMin(coords.cast<int>());


      //得到双线性插值的因子
      // | x |   | x0 |   | x-x0 |
      // |   | - |    | = |      |
      // | y |   | y0 |   | y-y0 |
    //get factors for bilinear interpolation
    Eigen::Vector2f factors(coords - indMin.cast<float>());

      //获得地图的X方向最大边界
    int sizeX = concreteGridMap->getSizeX();

      //将地图坐标转换成地图数组索引,地图是以一维数组的形式存储的,每个元素有2个值,val(概率),index(元素索引,初始化为-1)
      // 这么解释,就应该懂下一句的意思了,就是计算endpoint 在一维数组中的索引值
    int index = indMin[1] * sizeX + indMin[0];


      //下面四个if,就是获取当前实数坐标点相邻的四个栅格坐标,并获取该栅格的占用概率(没占用<0.4,占用>0.6),这里使用了个小技巧,
      // 就是用缓存,先判断当前缓存中数据是否有效,如果有效就读缓存,无效的话就直接读栅格地图中的值,地图想象不出来的话,看论文图片。
      // 获取当前坐标周围的4个网格点的网格值。 首先检查缓存数据,如果没有包含过滤器gridPoint with gaussian并存储在缓存中。
    // get grid values for the 4 grid points surrounding the current coords. Check cached data first, if not contained
    // filter gridPoint with gaussian and store in cache.
    if (!cacheMethod.containsCachedData(index, intensities[0]))
    {
        //当前栅格的周围的左下角的栅格索引对应的概率值(假设x轴向右,y轴向上)
      intensities[0] = getUnfilteredGridPoint(index);
      cacheMethod.cacheData(index, intensities[0]);
    }

    ++index;

    if (!cacheMethod.containsCachedData(index, intensities[1]))
    {
        //当前栅格的周围的右下角的栅格索引对应的概率值
      intensities[1] = getUnfilteredGridPoint(index);
      cacheMethod.cacheData(index, intensities[1]);
    }

    index += sizeX-1;

    if (!cacheMethod.containsCachedData(index, intensities[2]))
    {
        //当前栅格的周围的左上角的栅格索引对应的概率值
      intensities[2] = getUnfilteredGridPoint(index);
      cacheMethod.cacheData(index, intensities[2]);
    }

    ++index;

    if (!cacheMethod.containsCachedData(index, intensities[3]))
    {
        //当前栅格的周围的右上角的栅格索引对应的概率值
      intensities[3] = getUnfilteredGridPoint(index);
      cacheMethod.cacheData(index, intensities[3]);
    }


    //以下所有都是用于双线性差值用的,具体的就是对照论文中公式4、5、6,简单讲解下(以论文中符号为准)
    float dx1 = intensities[0] - intensities[1];
    float dx2 = intensities[2] - intensities[3];

    float dy1 = intensities[0] - intensities[2];
    float dy2 = intensities[1] - intensities[3];

      //得到双线性插值的因子,注意x0+1=x1,y0+1=y1,则
      //     | x-x0 |   | 1-x+x0 |   | x1-x |
      // 1 - |      | = |        | = |      |
      //     | y-y0 |   | 1-y+y0 |   | y1-y |
    float xFacInv = (1.0f - factors[0]);
    float yFacInv = (1.0f - factors[1]);


      //         y-y0 | x-x0            x1-x        |    y1-y | x-x0            x1-x        |
      //M(Pm) = ------|------ M(P11) + ------ M(P01)| + ------|------ M(P10) + ------ M(P00)|
      //         y1-y0| x1-x0           x1-x0       |    y1-y0| x1-x0           x1-x0       |
      //注意:此处y1-y0=x1-x0=1,那么对应函数返回值,可以写成
      //M(Pm) = (M(P00) * (x1-x) + M(P10) * (x-x0)) * (y1-y) + (M(P01) * (x1-x) + M(P11) * (x-x0)) * (y-y0)

      // d(M(Pm))     y-y0 |                |    y1-y |                |
      //---------- = ------| M(P11) - M(P01)| + ------| M(P10) - M(P00)|
      //    dx        y1-y0|                |    y1-y0|                |
      //同理,化简可得 d(M(Pm))/dx = -((M(P00) - M(P10)) * (y1-y) + (M(P01) - M(P11)) * (y-y0))
      //同样地,也有   d(M(Pm))/dy = -((M(P00) - M(P01)) * (x1-x) + (M(P10) - M(P11)) * (x-x0))
    return Eigen::Vector3f(
      ((intensities[0] * xFacInv + intensities[1] * factors[0]) * (yFacInv)) +
      ((intensities[2] * xFacInv + intensities[3] * factors[0]) * (factors[1])),
      -((dx1 * xFacInv) + (dx2 * factors[0])),
      -((dy1 * yFacInv) + (dy2 * factors[1]))
    );
  }

理解部分直接看注释哈!

参考链接

代码部分的参考链接:
源码参考1
源码参考2

线性插值部分的参考链接:
插值系列的12345
知乎
其他

好啦,这次的分享就先到这里啦!

  • 8
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值