Lightweight and Ground-Optimized Lidar Odometry and Mapping on Variable Terrain
这篇论文中主要针对小型嵌入式系统(轮式小车)来优化 LOAM 算法,从而得到高质量的六自由度的位姿。
SLAM 优化 https://www.zhihu.com/question/40829091
LOAM简介:
LeGO-LOAM 主要就是为了解决具有轻量级以及对地面情况的嵌入式轮式小车:
系统从激光雷达以 10 Hz 的频率得到激光点云,再以同样频率输出六自由度位姿。系统整体分为 5 个模块:
Segmentation:接收点云并将其投影至一个距离图像中进行识别
Feature Extraction:对识别好的点云进行角点和平面点的提取
Lidar Odometry:利用提取出的特征进行前后两帧点云位姿之间的变化
Lidar Mapping:同样接受提取出的特征并将其插入到全局点云地图中
Transform Integration:将里程计和建图模块输出的位姿进行融合来生成最终位姿
核心点:点云分割
t
时刻获取的点云,点云首先被投影到距离图像上,图像分辨率为 1800 × 16 1800\times16 1800×16(VLP-16 具有水平和垂直角分辨率分别为 0.2°和2°), P t P_t Pt中的每个有效点在图像中占据一个像素,每个点的像素值为该点到传感器的欧氏距离。平坦角度计算:
整体节点示意图:
该篇最大特点是 点云的处理,因此为核心
激光回调函数为:
// ros 点云转换为 pcl 格式
copyPointCloud(laserCloudMsg);
// 计算激光的起始角度(在图像上,从左到右变顺时针了)
findStartEndAngle();
// 确定每个点 行列值
projectPointCloud();
// 地面移除
groundRemoval();
// 点云分割
cloudSegmentation();
// 发布点云
publishCloud();
resetParameters();
rowIdn = (verticalAngle + ang_bottom) / ang_res_y
columnIdn = -round((horizonAngle-90.0)/ang_res_x) + Horizon_SCAN/2
(float)rowIdn + (float)columnIdn / 10000.0;
index = columnIdn + rowIdn * Horizon_SCAN;
/**
* @brief 分割点云
*/
void cloudSegmentation(){
// 按照列行遍历 每个点云
for (size_t i = 0; i < N_SCAN; ++i)
for (size_t j = 0; j < Horizon_SCAN; ++j)
// 如果labelMat[i][j]=0,表示没有对该点进行过分类,需要对该点进行聚类
if (labelMat.at<int>(i,j) == 0)
labelComponents(i, j);
int sizeOfSegCloud = 0;
for (size_t i = 0; i < N_SCAN; ++i) {
// segMsg.startRingIndex[i],segMsg.endRingIndex[i]
// 表示第i线的点云起始序列和终止序列,开始4点和末尾6点舍去不要
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){
// labelMat数值为999999表示这个点是因为聚类数量不够30而被舍弃的点
// 需要舍弃的点直接continue跳过本次循环,
// 当列数为5的倍数,并且行数较大,可以认为非地面点的,将它保存进异常点云(界外点云)中
// 然后再跳过本次循环
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;
}
}
// 如果是地面点,对于列数不为5的倍数的,直接跳过不处理
if (groundMat.at<int8_t>(i,j) == 1){
if (j%5!=0 && j>5 && j<Horizon_SCAN-5)
continue;
}
// 上面多个if语句已经去掉了不符合条件的点,这部分直接进行信息的拷贝和保存操作
// 保存完毕后sizeOfSegCloud递增
segMsg.segmentedCloudGroundFlag[sizeOfSegCloud] = (groundMat.at<int8_t>(i,j) == 1);
segMsg.segmentedCloudColInd[sizeOfSegCloud] = j;
segMsg.segmentedCloudRange[sizeOfSegCloud] = rangeMat.at<float>(i,j);
segmentedCloud->push_back(fullCloud->points[j + i*Horizon_SCAN]);
++sizeOfSegCloud;
}
}
segMsg.endRingIndex[i] = sizeOfSegCloud-1 - 5;
}
// 如果有节点订阅SegmentedCloudPure,
// 那么把点云数据保存到segmentedCloudPure中去
if (pubSegmentedCloudPure.getNumSubscribers() != 0){
for (size_t i = 0; i < N_SCAN; ++i){
for (size_t j = 0; j < Horizon_SCAN; ++j){
// 需要选择不是地面点(labelMat[i][j]!=-1)和没被舍弃的点
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);
}
}
}
}
}
// 图像中的 每一行 每一列 的 index
void labelComponents(int row, int col){
float d1, d2, alpha, angle;
int fromIndX, fromIndY, thisIndX, thisIndY;
// 行标记
bool lineCountFlag[N_SCAN] = {false};
// queueIndX、allPushedIndX 有一个就够了啊
// 队列下标: 行,列
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){ // 队列个数不为空时执行
// 取第 0 个队列的下标
fromIndX = queueIndX[queueStartInd];
fromIndY = queueIndY[queueStartInd];
--queueSize; // 查询队列 -1
++queueStartInd; // 队列的开始下表 加1
// labelCount的初始值为1,后面会递增
labelMat.at<int>(fromIndX, fromIndY) = labelCount;
// neighbor=[[-1,0];[0,1];[0,-1];[1,0]]
// 遍历点[fromIndX,fromIndY]边上的四个邻点
for (auto iter = neighborIterator.begin(); iter != neighborIterator.end(); ++iter){
// 周围某点的 坐标
thisIndX = fromIndX + (*iter).first;
thisIndY = fromIndY + (*iter).second;
// 竖直不在该图像上 跳过
if (thisIndX < 0 || thisIndX >= N_SCAN)
continue;
// 水平超出阈值时 首尾相连
if (thisIndY < 0)
thisIndY = Horizon_SCAN - 1;
if (thisIndY >= Horizon_SCAN)
thisIndY = 0;
// 如果点[thisIndX,thisIndY]已经标记过
// labelMat中,-1代表地面点,0代表未进行标记过,其余为其他的标记
// 如果当前的邻点已经标记过,则跳过该点。不是地面点为什么跳过???
// 如果labelMat已经标记为正整数,则已经聚类完成,不需要再次对该点聚类
if (labelMat.at<int>(thisIndX, thisIndY) != 0)
continue;
// 取当前点和周围点的 最大最小距离
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));
// alpha代表角度分辨率,
// X方向上角度分辨率是segmentAlphaX(rad)
// 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)));
// segmentTheta=1.0472<==>60度
// 如果算出角度大于60度,则假设这是个平面
if (angle > segmentTheta){
// 当前点放入队列,队列个数加1,队列结束加1
queueIndX[queueEndInd] = thisIndX;
queueIndY[queueEndInd] = thisIndY;
++queueSize;
++queueEndInd;
// 当前点标记设为 labelCount
labelMat.at<int>(thisIndX, thisIndY) = labelCount;
lineCountFlag[thisIndX] = true;
// allput更新
allPushedIndX[allPushedIndSize] = thisIndX;
allPushedIndY[allPushedIndSize] = thisIndY;
++allPushedIndSize;
}
}
}
bool feasibleSegment = false;
// 如果聚类超过30个点,直接标记为一个可用聚类,labelCount需要递增
if (allPushedIndSize >= 30)
feasibleSegment = true;
else if (allPushedIndSize >= segmentValidPointNum){
// 如果聚类点数小于30大于等于5,统计竖直方向上的聚类点数
int lineCount = 0;
for (size_t i = 0; i < N_SCAN; ++i)
if (lineCountFlag[i] == true)
++lineCount;
// 竖直方向上超过3个也将它标记为有效聚类
if (lineCount >= segmentValidLineNum)
feasibleSegment = true;
}
if (feasibleSegment == true){
++labelCount;
}else{
for (size_t i = 0; i < allPushedIndSize; ++i){
// 标记为999999的是需要舍弃的聚类的点,因为他们的数量小于30个
labelMat.at<int>(allPushedIndX[i], allPushedIndY[i]) = 999999;
}
}
}
主函数:
首先构造了 FeatureAssociation
对象
其次该主函数与其他不同,它使用了 ros::spin0nce()
按照200hz的频率 一直循环查询
调用了 runFeatureAssociation()
三种点云都未使用 互斥锁
laserCloudHandler
segmented_cloud
1segmentedCloud
laserCloudInfoHandler
segmented_cloud_info
1segInfo
outlierCloudHandler
outlier_cloud
1outlierCloud
topic:imuTopic
50
imu 数据 去重力,并进行坐标转换
接收 imu 角度四元素得到: roll,pitch,yaw
基于 roll,pitch 将加速度的重力方向去除,并进行坐标转换
x = y y=z z=x
将欧拉角,加速度,速度保存到循环队列中:
对速度,角速度,加速度进行积分,得到位移,角度和速度 AccumulateIMUShiftAndRotation
遍历所有激光点,做如下操作:
坐标转换,跟laboshin_loam代码中的一样经过坐标轴变换
point.x = y point.y = z point.z =x
计算偏航角 yaw: -atan2(p.x,p.z)==>-atan2(y,x)
,
计算与起始点的时间差 relTime
利用imu去畸变,先时间轴对齐,后进行去畸变
去完畸变赋值
遍历平面特征,计算dist,法向量等
坐标变换到开始时刻,TransformToStart
若迭代次数对5取余为0,找该特征最近的3个平面特征
计算法向量和误差距离,影响因子
未经变换的点放入laserCloudOri队列,距离,法向量值放入coeffSel
打印时加 \033[1;32m---->\033[0m 可以显示颜色,跟python一样
主函数:
mapOptimization
对象laserCloudOutlierLastHandler
laserCloudCornerLastHandler
laserCloudSurfLastHandler
transformSum
rpy xyz闭环线程
若不是能闭环线程时,直接 return
loopClosureEnableFlag == false
while 循环,1s周期
performLoopClosure()
potentialLoopFlag == false
pcl::IterativeClosestPoint
,最大距离100,最大迭代100,icp.align(*unused_result);
icp.getFinalTransformation();
noiseModel::Diagonal::Variances(Vector6)
isam->update(gtSAMgraph);
isam->update();
找出机器人位姿临近的关键帧
找到最近历史帧ID
若 无最近历史帧,则return
取系统中最近关键帧点云
latestSurfKeyFrameCloud
取临近历史帧周围的帧的特征点组成submap
nearHistorySurfKeyFrameCloud
历史帧submap 进行降采样 [0.4,0.4,0.4]
若有订阅,则发布 历史帧submap
都有新帧 且 各个激光数据与里程计时间间隔 < 5ms 时,才执行,否则return
距离上次 因子图优化 <300ms时,直接 return
执行:
// 将坐标转移到世界坐标系下,得到可用于建图的Lidar坐标,即修改了transformTobeMapped的值
transformAssociateToMap();
// 提取周围关键帧点,也可能是回环的
extractSurroundingKeyFrames();
// 降采样当前帧
downsampleCurrentScan();
// 优化求解
scan2MapOptimization();
// 保存关键帧
saveKeyFramesAndFactor();
correctPoses();
publishTF();
publishKeyPosesAndFrames();
clearCloud();
cloudKeyPoses3D->points.empty() == true
recentCornerCloudKeyFrames
保存的点云数量小于预设值时
recentCornerCloudKeyFrames
数量符合要求
surroundingKeyPoses
,并进行降采样isam->update(gtSAMgraph, initialEstimate);
isam->update();
isam
得到 当前估计值融合激光里程计 和 地图匹配的结果
主函数:
odomAftMappedHandler
函数获取精配准后的位姿作为transformAftMapped
,而获取配准后的速度信息作为transformBefMapped
准备下一次计算。
void odomAftMappedHandler(const nav_msgs::Odometry::ConstPtr& odomAftMapped)
{
double roll, pitch, yaw;
geometry_msgs::Quaternion geoQuat = odomAftMapped->pose.pose.orientation;
tf::Matrix3x3(tf::Quaternion(geoQuat.z, -geoQuat.x, -geoQuat.y, geoQuat.w)).getRPY(roll, pitch, yaw);
//位姿作为计算的基础
transformAftMapped[0] = -pitch;
transformAftMapped[1] = -yaw;
transformAftMapped[2] = roll;
transformAftMapped[3] = odomAftMapped->pose.pose.position.x;
transformAftMapped[4] = odomAftMapped->pose.pose.position.y;
transformAftMapped[5] = odomAftMapped->pose.pose.position.z;
//速度作为下一次计算的先验
transformBefMapped[0] = odomAftMapped->twist.twist.angular.x;
transformBefMapped[1] = odomAftMapped->twist.twist.angular.y;
transformBefMapped[2] = odomAftMapped->twist.twist.angular.z;
transformBefMapped[3] = odomAftMapped->twist.twist.linear.x;
transformBefMapped[4] = odomAftMapped->twist.twist.linear.y;
transformBefMapped[5] = odomAftMapped->twist.twist.linear.z;
}
laserOdometryHandler
是将粗配准的里程计信息与精配准的里程计信息融合计算,并在回调函数中便发送了最终外发的里程计话题。在该回调函数中的TF
与里程计话题才是最终决定的。void laserOdometryHandler(const nav_msgs::Odometry::ConstPtr& laserOdometry)
{
currentHeader = laserOdometry->header;
double roll, pitch, yaw;
geometry_msgs::Quaternion geoQuat = laserOdometry->pose.pose.orientation;
tf::Matrix3x3(tf::Quaternion(geoQuat.z, -geoQuat.x, -geoQuat.y, geoQuat.w)).getRPY(roll, pitch, yaw);
transformSum[0] = -pitch;
transformSum[1] = -yaw;
transformSum[2] = roll;
transformSum[3] = laserOdometry->pose.pose.position.x;
transformSum[4] = laserOdometry->pose.pose.position.y;
transformSum[5] = laserOdometry->pose.pose.position.z;
//点云坐标转化到世界坐标
//位姿与速度的融合计算
transformAssociateToMap();
geoQuat = tf::createQuaternionMsgFromRollPitchYaw
(transformMapped[2], -transformMapped[0], -transformMapped[1]);
laserOdometry2.header.stamp = laserOdometry->header.stamp;
laserOdometry2.pose.pose.orientation.x = -geoQuat.y;
laserOdometry2.pose.pose.orientation.y = -geoQuat.z;
laserOdometry2.pose.pose.orientation.z = geoQuat.x;
laserOdometry2.pose.pose.orientation.w = geoQuat.w;
laserOdometry2.pose.pose.position.x = transformMapped[3];
laserOdometry2.pose.pose.position.y = transformMapped[4];
laserOdometry2.pose.pose.position.z = transformMapped[5];
pubLaserOdometry2.publish(laserOdometry2);
laserOdometryTrans2.stamp_ = laserOdometry->header.stamp;
laserOdometryTrans2.setRotation(tf::Quaternion(-geoQuat.y, -geoQuat.z, geoQuat.x, geoQuat.w));
laserOdometryTrans2.setOrigin(tf::Vector3(transformMapped[3], transformMapped[4], transformMapped[5]));
tfBroadcaster2.sendTransform(laserOdometryTrans2);
}