0. 简介
现在越来越多的激光雷达方法如雨后春笋一般冒了出来,最近以FAST-LIO为代表的系列渐渐地被大众接受。而FAST-LIO2的工作越来越被大众所熟知与研究。最近作者也在研究与学习FAST-LIO2的相关知识,这里将会以长文的形式来介绍FAST-LIO2.0的学习心得。本文将通过代码的形式与论文结合,直观的来分析论文的核心算法部分。也同时作为自己的学习笔记来留给后来人学习。
1. 激光雷达
对于FAST-LIO2代码来说,主要的数据是通过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<pcl::PointXYZINormal> */
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: "<<plsize<<endl;
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[i].y - pl_full[i - 1].y) > 1e-7) || (abs(pl_full[i].z - pl_full[i - 1].z) > 1e-7) && (pl_full[i].x * pl_full[i].x + pl_full[i].y * pl_full[i].y > blind))
{
pl_surf.push_back(pl_full[i]);
}
}
}
}
}
}
在程序中我们可以看到give_feature(pl, types);
这个函数主要是特征提取,下面我们对该函数进行学习和了解,对于每条line的点云提取特征
void Preprocess::give_feature(pcl::PointCloud<PointType> &pl, vector<orgtype> &types)
{
int plsize = pl.size(); //单条线的点数
int plsize2;
if (plsize == 0)
{
printf("something wrong\n");
return;
}
uint head = 0;
//不能在盲区 从这条线非盲区的点开始
while (types[head].range < blind)
{
head++;
}
// Surf
// group_size默认等于8
plsize2 = (plsize > group_size) ? (plsize - group_size) : 0; //判断当前点后面是否还有8个点 够的话就逐渐减少
Eigen::Vector3d curr_direct(Eigen::Vector3d::Zero()); //当前平面的法向量
Eigen::Vector3d last_direct(Eigen::Vector3d::Zero()); //上一个平面的法向量
uint i_nex = 0, i2; // i2为当前点的下一个点
uint last_i = 0; // last_i为上一个点的保存的索引
uint last_i_nex = 0; // last_i_nex为上一个点的下一个点的索引
int last_state = 0; //为1代表上个状态为平面 否则为0
//判断面点
int plane_type;
// 拿到8个点用于判断平面
for (uint i = head; i < plsize2; i++)
{
if (types[i].range < blind) // 在盲区范围内的点不做处理
continue;
{
continue;
}
i2 = i; //更新i2
plane_type = plane_judge(pl, types, i, i_nex, curr_direct); //求得平面,并返回类型0 1 2
if (plane_type == 1) //返回1一般默认是平面
{
//设置确定的平面点和可能的平面点
for (uint j = i; j <= i_nex; j++)
{
if (j != i && j != i_nex)
{
//把起始点和终止点之间的所有点设置为确定的平面点
types[j].ftype = Real_Plane;
}
else
{
//把起始点和终止点设置为可能的平面点
types[j].ftype = Poss_Plane;
}
}
// if(last_state==1 && fabs(last_direct.sum())>0.5)
//最开始last_state=0直接跳过
//之后last_state=1
//如果之前状态是平面则判断当前点是处于两平面边缘的点还是较为平坦的平面的点
if (last_state == 1 && last_direct.norm() > 0.1)
{
double mod = last_direct.transpose() * curr_direct;
if (mod > -0.707 && mod < 0.707)
{
//修改ftype,两个面法向量夹角在45度和135度之间 认为是两平面边缘上的点
types[i].ftype = Edge_Plane;
}
else
{
//否则认为是真正的平面点
types[i].ftype = Real_Plane;
}
}
i = i_nex - 1;
last_state = 1;
}
else // if(plane_type == 2)
{
// plane_type=0或2的时候
i = i_nex;
last_state = 0; //设置为不是平面点
}
last_i = i2; //更新last_i
last_i_nex = i_nex; //更新last_i_nex
last_direct = curr_direct; //更新last_direct
}
//判断边缘点
plsize2 = plsize > 3 ? plsize - 3 : 0; //如果剩下的点数小于3则不判断边缘点,否则计算哪些点是边缘点
for (uint i = head + 3; i < plsize2; i++)
{
//点不能在盲区 或者 点必须属于正常点和可能的平面点
if (types[i].range < blind || types[i].ftype >= Real_Plane)
{
continue;
}
//该点与前后点的距离不能挨的太近
if (types[i - 1].dista < 1e-16 || types[i].dista < 1e-16)
{
continue;
}
Eigen::Vector3d vec_a(pl[i].x, pl[i].y, pl[i].z); //当前点组成的向量
Eigen::Vector3d vecs[2];
for (int j = 0; j < 2; j++)
{
int m = -1;
if (j == 1)
{
m = 1;
}
//若当前的前/后一个点在盲区内(4m)
if (types[i + m].range < blind)
{
if (types[i].range > inf_bound) //若其大于10m
{
types[i].edj[j] = Nr_inf; //赋予该点Nr_inf(跳变较远)
}
else
{
types[i].edj[j] = Nr_blind; //赋予该点Nr_blind(在盲区)
}
continue;
}
vecs[j] = Eigen::Vector3d(pl[i + m].x, pl[i + m].y, pl[i + m].z);
vecs[j] = vecs[j] - vec_a; //前/后点指向当前点的向量
//若雷达坐标系原点为O 当前点为A 前/后一点为M和N
//则下面OA点乘MA/(|OA|*|MA|)
//得到的是cos角OAM的大小
types[i].intersect = vecs[Prev].dot(vecs[Next]) / vecs[Prev].norm() / vecs[Next].norm(); //角MAN的cos值
//前一个点是正常点 && 下一个点在激光线上 && 当前点与后一个点的距离大于0.0225m && 当前点与后一个点的距离大于当前点与前一个点距离的四倍
//这种边缘点像是7字形这种的边缘?
if (types[i].angle[j] < jump_up_limit) // cos(170)
{
types[i].edj[j] = Nr_180; // M在OA延长线上
}
else if (types[i].angle[j] > jump_down_limit) // cos(8)
{
types[i].edj[j] = Nr_zero; // M在OA上
}
}
types[i].intersect = vecs[Prev].dot(vecs[Next]) / vecs[Prev].norm() / vecs[Next].norm();
if (types[i].edj[Prev] == Nr_nor && types[i].edj[Next] == Nr_zero && types[i].dista > 0.0225 && types[i].dista > 4 * types[i - 1].dista)
{
if (types[i].intersect > cos160) //角MAN要小于160度 不然就平行于激光了
{
if (edge_jump_judge(pl, types, i, Prev))
{
types[i].ftype = Edge_Jump;
}
}
}
//与上面类似
//前一个点在激光束上 && 后一个点正常 && 前一个点与当前点的距离大于0.0225m && 前一个点与当前点的距离大于当前点与后一个点距离的四倍
else if (types[i].edj[Prev] == Nr_zero && types[i].edj[Next] == Nr_nor && types[i - 1].dista > 0.0225 && types[i - 1].dista > 4 * types[i].dista)
{
if (types[i].intersect > cos160)
{
if (edge_jump_judge(pl, types, i, Next))
{
types[i].ftype = Edge_Jump;
}
}
}
//前面的是正常点 && (当前点到中心距离>10m并且后点在盲区)
else if (types[i].edj[Prev] == Nr_nor && types[i].edj[Next] == Nr_inf)
{
if (edge_jump_judge(pl, types, i, Prev))
{
types[i].ftype = Edge_Jump;
}
}
//(当前点到中心距离>10m并且前点在盲区) && 后面的是正常点
else if (types[i].edj[Prev] == Nr_inf && types[i].edj[Next] == Nr_nor)
{
if (edge_jump_judge(pl, types, i, Next))
{
types[i].ftype = Edge_Jump;
}
}
//前后点都不是正常点
else if (types[i].edj[Prev] > Nr_nor && types[i].edj[Next] > Nr_nor)
{
if (types[i].ftype == Nor)
{
types[i].ftype = Wire; //程序中应该没使用 当成空间中的小线段或者无用点了
}
}
}
plsize2 = plsize - 1;
double ratio;
//继续找平面点
for (uint i = head + 1; i < plsize2; i++)
{
//前面、当前、之后三个点都需要不在盲区内
if (types[i].range < blind || types[i - 1].range < blind || types[i + 1].range < blind)
{
continue;
}
//前面和当前 当前和之后的点与点之间距离都不能太近
if (types[i - 1].dista < 1e-8 || types[i].dista < 1e-8)
{
continue;
}
//还剩下来一些正常点继续找平面点
if (types[i].ftype == Nor)
{
//求点与点间距的比例 大间距/小间距
if (types[i - 1].dista > types[i].dista)
{
ratio = types[i - 1].dista / types[i].dista;
}
else
{
ratio = types[i].dista / types[i - 1].dista;
}
//如果夹角大于172.5度 && 间距比例<1.2
if (types[i].intersect < smallp_intersect && ratio < smallp_ratio)
{
//前后三个点认为是平面点
if (types[i - 1].ftype == Nor)
{
types[i - 1].ftype = Real_Plane;
}
if (types[i + 1].ftype == Nor)
{
types[i + 1].ftype = Real_Plane;
}
types[i].ftype = Real_Plane;
}
}
}
//存储平面点
int last_surface = -1;
for (uint j = head; j < plsize; j++)
{
//可能的平面点和确定的平面点
if (types[j].ftype == Poss_Plane || types[j].ftype == Real_Plane)
{
if (last_surface == -1)
{
last_surface = j;
}
//通常连着好几个都是面点
//必须在采样间隔上的平面点才使用(这里的是无差别滤波 从每次新找到面点开始每几个点才取一个)
if (j == uint(last_surface + point_filter_num - 1))
{
PointType ap;
ap.x = pl[j].x;
ap.y = pl[j].y;
ap.z = pl[j].z;
ap.intensity = pl[j].intensity;
ap.curvature = pl[j].curvature;
pl_surf.push_back(ap);
last_surface = -1;
}
}
else
{
//跳变较大的边缘边的点 位于平面边缘的点
if (types[j].ftype == Edge_Jump || types[j].ftype == Edge_Plane)
{
pl_corn.push_back(pl[j]);
}
//假如上次找到的面点被无差别滤波掉了,而此时已经到了边缘
if (last_surface != -1)
{
PointType ap;
//取上次面点到此次边缘线之间的所有点的重心当作一个面点存储进去
for (uint k = last_surface; k < j; k++)
{
ap.x += pl[k].x;
ap.y += pl[k].y;
ap.z += pl[k].z;
ap.intensity += pl[k].intensity;
ap.curvature += pl[k].curvature;
}
ap.x /= (j - last_surface);
ap.y /= (j - last_surface);
ap.z /= (j - last_surface);
ap.intensity /= (j - last_surface);
ap.curvature /= (j - last_surface);
pl_surf.push_back(ap);
}
last_surface = -1;
}
}
}
在give_feature函数中存在了提取面特征与提取角特征的函数,下面我们来详细分析,面特征主要是主要是依据点与点之间的距离来拟合成一个向量,并通过乘积来计算出面特征的提取。这里的面特征提取和边缘特征提取和LOAM的类似,这里就用LOAM的图片来表示了。
…详情请参照古月居
今天的文章fastfcn代码_fastjson解析复杂json数据分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/65323.html