SC-LEGO-LOAM 扩展以及深度解析
前言
本作者在16年大学开始接触ROS后,逐步向着机器人建图导航方面扩展,尤其是对激光雷达方向比较感兴趣,目前打算针对近阶段的SC-LEGO-LOAM进行分析讲述。从ScanContext和Lego LOAM两个部分进行分析阐述。一方面也是记录自己的学习成果,另一方面也是帮助他人一起熟悉这篇20年的经典文章。
LOAM系列发展
LOAM
LOAM作为该系列的鼻祖,在前几年kitti数据集中常年霸占榜首,文中的作者所写的代码由于可读性不高,所以有很多人对代码进行了重构。
A-LOAM
LeGO-LOAM
LeGO-LOAM针对处理运算量做了优化,它的运算速度增加,同时相较于LOAM并没有牺牲精度。LeGO-LOAM作为近三年的论文,由于其代码开源以及对设备性能要求低等优点,到现在依然用的比较多。
论文里面主要是和LOAM对比,其相比LOAM具有以下五个特点
轻量级,能在嵌入式设备上实时运行。
地面优化,在点云处理部分加入了分割模块,这样做能够去除地面点的干扰,只在聚类的目标中提取特征。其中主要包括一个地面的提取(并没有假设地面是平面),和一个点云的分割,使用筛选过后的点云再来提取特征点,这样会大大提高效率。
在提取特征点时,将点云分成小块,分别提取特征点,以保证特征点的均匀分布。在特征点匹配的时候,使用预处理得到的segmenation标签筛选(点云分割为边缘点类和平面点类两类),再次提高效率。
两步L-M优化法估计6个维度的里程计位姿。先使用平面点优化高度,同时利用地面的方向优化两个角度信息。地面提取的平面特征用于在第一步中获得[tzt_ztz,θroll\theta_{roll}θroll,θpitch\theta_{pitch}θpitch];再使用边角点优化剩下的三个变量,通过匹配从分段点云提取的边缘特征来获得其余部分的变换[txt_xtx,tyt_yty,θyaw\theta_{yaw}θyaw]。匹配方式还是scan2scan,以这种方式分别优化,效率提升40%,但是并没有造成精度的损失
- 集成了回环检测以校正运动估计漂移的能力(即使用gtsam作回环检测并作图优化,但是本质上仍然是基于欧式距离的回环检测,不存在全局描述子)。
LIO-SAM
- 里程计部分改为scan2localmap的匹配,特征提取部分去除了原LeGO-LOAM中的聚、分割并提取较为突出的边缘点和平面点,而是沿用LOAM中的边缘和平面点。(精度高一些,LeGO-LOAM主要考虑性能多一点)
- 维护两个因子图,预积分因子图可联合优化激光雷达odom和IMU,并估计IMU偏差,进行实时的里程计估算,这里将雷达位姿作为预测,而把imu作为观测,去更新imu的bias。全局因子图联合优化雷达odom和GPS因子以及回环因子。(即最终建图没有用到IMU优化后的轨迹,有点奇怪,这样做的好处就是优化速度快)
- 利用robot_localization中的EKF节点融合GPS和9轴IMU数据,得到提供初始姿态的gps odom。(这里初始化部分使用到了9轴IMU,参考VINS)
ISCLOAM
文中使用的ISC特征和SC特征对比,在回环检测部分,两个关键步骤,一是计算ISC特征,二是特征相似性计算。
ISC特征的相似性计算部分这里分两步计算,第一步计算其空间距离得分,第二步计算其强度距离得分。这里进行两个阶段的检测,首先是快速的几何结构匹配,然后密度结构重识别。
bool ISCGenerationClass::is_loop_pair(ISCDescriptor &desc1,ISCDescriptor &desc2,double &geo_score,double &inten_score) {int angle = 0;geo_score = calculate_geometry_dis(desc1, desc2, angle); // 计算几何距离if (geo_score > GEOMETRY_THRESHOLD) {inten_score = calculate_intensity_dis(desc1, desc2, angle); // 计算强度得分if (inten_score > INTENSITY_THRESHOLD) {return true;}}return false;
}double ISCGenerationClass::calculate_geometry_dis(const ISCDescriptor &desc1, const ISCDescriptor &desc2, int &angle) {double similarity = 0.0;// 几何结构匹配for (int i = 0; i < sectors; i++) { // 列int match_count = 0;// 矩阵逐元素异或for (int p = 0; p < sectors; p++) {int new_col = p + i >= sectors ? p + i - sectors : p + i;for (int q = 0; q < rings; q++) { // 行// 由于二值矩阵的列向量表示方位角,因此LiDAR的旋转可以通过矩阵的列移动反映.// 因此,为了检测视角变化,我们需要计算具有最大几何相似度的列移动if ((desc1.at<unsigned char>(q, p) == true && desc2.at<unsigned char>(q, new_col) == true)|| (desc1.at<unsigned char>(q, p) == false && desc2.at<unsigned char>(q, new_col) == false)) {match_count++;}}}if (match_count > similarity) {similarity = match_count;angle = i;}}return similarity / (sectors * rings);
}
double ISCGenerationClass::calculate_intensity_dis(const ISCDescriptor &desc1, const ISCDescriptor &desc2, int &angle) {double difference = 1.0;// 密度结构重识别 计算上一步最佳列旋转后的和候选帧的ISC的密度相似度// 取ISC对应列的余弦距离的均值double angle_temp = angle;for (int i = angle_temp - 10; i < angle_temp + 10; i++) {int match_count = 0;int total_points = 0;for (int p = 0; p < sectors; p++) {int new_col = p + i;if (new_col >= sectors)new_col = new_col - sectors;if (new_col < 0)new_col = new_col + sectors;for (int q = 0; q < rings; q++) {match_count += abs(desc1.at<unsigned char>(q, p) - desc2.at<unsigned char>(q, new_col));total_points++;}}double diff_temp = ((double) match_count) / (sectors * rings * 255);if (diff_temp < difference)difference = diff_temp;}return 1 - difference;
}
ICP && NDT
ICP和NDT原本是作为点云与点云的直接匹配方法来作为前端里程计估计的方法,但是目前由于LOAM系列的出现,导致这种方法已经很少使用。此处我们提到ICP和NDT是想说明其在回环检测的作用,由于回环检测需要寻找到回环点。以LeGO-LOAM算法为代表的回环检测就是使用ICP+欧式距离的方法来寻找到回环点。而以SC-LeGO-LOAM算法为代表的回环检测使用了scan context系列提取的全局特征子来进行查找,同时有些方法会在此基础上再加入ICP来提升回环检测后重定位的精度,方便图优化。这两种方法各有千秋,可以根据需求进行选择。
回环检测
在LOAM系列中回环检测主要存在有四种方法
- 传统的领域距离搜索+ICP匹配
- 基于scan context系列的粗匹配+ICP精准匹配的回环检测
- 基于scan context的回环检测
- 基于Intensity scan context+ICP的回环检测
在参考很多大佬的比对结果中我们发现,传统的领域距离搜索+ICP匹配是这三个方法中最耗时的,相较于基于scan context的回环检测而言利用类梯度下降法做ICP匹配需要消耗大量的计算资源。在使用基于scan context的回环检测时,会出现误检和漏检情况,而基于Intensity scan context的回环检测,对于scan context建图精度未有太大提升,但漏检的情况要少很多。
–>
SC-LeGO-LOAM算法详解
SC-LeGO-LOAM是在LeGO-LOAM的基础上新增了基于Scan context的回环检测,在回环检测的速度上相较于LeGO-LOAM有了一定的提升。下面我们将会对SC-LeGO-LOAM算法的代码进行详细分析。
首先我们从代码的launch文件开始入手进行分析,README文件中提示,我们需要从run.launch
文件入手。
<launch>........<node pkg="lego_loam" type="imageProjection" name="imageProjection" output="screen"/><node pkg="lego_loam" type="featureAssociation" name="featureAssociation" output="screen"/><node pkg="lego_loam" type="mapOptmization" name="mapOptmization" output="screen"/><node pkg="lego_loam" type="transformFusion" name="transformFusion" output="screen"/></launch>
从中我们可以看到launch文件中主要依赖四个node,其中最后一个node主要输出了一些数据坐标系的转换,对文章的理解影响不会很大,主要功能的是由前面3个node来实现的,而且是存在数据流的依次传递和处理。
imageProjection.cpp
首先我们先来看一下imageProjection
这一个node节点,这个部分主要是对激光雷达数据进行预处理。包括激光雷达数据获取、点云数据分割、点云类别标注、数据发布。
该文件中订阅了激光雷达发布的数据,并发布了多个分类点云结果,初始化lego_loam::ImageProjection
构造函数中,nodehandle
使用~
来表示在默认空间中。
ImageProjection::ImageProjection() : nh("~") {// init paramsInitParams();// subscribersubLaserCloud = nh.subscribe<sensor_msgs::PointCloud2>(pointCloudTopic.c_str(), 1,&ImageProjection::cloudHandler, this);// publisherpubFullCloud = nh.advertise<sensor_msgs::PointCloud2>("/full_cloud_projected", 1);pubFullInfoCloud = nh.advertise<sensor_msgs::PointCloud2>("/full_cloud_info", 1);pubGroundCloud = nh.advertise<sensor_msgs::PointCloud2>("/ground_cloud", 1);pubSegmentedCloud = nh.advertise<sensor_msgs::PointCloud2>("/segmented_cloud", 1);pubSegmentedCloudPure = nh.advertise<sensor_msgs::PointCloud2>("/segmented_cloud_pure", 1);pubSegmentedCloudInfo = nh.advertise<cloud_msgs::cloud_info>("/segmented_cloud_info", 1);pubOutlierCloud = nh.advertise<sensor_msgs::PointCloud2>("/outlier_cloud", 1); // 离群点或异常点nanPoint.x = std::numeric_limits<float>::quiet_NaN();nanPoint.y = std::numeric_limits<float>::quiet_NaN();nanPoint.z = std::numeric_limits<float>::quiet_NaN();nanPoint.intensity = -1;allocateMemory();resetParameters();}
在构造函数中除了使用allocateMemory
对点云进行reset、resize、assign
等重置、赋值等操作以外。主要的函数是ImageProjection::cloudHandler
,该函数内部清晰记录了激光雷达数据流的走向
void ImageProjection::cloudHandler(const sensor_msgs::PointCloud2ConstPtr &laserCloudMsg) {// 1. Convert ros message to pcl point cloudcopyPointCloud(laserCloudMsg);// 2. Start and end angle of a scanfindStartEndAngle();// 3. Range image projectionprojectPointCloud();// 4. Mark ground pointsgroundRemoval();// 5. Point cloud segmentationcloudSegmentation();// 6. Publish all cloudspublishCloud();// 7. Reset parameters for next iterationresetParameters();}
该回调函数调用了七个函数,完成了对单帧激光雷达数据的处理。下面我们对该流程进行梳理,并详细介绍地面分割方法。
copyPointCloud:该函数主要的功能是通过
pcl::fromROSMsg
函数将ROS的PointCloud保存成PCL的PointCloud,并通过pcl::removeNaNFromPointCloud
函数对nan噪点进行了滤除,从而避免了后面的计算中出现各种异常情况。findStartEndAngle:这个函数主要为了计算起止角度范围。因为运动,会导致多线激光雷达的第一个点和最后一个点并不是严格的360°,会存在一定的畸变。为此需要计算出起止角度,并将差值放在
segMsg
变量中,成员变量segMsg的类型cloud_msgs::cloud_info
是作者自定义的。它保存了当前帧的一些重要信息,包括起至角度,每个线的起至序号,及成员变量fullCloud
中每个点的状态。projectPointCloud:该函数中将激光点云按照角度展开成图像的形式,计算所在行列和深度。其中行表示激光线束数量,列表示每个线上同一时刻扫描到的点。这里以
x
轴的负方向开始逆时针列序列号逐渐递增,即图像中的从左到右。以Mat
图像保存深度,其中点集的行用fullCloud 和 fullInfoCloud
表示和深度用fullInfoCloud
表示。这里的fullInfoCloud
内部存放该帧下的所有数据,该处的点云相比于copyPointCloud
函数输入的点云数据,主要的不同之处在于,当前的点云根据计算出来的行列,重新排列了顺序。例如,激光雷达本身可能是先列后行的方向输出的点云,或者是livox雷达那种非重复扫描出来的结果,这里都会被重新组织成先行后列的顺序保存。void ImageProjection::projectPointCloud() {// 激光点云投影成二维图像,行表示激光线束数量,列表示每一个线上扫描到的点(0.1s扫描一圈,一个圆圈摊平就是360度)// 计算点云深度,保存到深度图中// range image projectionfloat verticalAngle, horizonAngle, range;size_t rowIdn, columnIdn, index, cloudSize;PointType thisPoint;cloudSize = laserCloudIn->points.size();for (size_t i = 0; i < cloudSize; ++i) {thisPoint.x = laserCloudIn->points[i].x;thisPoint.y = laserCloudIn->points[i].y;thisPoint.z = laserCloudIn->points[i].z;// find the row and column index in the iamge for this point// 计算竖直方向上的点的角度以及在整个雷达点云中的哪一条水平线上if (useCloudRing == true) {// 用vlp的时候有ring属性rowIdn = laserCloudInRing->points[i].ring;} else {// 其他lidar需要根据计算垂直方向的俯仰角?verticalAngle =atan2(thisPoint.z, sqrt(thisPoint.x * thisPoint.x + thisPoint.y * thisPoint.y)) * 180 / M_PI;rowIdn = (verticalAngle + ang_bottom) / ang_res_y;}if (rowIdn < 0 || rowIdn >= N_SCAN)continue;// 水平方向上的角度,一行1800个像素点horizonAngle = atan2(thisPoint.x, thisPoint.y) * 180 / M_PI;// round是四舍五入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;// 每个点的深度range = sqrt(thisPoint.x * thisPoint.x + thisPoint.y * thisPoint.y + thisPoint.z * thisPoint.z);if (range < sensorMinimumRange)continue;// 在rangeMat矩阵中保存该点的深度,保存单通道像素值rangeMat.at<float>(rowIdn, columnIdn) = range;thisPoint.intensity = (float) rowIdn + (float) columnIdn / 10000.0;index = columnIdn + rowIdn * Horizon_SCAN;fullCloud->points[index] = thisPoint;fullInfoCloud->points[index] = thisPoint;fullInfoCloud->points[index].intensity = range; // the corresponding range of a point is saved as "intensity"}}
groundRemoval:该步骤主要是提前滤除地面,从贴近地面的几个线中提取地面点。取七个扫描圈,每两个圈之间进行一次比较,角度相差10°以内的我们可以看做是平地。并且将扫描圈中的点加入到
groundCloud
点云中。地面的点云会在groundMat中标记为1,labelMat中标记-1 ,不会参与后面的分割。
void ImageProjection::groundRemoval() {// 利用不同的扫描圈来表示地面,进而检测地面是否水平。// 取七个扫描圈,每两个圈之间进行一次比较,角度相差10°以内的我们可以看做是平地。// 并且将扫描圈中的点加入到groundCloud点云size_t lowerInd, upperInd;float diffX, diffY, diffZ, angle;// groundMat// -1, no valid info to check if ground of not// 0, initial value, after validation, means not ground// 1, groundfor (size_t j = 0; j < Horizon_SCAN; ++j) { // 每行for (size_t i = 0; i < groundScanInd; ++i) { // 每列// 只用7个光束检测地面lowerInd = j + (i) * Horizon_SCAN;upperInd = j + (i + 1) * Horizon_SCAN;// intensity在投影的时候已经归一化if (fullCloud->points[lowerInd].intensity == -1 ||fullCloud->points[upperInd].intensity == -1) {// no info to check, invalid pointsgroundMat.at<int8_t>(i, j) = -1;continue;}diffX = fullCloud->points[upperInd].x - fullCloud->points[lowerInd].x;diffY = fullCloud->points[upperInd].y - fullCloud->points[lowerInd].y;diffZ = fullCloud->points[upperInd].z - fullCloud->points[lowerInd].z;angle = atan2(diffZ, sqrt(diffX * diffX + diffY * diffY)) * 180 / M_PI;// 相邻圈小于10度if (abs(angle - sensorMountAngle) <= 10) {groundMat.at<int8_t>(i, j) = 1;groundMat.at<int8_t>(i + 1, j) = 1;}}}// extract ground cloud (groundMat == 1)// mark entry that doesn't need to label (ground and invalid point) for segmentation// note that ground remove is from 0~N_SCAN-1, need rangeMat for mark label matrix for the 16th scanfor (size_t i = 0; i < N_SCAN; ++i) {for (size_t j = 0; j < Horizon_SCAN; ++j) {if (groundMat.at<int8_t>(i, j) == 1 || rangeMat.at<float>(i, j) == FLT_MAX) {labelMat.at<int>(i, j) = -1;}}}if (pubGroundCloud.getNumSubscribers() != 0) {for (size_t i = 0; i <= groundScanInd; ++i) {for (size_t j = 0; j < Horizon_SCAN; ++j) {if (groundMat.at<int8_t>(i, j) == 1)groundCloud->push_back(fullCloud->points[j + i * Horizon_SCAN]);}}}}
cloudSegmentation:这一部分主要是将地面点与异常点排除之后,对非地面点云的分割,并生成局部特征的步骤。这个函数主要完成了两个任务,一是通过广度优先搜索,从非地面点中找出所有连成片的入射角比较小的patch上的点,并在labelMat标注patch的编号(从1开始)
void ImageProjection::labelComponents(int row, int col) {// use std::queue std::vector std::deque will slow the program down greatly// 特征检测,检测点与其临近点的特征float d1, d2, alpha, angle;int fromIndX, fromIndY, thisIndX, thisIndY;bool lineCountFlag[N_SCAN] = {false};queueIndX[0] = row;queueIndY[0] = col;int queueSize = 1;int queueStartInd = 0;int queueEndInd = 1;allPushedIndX[0] = row;allPushedIndY[0] = col;int allPushedIndSize = 1;while (queueSize > 0) {// Pop pointfromIndX = queueIndX[queueStartInd];fromIndY = queueIndY[queueStartInd];--queueSize;++queueStartInd;// Mark popped pointlabelMat.at<int>(fromIndX, fromIndY) = labelCount;//检查上下左右四个邻点// Loop through all the neighboring grids of popped gridfor (auto iter = neighborIterator.begin(); iter != neighborIterator.end(); ++iter) {// new indexthisIndX = fromIndX + (*iter).first;thisIndY = fromIndY + (*iter).second;// index should be within the boundaryif (thisIndX < 0 || thisIndX >= N_SCAN)continue;// at range image margin (left or right side)if (thisIndY < 0)thisIndY = Horizon_SCAN - 1;if (thisIndY >= Horizon_SCAN)thisIndY = 0;// prevent infinite loop (caused by put already examined point back)if (labelMat.at<int>(thisIndX, thisIndY) != 0)continue;// d1与d2分别是该点与某邻点的深度d1 = std::max(rangeMat.at<float>(fromIndX, fromIndY),rangeMat.at<float>(thisIndX, thisIndY));d2 = std::min(rangeMat.at<float>(fromIndX, fromIndY),rangeMat.at<float>(thisIndX, thisIndY));if ((*iter).first == 0)alpha = segmentAlphaX;elsealpha = segmentAlphaY;// angle其实是该特点与某邻点的连线与XOZ平面的夹角,这个角代表了局部特征的敏感性angle = atan2(d2 * sin(alpha), (d1 - d2 * cos(alpha)));// 如果夹角大于60°,则将这个邻点纳入到局部特征中,该邻点可以用来配准使用if (angle > segmentTheta) {queueIndX[queueEndInd] = thisIndX;queueIndY[queueEndInd] = thisIndY;++queueSize;++queueEndInd;labelMat.at<int>(thisIndX, thisIndY) = labelCount;lineCountFlag[thisIndX] = true;allPushedIndX[allPushedIndSize] = thisIndX;allPushedIndY[allPushedIndSize] = thisIndY;++allPushedIndSize;}}}// check if this segment is valid// 当邻点数目达到30后,则该帧雷达点云的几何特征配置成功bool feasibleSegment = false;if (allPushedIndSize >= 30)feasibleSegment = true;else if (allPushedIndSize >= segmentValidPointNum) {int lineCount = 0;for (size_t i = 0; i < N_SCAN; ++i)if (lineCountFlag[i] == true)++lineCount;if (lineCount >= segmentValidLineNum)feasibleSegment = true;}// segment is valid, mark these pointsif (feasibleSegment == true) {++labelCount;} else { // segment is invalid, mark these pointsfor (size_t i = 0; i < allPushedIndSize; ++i) {labelMat.at<int>(allPushedIndX[i], allPushedIndY[i]) = 999999;}}}
二是把所有地面点和刚分割出来的
labelCount
上的点合并保存在segmentedCloud
中,此时当前帧的点已经有效的分割成多个类,这也是该node需要传递给下一个node进行特征提取和匹配的点云。并在segMsg中对应位置处保存每个点的属性(该点是不是地面,深度,属于第几列)。同时由于我们不需要那么多地面的点云,所以使用5个采一个的策略来实现地面点的降采样。
void ImageProjection::cloudSegmentation() {// segmentation process// /在排除地面点与异常点之后,逐一检测邻点特征并生成局部特征for (size_t i = 0; i < N_SCAN; ++i)for (size_t j = 0; j < Horizon_SCAN; ++j)if (labelMat.at<int>(i, j) == 0)labelComponents(i, j);int sizeOfSegCloud = 0;// extract segmented cloud for lidar odometryfor (size_t i = 0; i < N_SCAN; ++i) {segMsg.startRingIndex[i] = sizeOfSegCloud - 1 + 5;for (size_t j = 0; j < Horizon_SCAN; ++j) {// 如果是特征点或者是地面点,就可以纳入被分割点云if (labelMat.at<int>(i, j) > 0 || groundMat.at<int8_t>(i, j) == 1) {// outliers that will not be used for optimization (always continue)if (labelMat.at<int>(i, j) == 999999) {if (i > groundScanInd && j % 5 == 0) {outlierCloud->push_back(fullCloud->points[j + i * Horizon_SCAN]);continue;} else {continue;}}// majority of ground points are skipped// 地面点云每隔5个点纳入被分割点云if (groundMat.at<int8_t>(i, j) == 1) {if (j % 5 != 0 && j > 5 && j < Horizon_SCAN - 5)continue;}// mark ground points so they will not be considered as edge features latersegMsg.segmentedCloudGroundFlag[sizeOfSegCloud] = (groundMat.at<int8_t>(i, j) == 1);// mark the points' column index for marking occlusion latersegMsg.segmentedCloudColInd[sizeOfSegCloud] = j;// save range infosegMsg.segmentedCloudRange[sizeOfSegCloud] = rangeMat.at<float>(i, j);// save seg cloud 把当前点纳入分割点云中segmentedCloud->push_back(fullCloud->points[j + i * Horizon_SCAN]);// size of seg cloud++sizeOfSegCloud;}}segMsg.endRingIndex[i] = sizeOfSegCloud - 1 - 5;}// extract segmented cloud for visualization// 在当前有节点订阅便将分割点云的几何信息也发布出去if (pubSegmentedCloudPure.getNumSubscribers() != 0) {for (size_t i = 0; i < N_SCAN; ++i) {for (size_t j = 0; j < Horizon_SCAN; ++j) {if (labelMat.at<int>(i, j) > 0 && labelMat.at<int>(i, j) != 999999) {segmentedCloudPure->push_back(fullCloud->points[j + i * Horizon_SCAN]);segmentedCloudPure->points.back().intensity = labelMat.at<int>(i, j);}}}}}
publishCloud:下面j就是与ROS相关的发布任务了。包括该帧的segMsg,完整点云fullCloud/fullInfoCloud(步骤3),地面点云(步骤4),发从非地面提取出来的点和降采样的地面点(步骤5),外点(步骤5,实际为比较小的patch上的点【因此两个叠加会导致角度差较大,且点云数不满足需求】)。
void ImageProjection::publishCloud() {// 1. Publish Seg Cloud InfosegMsg.header = cloudHeader;pubSegmentedCloudInfo.publish(segMsg);// 2. Publish cloudssensor_msgs::PointCloud2 laserCloudTemp;pcl::toROSMsg(*outlierCloud, laserCloudTemp);laserCloudTemp.header.stamp = cloudHeader.stamp;laserCloudTemp.header.frame_id = "base_link";pubOutlierCloud.publish(laserCloudTemp);// segmented cloud with groundpcl::toROSMsg(*segmentedCloud, laserCloudTemp);laserCloudTemp.header.stamp = cloudHeader.stamp;laserCloudTemp.header.frame_id = "base_link";pubSegmentedCloud.publish(laserCloudTemp);// projected full cloudif (pubFullCloud.getNumSubscribers() != 0) {pcl::toROSMsg(*fullCloud, laserCloudTemp);laserCloudTemp.header.stamp = cloudHeader.stamp;laserCloudTemp.header.frame_id = "base_link";pubFullCloud.publish(laserCloudTemp);}// original dense ground cloudif (pubGroundCloud.getNumSubscribers() != 0) {pcl::toROSMsg(*groundCloud, laserCloudTemp);laserCloudTemp.header.stamp = cloudHeader.stamp;laserCloudTemp.header.frame_id = "base_link";pubGroundCloud.publish(laserCloudTemp);}// segmented cloud without groundif (pubSegmentedCloudPure.getNumSubscribers() != 0) {pcl::toROSMsg(*segmentedCloudPure, laserCloudTemp);laserCloudTemp.header.stamp = cloudHeader.stamp;laserCloudTemp.header.frame_id = "base_link";pubSegmentedCloudPure.publish(laserCloudTemp);}// projected full cloud infoif (pubFullInfoCloud.getNumSubscribers() != 0) {pcl::toROSMsg(*fullInfoCloud, laserCloudTemp);laserCloudTemp.header.stamp = cloudHeader.stamp;laserCloudTemp.header.frame_id = "base_link";pubFullInfoCloud.publish(laserCloudTemp);}}
resetParameters:清空成员变量,准备下一次的处理。
…详情请参照古月居
SC-LEGO-LOAM 扩展以及深度解析相关推荐
- 深度解析ASP.NET2.0中的Callback机制
callback的一般使用方法还算简单,直接参照msdn的帮助和范例就足够了.但是想要真正用好.用精,或者想开发一些基于callback机制的WEB组件,那么,就要先深入了解callback的实现机制 ...
- Docker 1.7.0 深度解析
6月16日,Docker 1.7.0 发布,重磅炸弹在Docker圈引起巨大轰动,同时也为6月22日在旧金山举办的DockerCon大会献礼. show_meitu_1.jpg 早在Docker 1. ...
- 推荐:微服务架构的深度解析!
通过采用微服务架构,企业最大的收益是帮助内部IT建设沿着可演进的方向发展.支持灵活扩展.降低运维成本.快速响应业务变化. 这些底层技术能力的提升让业务更加敏捷.成本可控,企业也可以从中获得技术红利和市 ...
- Kafka深度解析(如何在producer中指定partition)(转)
原文链接:Kafka深度解析 背景介绍 Kafka简介 Kafka是一种分布式的,基于发布/订阅的消息系统.主要设计目标如下: 以时间复杂度为O(1)的方式提供消息持久化能力,即使对TB级以上数据也能 ...
- dubbo源码深度解析_Spring源码深度解析:手把手教你搭建Spring开发环境
Spring环境搭建流程,如果是第一次接触spring源码的环境搭建,确实还是比较麻烦的. 作者使用的编译器为目前流行的lntelliJ IDEA,版本为2018旗舰版.Eclipse用户还需要自己揣 ...
- Hologres揭秘:深度解析高效率分布式查询引擎
简介:从阿里集团诞生到云上商业化,随着业务的发展和技术的演进,Hologres也在持续不断优化核心技术竞争力,为了让大家更加了解Hologres,我们计划持续推出Hologers底层技术原理揭秘系列, ...
- 深度解析数据湖存储方案Lakehouse架构
简介:从数据仓库.数据湖的优劣势,湖仓一体架构的应用和优势等多方面深度解析Lakehouse架构. 作者:张泊 Databricks 软件工程师 Lakehouse由lake和house两个词组合而成 ...
- (转载)(收藏)OceanBase深度解析
一.OceanBase不需要高可靠服务器和高端存储 OceanBase是关系型数据库,包含内核+OceanBase云平台(OCP).与传统关系型数据库相比,最大的不同点, 是OceanBase是分布式 ...
- 深度解析 | K8S API Server之请求处理
对Kubernetes API 请求流程进行解析.后续还将对API Server的存储和扩展点等主题进行介绍.本篇是Kubernetes API Server系列第三篇. 请求和处理流程 在介绍 ...
最新文章
- html5 制作风车,[网页设计]html5 requestAnimationFrame制作动画:旋转风车
- 分布式红锁的waitTime的设计原理
- android开发那些事儿(二)--Drawable资源
- 玩转Eclipse1--基本知识与配置
- 使用systemd来构建你的服务
- 摩尔吧 FPGA培训
- Scala(一):概述
- Android 开机速度优化-----ART 预先优化
- 千锋python培训班怎么样
- SpringClude核心组件之Eureka
- 在树莓派上做一个远程控制的小车(基于Python)
- 计算机上计算器不见,win10系统自带的计算器不见了的处理教程
- mysql将数据库表内容(表内字段/属性)导出为excel表格
- mysql强行结束程_如何强制结束进程?
- PySerial学习系列1--serial.tools
- 微信小程序前端调用python后端的模型
- MVC简单学习以及浅显理解
- 办公室搬迁的注意事项.办公室文件的整理和打包方法有哪些
- linux挂载opt磁盘,centos挂载硬盘到opt
- 五一假期不出门,宅在家躺着做梦,哎~就是玩儿
热门文章
- 艺赛旗(RPA)使用 opencv 进行图片颜色识别
- ChatGPT强势加入芯片设计!不用学专业硬件描述语言了,说人话就行
- 树状数组 数据结构详解与模板(可能是最详细的了)
- Word控件Spire.Doc 【页面背景】教程(3) ;如何在 C# 中设置单词段落底纹
- RK3326 RK817 codec左右声道反
- 局部遮荫光伏matlab,单极式光伏并网发电系统的仿真与实验研究
- 计算机仿真技术相关论文,计算机仿真技术论文.doc
- 数据分析思维分析方法和业务知识——分析方法(一)
- php 背单词系统_完美起航
- 解决mei_me 0000:00:16.0:initialization failed.错误