激光SLAM系统Fast LOAM (Lidar Odometry And Mapping)源码解析
github地址:Fast LOAM (Lidar Odometry And Mapping)
Fast LOAM提供了mapping和localization的两个节点,目前只使用其定位部分,以velodyne为例分析一下源码。
<node pkg="floam" type="floam_odom_estimation_node" name="floam_odom_estimation_node" output="screen"/><node pkg="floam" type="floam_laser_processing_node" name="floam_laser_processing_node" output="screen"/><node pkg="floam" type="floam_laser_mapping_node" name="floam_laser_mapping_node" output="screen"/>
即只需要运行floam_odom_estimation_node
和floam_laser_processing_node
两个节点即可!
1. laserProcessingNode.cpp
先从点云处理开始,看一下其订阅和发布的topic
ros::Subscriber subLaserCloud = nh.subscribe<sensor_msgs::PointCloud2>("/velodyne_points", 100, velodyneHandler);
pubLaserCloudFiltered = nh.advertise<sensor_msgs::PointCloud2>("/velodyne_points_filtered", 100);
pubEdgePoints = nh.advertise<sensor_msgs::PointCloud2>("/laser_cloud_edge", 100);
pubSurfPoints = nh.advertise<sensor_msgs::PointCloud2>("/laser_cloud_surf", 100);
其订阅了velodyne
发来的点云数据,并将其不断的存到pointCloudBuf
中,用于处理。
将其所有帧提取到的面特征和边缘特征拼接之后发出去velodyne_points_filtered
,其时间戳同处理的该帧点云,frame_id
则为base_link
。
//拼接
*pointcloud_filtered+=*pointcloud_edge;
*pointcloud_filtered+=*pointcloud_surf;
//设定时间戳和frame
laserCloudFilteredMsg.header.stamp = pointcloud_time;
laserCloudFilteredMsg.header.frame_id = "/base_link";
在该节点中主要函数为laser_processing()
,每次从pointCloudBuf
中取出一帧,即pointcloud_in
,提取其面特征和边缘特征。
laserProcessing.featureExtraction(pointcloud_in,pointcloud_edge,pointcloud_surf);
该函数后面会在laserProcessingClass.cpp
中介绍。
然后便发布了计算得到的面特征laser_cloud_surf
和边缘特征laser_cloud_edge
。
2.laserProcessingClass.cpp
参考博客:http://xchu.net/2020/08/17/49floam/
在该文件中主要有函数featureExtraction
和featureExtractionFromSector
(1)featureExtraction
首先计算点云线束并分类
// 遍历点云,给点云的每个点进行分类,具体属于哪个线束for (int i = 0; i < (int) pc_in->points.size(); i++){int scanID=0; // 比较远或者过近的点去掉double distance = sqrt(pc_in->points[i].x * pc_in->points[i].x + pc_in->points[i].y * pc_in->points[i].y);if(distance<lidar_param.min_distance || distance>lidar_param.max_distance)continue;// 计算点在垂直方向的角度,用来计算属于哪个线束double angle = atan(pc_in->points[i].z / distance) * 180 / M_PI;// 根据激光雷达的线束进行点的分类if (N_SCANS == 16){scanID = int((angle + 15) / 2 + 0.5);// 16线两线束间间隔30度,从-15到+15共16根线,这里为了避免出现负数,统一加15度if (scanID > (N_SCANS - 1) || scanID < 0){continue;}
}laserCloudScans[scanID]->push_back(pc_in->points[i]); // 将每个点装入不同的容器}
接下来计算每个点的曲率
// 按点云线束进行遍历
for(int i = 0; i < N_SCANS; i++){if(laserCloudScans[i]->points.size()<131){continue;} // 某一束点云少于131则不适用// 计算每个点的曲率(粗糙度),参考LOAMstd::vector<Double2d> cloudCurvature; int total_points = laserCloudScans[i]->points.size()-10;// 当前点前后各五个点,计算其三个方向上距离和的平方for(int j = 5; j < (int)laserCloudScans[i]->points.size() - 5; j++){... Double2d distance(j,diffX * diffX + diffY * diffY + diffZ * diffZ);cloudCurvature.push_back(distance);}
将点云均匀划分成6个子图,保证各方向都能提取到特征
for(int j=0;j<6;j++){int sector_length = (int)(total_points/6);int sector_start = sector_length *j;int sector_end = sector_length *(j+1)-1;if (j==5){sector_end = total_points - 1; } std::vector<Double2d> subCloudCurvature(cloudCurvature.begin()+sector_start,cloudCurvature.begin()+sector_end); // 特征提取featureExtractionFromSector(laserCloudScans[i],subCloudCurvature, pc_out_edge, pc_out_surf);
}
(2)LaserProcessingClass
在特征提取时,将点云按曲率大小进行排序,遍历点云,根据曲率判断其是平面点还是边缘点。
找到曲率最大的20个点,认为是边缘点,其他的则为平面点。
3.odomEstimationNode.cpp
在经过点云处理之后,发出面特征和边缘特征数据,在该节点中订阅。
ros::Subscriber subEdgeLaserCloud = nh.subscribe<sensor_msgs::PointCloud2>("/laser_cloud_edge", 100,velodyneEdgeHandler);
ros::Subscriber subSurfLaserCloud = nh.subscribe<sensor_msgs::PointCloud2>("/laser_cloud_surf", 100,velodyneSurfHandler);
首先是时间戳对齐,去掉较老时间戳的点云,然后有一个初始化的过程,该函数会在后面说到:
if(is_odom_inited == false){odomEstimation.initMapWithPoints(pointcloud_edge_in, pointcloud_surf_in);is_odom_inited = true;ROS_INFO("odom inited");
}
初始化完成之后,便将两种特征传入到updatePointsToMap
函数中,求解优化得到位姿odomEstimation.odom
。
odomEstimation.updatePointsToMap(pointcloud_edge_in, pointcloud_surf_in,filter_distance);
此处filter_distance
,是我修改引入的参数,为了解决保留点云太多时,求解位姿效率太低,导致发出的tf
延迟较高问题,后面在函数中会详细说明。
最终将其tf
广播出去,即transform
br.sendTransform(tf::StampedTransform(transform, ros::Time::now(), odom_frame_id, "base_link"));
此处的odom_frame_id
即为velodyne
发出的里程计,tf
的时间为当前的时间,如果为了解决两台主机时间不同步的问题,可以将该tf
的ros::Time::now()
修改成点云数据的时间pointcloud_time
,但是此时忽略了位姿求解的时间,影响还有待测试。
4.odomEstimationClass.cpp
初始化initMapWithPoints
的过程,即直接把第一帧加入到地图中去,毕竟第一帧也没什么可参考的。
当初始化完成后的updatePointsToMap
中,设置了一些优化的代价和参数。
在初始化完成之后optimization_count
便被设置成12,然后迭代次数不断减少至2,即只迭代两次。
假设当前帧和上一帧有相同的运动,然后开始优化:
for (int iterCount = 0; iterCount < optimization_count; iterCount++){ceres::LossFunction *loss_function = new ceres::HuberLoss(0.1);ceres::Problem::Options problem_options;ceres::Problem problem(problem_options);problem.AddParameterBlock(parameters, 7, new PoseSE3Parameterization()); addEdgeCostFactor(downsampledEdgeCloud,laserCloudCornerMap,problem,loss_function);addSurfCostFactor(downsampledSurfCloud,laserCloudSurfMap,problem,loss_function);ceres::Solver::Options options;options.linear_solver_type = ceres::DENSE_QR;options.max_num_iterations = 4;options.minimizer_progress_to_stdout = false;options.check_gradients = false;options.gradient_check_relative_precision = 1e-4;ceres::Solver::Summary summary;ceres::Solve(options, &problem, &summary);}
其中主要为添加两种特征点云分别到平面和边缘的残差项,即addEdgeCostFactor
和addSurfCostFactor
。
此外,在使用该里程计时,实测中高度信息并不准确,不知是环境问题还是参数问题,有待解决,因此目前只使用其2D信息,添加如下修改:
//like
Eigen::Vector3d tran(t_w_curr(0),t_w_curr(1),0);
odom.translation() = tran;
(1) addEdgeCostFactor && addSurfCostFactor
KD tree参考博客:https://blog.csdn.net/weixin_38275649/article/details/80972554
遍历所有边缘点!
以该点为中心,即point_temp
,计算近邻的索引值pointSearchInd
、近邻的中心距pointSearchSqDis
,寻找最邻近的5个点!
- 该点
point_temp
是经过pointAssociateToMap
,转换到地图坐标系下的点
kdtreeEdgeMap->nearestKSearch(point_temp, 5, pointSearchInd, pointSearchSqDis);
然后构建点到平面/直线的残差,具体便不再详细介绍!
(2) addPointsToMap
该函数则是将点云添加到地图中,并且进行过滤下采样。
首先则是把得到的边缘特征和面特征不断的加入到map
当中,虽然我们没用到其建图功能,但是在优化求解位姿时,需要和地图中的点云作为参考。
for (int i = 0; i < (int)downsampledEdgeCloud->points.size(); i++){pcl::PointXYZI point_temp;pointAssociateToMap(&downsampledEdgeCloud->points[i], &point_temp);laserCloudCornerMap->push_back(point_temp); }for (int i = 0; i < (int)downsampledSurfCloud->points.size(); i++){pcl::PointXYZI point_temp;pointAssociateToMap(&downsampledSurfCloud->points[i], &point_temp);laserCloudSurfMap->push_back(point_temp);}
为了避免场景过大导致优化计算位姿太耗时,需要对加入到地图中的点云进行剔除
double x_min = +odom.translation().x()-filter_distance;
double y_min = +odom.translation().y()-filter_distance;
double z_min = +odom.translation().z()-filter_distance;
double x_max = +odom.translation().x()+filter_distance;
double y_max = +odom.translation().y()+filter_distance;
double z_max = +odom.translation().z()+filter_distance;
cropBoxFilter.setMin(Eigen::Vector4f(x_min, y_min, z_min, 1.0));
cropBoxFilter.setMax(Eigen::Vector4f(x_max, y_max, z_max, 1.0));
cropBoxFilter.setNegative(false);
即以filter_distance
为范围,将当前位置附近阈值之外的点进行过滤。
然后经过VoxelGrid
体素滤波器对点云进行下采样,最终加入到地图中!
激光SLAM系统Fast LOAM (Lidar Odometry And Mapping)源码解析相关推荐
- Tightly Coupled LiDAR Inertial Odometry and Mapping源码解析(一)
Tightly Coupled LiDAR Inertial Odometry and Mapping源码解析(一) 1. LiDAR inertial odometry and mapping简介 ...
- Tightly Coupled LiDAR Inertial Odometry and Mapping源码解析(四)
Tightly Coupled LiDAR Inertial Odometry and Mapping源码解析(四) 3. Joint optimization 3.3 IMU preintegrat ...
- Tightly Coupled LiDAR Inertial Odometry and Mapping源码解析(二)
Tightly Coupled LiDAR Inertial Odometry and Mapping源码解析(二) 3. Joint optimization 3.1 Marginalization ...
- Tightly Coupled LiDAR Inertial Odometry and Mapping源码解析(三)
Tightly Coupled LiDAR Inertial Odometry and Mapping源码解析(三) 3. Joint optimization 3.2 IMU preintegrat ...
- LOAM: Lidar Odometry and Mapping in Real-time
一.概述 Loam主要是一种激光匹配slam的方法,也就是一种定位与建图的方法,相比于其他的制图方法,loam主要解决的是雷达快速移动的过程中的畸变问题,如果雷达的扫描速度和雷达的移动速度相差太大,会 ...
- 一套就够了!室内+室外激光SLAM关键算法讲解与工程实现(源码和数据开源)...
应用背景介绍 高精地图作为自动驾驶的眼睛,在自动驾驶研发中占据极大的份额,而激光SLAM则是高精地图定位导航算法的关键技术,其重要性不言而喻,在AI产品如矿卡.汽车.清扫车和扫地机器人等领域都占据一席 ...
- LOAM源码解析1一scanRegistration
鉴于工作和学习需要,学习了激光salm算法loam,并阅读了作者的原版论文,现将学习过程中的理解与一些源码剖析记录整理下来,也是对于学习slam的阶段性总结!!! 一.综述 LOAM这篇论文是发表于2 ...
- loam源码解析5 : laserOdometry(三)
transformMaintenance.cpp解析 八.位姿估计 1. 雅可比计算 2. 矩阵求解 3. 退化问题分析 4. 姿态更新 5. 坐标转换 loam源码地址: https://githu ...
- loam源码解析1 : scanRegistration(一)
scanRegistration.cpp解析 一.概述 二.变量说明 三.主函数 四.IMU回调函数laserCloudHandler 1 接受IMU的角度和加速度信息 2 AccumulateIMU ...
最新文章
- mysql之case_mysql存储过程之case语句
- 关于 运行root.sh 时出错误Timed out waiting for the CRS stack to start.的解决
- leetcode 66. 加一(C语言)
- 在IIS服务器上安装SSL证书
- 强化学习《基于策略 - on plolicy - off plolicy》
- html+css面试题 行内元素padding和margin
- 计算流体力学漫谈-1 (可压缩向)
- rda分析怎么做_PCA、PCoA、NMDS 、RDA和CCA等排序分析方法
- 《高性能MySQL》读书笔记(1~6章)
- 利用Eigen求广义逆矩阵
- win7系统提示此windows副本不是正版怎么办?
- IDEA 安装字体 安装JetBrains Mono字体
- 数据分析工具的深度对比:FineBI vs PowerBI
- 用request获取请求地址Ip
- 非谓语动词 + 情态动词学习笔记
- 3个套路带你玩转Excel动态图表
- android 轮询框架,Android 轮询总结
- Spring中同一个service类中方法相互调用事务不生效问题解决方案
- 盖茨基金会:全球至少要到2108年才能实现性别平等,比期望晚了三代人 | 美通社头条...
- 【转载】互联网知名博客收揽
热门文章
- 国家电网一二次融合配电终端(FTU)发展新方向馈线自动化:具备集中型馈线自动化/就地型馈线自动化包括电压时间型、电压电流型、自适应综合型)及零序电流,零序电压或外施信号法的单相接地故障选线功能
- 大数据处理算法--Bloom Filter布隆过滤
- [AHK]在当前目录中运行DOS命令行--DosHere
- 西安电大计算机文化基础中考,计算机文化基础试题3.pdf
- 周期训练理论与方法pdf_周期_PDF图书下载_(美) 图德·邦帕 (Tudor O.Bompa) (美)_免费PDF电子书下载_第一图书网...
- 封头名义厚度如何圆整_封头规格
- 单页双曲面 matlab,如何画双叶双曲面
- 2022-2028年中国电子商务行业市场深度分析及投资前景展望报告
- Java并发编程与技术内幕:ThreadFactory、ThreadLocal
- 当一个SQL语句同时出现了where,group by,having,order by的时候,执行顺序和编写顺序...