3D激光SLAM:ALOAM---后端 lasermapping构建角点约束与面点约束

简介: 后端的构建约束问题和前端不一样。原因就是前端从上一帧上去找,而后端是在局部地图上找,点要多很多,并且没有了线束信息,所以原理上不一样了。**线特征的提取**通过kdtree在局部地图中找到5个最近的线特征,为了判断他们是否符合线特征的特性,需要对5个点构成的协方差矩阵进行特征值分解,当上述5个点在一条直线上时,他们只有一个主方向,也就是特征值是一个大特征值,以及两个小特征值,大特征值对应的特征向量就是对应直线的方向向量。**面特征的提取**通过kdtree在地图中找到最近的面特征也是5个, 理论上也可以通过特种值分解的方式,最小的特征值对应的特征向量就是平面的法向量, 不过代码里选

前言

上一篇博客中地图栅格化处理与提取完成了从整个地图中提取出要和当前帧做匹配的局部地图

下面要做的就是要构建角点约束和面点约束

后端的构建约束问题和前端不一样。原因就是前端从上一帧上去找,而后端是在局部地图上找,点要多很多,并且没有了线束信息,所以原理上不一样了。

线特征的提取
通过kdtree在局部地图中找到5个最近的线特征,为了判断他们是否符合线特征的特性,需要对5个点构成的协方差矩阵进行特征值分解,当上述5个点在一条直线上时,他们只有一个主方向,也就是特征值是一个大特征值,以及两个小特征值,大特征值对应的特征向量就是对应直线的方向向量。

面特征的提取
通过kdtree在地图中找到最近的面特征也是5个, 理论上也可以通过特种值分解的方式,最小的特征值对应的特征向量就是平面的法向量, 不过代码里选用的是平面拟合的方式。

平面方程为 Ax+By+Cz+D=0,D可以当成1(相当于等号两边分别乘1/D),也就是3个未知数,5个方程。写成矩阵的形式,可以构成5*3的矩阵,线性方程组,可以求出解。得到结果后,验证的方法为分别求出5个点到平面的距离,如果太远,则说明平面拟合不成功。

代码分析

代码在lasermapping.cpp中
上一篇博客中地图栅格化处理与提取,得到了局部地图,继续代码的分析

            if (laserCloudCornerFromMapNum > 10 && laserCloudSurfFromMapNum > 50)//角点和面点个数满足要求
            {
   

局部地图中的有效点云数目进行判断
局部地图中的角点和面点不能太少,要求

  • 角点不少于10个
  • 面点不少于50个
                kdtreeCornerFromMap->setInputCloud(laserCloudCornerFromMap);
                kdtreeSurfFromMap->setInputCloud(laserCloudSurfFromMap);
                printf("build tree time %f ms \n", t_tree.toc());//打印建立kd-tree的时间

把局部地图的角点和面点 送入KD tree 之后进行最近邻搜索
通过pcl建立kd-tree 还是比较耗时的

之后进入优化的迭代

                for (int iterCount = 0; iterCount < 2; iterCount++)//迭代两次
                {
   

建立对应关系的迭代次数不超过2次

                    ceres::LossFunction *loss_function = new ceres::HuberLoss(0.1);//生成损失函数
                    ceres::LocalParameterization *q_parameterization =
                        new ceres::EigenQuaternionParameterization();// 添加四元数得参数模块
                    ceres::Problem::Options problem_options;//声明ceres的 优化问题

                    ceres::Problem problem(problem_options);// 构建问题
                    problem.AddParameterBlock(parameters, 4, q_parameterization);//添加四元数得参数模块
                    problem.AddParameterBlock(parameters + 4, 3);//平移的参数块,parameters+4就是上面四元数后的指针便宜,3就是平移的参数个数

建立ceres问题
建立参数块
这边和帧间里程计类似,就是参数块的建立有些区别,帧间是用了四元数和平移两个数组,这边用的parameters一个数组。
前4个是四元数,后三个是平移
在这里插入图片描述
下面开始构建角点约束

构建角点约束

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

循环上一帧(滤波后)的每个角点

                        pointOri = laserCloudCornerStack->points[i];//取出该点
                        // 把当前点根据初值投影到地图坐标系下去
                        pointAssociateToMap(&pointOri, &pointSel);

取出该点
把当前点根据初值投影到地图坐标系下去
投影的方法就是用当前的初始位姿估计,做位姿变换(乘四元数加平移)
在这里插入图片描述

                        // 地图中寻找和该点最近的5个点
                        kdtreeCornerFromMap->nearestKSearch(pointSel, 5, pointSearchInd, pointSearchSqDis);

通过kd-tree在局部地图中寻找和该点最近的5个点
点的索引在 pointSearchInd 数组中
点的距离在 pointSearchSqDis 数组中

                        // 判断最远的点的距离不能超过1m,否则就是无效约束
                        if (pointSearchSqDis[4] < 1.0)
                        {
   

判断最远的点的距离不能超过1m,否则就是无效约束

                            std::vector<Eigen::Vector3d> nearCorners;//5个最近点构成的数组
                            Eigen::Vector3d center(0, 0, 0);// 中心点
                            for (int j = 0; j < 5; j++)
                            {
   
                                Eigen::Vector3d tmp(laserCloudCornerFromMap->points[pointSearchInd[j]].x,
                                                    laserCloudCornerFromMap->points[pointSearchInd[j]].y,
                                                    laserCloudCornerFromMap->points[pointSearchInd[j]].z);
                                center = center + tmp;
                                nearCorners.push_back(tmp);//构建5个点的数组
                            }
                            //计算这5个点的均值
                            center = center / 5.0;

由这5个点构成一个数组,并计算中心点

                            //构建协方差矩阵
                            for (int j = 0; j < 5; j++)
                            {
   
                                Eigen::Matrix<double, 3, 1> tmpZeroMean = nearCorners[j] - center;//每个点减去均值
                                covMat = covMat + tmpZeroMean * tmpZeroMean.transpose();
                            }

构建协方差矩阵
方法就是:

  • 每个点减去均值
  • 各自点向量乘上各自点向量的转置,然后加在一起
                            //进行特征值分解
                            Eigen::SelfAdjointEigenSolver<Eigen::Matrix3d> saes(covMat);

通过Eigen的特征值分解器进行特征值分解

          Eigen::Vector3d unit_direction = saes.eigenvectors().col(2);

分解后特征向量是根据特征值大小,由大到小排列,这里取出最后一列的特征向量,即线的方向

                            if (saes.eigenvalues()[2] > 3 * saes.eigenvalues()[1])
                            {
   

最大特征值大于次特征值的3倍,认为该5个点构成的是一条线,满足线特征

                                Eigen::Vector3d point_on_line = center;
                                Eigen::Vector3d point_a, point_b;
                                // 根据拟合出来的线特征方向,以平均点为中心,建立两个虚拟点
                                point_a = 0.1 * unit_direction + point_on_line;
                                point_b = -0.1 * unit_direction + point_on_line;
                                // 构建角点约束,和lidar odom 约束一致

根据拟合出来的线特征方向,以平均点为中心,建立两个虚拟点
在这里插入图片描述

                                // 构建角点约束,和lidar odom 约束一致
                                ceres::CostFunction *cost_function = LidarEdgeFactor::Create(curr_point, point_a, point_b, 1.0);
                                problem.AddResidualBlock(cost_function, loss_function, parameters, parameters + 4);
                                corner_num++;

构建角点约束,和lidar odom 约束一致

构建面点约束

下面是构建面点约束

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

循环上一帧(滤波后)的每个面点

                        pointOri = laserCloudSurfStack->points[i];//取出该点
                        //double sqrtDis = pointOri.x * pointOri.x + pointOri.y * pointOri.y + pointOri.z * pointOri.z;
                        pointAssociateToMap(&pointOri, &pointSel);//投影到局部地图中
                        kdtreeSurfFromMap->nearestKSearch(pointSel, 5, pointSearchInd, pointSearchSqDis);//从kd-tree中找到5个最近邻的面点

把该投影到局部地图中,从kd-tree中找到5个最近邻的面点

下面开始构建方程,方程如下:
在这里插入图片描述
在这里插入图片描述

                        Eigen::Matrix<double, 5, 3> matA0;//等号左边5*3的矩阵
                        Eigen::Matrix<double, 5, 1> matB0 = -1 * Eigen::Matrix<double, 5, 1>::Ones();//等号右边5*1的矩阵

声明等号左边和右边的矩阵

                        //构建 Ax+By+Cz+1=0 的平面方程
                        // 通过构建超定方程来求解这个平面方程
                        if (pointSearchSqDis[4] < 1.0)//最远的面点满足条件
                        {
   

                            for (int j = 0; j < 5; j++)
                            {
   
                                matA0(j, 0) = laserCloudSurfFromMap->points[pointSearchInd[j]].x;
                                matA0(j, 1) = laserCloudSurfFromMap->points[pointSearchInd[j]].y;
                                matA0(j, 2) = laserCloudSurfFromMap->points[pointSearchInd[j]].z;
                                //printf(" pts %f %f %f ", matA0(j, 0), matA0(j, 1), matA0(j, 2));
                            }
                            // find the norm of plane
                            Eigen::Vector3d norm = matA0.colPivHouseholderQr().solve(matB0);
                            // 法向量模的倒数
                            double negative_OA_dot_norm = 1 / norm.norm();
                            //法向量归一化
                            norm.normalize();

最远的面点满足条件后,开始构建 Ax+By+Cz+1=0 的平面方程
然后通过构建超定方程来求解这个平面方程

用的Eigen 的colPivHouseholderQr 求解方法,结果 norm 就是[A B C ]
也就是平面的法向量

下面通过点到平面的距离来看是否符合平面约束
在这里插入图片描述

                            bool planeValid = true;
                            // 根据求出来的平面方程进行校验,看看是否符合平面约束
                            for (int j = 0; j < 5; j++)
                            {
   
                                // if OX * n > 0.2, then plane is not fit well
                                if (fabs(norm(0) * laserCloudSurfFromMap->points[pointSearchInd[j]].x +
                                         norm(1) * laserCloudSurfFromMap->points[pointSearchInd[j]].y +
                                         norm(2) * laserCloudSurfFromMap->points[pointSearchInd[j]].z + negative_OA_dot_norm) > 0.2)
                                {
   
                                    planeValid = false;
                                    break;
                                }
                            }

代码里好像和公式不太一样,其实是一样的
首先法向量已经被归一化了,所以分母部分已经是1了
D就成了上面求得法向量模的倒数
然后距离和0.2m做判断,大于0.2m则认为平面拟合不成功

                            Eigen::Vector3d curr_point(pointOri.x, pointOri.y, pointOri.z);//取出该帧的点
                            //如果平面有效,则添加面点的平面约束
                            if (planeValid)
                            {
   
                                //利用平面方程构建约束,和前端有所不同
                                ceres::CostFunction *cost_function = LidarPlaneNormFactor::Create(curr_point, norm, negative_OA_dot_norm);
                                problem.AddResidualBlock(cost_function, loss_function, parameters, parameters + 4);
                                surf_num++;
                            }

如果平面有效,则添加面点的平面约束
利用平面方程构建约束,和前端有所不同

总结

  • 对局部地图的角度和面点个数进行判断(是满足优化条件)
  • 局部地图构建KD-tree
  • 构建ceres问题与参数块
  • 构建角点约束
    a通过kd-tree找到5个最近邻角点
    b通过特征值分解求线的方向
    c通过最大特征值与次特征值的大小判断是否满足线特征
    d以中心点与线的方向构建2个虚拟点
    e构建角点约束(当前点和2个虚拟点)

  • 构建面点约束
    a通过kd-tree找到5个最近邻面点
    b通过求解平面方程得到法向量
    c通过5个点到平面的距离判断是否满足面特征
    d构建面点约束

相关文章
|
1月前
|
Web App开发 JavaScript 前端开发
深入浅出Node.js:从零开始构建后端服务
【10月更文挑战第42天】在数字时代的浪潮中,掌握一门后端技术对于开发者来说至关重要。Node.js,作为一种基于Chrome V8引擎的JavaScript运行环境,允许开发者使用JavaScript编写服务器端代码,极大地拓宽了前端开发者的技能边界。本文将从Node.js的基础概念讲起,逐步引导读者理解其事件驱动、非阻塞I/O模型的核心原理,并指导如何在实战中应用这些知识构建高效、可扩展的后端服务。通过深入浅出的方式,我们将一起探索Node.js的魅力和潜力,解锁更多可能。
|
1月前
|
缓存 API 持续交付
构建高效后端服务的实践之路
【10月更文挑战第34天】在数字化时代的浪潮中,后端服务是支撑整个互联网的脊梁。本文将深入探讨如何构建一个高效的后端服务,涵盖从选择合适的编程语言和框架,到设计高性能的数据库交互、实现缓存机制、优化API设计,以及确保服务的高可用性和可扩展性。我们将通过实际代码示例,展示如何在面对复杂业务逻辑时,依然能够保持后端服务的高效与稳定。无论你是后端开发的新手,还是希望提升现有服务性能的资深开发者,这篇文章都将为你提供宝贵的实践经验和启示。
|
27天前
|
弹性计算 持续交付 API
构建高效后端服务:微服务架构的深度解析与实践
在当今快速发展的软件行业中,构建高效、可扩展且易于维护的后端服务是每个技术团队的追求。本文将深入探讨微服务架构的核心概念、设计原则及其在实际项目中的应用,通过具体案例分析,展示如何利用微服务架构解决传统单体应用面临的挑战,提升系统的灵活性和响应速度。我们将从微服务的拆分策略、通信机制、服务发现、配置管理、以及持续集成/持续部署(CI/CD)等方面进行全面剖析,旨在为读者提供一套实用的微服务实施指南。
|
24天前
|
弹性计算 Kubernetes API
构建高效后端服务:微服务架构的深度剖析与实践####
本文深入探讨了微服务架构的核心理念、设计原则及实现策略,旨在为开发者提供一套系统化的方法论,助力其构建灵活、可扩展且易于维护的后端服务体系。通过案例分析与实战经验分享,揭示了微服务在提升开发效率、优化资源利用及增强系统稳定性方面的关键作用。文章首先概述了微服务架构的基本概念,随后详细阐述了其在后端开发中的应用优势与面临的挑战,最后结合具体实例,展示了如何从零开始规划并实施一个基于微服务的后端项目。 ####
|
28天前
|
JSON 缓存 测试技术
构建高效RESTful API的后端实践指南####
本文将深入探讨如何设计并实现一个高效、可扩展且易于维护的RESTful API。不同于传统的摘要概述,本节将直接以行动指南的形式,列出构建RESTful API时必须遵循的核心原则与最佳实践,旨在为开发者提供一套直接可行的实施框架,快速提升API设计与开发能力。 ####
|
1月前
|
缓存 安全 前端开发
构建高效后端服务:从理论到实战
在数字化浪潮的推动下,后端服务成为了支撑现代互联网应用的核心。本文旨在揭示如何打造一个既可靠又高效的后端系统,从基础架构设计、代码组织、性能优化到安全防护,全方位解析后端开发的艺术。通过实际代码示例和深入浅出的解释,引导读者理解并掌握后端开发的关键技术点。
|
1月前
|
JSON API 数据格式
探索后端开发:从零构建简易RESTful API
在数字时代的浪潮中,后端开发如同搭建一座桥梁,连接着用户界面与数据世界。本文将引导读者步入后端开发的殿堂,通过构建一个简易的RESTful API,揭示其背后的逻辑与魅力。我们将从基础概念出发,逐步深入到实际操作,不仅分享代码示例,更探讨如何思考和解决问题,让每一位读者都能在后端开发的道路上迈出坚实的一步。
|
1月前
|
缓存 负载均衡 JavaScript
构建高效后端服务:Node.js与Express框架实践
在数字化时代的浪潮中,后端服务的重要性不言而喻。本文将通过深入浅出的方式介绍如何利用Node.js及其强大的Express框架来搭建一个高效的后端服务。我们将从零开始,逐步深入,不仅涉及基础的代码编写,更会探讨如何优化性能和处理高并发场景。无论你是后端新手还是希望提高现有技能的开发者,这篇文章都将为你提供宝贵的知识和启示。
|
1月前
|
监控 持续交付 数据库
构建高效的后端服务:微服务架构的深度解析
在现代软件开发中,微服务架构已成为提升系统可扩展性、灵活性和维护性的关键。本文深入探讨了微服务架构的核心概念、设计原则和最佳实践,通过案例分析展示了如何在实际项目中有效地实施微服务策略,以及面临的挑战和解决方案。文章旨在为开发者提供一套完整的指导框架,帮助他们构建出更加高效、稳定的后端服务。
|
1月前
|
消息中间件 缓存 负载均衡
构建高效的后端服务:策略与实践
在本文中,我们将深入探讨如何设计和实现一个高效的后端服务。我们将从架构设计、性能优化、安全性和可扩展性等方面进行讨论,以帮助读者了解如何构建一个稳定、高效且安全的后端服务。