锐单电子商城 , 一站式电子元器件采购平台!
  • 电话:400-990-0325

FAST-LIO2代码解析(一)

时间:2022-09-25 14:00:00 ss系列传感器

0. 简介

如今,越来越多的激光雷达方法如雨后春笋般涌现。FAST-LIO代表性系列逐渐被公众接受。而FAST-LIO工作越来越为公众所熟知和研究。作者最近也在研究和学习FAST-LIO这里将以长文的形式介绍2的相关知识FAST-LIO2.0学习经验。本文将通过代码的形式与论文结合,直观的来分析论文的核心算法部分。也作为自己的学习笔记留给后来人学习。

1. 激光雷达

对于FAST-LIO对于2代码,主要数据是通过preprocess与IMU_Processing获取相应的传感器数据,并在主程序中调用ESF与IKD-Tree完成优化。先来看看激光雷达部分。

1.1 preprocess.h

首先,在上面定义了一系列enum信息,通过enum通过选择不同类型的激光雷达、特征特征等orgtype存储一些激光雷达点的其他属性。

// 枚举类型:表示支持的雷达类型 enum LID_TYPE { 
           AVIA = 1,   VELO16,   OUST64 }; //{1, 2, 3}  // 枚举类型:表示特征点的类型 enum Feature { 
           Nor,        // 正常点   Poss_Plane, // 可能的平面点   Real_Plane, // 确定的平面点   Edge_Jump,  // 有跨越的边   Edge_Plane, // 边缘平面点   Wire,       // 线段 这也许是无效点?也就是空间中的小线段?   ZeroPoint   // 无效点 未在程序中使用 }; // 枚举类型:位置标识 enum Surround { 
           Prev, // 前一个   Next  // 后一个 };  // 枚举类型:表示有跨越边的类型 enum E_jump { 
           Nr_nor,  // 正常   Nr_zero, // 0   Nr_180,  // 180   Nr_inf,  // 无穷大 跳变较远?   Nr_blind // 在盲区? }; // orgtype类别:用于存储激光雷达点的其他属性 struct orgtype { 
           double range; // 点云在xy平面与雷达中心的距离   double dista; // 当前点与后一点之间的距离   假设雷达的原点是O 前一个点为M 当前点为A 后一个点为N   double angle[2];  // 这个是角OAM和角OAN的cos值   double intersect; // 这个是角MAN的cos值   E_jump edj[2];    // 前后两点的类型   Feature ftype;    // 点类型    // 构造函数   orgtype()   { 
             range = 0;
    edj[Prev] = Nr_nor;
    edj[Next] = Nr_nor;
    ftype = Nor; //默认为正常点
    intersect = 2;
  }
};

其中LID_TYPE这些与下面的数据结构有关系。通过enum选择不同的激光雷达数据,来设置不同的数据结构。

// velodyne数据结构
namespace velodyne_ros
{ 
        
  struct EIGEN_ALIGN16 Point
  { 
        
    PCL_ADD_POINT4D;                // 4D点坐标类型
    float intensity;                // 强度
    float time;                     // 时间
    uint16_t ring;                  // 点所属的圈数
    EIGEN_MAKE_ALIGNED_OPERATOR_NEW // 进行内存对齐
  };
} // namespace velodyne_ros

// 注册velodyne_ros的Point类型
POINT_CLOUD_REGISTER_POINT_STRUCT(velodyne_ros::Point,
                                  (float, x, x)(float, y, y)(float, z, z)(float, intensity, intensity)(float, time, time)(uint16_t, ring, ring))

// ouster数据结构
namespace ouster_ros
{ 
        
  struct EIGEN_ALIGN16 Point
  { 
        
    PCL_ADD_POINT4D;                // 4D点坐标类型
    float intensity;                // 强度
    uint32_t t;                     // 时间
    uint16_t reflectivity;          // 反射率
    uint8_t ring;                   // 点所属的圈数
    uint16_t ambient;               // 没用到
    uint32_t range;                 // 距离
    EIGEN_MAKE_ALIGNED_OPERATOR_NEW // 进行内存对齐
  };
} // namespace ouster_ros

// clang-format off
// 注册ouster的Point类型
POINT_CLOUD_REGISTER_POINT_STRUCT(ouster_ros::Point,
    (float, x, x)
    (float, y, y)
    (float, z, z)
    (float, intensity, intensity)
    // use std::uint32_t to avoid conflicting with pcl::uint32_t
    (std::uint32_t, t, t)
    (std::uint16_t, reflectivity, reflectivity)
    (std::uint8_t, ring, ring)
    (std::uint16_t, ambient, ambient)
    (std::uint32_t, range, range)
)

然后下面的这些就是Preprocess的主体函数了,主要作用是用于对激光雷达点云数据进行预处理。

// Preproscess类:用于对激光雷达点云数据进行预处理
class Preprocess
{ 
        
  public:
// EIGEN_MAKE_ALIGNED_OPERATOR_NEW

  Preprocess();  // 构造函数
  ~Preprocess(); // 析构函数
  
  void process(const livox_ros_driver::CustomMsg::ConstPtr &msg, PointCloudXYZI::Ptr &pcl_out);  // 对Livox自定义Msg格式的激光雷达数据进行处理
  void process(const sensor_msgs::PointCloud2::ConstPtr &msg, PointCloudXYZI::Ptr &pcl_out);     // 对ros的Msg格式的激光雷达数据进行处理
  void set(bool feat_en, int lid_type, double bld, int pfilt_num);

  // sensor_msgs::PointCloud2::ConstPtr pointcloud;
  PointCloudXYZI pl_full, pl_corn, pl_surf;  // 全部点、边缘点、平面点
  PointCloudXYZI pl_buff[128]; //maximum 128 line lidar
  vector<orgtype> typess[128]; //maximum 128 line lidar
  int lidar_type, point_filter_num, N_SCANS, SCAN_RATE;  // 雷达类型、采样间隔、扫描线数、扫描频率
  double blind;  // 最小距离阈值(盲区)
  bool feature_enabled, given_offset_time;  // 是否提取特征、是否进行时间偏移
  ros::Publisher pub_full, pub_surf, pub_corn;  // 发布全部点、发布平面点、发布边缘点
    

  private:
  void avia_handler(const livox_ros_driver::CustomMsg::ConstPtr &msg);  // 用于对Livox激光雷达数据进行处理
  void oust64_handler(const sensor_msgs::PointCloud2::ConstPtr &msg);   // 用于对ouster激光雷达数据进行处理
  void velodyne_handler(const sensor_msgs::PointCloud2::ConstPtr &msg); // 用于对velodyne激光雷达数据进行处理
  void give_feature(PointCloudXYZI &pl, vector<orgtype> &types);
  void pub_func(PointCloudXYZI &pl, const ros::Time &ct);
  int  plane_judge(const PointCloudXYZI &pl, vector<orgtype> &types, uint i, uint &i_nex, Eigen::Vector3d &curr_direct);
  bool small_plane(const PointCloudXYZI &pl, vector<orgtype> &types, uint i_cur, uint &i_nex, Eigen::Vector3d &curr_direct);  // 没有用到
  bool edge_jump_judge(const PointCloudXYZI &pl, vector<orgtype> &types, uint i, Surround nor_dir);
  
  int group_size;
  double disA, disB, inf_bound;
  double limit_maxmid, limit_midmin, limit_maxmin;
  double p2l_ratio;
  double jump_up_limit, jump_down_limit;
  double cos160;
  double edgea, edgeb;
  double smallp_intersect, smallp_ratio;
  double vx, vy, vz;
};

1.2 preprocess.cpp

上面是头文件,详细的代码在preprocess.cpp中,这里我们来详细介绍每一个函数,首先是构造函数和析构函数,里面主要存放一些雷达的参数

Preprocess::Preprocess()
    : feature_enabled(0), lidar_type(AVIA), blind(0.01), point_filter_num(1)
{ 
        
  inf_bound = 10;      // 有效点集合,大于10m则是盲区
  N_SCANS = 6;         //多线激光雷达的线数
  group_size = 8;      // 8个点为一组
  disA = 0.01;         // 点集合的距离阈值,判断是否为平面
  disB = 0.1;          // 点集合的距离阈值,判断是否为平面
  p2l_ratio = 225;     // 点到线的距离阈值,需要大于这个值才能判断组成面
  limit_maxmid = 6.25; // 中点到左侧的距离变化率范围
  limit_midmin = 6.25; // 中点到右侧的距离变化率范围
  limit_maxmin = 3.24; // 左侧到右侧的距离变化率范围
  jump_up_limit = 170.0;
  jump_down_limit = 8.0;
  cos160 = 160.0;
  edgea = 2;   //点与点距离超过两倍则认为遮挡
  edgeb = 0.1; //点与点距离超过0.1m则认为遮挡
  smallp_intersect = 172.5;
  smallp_ratio = 1.2;        //三个点如果角度大于172.5度,且比例小于1.2倍,则认为是平面
  given_offset_time = false; //是否提供时间偏移

  jump_up_limit = cos(jump_up_limit / 180 * M_PI);       //角度大于170度的点跳过,认为在
  jump_down_limit = cos(jump_down_limit / 180 * M_PI);   //角度小于8度的点跳过
  cos160 = cos(cos160 / 180 * M_PI);                     //夹角限制
  smallp_intersect = cos(smallp_intersect / 180 * M_PI); //三个点如果角度大于172.5度,且比例小于1.2倍,则认为是平面
}

下面一个函数是set函数,这里可以在laserMapping.cpp被使用,可是该函数并未被使用。

void Preprocess::set(bool feat_en, int lid_type, double bld, int pfilt_num)
{ 
        
  feature_enabled = feat_en;    //是否提取特征点
  lidar_type = lid_type;        //雷达的种类
  blind = bld;                  //最小距离阈值,即过滤掉0~blind范围内的点云
  point_filter_num = pfilt_num; //采样间隔,即每隔point_filter_num个点取1个点
}

下面的Preprocess预处理函数主要包含了不同激光雷达的处理,这里在laserMapping.cpp被调用,从而拿到处理后的点云,并初步完成了筛选。下面两个大同小异就不仔细说了

/** * @brief Livox激光雷达点云预处理函数 * * @param msg livox激光雷达点云数据,格式为livox_ros_driver::CustomMsg * @param pcl_out 输出处理后的点云数据,格式为pcl::PointCloud */
void Preprocess::process(const livox_ros_driver::CustomMsg::ConstPtr &msg, PointCloudXYZI::Ptr &pcl_out)
{ 
        
  avia_handler(msg);
  *pcl_out = pl_surf;
}

void Preprocess::process(const sensor_msgs::PointCloud2::ConstPtr &msg, PointCloudXYZI::Ptr &pcl_out)
{ 
        
  switch (lidar_type)
  { 
        
  case OUST64:
    oust64_handler(msg);
    break;

  case VELO16:
    velodyne_handler(msg);
    break;

  default:
    printf("Error LiDAR Type");
    break;
  }
  *pcl_out = pl_surf;
}

下面就是handle函数来预处理发出的点云数据。这里我们展示了Livox激光雷达点云数据进行预处理。这里的操作是拿到livox原始数据,然后通过线束和反射率重新拼接了

void Preprocess::avia_handler(const livox_ros_driver::CustomMsg::ConstPtr &msg)
{ 
        
  // 清除之前的点云缓存
  pl_surf.clear();             // 清除之前的平面点云缓存
  pl_corn.clear();             // 清除之前的角点云缓存
  pl_full.clear();             // 清除之前的全点云缓存
  double t1 = omp_get_wtime(); // 后面没用到
  int plsize = msg->point_num; // 一帧中的点云总个数
  // cout<<"plsie: "<

  pl_corn.reserve(plsize); // 分配空间
  pl_surf.reserve(plsize); // 分配空间
  pl_full.resize(plsize);  // 分配空间

  for (int i = 0; i < N_SCANS; i++)
  { 
        
    pl_buff[i].clear();
    pl_buff[i].reserve(plsize); // 每一个scan保存的点云数量
  }
  uint valid_num = 0; // 有效的点云数

  // 特征提取(FastLIO2默认不进行特征提取)
  if (feature_enabled)
  { 
        
    // 分别对每个点云进行处理
    for (uint i = 1; i < plsize; i++)
    { 
        
      // 只取线数在0~N_SCANS内并且回波次序为0或者1的点云
      if ((msg->points[i].line < N_SCANS) && ((msg->points[i].tag & 0x30) == 0x10 || (msg->points[i].tag & 0x30) == 0x00))
      { 
        
        pl_full[i].x = msg->points[i].x;                                    // 点云x轴坐标
        pl_full[i].y = msg->points[i].y;                                    // 点云y轴坐标
        pl_full[i].z = msg->points[i].z;                                    // 点云z轴坐标
        pl_full[i].intensity = msg->points[i].reflectivity;                 // 点云强度
        pl_full[i].curvature = msg->points[i].offset_time / float(1000000); // 使用曲率作为每个激光点的时间

        bool is_new = false;
        // 只有当当前点和上一点的间距足够大(>1e-7),才将当前点认为是有用的点,分别加入到对应line的pl_buff队列中
        if ((abs(pl_full[i].x - pl_full[i - 1].x) > 1e-7) || (abs(pl_full[i].y - pl_full[i - 1].y) > 1e-7) || (abs(pl_full[i].z - pl_full[i - 1].z) > 1e-7))
        { 
        
          pl_buff[msg->points[i].line].push_back(pl_full[i]); // 将当前点加入到对应line的pl_buff队列中
        }
      }
    }
    static int count = 0;
    static double time = 0.0;
    count++;
    double t0 = omp_get_wtime();
    // 对每个line中的激光雷达分别进行处理
    for (int j = 0; j < N_SCANS; j++)
    { 
        
      // 如果该line中的点云过小,则继续处理下一条line
      if (pl_buff[j].size() <= 5)
        continue;
      pcl::PointCloud<PointType> &pl = pl_buff[j];
      plsize = pl.size();
      vector<orgtype> &types = typess[j];
      types.clear();
      types.resize(plsize);
      plsize--;
      for (uint i = 0; i < plsize; i++)
      { 
        
        types[i].range = pl[i].x * pl[i].x + pl[i].y * pl[i].y; // 计算每个点到机器人本身的距离
        vx = pl[i].x - pl[i + 1].x;
        vy = pl[i].y - pl[i + 1].y;
        vz = pl[i].z - pl[i + 1].z;
        types[i].dista = vx * vx + vy * vy + vz * vz; // 计算两个间隔点的距离
      }
      //因为i最后一个点没有i+1了所以就单独求了一个range,没有distance
      types[plsize].range = pl[plsize].x * pl[plsize].x + pl[plsize].y * pl[plsize].y;
      give_feature(pl, types); //给特征
      // pl_surf += pl;
    }
    time += omp_get_wtime() - t0;
    printf("Feature extraction time: %lf \n", time / count);
  }
  else
  { 
        
    // 分别对每个点云进行处理
    for (uint i = 1; i < plsize; i++)
    { 
        
      // 只取线数在0~N_SCANS内并且回波次序为0或者1的点云
      if ((msg->points[i].line < N_SCANS) && ((msg->points[i].tag & 0x30) == 0x10 || (msg->points[i].tag & 0x30) == 0x00))
      { 
        
        valid_num++; // 有效的点云数

        // 等间隔降采样
        if (valid_num % point_filter_num == 0)
        { 
        
          pl_full[i].x = msg->points[i].x;                                    // 点云x轴坐标
          pl_full[i].y = msg->points[i].y;                                    // 点云y轴坐标
          pl_full[i].z = msg->points[i].z;                                    // 点云z轴坐标
          pl_full[i].intensity = msg->points[i].reflectivity;                 // 点云强度
          pl_full[i].curvature = msg->points[i].offset_time / float(1000000); // use curvature as time of each laser points

          // 只有当当前点和上一点的间距足够大(>1e-7),并且在最小距离阈值之外,才将当前点认为是有用的点,加入到pl_surf队列中
          if ((abs(pl_full[i].x - pl_full[i - 1].x) > 1e-7) || (abs(pl_full 

相关文章