3D激光SLAM:A-LOAM :前端lidar点预处理部分代码解读

本文涉及的产品
资源编排,不限时长
简介: A-LOAM的cpp有四个,其中 kittiHelper.cpp 的作用是将kitti数据集转为rosbag剩下的三个是作为 slam 的 部分,分别是:- laserMappin.cpp ++++ 当前帧到地图的优化- laserOdometry.cpp ++++ 帧间里程计- scanRegistration.cpp ++++ 前端lidar点预处理及特征提取本片主要解读 前端lidar点预处理部分的代码

A-LOAM代码的结构

A-LOAM的cpp有四个,其中 kittiHelper.cpp 的作用是将kitti数据集转为rosbag
剩下的三个是作为 slam 的 部分,分别是:

  • laserMappin.cpp ++++ 当前帧到地图的优化
  • laserOdometry.cpp ++++ 帧间里程计
  • scanRegistration.cpp ++++ 前端lidar点预处理及特征提取

本片主要解读 前端lidar点预处理部分的代码

Code

int main(int argc, char **argv)
{
    //节点 名称:scanRegistration
    ros::init(argc, argv, "scanRegistration");
    ros::NodeHandle nh;//ros 句柄   

    //从配置参数中 读取 scan_line 参数, 多少线的激光雷达  在launch文件中配置的
    nh.param<int>("scan_line", N_SCANS, 16);

    //从配置参数中 读取 minimum_range 参数, 最小有效距离  在launch文件中配置的   踢出雷达上的载体出现在视野里的影响
    nh.param<double>("minimum_range", MINIMUM_RANGE, 0.1);

从main函数开始

首先是ros的基本操作,初始化节点和 声明句柄

然后从参数服务器中读取两个参数

  • scan_line 多少线的激光雷达 在launch文件中配置的
  • minimum_range 最小有效距离 在launch文件中配置的 踢出雷达上的载体出现在视野里的影响

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    //打印线束
    printf("scan line number %d \n", N_SCANS);

    //做一个线束的判断 
    if(N_SCANS != 16 && N_SCANS != 32 && N_SCANS != 64)
    {
        printf("only support velodyne with 16, 32 or 64 scan line!");
        return 0;
    }

做一个线束的判断
目前仅支持 16线 32线 64线的 机械式lidar
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    //订阅 velodyne 的 lidar消息 收到一个消息包 则进入 laserCloudHandler 回调函数一次
    ros::Subscriber subLaserCloud = nh.subscribe<sensor_msgs::PointCloud2>("/velodyne_points", 100, laserCloudHandler);

    //定义要发布的消息
    pubLaserCloud = nh.advertise<sensor_msgs::PointCloud2>("/velodyne_cloud_2", 100);

    pubCornerPointsSharp = nh.advertise<sensor_msgs::PointCloud2>("/laser_cloud_sharp", 100);

    pubCornerPointsLessSharp = nh.advertise<sensor_msgs::PointCloud2>("/laser_cloud_less_sharp", 100);

    pubSurfPointsFlat = nh.advertise<sensor_msgs::PointCloud2>("/laser_cloud_flat", 100);

    pubSurfPointsLessFlat = nh.advertise<sensor_msgs::PointCloud2>("/laser_cloud_less_flat", 100);

    pubRemovePoints = nh.advertise<sensor_msgs::PointCloud2>("/laser_remove_points", 100);

本节点订阅和发布的消息
从这里可以看出来,这个节点是接收lidar的原始数据

  • "/velodyne_points"

然后发布处理后的数据

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

下面来看这个节点的主要工作,接收到lidar数据后的处理,和特征提取部分,在回调函数laserCloudHandler 中

void laserCloudHandler(const sensor_msgs::PointCloud2ConstPtr &laserCloudMsg)
{
    // 判断系统是否进行初始化, 如果没有 则缓冲几帧 目前 systemDelay为0,自己用可以设置
    if (!systemInited)
    { 
        systemInitCount++;
        if (systemInitCount >= systemDelay)
        {
            systemInited = true;
        }
        else
            return;
    }

首先是进行一个初始化的判断,前几帧可以不要
源码的systemDelay为0
实际使用的时候可以设置个大小
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    //将ros点云转为pcl点云格式
    pcl::PointCloud<pcl::PointXYZ> laserCloudIn;//声明pcl点云
    pcl::fromROSMsg(*laserCloudMsg, laserCloudIn);//将ros点云转为pcl点云格式

将ros点云转为pcl点云格式

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    std::vector<int> indices;//这个变量保存了下面去除nan点的序号
    //去除点云中的nan点
    pcl::removeNaNFromPointCloud(laserCloudIn, laserCloudIn, indices);
    //去除点云中的距离小于阈值的点  
    removeClosedPointCloud(laserCloudIn, laserCloudIn, MINIMUM_RANGE);

去除点云中的nan点
去除点云中的距离小于阈值的点
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    //计算起始点和结束点的水平角度,与x轴的夹角,由于激光雷达是顺时针旋转,这里取反就相当于转成了逆时针
    int cloudSize = laserCloudIn.points.size();

    float startOri = -atan2(laserCloudIn.points[0].y, laserCloudIn.points[0].x);
    //atan2的范围是 [-Pi,Pi] ,这里加上2Pi是为了保证起始到结束相差2PI,符合实际
    float endOri = -atan2(laserCloudIn.points[cloudSize - 1].y,
                          laserCloudIn.points[cloudSize - 1].x) +
                   2 * M_PI;

    // 总会有一些例外, 转换到合理的范围内
    if (endOri - startOri > 3 * M_PI)
    {
        endOri -= 2 * M_PI;
    }
    else if (endOri - startOri < M_PI)
    {
        endOri += 2 * M_PI;
    }

计算起始点和结束点的水平角度,与x轴的夹角
为了给后面计算相对起点的时间戳用的

这里要说下 机械式lidar的坐标系
在这里插入图片描述

所以在求与x轴的夹角的时候是 arctan(y/x)
结束点的水平角加上了2pi,主要的目的是 将角的范围转为 0~360度,因为结束点大部分为负值,比如起点,30度,结束点为-90,转完即为[30,270]

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    //遍历每一个点
    for (int i = 0; i < cloudSize; i++)
    {
        point.x = laserCloudIn.points[i].x;
        point.y = laserCloudIn.points[i].y;
        point.z = laserCloudIn.points[i].z;

        //计算俯仰角  单位是角度  目的是用来判断是第几个线束
        float angle = atan(point.z / sqrt(point.x * point.x + point.y * point.y)) * 180 / M_PI;
        int scanID = 0;//线束的id
        //计算是第几个线束 scan
        if (N_SCANS == 16)
        {
            //垂直是30度  每个线之间的夹角是2度
            scanID = int((angle + 15) / 2 + 0.5);
            if (scanID > (N_SCANS - 1) || scanID < 0)
            {
                count--;
                continue;
            }
        }
        else if (N_SCANS == 32)
        {
            scanID = int((angle + 92.0/3.0) * 3.0 / 4.0);
            if (scanID > (N_SCANS - 1) || scanID < 0)
            {
                count--;
                continue;
            }
        }
        else if (N_SCANS == 64)
        {   
            if (angle >= -8.83)
                scanID = int((2 - angle) * 3.0 + 0.5);
            else
                scanID = N_SCANS / 2 + int((-8.83 - angle) * 2.0 + 0.5);

            // use [0 50]  > 50 remove outlies 
            if (angle > 2 || angle < -24.33 || scanID > 50 || scanID < 0)
            {
                count--;
                continue;
            }
        }
        else
        {
            printf("wrong scan number\n");
            ROS_BREAK();
        }
        //printf("angle %f scanID %d \n", angle, scanID);

遍历每一个点
通过计算的俯仰角度 来判断 这个点 在哪个线的scan上面

在这里插入图片描述
float angle = atan(point.z / sqrt(point.x point.x + point.y point.y)) * 180 / M_PI;
计算俯仰角的代码就是根据上面这个图

在这里插入图片描述
scanID = int((angle + 15) / 2 + 0.5);
根据俯仰角度求对应的线束就是 根据 上面这图 ,从最下面的线束算1,然后大约每2°,一根线,所以是 (angle + 15) / 2。最后加的0.5是为了进一位,因为是从1开始数的嘛

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

        //计算水平角
        float ori = -atan2(point.y, point.x);
        /* 把计算的水平角 放到 开始角度和结束角度 合理 的区间之内  */
        if (!halfPassed)
        { 
            if (ori < startOri - M_PI / 2)
            {
                ori += 2 * M_PI;
            }
            else if (ori > startOri + M_PI * 3 / 2)//这种情况不会发生
            {
                ori -= 2 * M_PI;
            }
            //如果超过了180,就说明过一半了
            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;
            }
        }

计算水平角
主要有 -pi 到 pi 的区间, 分成两个半圆算的,
主要就是保证把计算的水平角 放到 开始角度和结束角度 合理 的区间之内
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

        //角度的计算是为了计算相对的起始时刻的时间  为点云畸变补偿使用
        float relTime = (ori - startOri) / (endOri - startOri);//计算当前点 在起始和结束之间的比率
        //整数部分是sacn的id ,小数部分是相对起始时刻的时间
        point.intensity = scanID + scanPeriod * relTime;
        //根据scan的ID存入各自数组
        laserCloudScans[scanID].push_back(point); 
    }

角度的计算是为了计算相对的起始时刻的时间 为点云畸变补偿使用
计算当前点 在起始和结束之间的比率
整数部分是sacn的id ,小数部分是相对起始时刻的时间
根据scan的ID存入各自数组

以上从回调函数开始 整个 很多行的代码 就在 完成一个功能,求点 相对的起始时刻的时间

现在有的lidar是把每个点的时间戳自带了
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    //当前有效的点云的数目
    cloudSize = count;

    printf("points size %d \n", cloudSize);

    pcl::PointCloud<PointType>::Ptr laserCloud(new pcl::PointCloud<PointType>());//缓存叠加的每条scan的点云

    //处理每个scan  每条scan上面的 点的起始 id 为 前5个点不要  结束的id 为后6个点不要
    for (int i = 0; i < N_SCANS; i++)
    { 
        scanStartInd[i] = laserCloud->size() + 5;
        *laserCloud += laserCloudScans[i];
        scanEndInd[i] = laserCloud->size() - 6;
    }

处理每个scan 每条scan上面的 点的起始 id 为 前5个点不要 结束的id 为后6个点不要
点很多的,这10个点无所谓,主要是求曲率方便

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

以上为A-LOAM中点的预处理部分代码内容

相关实践学习
使用ROS创建VPC和VSwitch
本场景主要介绍如何利用阿里云资源编排服务,定义资源编排模板,实现自动化创建阿里云专有网络和交换机。
阿里云资源编排ROS使用教程
资源编排(Resource Orchestration)是一种简单易用的云计算资源管理和自动化运维服务。用户通过模板描述多个云计算资源的依赖关系、配置等,并自动完成所有资源的创建和配置,以达到自动化部署、运维等目的。编排模板同时也是一种标准化的资源和应用交付方式,并且可以随时编辑修改,使基础设施即代码(Infrastructure as Code)成为可能。 产品详情:https://www.aliyun.com/product/ros/
相关文章
|
3天前
|
前端开发 小程序 JavaScript
信前端里的循环显示如何编写代码?
信前端里的循环显示如何编写代码?
12 5
|
2月前
|
缓存 前端开发 数据格式
构建前端防腐策略问题之保证组件层的代码不受到接口版本变化的问题如何解决
构建前端防腐策略问题之保证组件层的代码不受到接口版本变化的问题如何解决
|
2月前
|
JavaScript 前端开发 小程序
【技巧】JS代码这么写,前端小姐姐都会爱上你
本文介绍了JavaScript编程中的实用技巧,包括解构赋值的多种妙用、数组操作技巧及常用JS功能片段。解构赋值部分涵盖短路语法防错、深度解构及默认值赋值;数组技巧包括按条件添加数据、获取最后一个元素及使用`includes`优化`if`语句;常用功能片段则涉及URL参数解析、页面滚动回顶部及获取滚动距离等。通过这些技巧,提升代码质量和效率。
22 0
【技巧】JS代码这么写,前端小姐姐都会爱上你
|
2月前
|
前端开发 API 开发者
构建前端防腐策略问题之防腐层的核心代码实现以RxJS Observable为中心的的问题如何解决
构建前端防腐策略问题之防腐层的核心代码实现以RxJS Observable为中心的的问题如何解决
|
2月前
|
开发者 图形学 C#
深度解密:Unity游戏开发中的动画艺术——Mecanim状态机如何让游戏角色栩栩如生:从基础设置到高级状态切换的全面指南,助你打造流畅自然的游戏动画体验
【8月更文挑战第31天】Unity动画系统是游戏开发的关键部分,尤其适用于复杂角色动画。本文通过具体案例讲解Mecanim动画状态机的使用方法及原理。我们创建一个游戏角色并设计行走、奔跑和攻击动画,详细介绍动画状态机设置及脚本控制。首先导入动画资源并添加Animator组件,然后创建Animator Controller并设置状态间的转换条件。通过编写C#脚本(如PlayerMovement)控制动画状态切换,实现基于玩家输入的动画过渡。此方法不仅适用于游戏角色,还可用于任何需动态动画响应的对象,增强游戏的真实感与互动性。
58 0
|
2月前
|
Android开发 iOS开发 C#
Xamarin:用C#打造跨平台移动应用的终极利器——从零开始构建你的第一个iOS与Android通用App,体验前所未有的高效与便捷开发之旅
【8月更文挑战第31天】Xamarin 是一个强大的框架,允许开发者使用单一的 C# 代码库构建高性能的原生移动应用,支持 iOS、Android 和 Windows 平台。作为微软的一部分,Xamarin 充分利用了 .NET 框架的强大功能,提供了丰富的 API 和工具集,简化了跨平台移动应用开发。本文通过一个简单的示例应用介绍了如何使用 Xamarin.Forms 快速创建跨平台应用,包括设置开发环境、定义用户界面和实现按钮点击事件处理逻辑。这个示例展示了 Xamarin.Forms 的基本功能,帮助开发者提高开发效率并实现一致的用户体验。
78 0
|
2月前
|
前端开发 开发者 Apache
揭秘Apache Wicket项目结构:如何打造Web应用的钢铁长城,告别混乱代码!
【8月更文挑战第31天】Apache Wicket凭借其组件化设计深受Java Web开发者青睐。本文详细解析了Wicket项目结构,帮助你构建可维护的大型Web应用。通过示例展示了如何使用Maven管理依赖,并组织页面、组件及业务逻辑,确保代码清晰易懂。Wicket提供的页面继承、组件重用等功能进一步增强了项目的可维护性和扩展性。掌握这些技巧,能够显著提升开发效率,构建更稳定的Web应用。
76 0
|
2月前
|
前端开发 程序员 API
从后端到前端的无缝切换:一名C#程序员如何借助Blazor技术实现全栈开发的梦想——深入解析Blazor框架下的Web应用构建之旅,附带实战代码示例与项目配置技巧揭露
【8月更文挑战第31天】本文通过详细步骤和代码示例,介绍了如何利用 Blazor 构建全栈 Web 应用。从创建新的 Blazor WebAssembly 项目开始,逐步演示了前后端分离的服务架构设计,包括 REST API 的设置及 Blazor 组件的数据展示。通过整合前后端逻辑,C# 开发者能够在统一环境中实现高效且一致的全栈开发。Blazor 的引入不仅简化了 Web 应用开发流程,还为习惯于后端开发的程序员提供了进入前端世界的桥梁。
52 0
|
2月前
|
JavaScript 前端开发
揭秘Vue.js组件魔法:如何轻松驾驭前端代码,让维护变得轻而易举?
【8月更文挑战第30天】本文探讨了如何利用Vue.js的组件化开发提升前端代码的可维护性。组件化开发将复杂页面拆分为独立、可复用的组件,提高开发效率和代码可维护性。Vue.js支持全局及局部组件注册,并提供了多种组件间通信方式如props、事件等。通过示例展示了组件定义、数据传递及复用组合的方法,强调了组件化开发在实际项目中的重要性。
21 0
|
2月前
|
机器学习/深度学习 分布式计算 前端开发
构建前端防腐策略问题之前端代码会随着技术引擎的迭代而腐烂的问题如何解决
构建前端防腐策略问题之前端代码会随着技术引擎的迭代而腐烂的问题如何解决
下一篇
无影云桌面