LOAM_velodyne学习(一)

在研读了论文及开源代码后,对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可以让之前的点云不被刷新截掉。地图稠密。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/253952.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

实战Vue简易项目(2)定制开发环境

本章内容包含上一章思考的解决&#xff0c;还有一些其它的定制... CSS预处理 关于对.vue文件模块处理规则的配置依次可在build/webpack.base.conf.js->build/vue-loader.conf.js->build/utils.js文件中跟踪&#xff1b; 而loaders的关键在于build/vue-loader.conf.js文件…

[POI2007]POW-The Flood

题目描述 给定一张地势图&#xff0c;所有的点都被水淹没&#xff0c;现在有一些关键点&#xff0c;要求放最少的水泵使所有关键点的水都被抽干 输入输出格式 输入格式&#xff1a; In the first line of the standard input there are two integers and , separated by a sin…

LOAM_velodyne学习(二)

LaserOdometry 这一模块&#xff08;节点&#xff09;主要功能是&#xff1a;进行点云数据配准&#xff0c;完成运动估计 利用ScanRegistration中提取到的特征点&#xff0c;建立相邻时间点云数据之间的关联&#xff0c;由此推断lidar的运动。我们依旧从主函数开始&#xff1…

户外穿越

晚上很早就睡了&#xff0c;并且&#xff0c;太过激动&#xff0c;所以早上四点五十分就被惊醒&#xff0c;然后到早上闹钟响。 早上匆匆忙吃过早餐&#xff0c;就赶去坐车&#xff0c;到登山之前&#xff0c;坐了大巴车&#xff0c;又坐了景区的车&#xff0c;景区的路是山路十…

【oracle】关于创建表时用default指定默认值的坑

刚开始学create table的时候没注意&#xff0c;学到后面发现可以指定默认值。于是写了如下语句&#xff1a; 当我查询的时候发现&#xff0c;查出来的结果是这样的。。 很纳闷有没有&#xff0c;我明明指定默认值了呀&#xff0c;为什么创建出来的表还是空的呢&#xff1f;又跑…

[OpenGL ES 03]3D变换:模型,视图,投影与Viewport

[OpenGL ES 03]3D变换&#xff1a;模型&#xff0c;视图&#xff0c;投影与Viewport 罗朝辉 (http://blog.csdn.net/kesalin) 本文遵循“署名-非商业用途-保持一致”创作公用协议 系列文章&#xff1a;[OpenGL ES 01]OpenGL ES之初体验[OpenGL ES 02]OpenGL ES渲染管线与着色器…

LOAM_velodyne学习(三)

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

监控视频线种类 视频信号传输介绍及各种视频接口的传输距离

一.视频信号接口 监控视频线种类介绍&#xff1a; 按照材料区分有SYV及SYWV两种&#xff0c;绝缘层的物理材料结构不同&#xff0c;SYV是实心聚乙烯电缆&#xff0c;SYWV是高物理发泡电缆&#xff0c;物理发泡电缆传输性能优于聚乙烯。 S--同轴电缆 Y--聚乙烯 V--聚氯乙烯 W…

Ajax工作原理

详见&#xff1a;http://blog.yemou.net/article/query/info/tytfjhfascvhzxcyt238 在这篇文章中&#xff0c;我将从10个方面来对AJAX技术进行系统的讲解。 1、ajax技术的背景 不可否认&#xff0c;ajax技术的流行得益于google的大力推广&#xff0c;正是由于google earth、go…

各种视频信号格式及端子介绍/VGA DVI HDMI区别

视频信号是我们接触最多的显示信号&#xff0c;但您并不一定对各种视频信号有所了解。因为国内用到的视频信号格式和端子非常有限&#xff0c;一般就是复合视频和S端子&#xff0c;稍高级一些的就是色差及VGA。对于那些经常接触国外电器和二手设备的朋友&#xff0c;就会遇到各…

LOAM_velodyne学习(四)

TransformMaintenance 来到了最后一个模块&#xff0c;代码不是很长&#xff0c;我们在看完代码之后&#xff0c;再详细说明这个模块的功能 依然主函数开始 int main(int argc, char** argv) {ros::init(argc, argv, "transformMaintenance");ros::NodeHandle nh;…

oracle参数文件、控制文件、数据文件、日志文件的位置及查询方法

参数文件&#xff1a;所有参数文件一般在 $ORACLE_HOME/dbs 下 sqlplus查询语句&#xff1a;show parameter spfile; 网络连接文件&#xff1a; $ORACLE_HOME/dbs/network/admin 目录中 控制文件&#xff1a;select * from v$controlfile; 数据文件&#xff1a;一般在oracleda…

LeGO-LOAM学习

前言 在学习了LOAM之后&#xff0c;了解到LeGO-LOAM&#xff08;面向复杂情况的轻量级优化地面的雷达里程计&#xff09;&#xff0c;进行了一个学习整理。 Github&#xff1a;https://github.com/RobustFieldAutonomyLab/LeGO-LOAM 论文&#xff1a;https://github.com/Robu…

(一)低功耗设计目的与功耗的类型

一、低功耗设计的目的 1.便携性设备等需求 电子产品在我们生活中扮演了极其重要的作用&#xff0c;便携性的电子设备便是其中一种。便携性设备需要电池供电、需要消耗电池的能量。在同等电能提供下&#xff0c;低功耗设计的产品就能够工作更长的时间。时间的就是生命&#xff…

(转)彻底学会使用epoll(一)——ET模式实现分析

注&#xff1a;之前写过两篇关于epoll实现的文章&#xff0c;但是感觉懂得了实现原理并不一定会使用&#xff0c;所以又决定写这一系列文章&#xff0c;希望能够对epoll有比较清楚的认识。是请大家转载务必注明出处&#xff0c;算是对我劳动成果的一点点尊重吧。另外&#xff0…

Apache 设置http跳转至HTTPS访问

为什么80%的码农都做不了架构师&#xff1f;>>> <VirtualHost>...</VirtualHost> 中添加如下配置 <IfModule mod_rewrite.c>RewriteEngine onRewriteCond %{SERVER_PORT} 80RewriteRule ^(.*)$ https://域名/$1 [R301,L] </IfModule> 转…

(二)功耗的分析

前面学习了进行低功耗的目的个功耗的构成&#xff0c;今天就来分享一下功耗的分析。由于是面向数字IC前端设计的学习&#xff0c;所以这里的功耗分析是基于DC中的power compiler工具&#xff1b;更精确的功耗分析可以采用PT&#xff0c;关于PT的功耗分析可以查阅其他资料&#…

Hibernate创建hqll时报错

Hibernate 问题,在执行Query session.createQuery(hql) 报错误 出错截图&#xff1a; 这条语句在java运行环境下&#xff0c;直接连数据库不出错&#xff0c;如果在hiberante,struts环境下就出错 出错原因&#xff1a;jar包冲突&#xff0c;struts2和hibernate框架中都有antlr包…

.NET Core TDD 前传: 编写易于测试的代码 -- 全局状态

第1篇: 讲述了如何创造"缝". "缝"(seam)是需要知道的概念. 第2篇, 避免在构建对象时写出不易测试的代码. 第3篇, 依赖项和迪米特法则. 本文是第4篇, 将介绍全局状态引起的问题. 全局状态 全局状态, 也可以叫做应用程序状态, 它是一组变量, 这些变量维护着…

(三)系统与架构级低功耗设计

前面讲解了使用EDA工具&#xff08;主要是power compiler&#xff09;进行功耗分析的流程&#xff0c;这里我们将介绍在数字IC中进行低功耗设计的方法&#xff0c;同时也结合EDA工具&#xff08;主要是Design Compiler&#xff09;如何实现。我们的讲解的低功耗设计主要是自顶向…