3D激光slam:ALOAM---后端lasermapping最终篇地图更新及消息发布

简介: 本篇为ALOAM后端的最终篇,地图更新及消息发布。**地图更新原因:**- 当地图调整之后,栅格有些空着的,需要进行填充- 保障地图的实时更新- 当前帧的点云加到地图中去,下帧会有更多的匹配点

前言

在之前的博客中对ALOAM的前端和后端做了代码详细的梳理

这里先对前几篇后端的内容做一个总结

本篇为ALOAM后端的最终篇,地图更新及消息发布。

地图更新原因:

  • 当地图调整之后,栅格有些空着的,需要进行填充
  • 保障地图的实时更新
  • 当前帧的点云加到地图中去,下帧会有更多的匹配点

代码解析

            for (int i = 0; i < laserCloudCornerStackNum; i++)
            {

遍历角点

pointAssociateToMap(&laserCloudCornerStack->points[i], &pointSel);

将该点转到地图坐标系下

                int cubeI = int((pointSel.x + 25.0) / 50.0) + laserCloudCenWidth;
                int cubeJ = int((pointSel.y + 25.0) / 50.0) + laserCloudCenHeight;
                int cubeK = int((pointSel.z + 25.0) / 50.0) + laserCloudCenDepth;
                //负数的四舍五入
                if (pointSel.x + 25.0 < 0)
                    cubeI--;
                if (pointSel.y + 25.0 < 0)
                    cubeJ--;
                if (pointSel.z + 25.0 < 0)
                    cubeK--;

计算该点的栅格索引

                if (cubeI >= 0 && cubeI < laserCloudWidth &&
                    cubeJ >= 0 && cubeJ < laserCloudHeight &&
                    cubeK >= 0 && cubeK < laserCloudDepth)
                {    

判断索引满足条件后

int cubeInd = cubeI + laserCloudWidth * cubeJ + laserCloudWidth * laserCloudHeight * cubeK;

根据 xyz 的索引计算一维数组中的索引

laserCloudCornerArray[cubeInd]->push_back(pointSel);

将该点加入 角点的 地图 对应的栅格索引 的 数组中

            for (int i = 0; i < laserCloudSurfStackNum; i++)
            {
                pointAssociateToMap(&laserCloudSurfStack->points[i], &pointSel);

                int cubeI = int((pointSel.x + 25.0) / 50.0) + laserCloudCenWidth;
                int cubeJ = int((pointSel.y + 25.0) / 50.0) + laserCloudCenHeight;
                int cubeK = int((pointSel.z + 25.0) / 50.0) + laserCloudCenDepth;

                if (pointSel.x + 25.0 < 0)
                    cubeI--;
                if (pointSel.y + 25.0 < 0)
                    cubeJ--;
                if (pointSel.z + 25.0 < 0)
                    cubeK--;

                if (cubeI >= 0 && cubeI < laserCloudWidth &&
                    cubeJ >= 0 && cubeJ < laserCloudHeight &&
                    cubeK >= 0 && cubeK < laserCloudDepth)
                {
                    int cubeInd = cubeI + laserCloudWidth * cubeJ + laserCloudWidth * laserCloudHeight * cubeK;
                    laserCloudSurfArray[cubeInd]->push_back(pointSel);
                }
            }

对于面点也是同样的操作

printf("add points time %f ms\n", t_add.toc());

终端输出 添加点的时间

下面要做的是把 当前帧涉及到的局部地图的栅格做一个下采样

            for (int i = 0; i < laserCloudValidNum; i++)
            {
                int ind = laserCloudValidInd[i];//栅格索引
                // 对该栅格索引内的角点点云进行 体素 滤波
                pcl::PointCloud<PointType>::Ptr tmpCorner(new pcl::PointCloud<PointType>());
                downSizeFilterCorner.setInputCloud(laserCloudCornerArray[ind]);
                downSizeFilterCorner.filter(*tmpCorner);
                laserCloudCornerArray[ind] = tmpCorner;
                // 对该栅格索引内的面点点云进行 体素 滤波
                pcl::PointCloud<PointType>::Ptr tmpSurf(new pcl::PointCloud<PointType>());
                downSizeFilterSurf.setInputCloud(laserCloudSurfArray[ind]);
                downSizeFilterSurf.filter(*tmpSurf);
                laserCloudSurfArray[ind] = tmpSurf;
            }

遍历当前帧涉及的栅格个数
laserCloudValidNum 就是当前帧涉及的栅格个数,在上面通过取局部地图栅格的时候在for循环里做的累加
局部地图的选取规则就是通过判断当前帧位置的索引,然后在I和J方向前后取2个,K方向前后1个
在这里插入图片描述
当前帧所在栅格和局部地图,局部地图为了方便看在J方向上前后取了1个,实际为2个

因为当前帧大概率会落在这几个栅格中,所以仅对这几个栅格做滤波处理即可,在这几个栅格之外的点是很稀疏的,可以不处理,这样提高计算效率

printf("filter time %f ms \n", t_filter.toc());//终端输出滤波时间

终端输出滤波时间

至此完成了整体的地图更新

下面要做的就是点云地图的发布
局部地图每隔5帧进行要给发布

            //后端每处理5帧发布一个
            if (frameCount % 5 == 0)
            {
                laserCloudSurround->clear();//局部地图清空
                for (int i = 0; i < laserCloudSurroundNum; i++)
                {
                    int ind = laserCloudSurroundInd[i];
                    //把 局部地图 角点和面点 都加入局部地图中
                    *laserCloudSurround += *laserCloudCornerArray[ind];
                    *laserCloudSurround += *laserCloudSurfArray[ind];
                }
                // 转成ROS的格式发布
                sensor_msgs::PointCloud2 laserCloudSurround3;
                pcl::toROSMsg(*laserCloudSurround, laserCloudSurround3);
                laserCloudSurround3.header.stamp = ros::Time().fromSec(timeLaserOdometry);
                laserCloudSurround3.header.frame_id = "/camera_init";
                pubLaserCloudSurround.publish(laserCloudSurround3);
            }

局部地图清空
把 局部地图 角点和面点 都加入局部地图中
转成ROS的格式发布

            //每隔 20帧 发布全量地图
            if (frameCount % 20 == 0)
            {
                pcl::PointCloud<PointType> laserCloudMap;
                // 21*21*11
                for (int i = 0; i < 4851; i++)
                {
                    laserCloudMap += *laserCloudCornerArray[i];
                    laserCloudMap += *laserCloudSurfArray[i];
                }
                sensor_msgs::PointCloud2 laserCloudMsg;
                pcl::toROSMsg(laserCloudMap, laserCloudMsg);
                laserCloudMsg.header.stamp = ros::Time().fromSec(timeLaserOdometry);
                laserCloudMsg.header.frame_id = "/camera_init";
                pubLaserCloudMap.publish(laserCloudMsg);
            }

每隔 20帧 发布全量地图

            //当前帧发布
            int laserCloudFullResNum = laserCloudFullRes->points.size();
            for (int i = 0; i < laserCloudFullResNum; i++)
            {
                pointAssociateToMap(&laserCloudFullRes->points[i], &laserCloudFullRes->points[i]);
            }

            sensor_msgs::PointCloud2 laserCloudFullRes3;
            pcl::toROSMsg(*laserCloudFullRes, laserCloudFullRes3);
            laserCloudFullRes3.header.stamp = ros::Time().fromSec(timeLaserOdometry);
            laserCloudFullRes3.header.frame_id = "/camera_init";
            pubLaserCloudFullRes.publish(laserCloudFullRes3);

当前帧转到地图坐标系后发布

            nav_msgs::Odometry odomAftMapped;
            odomAftMapped.header.frame_id = "/camera_init";
            odomAftMapped.child_frame_id = "/aft_mapped";
            odomAftMapped.header.stamp = ros::Time().fromSec(timeLaserOdometry);
            odomAftMapped.pose.pose.orientation.x = q_w_curr.x();
            odomAftMapped.pose.pose.orientation.y = q_w_curr.y();
            odomAftMapped.pose.pose.orientation.z = q_w_curr.z();
            odomAftMapped.pose.pose.orientation.w = q_w_curr.w();
            odomAftMapped.pose.pose.position.x = t_w_curr.x();
            odomAftMapped.pose.pose.position.y = t_w_curr.y();
            odomAftMapped.pose.pose.position.z = t_w_curr.z();
            pubOdomAftMapped.publish(odomAftMapped);

建图后里程计的发布

            geometry_msgs::PoseStamped laserAfterMappedPose;
            laserAfterMappedPose.header = odomAftMapped.header;
            laserAfterMappedPose.pose = odomAftMapped.pose.pose;
            laserAfterMappedPath.header.stamp = odomAftMapped.header.stamp;
            laserAfterMappedPath.header.frame_id = "/camera_init";
            laserAfterMappedPath.poses.push_back(laserAfterMappedPose);
            pubLaserAfterMappedPath.publish(laserAfterMappedPath);

建图后path的发布

            static tf::TransformBroadcaster br;
            tf::Transform transform;
            tf::Quaternion q;
            transform.setOrigin(tf::Vector3(t_w_curr(0),
                                            t_w_curr(1),
                                            t_w_curr(2)));
            q.setW(q_w_curr.w());
            q.setX(q_w_curr.x());
            q.setY(q_w_curr.y());
            q.setZ(q_w_curr.z());
            transform.setRotation(q);
            br.sendTransform(tf::StampedTransform(transform, odomAftMapped.header.stamp, "/camera_init", "/aft_mapped"));

建图后tf的发布

总结

后端的地图更新总结

  • 将该帧的所有角点转到地图坐标系下根据位置计算栅格索引,存入该栅格的点云数组中
  • 将该帧的所有面点转到地图坐标系下根据位置计算栅格索引,存入该栅格的点云数组中
  • 对添加点后的局部地图进行下采用
  • 发布ROS的消息格式

后端消息发布总结

这时候可以根据main函数里面的advertise进行一个总结

pubLaserCloudSurround = nh.advertise<sensor_msgs::PointCloud2>("/laser_cloud_surround", 100);
  • 局部地图 后端没处理 5 帧发布一次,当前帧的栅格索引的I、J方向前后2个,K方向前后一个
pubLaserCloudMap = nh.advertise<sensor_msgs::PointCloud2>("/laser_cloud_map", 100);
  • 整体的栅格地图 后端每处理20帧发布一次。21 21 11的栅格尺寸,一个格子边长50m
pubOdomAftMapped = nh.advertise<nav_msgs::Odometry>("/aft_mapped_to_init", 100);
  • 发布当前帧到map 下的位姿 这个位姿是 准确的 是通过 优化 求得的 频率是 后端处理完一帧后即分布一帧的频率
pubOdomAftMappedHighFrec = nh.advertise<nav_msgs::Odometry>("/aft_mapped_to_init_high_frec", 100);
  • 发布当前帧转到map坐标系下的位姿 高频率的位姿 通过上一帧的优化值,估计的
pubLaserAfterMappedPath = nh.advertise<nav_msgs::Path>("/aft_mapped_path", 100);
  • 发布 path ,该 路径是准确的,通过 优化后求得的, 频率是 后端处理完一帧后即分布一帧的频率

通过在以往博客中搭建的仿真场景,进行相关消息的可视化
在这里插入图片描述
gazebo场景
在这里插入图片描述
在这里插入图片描述
频率为 大约 2hz

在这里插入图片描述
整体栅格地图
在这里插入图片描述
频率大约为 0.5hz 也就是2s更新一次

在这里插入图片描述
当前帧转到地图坐标系的

相关实践学习
使用ROS创建VPC和VSwitch
本场景主要介绍如何利用阿里云资源编排服务,定义资源编排模板,实现自动化创建阿里云专有网络和交换机。
ROS入门实践
本课程将基于基础设施即代码 IaC 的理念,介绍阿里云自动化编排服务ROS的概念、功能和使用方式,并通过实际应用场景介绍如何借助ROS实现云资源的自动化部署,使得云上资源部署和运维工作更为高效。
相关文章
|
前端开发 定位技术 索引
3D激光SLAM:ALOAM---后端 lasermapping构建角点约束与面点约束
后端的构建约束问题和前端不一样。原因就是前端从上一帧上去找,而后端是在局部地图上找,点要多很多,并且没有了线束信息,所以原理上不一样了。 **线特征的提取** 通过kdtree在局部地图中找到5个最近的线特征,为了判断他们是否符合线特征的特性,需要对5个点构成的协方差矩阵进行特征值分解,当上述5个点在一条直线上时,他们只有一个主方向,也就是特征值是一个大特征值,以及两个小特征值,大特征值对应的特征向量就是对应直线的方向向量。 **面特征的提取** 通过kdtree在地图中找到最近的面特征也是5个, 理论上也可以通过特种值分解的方式,最小的特征值对应的特征向量就是平面的法向量, 不过代码里选
3D激光SLAM:ALOAM---后端 lasermapping构建角点约束与面点约束
|
存储 前端开发 定位技术
3D激光SLAM:ALOAM---后端lasermapping 里程计到地图位姿更新维护
在上一篇[博客](https://www.guyuehome.com/38662)(ALOAM:后端lasermapping通过Ceres进行帧到地图的位姿优化)中,通过Ceres优化得到了 **当前帧到地图的最优位姿** 下面要做的是更新地图模块中维护的一个位姿,这个位姿就是**odom到map之间的位姿变换**。 **为什么要更新这个位姿呢?** 因为在前面[这篇](https://www.guyuehome.com/38611)博客中(ALOAM:后端laserMapping代码结构与数据处理分析),在收到前端里程计数据后,会以前端里程计的频率,向外发布一个高频率当前帧到地图坐标系下
3D激光SLAM:ALOAM---后端lasermapping 里程计到地图位姿更新维护
|
前端开发 算法 数据处理
激光SLAM:ALOAM---后端lasermapping数据处理低延时性保障操作
ALOAM方法实现了低的漂移,并且计算的复杂度低,实时性很好.并且不需要高精度的lidar和惯导 这个方法的核心思想就是把SLAM问题进行了拆分,通过两个算法来进行.一个是执行高频率的前端里程计但是低精度的运动估计(定位),另一个算法在比定位低一个数量级的频率执行后端建图(建图和校正里程计).
激光SLAM:ALOAM---后端lasermapping数据处理低延时性保障操作
|
5月前
|
存储 消息中间件 前端开发
PHP后端与uni-app前端协同的校园圈子系统:校园社交场景的跨端开发实践
校园圈子系统校园论坛小程序采用uni-app前端框架,支持多端运行,结合PHP后端(如ThinkPHP/Laravel),实现用户认证、社交关系管理、动态发布与实时聊天功能。前端通过组件化开发和uni.request与后端交互,后端提供RESTful API处理业务逻辑并存储数据于MySQL。同时引入Redis缓存热点数据,RabbitMQ处理异步任务,优化系统性能。核心功能包括JWT身份验证、好友系统、WebSocket实时聊天及活动管理,确保高效稳定的用户体验。
301 4
PHP后端与uni-app前端协同的校园圈子系统:校园社交场景的跨端开发实践
|
7月前
|
JSON 自然语言处理 前端开发
【01】对APP进行语言包功能开发-APP自动识别地区ip后分配对应的语言功能复杂吗?-成熟app项目语言包功能定制开发-前端以uniapp-基于vue.js后端以laravel基于php为例项目实战-优雅草卓伊凡
【01】对APP进行语言包功能开发-APP自动识别地区ip后分配对应的语言功能复杂吗?-成熟app项目语言包功能定制开发-前端以uniapp-基于vue.js后端以laravel基于php为例项目实战-优雅草卓伊凡
310 72
【01】对APP进行语言包功能开发-APP自动识别地区ip后分配对应的语言功能复杂吗?-成熟app项目语言包功能定制开发-前端以uniapp-基于vue.js后端以laravel基于php为例项目实战-优雅草卓伊凡
|
10月前
|
存储 缓存 负载均衡
后端开发中的性能优化策略
本文将探讨几种常见的后端性能优化策略,包括代码层面的优化、数据库查询优化、缓存机制的应用以及负载均衡的实现。通过这些方法,开发者可以显著提升系统的响应速度和处理能力,从而提供更好的用户体验。
317 6
|
6月前
|
前端开发 JavaScript 关系型数据库
2025 年前端与后端开发方向的抉择与展望-优雅草卓伊凡
2025 年前端与后端开发方向的抉择与展望-优雅草卓伊凡
276 5
2025 年前端与后端开发方向的抉择与展望-优雅草卓伊凡
|
6月前
|
监控 前端开发 小程序
陪练,代练,护航,代打小程序源码/前端UNIAPP-VUE2.0开发 后端Thinkphp6管理/具备家政服务的综合型平台
这款APP通过技术创新,将代练、家政、娱乐社交等场景融合,打造“全能型生活服务生态圈”。以代练为切入点,提供模块化代码支持快速搭建平台,结合智能匹配与技能审核机制,拓展家政服务和商业管理功能。技术架构具备高安全性和扩展性,支持多业务复用,如押金冻结、录屏监控等功能跨领域应用。商业模式多元,包括交易抽成、增值服务及广告联名,同时设计跨领域积分体系提升用户粘性,实现生态共生与B端赋能。
499 12
|
6月前
|
人工智能 小程序 NoSQL
【一步步开发AI运动小程序】二十一、如何将AI运动项目配置持久化到后端?
本文介绍基于云智「Ai运动识别引擎」的运动配置持久化方案,旨在优化小程序或Uni APP中AI运动识别能力。通过将运动检测参数(如`Key`、`Name`、`TickMode`、`rules`或`samples`)持久化到后端,可避免因频繁调整运动参数而重新发布应用,提升用户体验。持久化数据结构支持规则和姿态样本存储,适用于关系数据库、文件或文档数据库(如MongoDB)。此外,云智还提供运动自动适配工具及「AI乐运动」产品,助力快速实现AI体育、全民健身等场景。
|
9月前
|
前端开发 Java 数据库连接
Java后端开发-使用springboot进行Mybatis连接数据库步骤
本文介绍了使用Java和IDEA进行数据库操作的详细步骤,涵盖从数据库准备到测试类编写及运行的全过程。主要内容包括: 1. **数据库准备**:创建数据库和表。 2. **查询数据库**:验证数据库是否可用。 3. **IDEA代码配置**:构建实体类并配置数据库连接。 4. **测试类编写**:编写并运行测试类以确保一切正常。
325 2

热门文章

最新文章