欢迎光临散文网 会员登陆 & 注册

总结!一文详解lio-livox中的特征提取

2023-09-12 11:09 作者:3D视觉工坊  | 我要投稿

lio-livox是livox官方在2021年开源的一款Lidar+IMU算法,可以用于livox mid360, horizon, HAP等不同型号的激光雷达运行LIO算法。然而,笔者并没有在网上找到详细的原理说明文档,因此尝试通过代码分析算法原理。本文首先介绍特征提取的部分,由于能力有限,难免解读出现错误,请读者即使批评指正。

作者:小L | 来源:3DCV

在公众号「3DCV」后台,回复「原论文」即可获取论文pdf和代码。

添加微信:dddvisiona,备注:SLAM,拉你入群。文末附行业细分群。

开源代码:https://github.com/livox-SDK/LIO-Livox

1. 程序整体运行

首先放上正常运行时的rqt_graph,可以看出,lio-livox主要有两个节点:ScanRegistrationPoseEstimation。显然,第一个时特征提取部分,后面的是位姿轨迹部分。但有趣的是,前者发布了完整点云livox_full_cloud、不那么sharp和flat的点云、以及非特征点,但并没有被后面节点接收。所以说,这个livox_full_cloud 就包含了特征点的信息。

2. 参数配置

ScanRegistration.cpp中的main函数中,主要载入了相关的配置文件,和收发数据的节点。因此,首先了解下配置文件中有哪些基本配置。

首先在运行代码时,会运行:

roslaunch lio-livox mid.launch

所以查看这个launch文件,文件截图如下:

首先载入了mid360的配置文件,"mid360_config.yaml",然后设置了msg_type为0,和其他参数。msg_type为激光雷达发出的数据格式,有livox自定义数据类型格式CustomMsg和ros标准的PointCloud2格式,因此首先需要确定激光雷达端配置的是哪种格式。可以从激光雷达的ros驱动中看到驱动端有三种格式可以选,驱动端的数字2对应lio-livox中的1,驱动端的0对应lio-livox的格式0。

接着在"mid360_config.yaml"文件中定义了一些配置,如下:

其中,

  • lidar_type表示采用horizon还是mid360激光雷达。查了一下horizon的雷达扫描,虽然是固态的激光雷达,但是扫描是水平的,因此我推测应该和mid360这种水平扫描的数据在特征提取这些方法上是类似的,因此这一个配置文件可以同时支持horizon和mid360两种型号。

  • Used_Line为采用的线束,mid360数据格式具有line字段,可以输出以下这个值是0~3,因此推测mid360的线束就是4,因此这里采用4条线进行处理。

  • Feature_Mode的参数值可选0或1,但代码中并没有用到这个变量,我推测可能是之前或之后版本想实现基于特征的配准或直接不用特征的配准吧。

  • Use_Seg是是否启用动态物体分割,如果启用,则特征提取前先经过动态物体滤波。

再往后的一些参数从名字可以看出含义,不多做解释。

if (Lidar_Type == 0)  {    customCloud = nodeHandler.subscribe<livox_ros_driver::CustomMsg>("/livox/lidar", 100, &lidarCallBackHorizon);  }  else if (Lidar_Type == 1)  {    customCloud = nodeHandler.subscribe<livox_ros_driver::CustomMsg>("/livox/lidar", 100, &lidarCallBackHAP);  }  else if(Lidar_Type==2){      if (msg_type==0)          customCloud = nodeHandler.subscribe<livox_ros_driver::CustomMsg>("/livox/lidar", 100, &lidarCallBackHorizon);      else if(msg_type==1)          pc2Cloud=nodeHandler.subscribe<sensor_msgs::PointCloud2>("/livox/lidar", 100, &lidarCallBackPc2);  }

在接下来的main函数中,可以看出,如果选用的是mid360,采用的默认livox的数据格式,则进入的是Horizon回调函数进行数据处理lidarCallBackHorizon

3. 特征提取

下面正式进入特征提取。不展开介绍动态物体分割部分的代码,直接看特征提取。可以看到,在Horizon回调中,最核心的函数就是FeatureExtract。特征提取后,得到带有特征标记的完整点云,和角点、平面点点云(但这两个没有用到)。

回调函数为lidarCallBackHorizon,如果不需要滤除动态物体,则调用FeatureExtract,否则调用FeatureExtract_with_segment

void lidarCallBackHorizon(const livox_ros_driver::CustomMsgConstPtr &msg) {  sensor_msgs::PointCloud2 msg2;  if(Use_seg){    lidarFeatureExtractor->FeatureExtract_with_segment(msg, laserCloud, laserConerCloud, laserSurfCloud, laserNonFeatureCloud, msg2,N_SCANS);  }  else{    lidarFeatureExtractor->FeatureExtract(msg, laserCloud, laserConerCloud, laserSurfCloud,N_SCANS,Lidar_Type);  }   // 省略后续格式转化}

FeatureExtract函数在LidarFeatureExtractor.cpp中,代码核心部分如下:

void LidarFeatureExtractor::FeatureExtract(){  // 省略一些预处理 ...  // 多线程特征提取  std::thread threads[N_SCANS];  for(int i=0; i<N_SCANS; ++i){    threads[i] = std::thread(&LidarFeatureExtractor::detectFeaturePoint, this, std::ref(vlines[i]), std::ref(vcorner[i]), std::ref(vsurf[i]));  }  for(int i=0; i<N_SCANS; ++i){    // 线程合并    threads[i].join();  }  for(int i=0; i<N_SCANS; ++i){    for(int j=0; j<vcorner[i].size(); ++j){      laserCloud->points[_float_as_int(vlines[i]->points[vcorner[i][j]].normal_z)].normal_z = 1.0;    }    for(int j=0; j<vsurf[i].size(); ++j){      laserCloud->points[_float_as_int(vlines[i]->points[vsurf[i][j]].normal_z)].normal_z = 2.0;    }  }  //~ 省略后续处理}

可以看出,对于每条扫描的线束,开了N_SCANS个线程(这里是4)分别提取每线束的特征,然后再做合并。每条线束的特征提取函数为detectFeaturePoint,输入为这条线束的点,输出为corner点和surf点。比较有趣的是后面几行,lio-livox代码中点采用PointXYZINormal格式,法向量normal的z值为特征的类型:1为角点特征,2为平面特征,3为非特征点(mid360的代码运行时没有用到非特征点)。

具体地,下面这一行的含义为:对于第i条线束,将被判定为corner点的所有的点的法向量的z值设定为1。其中,vlines[i]的每个点的normal_z为这个点再laserCloud->points中的索引值,vcorner[i][j]为第i线束的第j个角点的索引。不得不说一下这一行代码为了省变量,信息量是真的大。

laserCloud->points[_float_as_int(vlines[i]->points[vcorner[i][j]].normal_z)].normal_z = 1.0;

接下来进入detectFeaturePoint函数看具体如何寻找的corner和surf两种点。在这个函数中,首先提取了三类点:平面点(surf/flat),两平面相交的线(line feature),和break point。

一般我们比较熟悉平面点和角点,对这个break point不太了解。我个人认为,代码对角点中进一步细分为了line feature和break point,前者的基本判定是左右的一些点构成有一定夹角的两个平面,后者判定为左右的点是"断开的",物理空间上没有连接,所以需要引入一些距离的判定。

每种点提取的时候,先初步提取,再做筛选。通过CloudFeatureFlag这个变量的数值表示这些点分别是什么类型。

展开介绍CloudFeatureFlag的一些值:

  • 0,这个点暂不是任何特征类型,需要执行相应计算与判断。

  • 1,平面点附近的点被标记为1,不作为任何特征,只是在判断点是否为0时用于跳过这个点,加快计算速度

  • 2,对初次标记为平面点的再一次筛选,满足一定准则的设定为2

  • 3,根据平面曲率初次提取的平面点,严格符合条件的会被标记为3

  • 100:初次提取的break point

  • 101:对标记为100的点做筛选,不符合严格的break point的设定为101

  • 150:两个平面相交线的点

  • 300:基于反射率和距离等判定得到的一些点,后续并没有用到

具体一些,在平面点提取阶段,通过每个点和前后点的夹角等,CloudFeatureFlag会赋值1,2,3,300;在相交线特征点提取过程中,会根据左右的点是否能构成平面,给CloudFeatureFlag赋值150;在break point提取过程中,CloudFeatureFlag会出现100和101。由于这部分代码较为冗杂,不展开介绍。理解了这些数字的基本含义可以去探究具体的判定准则是什么。

在得到这些用数值表示的特征后,将标记为100和150的点作为角点,将标记为2的点标记为平面点。代码如下:

if(CloudFeatureFlag[i] == 2){    pointsLessFlat.push_back(i);    num_surf++;    continue;  }if(CloudFeatureFlag[i] == 100 || CloudFeatureFlag[i] == 150){ //  pointsLessSharp_ori.push_back(i);  laserCloudCorner->push_back(_laserCloud->points[i]);}

至此,完成了特征提取的所有部分的代码解释。

提取完角点/平面特征后,要干什么用呢?这就是后续的配准和优化部分。在Estimator.cpp中,可以看到,对于角点执行的是点-线icp,平面点执行的是点-面icp:

threads[0] = std::thread(&Estimator::processPointToLine, this,                         std::ref(edgesLine[f]),                         std::ref(vLineFeatures[f]),                         std::ref(laserCloudCornerStack[f]),                         std::ref(laserCloudCornerFromLocal),                         std::ref(kdtreeCornerFromLocal),                         std::ref(exTlb),                         std::ref(transformTobeMapped));threads[1] = std::thread(&Estimator::processPointToPlanVec, this,                         std::ref(edgesPlan[f]),                         std::ref(vPlanFeatures[f]),                         std::ref(laserCloudSurfStack[f]),                         std::ref(laserCloudSurfFromLocal),                         std::ref(kdtreeSurfFromLocal),                         std::ref(exTlb),                         std::ref(transformTobeMapped));

后续内容本文暂不做介绍。

总结!一文详解lio-livox中的特征提取的评论 (共 条)

分享到微博请遵守国家法律