LeGO-LOAM 源码阅读笔记(imageProjecion.cpp)

2023-10-27

LeGO-LOAM是一种在LOAM之上进行改进的激光雷达建图方法,建图效果比LOAM要好,但是建图较为稀疏,计算量也更小了。

本文原地址:wykxwyc的博客

github注释后LeGO-LOAM源码:LeGO-LOAM_NOTED
关于代码的详细理解,建议阅读:

1.地图优化代码理解

2.图像重投影代码理解

3.特征关联代码理解

4.LeGO-LOAM中的数学公式推导

以上博客会随时更新,如果对你有帮助,请点击注释代码的github仓库右上角star按钮,你的鼓励将给我更多动力。

imageProjecion.cpp概述

imageProjecion.cpp进行的数据处理是图像映射,将得到的激光数据分割,并在得到的激光数据上进行坐标变换。

imageProjecion

imageProjecion()构造函数的内容如下:

  1. 订阅话题:订阅来自velodyne雷达驱动的topic
    • "/velodyne_points"(sensor_msgs::PointCloud2),订阅的subscriber是subLaserCloud
  2. 发布话题,这些topic有:
    • "/full_cloud_projected"(sensor_msgs::PointCloud2)
    • "/full_cloud_info"(sensor_msgs::PointCloud2)
    • "/ground_cloud"(sensor_msgs::PointCloud2)
    • "/segmented_cloud"(sensor_msgs::PointCloud2)
    • "/segmented_cloud_pure"(sensor_msgs::PointCloud2)
    • "/segmented_cloud_info"(cloud_msgs::cloud_info)
    • "/outlier_cloud"(sensor_msgs::PointCloud2)

然后分配内存(对智能指针初始化),初始化各类参数。

上述的cloud_msgs::cloud_info是自定义的消息类型,其具体定义如下:

Header header 

int32[] startRingIndex  // 长度:N_SCAN
int32[] endRingIndex    // 长度:N_SCAN

float32 startOrientation
float32 endOrientation
float32 orientationDiff

// 以下长度都是 N_SCAN*Horizon_SCAN
bool[]    segmentedCloudGroundFlag 
uint32[]  segmentedCloudColInd 
float32[] segmentedCloudRange

关于上面的自定义消息,另外还需要说明的是,segMsg.startRingIndex[i] = sizeOfSegCloud-1 + 5;或者segMsg.endRingIndex[i] = sizeOfSegCloud-1 - 5;表示的是将第0线和第16线点云横着排列后。每一线点云有一个startRingIndexendRingIndex,表示这一线点云中的一部分,如下图绿色部分。
黑色部分是整体这一线点云中筛选出来满足条件的。
在这里插入图片描述

cloudHandler

void cloudHandler(const sensor_msgs::PointCloud2ConstPtr& laserCloudMsg)是这个文件中最主要的一个函数。由它调用其他的函数:

void cloudHandler(const sensor_msgs::PointCloud2ConstPtr& laserCloudMsg){
    copyPointCloud(laserCloudMsg);
    findStartEndAngle();
    projectPointCloud();
    groundRemoval();
    cloudSegmentation();
    publishCloud();
    resetParameters();
}
  • 整体过程如下:
    1.复制点云数据
    2.找到开始和结束的角度
    3.移除地面点
    4.点云分块
    5.发布处理后的点云数据
    6.重置参数

上面第一部分复制点云数据函数copyPointCloud(laserCloudMsg) 是将ROS中的sensor_msgs::PointCloud2ConstPtr类型转换到pcl点云库指针。

velodyne 雷达数据

首先从VLP给的雷达数据手册上(63-9243 Rev B User Manual and Programming Guide,VLP-16.pdf)查找一下它的坐标系定义:
VLP雷达坐标系
velodyne雷达在上面的坐标系下输出"/velodyne_points"(sensor_msgs::PointCloud2) 的点云数据。其数据格式可以理解为x,y,z,intensity 这4个量(它的定义比sensor_msgs::PointCloud要复杂一点,但本质还是这几个量)。

findStartEndAngle

void findStartEndAngle()进行关于segMsg(cloud_msgs::cloud_info segMsg)的三个内容的计算:
1)计算开始角度(segMsg.startOrientation);
2)计算结束角度(segMsg.endOrientation);
3)计算雷达转过的角度(开始和结束的角度差,segMsg.orientationDiff)。

关于具体计算,需要清楚整个雷达的坐标系定义,参考上面雷达坐标系的那张图。

另外在计算segMsg.startOrientationsegMsg.endOrientation时,atan2(..)函数取了负数的原因是:雷达旋转方向和坐标系定义下的右手定则正方向不一致。参考下图:
在这里插入图片描述

projectPointCloud

void projectPointCloud()将激光雷达得到的数据看成一个16x1800的点云阵列。然后根据每个点云返回的XYZ数据将他们对应到这个阵列里去。

  1. 计算竖直角度,用atan2函数进行计算。
  2. 通过计算的竖直角度得到对应的行的序号rowIdnrowIdn计算出该点激光雷达是水平方向上第几线的。从下往上计数,-15度记为初始线,第0线,一共16线(N_SCAN=16)。
  3. 求水平方向上的角度horizonAngle = atan2(thisPoint.x, thisPoint.y) * 180 / M_PI;
  4. 根据水平方向上的角度计算列向量columnIdn
    这里对数据的处理比较巧妙,一开始觉得很奇怪,但后来发现这样做其实让数据更不容易失真。
    计算columnIdn主要是下面这三个语句:
columnIdn = -round((horizonAngle-90.0)/ang_res_x) + Horizon_SCAN/2;
if (columnIdn >= Horizon_SCAN)
		columnIdn -= Horizon_SCAN;
if (columnIdn < 0 || columnIdn >= Horizon_SCAN)
		continue;

先把columnIdnhorizonAngle:(-PI,PI]转换到columnIdn:[H/4,5H/4],然后判断columnIdn大小,再讲它的范围转换到了[0,H] (H:Horizon_SCAN)
这样就把扫描开始的地方角度为0与角度为360的连在了一起,非常巧妙。
5. 接着在thisPoint.intensity中保存一个点的位置信息rowIdn+columnIdn / 10000.0fullInfoCloud的点保存点的距离信息;

具体的转换过程可以看下面这个两张图:
在这里插入图片描述

各种标记的含义

  • groundMat:
    1) groundMat.at<int8_t>(i,j) = 0,初始值;
    2) groundMat.at<int8_t>(i,j) = 1,有效的地面点;
    3) groundMat.at<int8_t>(i,j) = -1,无效地面点;

  • rangeMat
    1) rangeMat.at(i,j) = FLT_MAX,浮点数的最大值,初始化信息;
    2) rangeMat.at(rowIdn, columnIdn) = range,保存图像深度;

  • labelMat
    1) labelMat.at(i,j) = 0,初始值;
    2) labelMat.at(i,j) = -1,无效点;
    3)labelMat.at(thisIndX, thisIndY) = labelCount,平面点;
    4)labelMat.at(allPushedIndX[i], allPushedIndY[i]) = 999999,需要舍弃的点,数量不到30。

groundRemoval

void groundRemoval()由三个部分的程序组成。

  1. 由上下两线之间点的XYZ位置得到两线之间的俯仰角,如果俯仰角在10度以内,则判定(i,j)和(i,j+1)为地面点,groundMat[i][j]=1,否则,则不是地面点,进行后续操作;
  2. 找到所有点中的地面点,并将他们的labelMat标记为-1,rangeMat[i][j]==FLT_MAX 判定为无效的另一个条件。
  3. 如果有节点订阅groundCloud,那么就需要把地面点发布出来。具体实现过程:把点放到groundCloud队列中去。这样就把地面点和非地面点标记并且区分开来了。

cloudSegmentation

void cloudSegmentation()进行的是点云分割的操作,将不同类型的点云放到不同的点云块中去,例如outlierCloudsegmentedCloudPure等。具体步骤:

  1. 首先判断点云标签,这个点云没有进行过分类(在原先的处理中没有被分到地面点中去或没有分到平面中),则通过labelComponents(i, j);对点云进行分类。进行分类的过程在labelComponents中进行介绍。
  2. 分类完成后,找到可用的特征点或者地面点(不选择labelMat[i][j]=0的点),按照它的标签值进行判断,将部分界外点放进outlierCloud中。continue继续处理下一个点。
  3. 然后将大部分地面点去掉,省下的那些点进行信息的拷贝与保存操作。
  4. 最后如果有节点订阅SegmentedCloudPure,那么把点云数据保存到segmentedCloudPure中去。

labelComponents

void labelComponents(int row, int col)对点云进行标记。通过标准的BFS算法对点进行标记:以(row,col)为中心向外面扩散,判断(row,col)是否属于平面中一点。

  • BFS过程:
    1.用queueIndXqueueIndY保存进行分割的点云行列值,用queueStartInd作为索引。
    2.求这个点的4个邻接点,求其中离原点距离的最大值d1最小值d2。根据下面这部分代码来评价这两点之间是否具有平面特征。注意因为两个点上下或者水平对应的分辨率不一样,所以alpha是用来选择分辨率的。
// alpha代表角度分辨率,
// Y方向上角度分辨率是segmentAlphaY(rad)
if ((*iter).first == 0)
		alpha = segmentAlphaX;
else
		alpha = segmentAlphaY;

// 通过下面的公式计算这两点之间是否有平面特征
// atan2(y,x)的值越大,d1,d2之间的差距越小,越平坦
angle = atan2(d2*sin(alpha), (d1 -d2*cos(alpha)));
  • 在这之后通过判断角度是否大于60度来决定是否要将这个点加入保存的队列。加入的话则假设这个点是个平面点。
  • 然后进行聚类,聚类的规则是:
    1.如果聚类超过30个点,直接标记为一个可用聚类,labelCount需要递增;
    2.如果聚类点数小于30大于等于5,统计竖直方向上的聚类点数
    3.竖直方向上超过3个也将它标记为有效聚类
    4.标记为999999的是需要舍弃的聚类的点,因为他们的数量小于30个

publishCloud

void publishCloud()发布各类点云数据。

// 发布各类点云内容
void publishCloud(){
	// 发布cloud_msgs::cloud_info消息
    segMsg.header = cloudHeader;
    pubSegmentedCloudInfo.publish(segMsg);

    sensor_msgs::PointCloud2 laserCloudTemp;

	// pubOutlierCloud发布界外点云
    pcl::toROSMsg(*outlierCloud, laserCloudTemp);
    laserCloudTemp.header.stamp = cloudHeader.stamp;
    laserCloudTemp.header.frame_id = "base_link";
    pubOutlierCloud.publish(laserCloudTemp);

	// pubSegmentedCloud发布分块点云
    pcl::toROSMsg(*segmentedCloud, laserCloudTemp);
    laserCloudTemp.header.stamp = cloudHeader.stamp;
    laserCloudTemp.header.frame_id = "base_link";
    pubSegmentedCloud.publish(laserCloudTemp);

    if (pubFullCloud.getNumSubscribers() != 0){
        pcl::toROSMsg(*fullCloud, laserCloudTemp);
        laserCloudTemp.header.stamp = cloudHeader.stamp;
        laserCloudTemp.header.frame_id = "base_link";
        pubFullCloud.publish(laserCloudTemp);
    }

    if (pubGroundCloud.getNumSubscribers() != 0){
        pcl::toROSMsg(*groundCloud, laserCloudTemp);
        laserCloudTemp.header.stamp = cloudHeader.stamp;
        laserCloudTemp.header.frame_id = "base_link";
        pubGroundCloud.publish(laserCloudTemp);
    }

    if (pubSegmentedCloudPure.getNumSubscribers() != 0){
        pcl::toROSMsg(*segmentedCloudPure, laserCloudTemp);
        laserCloudTemp.header.stamp = cloudHeader.stamp;
        laserCloudTemp.header.frame_id = "base_link";
        pubSegmentedCloudPure.publish(laserCloudTemp);
    }

    if (pubFullInfoCloud.getNumSubscribers() != 0){
        pcl::toROSMsg(*fullInfoCloud, laserCloudTemp);
        laserCloudTemp.header.stamp = cloudHeader.stamp;
        laserCloudTemp.header.frame_id = "base_link";
        pubFullInfoCloud.publish(laserCloudTemp);
    }
}

resetParameters

void resetParameters()具体代码:

// 初始化/重置各类参数内容
void resetParameters(){
    laserCloudIn->clear();
    groundCloud->clear();
    segmentedCloud->clear();
    segmentedCloudPure->clear();
    outlierCloud->clear();

    rangeMat = cv::Mat(N_SCAN, Horizon_SCAN, CV_32F, cv::Scalar::all(FLT_MAX));
    groundMat = cv::Mat(N_SCAN, Horizon_SCAN, CV_8S, cv::Scalar::all(0));
    labelMat = cv::Mat(N_SCAN, Horizon_SCAN, CV_32S, cv::Scalar::all(0));
    labelCount = 1;

    std::fill(fullCloud->points.begin(), fullCloud->points.end(), nanPoint);
    std::fill(fullInfoCloud->points.begin(), fullInfoCloud->points.end(), nanPoint);
}

(imageProjection.cpp完)

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

LeGO-LOAM 源码阅读笔记(imageProjecion.cpp) 的相关文章

  • vscode_c++_slambook 编译配置

    工作目录 配置文件 launch json version 0 2 0 configurations name slamBook程序调试 type cppdbg request launch program fileDirname buil
  • 从0.3开始搭建LeGO-LOAM+VLP雷达+小车实时建图(保姆级教程,小白踩坑日记)

    背景 SLAM小白 因为项目需要花了两天时间编译代码 连接雷达实现了交互 踩了很多坑 简单记录一下 让后面感兴趣的朋友少走点弯路 肯定有很多不专业的 错误的地方 还请大家不吝赐教 噗通 也可以见知乎 https zhuanlan zhihu
  • 《视觉SLAM十四讲》第一版源码slambook编译调试

    slambook master ch2 编译正常 log如下 slambook master ch2 mkdir build cd build cmake make j8 The C compiler identification is G
  • 激光SLAM直接线性方法里程计运动模型及标定

    原创作者 W Tortoise 原创作者文章 https blog csdn net learning tortosie article details 107763626 1 里程计运动模型 1 1 两轮差分底盘的运动模型 1 2 三轮全
  • np.meshgrid()函数 以及 三维空间中的坐标位置生成 以及 numpy.repeat()函数介绍

    一 np meshgrid 函数 1 np meshgrid 介绍 X Y np meshgrid x y 代表的是将x中每一个数据和y中每一个数据组合生成很多点 然后将这些点的x坐标放入到X中 y坐标放入Y中 并且相应位置是对应的 下面是
  • 【SLAM】卡尔曼滤波(Kalman Filter)

    卡尔曼滤波 Kalman filter 一种利用线性系统状态方程 通过系统输入输出观测数据 对系统状态进行最优估计的算法 由于观测数据中包括系统中的噪声和干扰的影响 所以最优估计也可看作是滤波过程 卡尔曼滤波器的原理解释如下 首先 我们先要
  • 【大一立项】如何亲手搭建ROS小车:硬件和软件介绍

    本次博客将详细介绍上篇博客中提到的ROS小车的硬件和软件部分 由于十一实验室不开门 所以部分代码还没有上传到Github 下位机 下位机使用Arduino 因为大一上刚学完用Arduino做循迹小车 其实Arduino作为ROS小车的下位机
  • Sophus使用记录

    sophus库是一个基于Eigen的C 李群李代数库 可以用来方便地进行李群李代数的运算 头文件 主要用到以下两个头文件 include
  • 使用EKF融合odometry及imu数据

    整理资料发现早前学习robot pose ekf的笔记 大抵是一些原理基础的东西加一些自己的理解 可能有不太正确的地方 当时做工程遇到的情况为机器人在一些如光滑的地面上打滑的情形 期望使用EKF利用imu对odom数据进行校正 就结果来看
  • 视觉SLAM实践入门——(20)视觉里程计之直接法

    多层直接法的过程 1 读图 随机取点并计算深度 2 创建图像金字塔 相机内参也需要缩放 并计算对应点的像素坐标 3 应用单层直接法 使用G N L M等方法 或者使用g2o ceres库 进行优化 源码中有一些地方会引起段错误 修改方法见下
  • 对最小二乘法的一点理解 - slam学习笔记

    我对最小二乘法的理解 在给定参数个数和函数模型之后 根据测试数据 找出与所有测试数据的偏差的平方和最小的参数 这里面应该有两个问题 1 为什么选取与真实数据平方和最小的拟合函数 2 如何求参数 为什么选取与真实数据平方和最小的拟合函数 极大
  • 单目视觉里程记代码

    在Github上发现了一个简单的单目vo 有接近500星 链接如下 https github com avisingh599 mono vo 这个单目里程计主要依靠opencv实现 提取fast角点并进行光流跟踪 然后求取本质矩阵并恢复两帧
  • LeGO-LOAM论文翻译(内容精简)

    LeGO LOAM是一种在LOAM之上进行改进的激光雷达建图方法 建图效果比LOAM要好 但是建图较为稀疏 计算量也更小了 本文原地址 wykxwyc的博客 github注释后LeGO LOAM源码 LeGO LOAM NOTED 关于代码
  • 视觉SLAM技术及其应用(章国锋--复杂环境下的鲁棒SfM与SLAM)

    SLAM 同时定位与地图构建 机器人和计算机视觉领域的基本问题 在未知环境中定位自身方位并同时构建环境三维地图 应用广泛 增强现实 虚拟现实 机器人 无人驾驶 SLAM常用的传感器 红外传感器 较近距离感应 常用与扫地机器人 激光雷达 单线
  • Eigen::aligned_allocator

    如果STL容器中的元素是Eigen库数据结构 例如这里定义一个vector容器 元素是Matrix4d 如下所示 vector
  • Ceres Solver从零开始手把手教学使用

    目录 一 简介 二 安装 三 介绍 四 Hello Word 五 导数 1 数值导数 2解析求导 六 实践 Powell函数 一 简介 笔者已经半年没有更新新的内容了 最近学习视觉SLAM的过程中发现自己之前学习的库基础不够扎实 Ceres
  • BLAM跑自己的数据包无法显示全局点云地图解决(速腾聚创RS-LiDAR-16 雷达 )-SLAM不学无术小问题

    BLAM算法跑自己的数据包无法显示全局点云地图解决 适配速腾聚创RS LiDAR 16 雷达 提示 本文笔者使用环境Ubuntu18 04 ROS melodic版本 首先放一个效果链接 由b站up VladimirDuan上传 非官方 官
  • 3.Open3D教程——点云数据操作

    点云数据 本教程阐述了基本的点云用法 随需要的文件链接 1 显示点云 import open3d as o3d import numpy as np print Load a ply point cloud print it and ren
  • 高翔博士Faster-LIO论文和算法解析

    说明 题目 Faster LIO 快速激光IMU里程计 参考链接 Faster LIO 快速激光IMU里程计 iVox Faster Lio 智行者高博团队开源的增量式稀疏体素结构 Faster Lio是高翔博士在Fast系列的新作 对标基
  • KITTI校准文件中参数的格式

    我从以下位置访问了校准文件KITTI 的部分里程计 http www cvlibs net datasets kitti eval odometry php 其中一个校准文件的内容如下 P0 7 188560000000e 02 0 000

随机推荐

  • 4.4.2 中文标点符号验证

    英文标点符号比较多 如 逗号 点号 问号 冒号 分号 单引号 感叹号 双引号 连接号 破折号 省略号 小括号 中括号 大括号 顿号 书名号等 以下正则表达式能够验证英文标点符号 2 64 正则表达式 64 解释 匹配 符号 2 匹配破折号
  • Java 添加背景图片

    import java awt import javax swing public class TestBackgroundColor extends JFrame public static void main String args T
  • IF语句例题(一)

    石头剪刀布 需求 1 从控制台输入要出的拳 石头 1 剪刀 2 布 3 2 电脑随机出拳 先假定电脑会出石头 完成代码功能 3 比较正负 解题 首先我们先会议一些input函数 在input函数中内部都是字符串 所以说要把字符串变成整数 p
  • k8s安全管理:认证、授权、准入控制概述

    概述信息 k8s对我们整个系统的认证 授权 访问控制做了精密的设置 对于k8s集群来说 apiserver是整个集群访问控制的唯一入口 我们在k8s集群之上部署应用程序的时候 也可以通过宿主机的NodePort暴露的端口访问里面的程序 用户
  • linux备份工具

    这本阿里P8撰写的算法笔记 再次推荐给大家 身边不少朋友学完这本书最后加入大厂 Github 疯传 史上最强悍 阿里大佬 LeetCode刷题手册 开放下载了 经常备份计算机上的数据是个好的做法 它可以手动完成 也可以设置成自动执行 许多备
  • Java____西财大图书管理系统 代码实现

    西财大图书管理项目代码 book类 1 book 2 bookist operation类 1 AddOperation 2 BorrowOperation 3 DisplayOperation 4 FindOperation 5 Remo
  • 信号与系统matlab心得体会,实验五 Matlab在信号与系统分析中的应用

    实验五Matlab在信号与系统分析中的应用 08电子 3 班E08610308 陈建能 一 实验目的 1 学会用MATLAB进行Laplace正 反变换及Z正 反变换 2 掌握利用MATLAB画出系统的幅频响应 相频响应的方法 3 掌握利用
  • 【C语言】N 阶矩阵的转置

    非主对角线元素的调换 只需要将蓝色虚线左边的元素进行调换 include
  • u盘装系统

    1 用ultraiso将ios写入u盘 2 U盘插入电脑 3 开机狂按某键进入 boot启动页面 4 选择该u盘 enter回车确认安装
  • ag-grid表格基本使用方法-React版本

    AG表格基本用法及Api 在要使用的项目中 首次使用需要引入相关组件包 注 项目中所有组件都是封装之后的 引入方式如下 import Table from pkg common table 引入完成后 在view层需要用到表格的地方直接放入
  • Vue3的filter过滤器代替方法

    Vue3的过滤器代替方法 前言 一 使用步骤 1 vue2的filter 2 vue3的computed 总结 前言 最近博主从vue2转到vue3 惊奇的发现vue2里面的filter在vue3中不再支持 官方建议用计算属性或方法代替过滤
  • 现代博弈论与多智能体强化学习系统

    如今 大多数人工智能 AI 系统都是基于处理任务的单个代理 或者在对抗模型的情况下 是一些相互竞争以改善系统整体行为的代理 然而 现实世界中的许多认知问题是大群人建立的知识的结果 以自动驾驶汽车场景为例 任何座席的决策都是场景中许多其他座席
  • 遭遇难缠的病毒群ntldr.exe和c0nime.exe等,可杀

    据当事人说中毒后就上不了网了 我没有确认 每个硬盘分区下都有 Autorun inf和ntldr exe C Windows System 目录 下有 c0nime exe 那是数字0不是字母o 等若干个可疑的可执行文件 硬盘上N多exe可
  • Vue模板语法

    Vue js使用了基于HTML的模板语法 允许开发者声明式地将DOM绑定至底层Vue实例的数据 所有Vue js的模板都是合法的HTML 所以能够被遵循规范的浏览器和HTML解析器解析 在底层的实现上 Vue将模板编译成虚拟DOM渲染函数
  • Andorid与其他操作系统的知识

    安卓用的是LINUX的内核 利用LINUX的几个库 应用层运行JAVA虚拟机上 这点和iPhone很想 只不过 iphone是基于unix系统 是微内核结构 同样运行在java虚拟机上 所以 安卓只是一个linux的衍生系统 是LINUX的
  • Opecv检测多个圆形(霍夫圆检测,轮廓面积筛选,C/C++)

    主要是利用霍夫圆检测 面积筛选等完成多个圆形检测 具体代码及结果如下 第一部分是头文件 common h pragma once include
  • Springboot+Mybatis中typeAliasesPackage正则扫描实现

    mybatis默认配置typeAliasesPackage是不支持正则扫描package的 因此需要手动继承org mybatis spring SqlSessionFactoryBean 自己实现正则扫描 方法和传统的spring myb
  • uniapp封装请求失败的提示

    当数据请求失败之后 经常需要调用uni showToast 方法来提示用户 可以在全局封装一个uni showMsg 方法 来简化uni showToast 方法的调用 在main js中 将自定义的 showMsg 方法挂载到uni对象上
  • JSONObject 获取全部键(键值对)

    JSONObject也是迭代器 故可使用Itear的方式进行获取全部键 主要是针对不确定键的情况下使用 直接上代码 JSONObject jsonObject new JSONObject response response 为str It
  • LeGO-LOAM 源码阅读笔记(imageProjecion.cpp)

    LeGO LOAM是一种在LOAM之上进行改进的激光雷达建图方法 建图效果比LOAM要好 但是建图较为稀疏 计算量也更小了 本文原地址 wykxwyc的博客 github注释后LeGO LOAM源码 LeGO LOAM NOTED 关于代码