在研读了论文及开源代码后,对LOAM的一些理解做一个整理。

文章:Low-drift and real-time lidar odometry and mapping

开源代码:https://github.com/daobilige-su/loam_velodyne

系统概述

LOAM的整体思想就是将复杂的SLAM问题分为:1. 高频的运动估计; 2. 低频的环境建图。

Lidar接收数据,首先进行Point Cloud Registration,Lidar Odometry以10Hz的频率进行运动估计和坐标转换,Lidar Mapping以1Hz的频率构建三维地图,Transform Integration完成位姿的优化。这样并行的结构保证了系统的实时性。

接下来是代码的框架图:

整个算法分为四个模块,相对于其它直接匹配两个点云的算法,LOAM是通过提取特征点进行匹配之后计算坐标变换。具体流程为:ScanRegistration 提取特征点并排除瑕点;LaserOdometry从特征点中估计运动,然后整合数据发送给LaserMapping;LaserMapping输出的laser_cloud_surround为地图;TransformMaintenance订阅LaserOdometry与LaserMapping发布的Odometry消息,对位姿进行融合优化。后面将详细进行说明。

ScanRegistration

这一模块(节点)主要功能是:特征点的提取

一次扫描的点通过曲率值来分类,特征点曲率大于阈值的为边缘点;特征点曲率小于阈值的为平面点。为了使特征点均匀的分布在环境中,将一次扫描划分为4个独立的子区域。每个子区域最多提供2个边缘点和4个平面点。此外,将不稳定的特征点(瑕点)排除。下面将通过代码进行说明。

从主函数开始:

int main(int argc, char** argv)
{ros::init(argc, argv, "scanRegistration");/** NodeHandle 是节点同ROS系统交流的主要接口* NodeHandle 在构造的时候会完整地初始化本节点 * NodeHandle 析构的时候会关闭此节点*/ros::NodeHandle nh;/** 参数1:话题名称    * 参数2:信息队列长度    * 参数3:回调函数,每当一个信息到来的时候,这个函数会被调用    * 返回一个ros::Subscriber类的对象,当此对象的所有引用都被销毁是,本节点将不再是该话题的订阅者 */   // 订阅了velodyne_points和imu/dataros::Subscriber subLaserCloud = nh.subscribe<sensor_msgs::PointCloud2> ("/velodyne_points", 2, laserCloudHandler);  ros::Subscriber subImu = nh.subscribe<sensor_msgs::Imu> ("/imu/data", 50, imuHandler);/** 我们通过advertise() 函数指定我们如何在给定的topic上发布信息* 它会触发对ROS master的调用,master会记录话题发布者和订阅者* 在advertise()函数执行之后,master会通知每一个订阅此话题的节点* 两节点间由此可以建立直接的联系* advertise()会返回一个Publisher对象,使用这个对象的publish方法我们就可以在此话题上发布信息* 当返回的Publisher对象的所有引用都被销毁的时候,本节点将不再是该话题的发布者* 此函数是一个带模板的函数,需要传入具体的类型进行实例化* 传入的类型就是要发布的信息的类型,在这里是String* 第一个参数是话题名称* 第二个参数是信息队列的长度,相当于信息的一个缓冲区* 在我们发布信息的速度大于处理信息的速度时* 信息会被缓存在先进先出的信息队列里*/// 发布了6个话题:velodyne_cloud_2、laser_cloud_sharp、laser_cloud_flat、laser_cloud_less_flat、laser_cloud_less_sharp、imu_transpubLaserCloud = nh.advertise<sensor_msgs::PointCloud2>("/velodyne_cloud_2", 2);pubCornerPointsSharp = nh.advertise<sensor_msgs::PointCloud2>("/laser_cloud_sharp", 2);pubCornerPointsLessSharp = nh.advertise<sensor_msgs::PointCloud2>("/laser_cloud_less_sharp", 2);pubSurfPointsFlat = nh.advertise<sensor_msgs::PointCloud2>("/laser_cloud_flat", 2);pubSurfPointsLessFlat = nh.advertise<sensor_msgs::PointCloud2>("/laser_cloud_less_flat", 2);pubImuTrans = nh.advertise<sensor_msgs::PointCloud2> ("/imu_trans", 5);/*** 它可以保证你指定的回调函数会被调用* 程序执行到spin()后就不调用其他语句了*/ros::spin();return 0;
}
回调函数,每当一个信息到来的时候,这个函数会被调用    * 返回一个ros::Subscriber类的对象,当此对象的所有引用都被销毁是,本节点将不再是该话题的订阅者 */   // 订阅了velodyne_points和imu/dataros::Subscriber subLaserCloud = nh.subscribe<sensor_msgs::PointCloud2> ("/velodyne_points", 2, laserCloudHandler);  ros::Subscriber subImu = nh.subscribe<sensor_msgs::Imu> ("/imu/data", 50, imuHandler);/** 我们通过advertise() 函数指定我们如何在给定的topic上发布信息* 它会触发对ROS master的调用,master会记录话题发布者和订阅者* 在advertise()函数执行之后,master会通知每一个订阅此话题的节点* 两节点间由此可以建立直接的联系* advertise()会返回一个Publisher对象,使用这个对象的publish方法我们就可以在此话题上发布信息* 当返回的Publisher对象的所有引用都被销毁的时候,本节点将不再是该话题的发布者* 此函数是一个带模板的函数,需要传入具体的类型进行实例化* 传入的类型就是要发布的信息的类型,在这里是String* 第一个参数是话题名称* 第二个参数是信息队列的长度,相当于信息的一个缓冲区* 在我们发布信息的速度大于处理信息的速度时* 信息会被缓存在先进先出的信息队列里*/// 发布了6个话题:velodyne_cloud_2、laser_cloud_sharp、laser_cloud_flat、laser_cloud_less_flat、laser_cloud_less_sharp、imu_transpubLaserCloud = nh.advertise<sensor_msgs::PointCloud2>("/velodyne_cloud_2", 2);pubCornerPointsSharp = nh.advertise<sensor_msgs::PointCloud2>("/laser_cloud_sharp", 2);pubCornerPointsLessSharp = nh.advertise<sensor_msgs::PointCloud2>("/laser_cloud_less_sharp", 2);pubSurfPointsFlat = nh.advertise<sensor_msgs::PointCloud2>("/laser_cloud_flat", 2);pubSurfPointsLessFlat = nh.advertise<sensor_msgs::PointCloud2>("/laser_cloud_less_flat", 2);pubImuTrans = nh.advertise<sensor_msgs::PointCloud2> ("/imu_trans", 5);/*** 它可以保证你指定的回调函数会被调用* 程序执行到spin()后就不调用其他语句了*/ros::spin();return 0;
}

主函数比较简单,订阅了2个节点和发布了6个节点。通过回调函数的处理,将处理后的点云重新发出去。主要涉及的函数为

laserCloudHandler与imuHandler。

·laserHandler

laserCloudHandler是这一模块的重点部分,主要功能是对接收到的点云进行预处理,完成分类。具体分类内容为:一是将点云划入不同线中存储;二是对其进行特征分类。

首先对收到的点云进行处理

void laserCloudHandler(const sensor_msgs::PointCloud2ConstPtr& laserCloudMsg)
{if (!systemInited) {systemInitCount++;if (systemInitCount >= systemDelay) {// systemDelay 有延时作用,保证有imu数据后在调用laserCloudHandlersystemInited = true;}return;}std::vector<int> scanStartInd(N_SCANS, 0);std::vector<int> scanEndInd(N_SCANS, 0);// Lidar的时间戳double timeScanCur = laserCloudMsg->header.stamp.toSec();pcl::PointCloud<pcl::PointXYZ> laserCloudIn;// fromROSmsg(input,cloud1) 转为为模板点云laserCloudInpcl::fromROSMsg(*laserCloudMsg, laserCloudIn);std::vector<int> indices;//去除无效值pcl::removeNaNFromPointCloud(laserCloudIn, laserCloudIn, indices);int cloudSize = laserCloudIn.points.size();//计算点云的起始角度/终止角度float startOri = -atan2(laserCloudIn.points[0].y, laserCloudIn.points[0].x);float endOri = -atan2(laserCloudIn.points[cloudSize - 1].y,laserCloudIn.points[cloudSize - 1].x) + 2 * M_PI;

接下来的处理是根据角度将点划入不同数组中

  for (int i = 0; i < cloudSize; i++) {point.x = laserCloudIn.points[i].y;point.y = laserCloudIn.points[i].z;point.z = laserCloudIn.points[i].x;float angle = atan(point.y / sqrt(point.x * point.x + point.z * point.z)) * 180 / M_PI;int scanID;int roundedAngle = int(angle + (angle<0.0?-0.5:+0.5)); if (roundedAngle > 0){scanID = roundedAngle;}else {// 角度大于0,由小到大划入偶数线0-16;角度小于0,由大到小划入奇数线15-1scanID = roundedAngle + (N_SCANS - 1);}if (scanID > (N_SCANS - 1) || scanID < 0 ){// 不在16线附近的点作为杂点进行剔除count--;continue;}

接下来计算每个点的相对方位角计算出相对时间,根据线性插值的方法计算速度及角度,并转换到sweep k的初始imu坐标系下,再划入16线数组中

  float ori = -atan2(point.x, point.z);if (!halfPassed) {if (ori < startOri - M_PI / 2) {ori += 2 * M_PI;} else if (ori > startOri + M_PI * 3 / 2) {ori -= 2 * M_PI;}if (ori - startOri > M_PI) {halfPassed = true;}} else {ori += 2 * M_PI;if (ori < endOri - M_PI * 3 / 2) {ori += 2 * M_PI;} else if (ori > endOri + M_PI / 2) {ori -= 2 * M_PI;} }//scanPeriod=0.1,是因为lidar工作周期是10HZ,意味着转一圈是0.1秒;intensity是一个整数+小数,小数不会超过0.1,完成了按照时间排序的需求float relTime = (ori - startOri) / (endOri - startOri);point.intensity = scanID + scanPeriod * relTime;// imuPointerLast 是当前点,变量只在imu中改变,设为t时刻// 对每一个cloud point处理if (imuPointerLast >= 0) {float pointTime = relTime * scanPeriod;while (imuPointerFront != imuPointerLast) {// (timeScanCur + pointTime)设为ti时刻;imuPointerFront 是 ti后一个时刻if (timeScanCur + pointTime < imuTime[imuPointerFront]) {break;}imuPointerFront = (imuPointerFront + 1) % imuQueLength;}if (timeScanCur + pointTime > imuTime[imuPointerFront]) {// 这个的意思是 imuPointerFront=imuPointerLast时候imuRollCur = imuRoll[imuPointerFront];imuPitchCur = imuPitch[imuPointerFront];imuYawCur = imuYaw[imuPointerFront];imuVeloXCur = imuVeloX[imuPointerFront];imuVeloYCur = imuVeloY[imuPointerFront];imuVeloZCur = imuVeloZ[imuPointerFront];imuShiftXCur = imuShiftX[imuPointerFront];imuShiftYCur = imuShiftY[imuPointerFront];imuShiftZCur = imuShiftZ[imuPointerFront];} else {// imuPointerBack = imuPointerFront - 1 线性插值求解出当前点对应的imu角度,位移和速度int imuPointerBack = (imuPointerFront + imuQueLength - 1) % imuQueLength;float ratioFront = (timeScanCur + pointTime - imuTime[imuPointerBack]) / (imuTime[imuPointerFront] - imuTime[imuPointerBack]);float ratioBack = (imuTime[imuPointerFront] - timeScanCur - pointTime) / (imuTime[imuPointerFront] - imuTime[imuPointerBack]);imuRollCur = imuRoll[imuPointerFront] * ratioFront + imuRoll[imuPointerBack] * ratioBack;imuPitchCur = imuPitch[imuPointerFront] * ratioFront + imuPitch[imuPointerBack] * ratioBack;if (imuYaw[imuPointerFront] - imuYaw[imuPointerBack] > M_PI) {imuYawCur = imuYaw[imuPointerFront] * ratioFront + (imuYaw[imuPointerBack] + 2 * M_PI) * ratioBack;} else if (imuYaw[imuPointerFront] - imuYaw[imuPointerBack] < -M_PI) {imuYawCur = imuYaw[imuPointerFront] * ratioFront + (imuYaw[imuPointerBack] - 2 * M_PI) * ratioBack;} else {imuYawCur = imuYaw[imuPointerFront] * ratioFront + imuYaw[imuPointerBack] * ratioBack;}imuVeloXCur = imuVeloX[imuPointerFront] * ratioFront + imuVeloX[imuPointerBack] * ratioBack;imuVeloYCur = imuVeloY[imuPointerFront] * ratioFront + imuVeloY[imuPointerBack] * ratioBack;imuVeloZCur = imuVeloZ[imuPointerFront] * ratioFront + imuVeloZ[imuPointerBack] * ratioBack;imuShiftXCur = imuShiftX[imuPointerFront] * ratioFront + imuShiftX[imuPointerBack] * ratioBack;imuShiftYCur = imuShiftY[imuPointerFront] * ratioFront + imuShiftY[imuPointerBack] * ratioBack;imuShiftZCur = imuShiftZ[imuPointerFront] * ratioFront + imuShiftZ[imuPointerBack] * ratioBack;}if (i == 0) {imuRollStart = imuRollCur;imuPitchStart = imuPitchCur;imuYawStart = imuYawCur;imuVeloXStart = imuVeloXCur;imuVeloYStart = imuVeloYCur;imuVeloZStart = imuVeloZCur;imuShiftXStart = imuShiftXCur;imuShiftYStart = imuShiftYCur;imuShiftZStart = imuShiftZCur;} else {// 将Lidar位移转到IMU起始坐标系下ShiftToStartIMU(pointTime);// 将Lidar运动速度转到IMU起始坐标系下VeloToStartIMU();// 将点坐标转到起始IMU坐标系下TransformToStartIMU(&point);}}// 将点按照每一层线,分类压入16个数组中laserCloudScans[scanID].push_back(point);}

之后将对所有点进行曲率值的计算并记录每一层曲率数组的起始和终止

cloudSize = count;pcl::PointCloud<PointType>::Ptr laserCloud(new pcl::PointCloud<PointType>());//将所有点存入laserCloud中,点按线序进行排列for (int i = 0; i < N_SCANS; i++) {*laserCloud += laserCloudScans[i];}int scanCount = -1;for (int i = 5; i < cloudSize - 5; i++) {// 对所有的激光点一个一个求出在该点前后5个点(10点)的偏差,作为cloudCurvature点云数据的曲率float diffX = laserCloud->points[i - 5].x + laserCloud->points[i - 4].x + laserCloud->points[i - 3].x + laserCloud->points[i - 2].x + laserCloud->points[i - 1].x - 10 * laserCloud->points[i].x + laserCloud->points[i + 1].x + laserCloud->points[i + 2].x+ laserCloud->points[i + 3].x + laserCloud->points[i + 4].x+ laserCloud->points[i + 5].x;float diffY = laserCloud->points[i - 5].y + laserCloud->points[i - 4].y + laserCloud->points[i - 3].y + laserCloud->points[i - 2].y + laserCloud->fpoints[i - 1].y - 10 * laserCloud->points[i].y + laserCloud->points[i + 1].y + laserCloud->points[i + 2].y+ laserCloud->points[i + 3].y + laserCloud->points[i + 4].y+ laserCloud->points[i + 5].y;float diffZ = laserCloud->points[i - 5].z + laserCloud->points[i - 4].z + laserCloud->points[i - 3].z + laserCloud->points[i - 2].z + laserCloud->points[i - 1].z - 10 * laserCloud->points[i].z + laserCloud->points[i + 1].z + laserCloud->points[i + 2].z+ laserCloud->points[i + 3].z + laserCloud->points[i + 4].z+ laserCloud->points[i + 5].z;cloudCurvature[i] = diffX * diffX + diffY * diffY + diffZ * diffZ;cloudSortInd[i] = i;cloudNeighborPicked[i] = 0;cloudLabel[i] = 0;if (int(laserCloud->points[i].intensity) != scanCount) {scanCount = int(laserCloud->points[i].intensity);//scanCount=scanID// 记录每一层起始点和终止点的位置,需要根据这个起始/终止来操作点云曲率,在求曲率的过程中已经去除了前5个点和后5个点if (scanCount > 0 && scanCount < N_SCANS) {scanStartInd[scanCount] = i + 5;scanEndInd[scanCount - 1] = i - 5;}}}

接下来,对提到的两种瑕点进行排除

 for (int i = 5; i < cloudSize - 6; i++) {float diffX = laserCloud->points[i + 1].x - laserCloud->points[i].x;float diffY = laserCloud->points[i + 1].y - laserCloud->points[i].y;float diffZ = laserCloud->points[i + 1].z - laserCloud->points[i].z;float diff = diffX * diffX + diffY * diffY + diffZ * diffZ;if (diff > 0.1) {float depth1 = sqrt(laserCloud->points[i].x * laserCloud->points[i].x + laserCloud->points[i].y * laserCloud->points[i].y +laserCloud->points[i].z * laserCloud->points[i].z);float depth2 = sqrt(laserCloud->points[i + 1].x * laserCloud->points[i + 1].x + laserCloud->points[i + 1].y * laserCloud->points[i + 1].y +laserCloud->points[i + 1].z * laserCloud->points[i + 1].z);/*— 针对论文的(b)情况,两向量夹角小于某阈值b时(夹角小就可能存在遮挡),将其一侧的临近6个点设为不可标记为特征点的点 —*//*— 构建了一个等腰三角形的底向量,根据等腰三角形性质,判断X[i]向量与X[i+1]的夹角小于5.732度(threshold=0.1) —*//*— depth1>depth2 X[i+1]距离更近,远侧点标记不特征;depth1<depth2 X[i]距离更近,远侧点标记不特征 —*/if (depth1 > depth2) {diffX = laserCloud->points[i + 1].x - laserCloud->points[i].x * depth2 / depth1;diffY = laserCloud->points[i + 1].y - laserCloud->points[i].y * depth2 / depth1;diffZ = laserCloud->points[i + 1].z - laserCloud->points[i].z * depth2 / depth1;if (sqrt(diffX * diffX + diffY * diffY + diffZ * diffZ) / depth2 < 0.1) {cloudNeighborPicked[i - 5] = 1;cloudNeighborPicked[i - 4] = 1;cloudNeighborPicked[i - 3] = 1;cloudNeighborPicked[i - 2] = 1;cloudNeighborPicked[i - 1] = 1;cloudNeighborPicked[i] = 1;}} else {diffX = laserCloud->points[i + 1].x * depth1 / depth2 - laserCloud->points[i].x;diffY = laserCloud->points[i + 1].y * depth1 / depth2 - laserCloud->points[i].y;diffZ = laserCloud->points[i + 1].z * depth1 / depth2 - laserCloud->points[i].z;if (sqrt(diffX * diffX + diffY * diffY + diffZ * diffZ) / depth1 < 0.1) {cloudNeighborPicked[i + 1] = 1;cloudNeighborPicked[i + 2] = 1;cloudNeighborPicked[i + 3] = 1;cloudNeighborPicked[i + 4] = 1;cloudNeighborPicked[i + 5] = 1;cloudNeighborPicked[i + 6] = 1;}}}/*— 针对论文的(a)情况,当某点及其后点间的距离平方大于某阈值a(说明这两点有一定距离) ———*//*— 若某点到其前后两点的距离均大于c倍的该点深度,则该点判定为不可标记特征点的点 ———————*//*—(入射角越小,点间距越大,即激光发射方向与投射到的平面越近似水平) ———————————————*/float diffX2 = laserCloud->points[i].x - laserCloud->points[i - 1].x;float diffY2 = laserCloud->points[i].y - laserCloud->points[i - 1].y;float diffZ2 = laserCloud->points[i].z - laserCloud->points[i - 1].z;float diff2 = diffX2 * diffX2 + diffY2 * diffY2 + diffZ2 * diffZ2;float dis = laserCloud->points[i].x * laserCloud->points[i].x+ laserCloud->points[i].y * laserCloud->points[i].y+ laserCloud->points[i].z * laserCloud->points[i].z;if (diff > 0.0002 * dis && diff2 > 0.0002 * dis) {cloudNeighborPicked[i] = 1;}}

之后对平面点以及角点进行筛选

  pcl::PointCloud<PointType> cornerPointsSharp;pcl::PointCloud<PointType> cornerPointsLessSharp;pcl::PointCloud<PointType> surfPointsFlat;pcl::PointCloud<PointType> surfPointsLessFlat;for (int i = 0; i < N_SCANS; i++) {/*—— 对于每一层激光点(总16层),将每层区域分成6份,起始位置为sp,终止位置为ep。——————*//*—— 有两个循环,作用是对cloudCurvature从小到大进行排序,cloudSortedInd是它的索引数组 ————*/pcl::PointCloud<PointType>::Ptr surfPointsLessFlatScan(new pcl::PointCloud<PointType>);for (int j = 0; j < 6; j++) {int sp = (scanStartInd[i] * (6 - j)  + scanEndInd[i] * j) / 6;int ep = (scanStartInd[i] * (5 - j)  + scanEndInd[i] * (j + 1)) / 6 - 1;for (int k = sp + 1; k <= ep; k++) {for (int l = k; l >= sp + 1; l--) {if (cloudCurvature[cloudSortInd[l]] < cloudCurvature[cloudSortInd[l - 1]]) {int temp = cloudSortInd[l - 1];cloudSortInd[l - 1] = cloudSortInd[l];cloudSortInd[l] = temp;}}}/*—— 筛选特征角点 Corner: label=2; LessCorner: label=1 ————*/   int largestPickedNum = 0;for (int k = ep; k >= sp; k--) {int ind = cloudSortInd[k];if (cloudNeighborPicked[ind] == 0 &&cloudCurvature[ind] > 0.1) {largestPickedNum++;if (largestPickedNum <= 2) {cloudLabel[ind] = 2;cornerPointsSharp.push_back(laserCloud->points[ind]);cornerPointsLessSharp.push_back(laserCloud->points[ind]);} else if (largestPickedNum <= 20) {cloudLabel[ind] = 1;cornerPointsLessSharp.push_back(laserCloud->points[ind]);} else {break;}// 遍历该曲率点后,将该点标记,并将该曲率点附近的前后5个点标记不被选取为特征点cloudNeighborPicked[ind] = 1;for (int l = 1; l <= 5; l++) {float diffX = laserCloud->points[ind + l].x - laserCloud->points[ind + l - 1].x;float diffY = laserCloud->points[ind + l].y - laserCloud->points[ind + l - 1].y;float diffZ = laserCloud->points[ind + l].z - laserCloud->points[ind + l - 1].z;if (diffX * diffX + diffY * diffY + diffZ * diffZ > 0.05) {break;}cloudNeighborPicked[ind + l] = 1;}for (int l = -1; l >= -5; l--) {float diffX = laserCloud->points[ind + l].x - laserCloud->points[ind + l + 1].x;float diffY = laserCloud->points[ind + l].y - laserCloud->points[ind + l + 1].y;float diffZ = laserCloud->points[ind + l].z - laserCloud->points[ind + l + 1].z;if (diffX * diffX + diffY * diffY + diffZ * diffZ > 0.05) {break;}cloudNeighborPicked[ind + l] = 1;}}}/*—— 筛选特征平面点 Flat: label=-1 普通点和Flat点降采样形成LessFlat: label=0 ————*/int smallestPickedNum = 0;for (int k = sp; k <= ep; k++) {int ind = cloudSortInd[k];if (cloudNeighborPicked[ind] == 0 &&cloudCurvature[ind] < 0.1) {cloudLabel[ind] = -1;surfPointsFlat.push_back(laserCloud->points[ind]);smallestPickedNum++;if (smallestPickedNum >= 4) {break;}cloudNeighborPicked[ind] = 1;for (int l = 1; l <= 5; l++) {float diffX = laserCloud->points[ind + l].x - laserCloud->points[ind + l - 1].x;float diffY = laserCloud->points[ind + l].y - laserCloud->points[ind + l - 1].y;float diffZ = laserCloud->points[ind + l].z - laserCloud->points[ind + l - 1].z;if (diffX * diffX + diffY * diffY + diffZ * diffZ > 0.05) {break;}cloudNeighborPicked[ind + l] = 1;}for (int l = -1; l >= -5; l--) {float diffX = laserCloud->points[ind + l].x - laserCloud->points[ind + l + 1].x;float diffY = laserCloud->points[ind + l].y - laserCloud->points[ind + l + 1].y;float diffZ = laserCloud->points[ind + l].z - laserCloud->points[ind + l + 1].z;if (diffX * diffX + diffY * diffY + diffZ * diffZ > 0.05) {break;}cloudNeighborPicked[ind + l] = 1;}}}// surfPointsLessFlat为降采样后的flat点,采样前包含太多label=0的点for (int k = sp; k <= ep; k++) {if (cloudLabel[k] <= 0) {surfPointsLessFlatScan->push_back(laserCloud->points[k]);}}}// 降采样pcl::PointCloud<PointType> surfPointsLessFlatScanDS;pcl::VoxelGrid<PointType> downSizeFilter;downSizeFilter.setInputCloud(surfPointsLessFlatScan);downSizeFilter.setLeafSize(0.2, 0.2, 0.2);downSizeFilter.filter(surfPointsLessFlatScanDS);surfPointsLessFlat += surfPointsLessFlatScanDS;}

之后再将信息发布出去

·imuHandler

imuHandler功能为imu信息的解析:

  1. 减去重力对imu的影响
  2. 解析出当前时刻的imu时间戳,角度以及各个轴的加速度
  3. 将加速度转换到世界坐标系轴下
  4. 进行航距推算,假定为匀速运动推算出当前时刻的位置)
  5. 推算当前时刻的速度信息
void imuHandler(const sensor_msgs::Imu::ConstPtr& imuIn)
{double roll, pitch, yaw;tf::Quaternion orientation;tf::quaternionMsgToTF(imuIn->orientation, orientation);tf::Matrix3x3(orientation).getRPY(roll, pitch, yaw);// 减去重力加速度的影响float accX = imuIn->linear_acceleration.y - sin(roll) * cos(pitch) * 9.81;float accY = imuIn->linear_acceleration.z - cos(roll) * cos(pitch) * 9.81;float accZ = imuIn->linear_acceleration.x + sin(pitch) * 9.81;// 表明数组是一个长度为imuQueLength的循环数组imuPointerLast = (imuPointerLast + 1) % imuQueLength;imuTime[imuPointerLast] = imuIn->header.stamp.toSec();imuRoll[imuPointerLast] = roll;imuPitch[imuPointerLast] = pitch;imuYaw[imuPointerLast] = yaw;imuAccX[imuPointerLast] = accX;imuAccY[imuPointerLast] = accY;imuAccZ[imuPointerLast] = accZ;AccumulateIMUShift();
}
void AccumulateIMUShift()
{float roll = imuRoll[imuPointerLast];float pitch = imuPitch[imuPointerLast];float yaw = imuYaw[imuPointerLast];float accX = imuAccX[imuPointerLast];float accY = imuAccY[imuPointerLast];float accZ = imuAccZ[imuPointerLast];// 转换到世界坐标系下float x1 = cos(roll) * accX - sin(roll) * accY;float y1 = sin(roll) * accX + cos(roll) * accY;float z1 = accZ;float x2 = x1;float y2 = cos(pitch) * y1 - sin(pitch) * z1;float z2 = sin(pitch) * y1 + cos(pitch) * z1;accX = cos(yaw) * x2 + sin(yaw) * z2;accY = y2;accZ = -sin(yaw) * x2 + cos(yaw) * z2;// imuPointerBack 为 imuPointerLast-1, 这样处理是为了防止内存溢出int imuPointerBack = (imuPointerLast + imuQueLength - 1) % imuQueLength;double timeDiff = imuTime[imuPointerLast] - imuTime[imuPointerBack];if (timeDiff < scanPeriod) {// 推算当前时刻的位置信息imuShiftX[imuPointerLast] = imuShiftX[imuPointerBack] + imuVeloX[imuPointerBack] * timeDiff + accX * timeDiff * timeDiff / 2;imuShiftY[imuPointerLast] = imuShiftY[imuPointerBack] + imuVeloY[imuPointerBack] * timeDiff + accY * timeDiff * timeDiff / 2;imuShiftZ[imuPointerLast] = imuShiftZ[imuPointerBack] + imuVeloZ[imuPointerBack] * timeDiff + accZ * timeDiff * timeDiff / 2;// 推算当前时刻的速度信息imuVeloX[imuPointerLast] = imuVeloX[imuPointerBack] + accX * timeDiff;imuVeloY[imuPointerLast] = imuVeloY[imuPointerBack] + accY * timeDiff;imuVeloZ[imuPointerLast] = imuVeloZ[imuPointerBack] + accZ * timeDiff;}
}

后面涉及的坐标变换即欧拉角的旋转计算,这部分尚不熟悉,需要进一步学习。

·小结

以上数据传输流帮助理解这个模块源代码的功能。

——————————————————————————————————

感谢@清酒不是九提出的问题与解决方案

增加以下内容:loam在运行较大的图时,修改decay time可以让之前的点云不被刷新截掉。地图稠密。

LOAM_velodyne学习(一)相关推荐

  1. LOAM_velodyne学习(四)

    TransformMaintenance 来到了最后一个模块,代码不是很长,我们在看完代码之后,再详细说明这个模块的功能 依然主函数开始 int main(int argc, char** argv) ...

  2. LOAM_velodyne学习(三)

    终于到第三个模块了,我们先来回顾下之前的工作:点云数据进来后,经过前两个节点的处理可以完成一个完整但粗糙的里程计,可以概略地估计出Lidar的相对运动.如果不受任何测量噪声的影响,这个运动估计的结果足 ...

  3. LOAM_velodyne学习(二)

    LaserOdometry 这一模块(节点)主要功能是:进行点云数据配准,完成运动估计 利用ScanRegistration中提取到的特征点,建立相邻时间点云数据之间的关联,由此推断lidar的运动. ...

  4. LeGO-LOAM学习

    前言 在学习了LOAM之后,了解到LeGO-LOAM(面向复杂情况的轻量级优化地面的雷达里程计),进行了一个学习整理. Github:https://github.com/RobustFieldAut ...

  5. 对LOAM算法原理和代码的理解

    LOAM概况 V-LOAM和LOAM在KITTI上的排名一直是前两名. LOAM在KITTI上的排名 原始LOAM代码(带中文注释)的地址:https://github.com/cuitaixiang ...

  6. SLAM学习笔记(二十)LIO-SAM流程及代码详解(最全)

    写在前面 关于安装配置,博客LIO_SAM实测运行,论文学习及代码注释[附对应google driver数据] 我觉得已经写的比较完善了.但是我觉得在注释方面,这位博主写的还不够完善,因此在学习以后, ...

  7. SLAM学习资料收集

    这是一个不定期更新的SLAM学习历程中收集到的资料... 一.激光SLAM 1.三维激光SLAM (1)LOAM: 论文:http://www.roboticsproceedings.org/rss1 ...

  8. LOAM学习-安装与运行

    LOAM-安装与运行 引言 LOAM安装 LOAM运行 结语 引言 由于最近想学习SLAM,发现LOAM是较为经典的入门算法. 于是就开始了LOAM的学习. LOAM安装 LOAM的安装真的是一言难尽 ...

  9. LIO-SAM学习总结

    # LIO-SAM学习总结目录 提示:这里可以添加系列文章的所有文章的目录,目录需要自己手动添加 提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言 一.参考资料 **项 ...

最新文章

  1. 管理学中的知名定律之阿尔布莱特法则
  2. WebSphere SSLC0008E 无法初始化 SSL 连接。未授权访问被拒绝,或者安全性设置已到期 解决方法...
  3. Linux之Sed详解
  4. Using dbms_shared_pool.purge to remove a single task from the library cache
  5. 人工智能秘史(四):憧憬人机共生的“DARPA梦想家”
  6. HttpContext HttpRuntime
  7. ROS笔记(39) 串口配置
  8. Firemonkey的旁门左道[六]
  9. MaxRects纹理合并算法as3实现
  10. 用MDT 2012为企业部署windows 7(三)--安装MDT 2012,ADK以及安装后情况
  11. 通过迁移的方式解决Active Directory服务器问题之5-恢复服务器的应用
  12. 网站性能优化 - 数据库及服务器架构篇
  13. 微信小程序下载图片与缓存
  14. 携手腾讯官方打造,微信(统信UOS版)首发
  15. 【排队助手】投屏模式-使用指南
  16. rtx和gtx区别_gtx和rtx显卡哪个好
  17. UE4 安卓手机launch报错
  18. Linux 学习 第六单元
  19. ps抠图——抠出自己想要的素材(钢笔工具的使用)
  20. Python:max函数获取列表最大值

热门文章

  1. powerdesigner中如何在自动生成建表SQL时添加模式名schema
  2. EC+VO+SCOPE for ES3
  3. 第一个程序,Hello World
  4. 数据可视化(BI报表的开发)第一天
  5. Javascript阻止表单提交
  6. iOS 升级https的方案选择
  7. Dojo中跨域获取新浪股票接口返回的数据(练习)
  8. Access导入文本文件的Schema.ini文件格式
  9. C#面向对象名词比较(一)
  10. JavaScript --- this