【原文核心对照代码】【一文足以系列】A-LOAM里程计部分简短精解

news2024/9/25 9:33:28

前言

本文将通过论文对照代码的方式阐述A-LOAM这一神奇算法。全文保持各个章节短小精悍的风格。本文会省去一些细节,但是了解大部分的论文和代码实现已经足够了。

点曲率计算与边缘点面点区分

论文中通过对点云点的曲率进行如下求曲率的计算。将计算的结果跟阈值进行比较。大于阈值认为是边缘点角点,小于另一个阈值认为是面点。

如下是计算曲率:
在这里插入图片描述

for (int i = 5; i < cloudSize - 5; i++)
{ 
    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->points[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;
}

根据论文中这部分
在这里插入图片描述

论文中为了在选取面点和边缘点时均匀选取,所以采取了将每个scan的点云进行分组,论文中分成了4组,a-loam里分成了6组。
且每组选取两个边缘点和四个面点。
代码会进行部分截取

这里就是分组:

int sp = scanStartInd[i] + (scanEndInd[i] - scanStartInd[i]) * j / 6; 
int ep = scanStartInd[i] + (scanEndInd[i] - scanStartInd[i]) * (j + 1) / 6 - 1;

选取曲率最大的点和曲率稍微大一点的点:

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]);
    }

选取面点:

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]);

剩下的点认为是平坦程度一般的点:

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);

激光雷达运动补偿

A-LOAM中由于没有集成轮速计或者imu,所以只能使用帧间匀速运动模型。这在车辆运动过程中使用这个模型是可以的,如果是手持激光雷达使用匀速运动模型,通常会有很大的误差。

对应论文中这个图片:
在这里插入图片描述
重投影到每一帧开始时刻的代码实现:
kitti数据已经对点云数据做了运动补偿处理,所以DISTORTION为0 不做具体的补偿

void TransformToStart(PointType const *const pi, PointType *const po)
{
    double s;
    if (DISTORTION)
        s = (pi->intensity - int(pi->intensity)) / SCAN_PERIOD;
    else
        s = 1.0;

    Eigen::Quaterniond q_point_last = Eigen::Quaterniond::Identity().slerp(s, q_last_curr);
    Eigen::Vector3d t_point_last = s * t_last_curr;
    Eigen::Vector3d point(pi->x, pi->y, pi->z);
    Eigen::Vector3d un_point = q_point_last * point + t_point_last;

    po->x = un_point.x();
    po->y = un_point.y();
    po->z = un_point.z();
    po->intensity = pi->intensity;
}

我们已知上一时刻到当前时刻的位姿变换,又因为是匀速运动模型,所以当前时刻到下一时刻,或者说这一帧从第0ms到第100ms的位姿变换,等同于上一时刻到当前时刻的位姿变换。这里求一下当前这个点在0ms到100ms的一个比例,然后利用上一时刻到当前时刻的位姿进行差值,就能求出当前时刻的点的位姿。

下面的代码是将全部点转换到末尾时刻的:
可以看到若是将全部点转换到末尾时刻,首先要将点转换到初始时刻

void TransformToEnd(PointType const *const pi, PointType *const po)
{
    // undistort point first
    pcl::PointXYZI un_point_tmp;
    TransformToStart(pi, &un_point_tmp);

    Eigen::Vector3d un_point(un_point_tmp.x, un_point_tmp.y, un_point_tmp.z);
    Eigen::Vector3d point_end = q_last_curr.inverse() * (un_point - t_last_curr);

    po->x = point_end.x();
    po->y = point_end.y();
    po->z = point_end.z();

    //Remove distortion time info
    po->intensity = int(pi->intensity);
}

我们已知的是q_start_end 这个是从初始时刻到末尾时刻的位姿变换,也是可以将点从末尾时刻转换到初始时刻的位姿变换。
所以我们要求的是q_end_start 即将全部初始时刻的点全部转换到末尾时刻。

帧间运动估计

论文中使用的是点到线的距离和点到面的距离,通过优化帧间位姿q t,来减小两个距离。
在这里插入图片描述

图中我们已知一个当前的特征点,但这个特征点未在图中表示出来。

左图 j 点是一个和当前时刻特征点上一时刻最近的一个点,且属于同一个scanID,l 点也是距离当前时刻特征点最近的一个点,只是要求不是同一个scanID,并且如果找到了,scanID不能距离当前特征点的scanID太远。

右图是寻找两个和当前时刻特征点距离最近的点,且属于同一个scanID。另一个点也是距离最近,同样有不是同一个scanID和scanID不能距离当前特征点scanID太远两个要求。

其中论文中点到线的距离求法:
在这里插入图片描述

对应代码如下:

struct LidarEdgeFactor
{
	LidarEdgeFactor(Eigen::Vector3d curr_point_, Eigen::Vector3d last_point_a_,
					Eigen::Vector3d last_point_b_, double s_)
		: curr_point(curr_point_), last_point_a(last_point_a_), last_point_b(last_point_b_), s(s_) {}

	template <typename T>
	bool operator()(const T *q, const T *t, T *residual) const
	{
		Eigen::Matrix<T, 3, 1> cp{T(curr_point.x()), T(curr_point.y()), T(curr_point.z())};
		Eigen::Matrix<T, 3, 1> lpa{T(last_point_a.x()), T(last_point_a.y()), T(last_point_a.z())};
		Eigen::Matrix<T, 3, 1> lpb{T(last_point_b.x()), T(last_point_b.y()), T(last_point_b.z())};

		Eigen::Quaternion<T> q_last_curr{q[3], q[0], q[1], q[2]};
		Eigen::Quaternion<T> q_identity{T(1), T(0), T(0), T(0)};

		q_last_curr = q_identity.slerp(T(s), q_last_curr);
		Eigen::Matrix<T, 3, 1> t_last_curr{T(s) * t[0], T(s) * t[1], T(s) * t[2]};

		Eigen::Matrix<T, 3, 1> lp;

		lp = q_last_curr * cp + t_last_curr;

		Eigen::Matrix<T, 3, 1> nu = (lp - lpa).cross(lp - lpb);	// 模是三角形的面积
		Eigen::Matrix<T, 3, 1> de = lpa - lpb;

		residual[0] = nu.x() / de.norm();
		residual[1] = nu.y() / de.norm();
		residual[2] = nu.z() / de.norm();

		return true;
	}

在这里插入图片描述

对应代码:

struct LidarPlaneNormFactor
{

	LidarPlaneNormFactor(Eigen::Vector3d curr_point_, Eigen::Vector3d plane_unit_norm_,
						 double negative_OA_dot_norm_) : curr_point(curr_point_), plane_unit_norm(plane_unit_norm_),
														 negative_OA_dot_norm(negative_OA_dot_norm_) {}

	template <typename T>
	bool operator()(const T *q, const T *t, T *residual) const
	{
		Eigen::Quaternion<T> q_w_curr{q[3], q[0], q[1], q[2]};
		Eigen::Matrix<T, 3, 1> t_w_curr{t[0], t[1], t[2]};
		Eigen::Matrix<T, 3, 1> cp{T(curr_point.x()), T(curr_point.y()), T(curr_point.z())};
		Eigen::Matrix<T, 3, 1> point_w;
		point_w = q_w_curr * cp + t_w_curr;

		Eigen::Matrix<T, 3, 1> norm(T(plane_unit_norm.x()), T(plane_unit_norm.y()), T(plane_unit_norm.z()));
		residual[0] = norm.dot(point_w) + T(negative_OA_dot_norm);
		return true;
	}

残差细节可以细读源码。
大概意思是:基于匀速运动模型,根据当前点的比例,计算当前点基于上一时刻的位姿。通过这个位姿将这个点转换到上一时刻,在和上一时刻的两个点或者三个点求点到线的距离或者点到面的距离。

寻找最近点的函数:

KD树构造:

pcl::KdTreeFLANN<pcl::PointXYZI>::Ptr kdtreeCornerLast(new pcl::KdTreeFLANN<pcl::PointXYZI>());

将上一帧点云数据存入KD树:kdtreeCornerLast->setInputCloud(laserCloudCornerLast);

使用KD树寻找最近的点:

kdtreeCornerLast->nearestKSearch(pointSel, 1, pointSearchInd, pointSearchSqDis);

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

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

相关文章

org.slf4j.Logger无法输出日志的BUG

场景依赖<dependency><groupId>org.apache.zookeeper</groupId><artifactId>zookeeper</artifactId><version>3.4.13</version></dependency><dependency><groupId>org.slf4j</groupId><artifactId>s…

第一章 Iceberg入门介绍

1、Iceberg简介 本质&#xff1a;一种数据组织格式 1.1、应用场景 ①面向大表&#xff1a;单表包含数十个PB的数据 ②分布式引擎非必要&#xff1a;不需要分布式SQL引擎来读取或查找文件 ③高级过滤&#xff1a;使用表元数据&#xff0c;使用分区和列级统计信息修建数据文…

技术管理者如何获得下属的认同?

你好&#xff0c;我是童军&#xff0c;目前是华锐技术资管营销研发团队总监。今天我将从自己的工作场景出发&#xff0c;讲讲我是如何和团队小伙伴相处沟通&#xff0c;并获得认同的。 我们先看一个小故事。 我刚当上主管那会儿&#xff0c;在和新入职同事沟通具体工作时&…

【数组相关面试题】LeetCode试题

前言&#xff1a;在之前我们已经学习过了顺序表的相关概念以及实现的方法&#xff0c;今天我们通过几个题来进行应用了解。 目录1.第一题([oj链接](https://leetcode.cn/problems/remove-element/))2.第二题&#xff08;[oj链接](https://leetcode.cn/problems/remove-duplicat…

Ubuntu和Linux开发板网络环境搭建

参考&#xff1a;https://www.bilibili.com/video/BV1n541197rk?spm_id_from333.999.0.0 目录前言STM32MP157 开发板网络环境搭建开发工具网络拓扑结构Ubuntu 常用工具安装同一网段ping 测试概念关闭Ubuntu 和Windows 防火墙电脑和开发板直连同个路由器准备工作VMware 设置查看…

Java:每个开发人员职业生涯的基本Java技能

早在1996年&#xff0c;Java就首次被引入世界&#xff0c;如今仍然非常受欢迎。2021&#xff0c;全球超过35%的程序员使用这种语言。此外&#xff0c;它是TIOBE索引中最受欢迎的三种编程语言之一。作为Java初学者&#xff0c;这对你意味着什么?这意味着你必须获得竞争优势&…

浏览器的URL中每个中字符的“乱码”问题,字符集的解码和编码

uft-8和Unicode字符表对应&#xff0c;查找可参考&#xff1a;https://www.utf8-chartable.de/unicode-utf8-table.pl 几个好用的字符集转换网址&#xff1a;http://web.chacuo.net/charseturlencode&#xff0c;https://123.w3cschool.cn/webtools&#xff0c;http://mytju.co…

JVM详解--内存结构

文章目录什么是JVM内存结构程序计数器&#xff08;Program Counter Register&#xff09;虚拟机栈&#xff08;Java Virtual Machine Stacks&#xff09;概述栈内存溢出本地方法栈堆&#xff08;Heap&#xff09;堆内存溢出堆内存诊断方法区方法区内存溢出常量池运行时常量池St…

PHP手册

NULL 未定义和unset()的变量都将解析为值null unset() unset( $var, ...$vars) 如果在函数中 unset() 一个全局变量&#xff0c;则只是局部变量被销毁&#xff0c;而在调用环境中的变量将保持调用 unset() 之前一样的值。 <?php function destroy_foo() {global $foo;un…

双系统下linux分区被误删的解决办法

前言在windows系统的磁盘管理中误删了ubuntu的磁盘分区&#xff0c;开机后一直卡在grub界面。Windows/Linux双启动的机器一般都使用grub作为引导程序。如果不小心在Windows中删除了linux分区&#xff0c;grub就会因为找不到配置文件而造成无法启动。 系统配置 系统类型&#x…

Qt新手入门指南 - 如何创建模型/视图(一)

每个UI开发人员都应该了解ModelView编程&#xff0c;本教程的目标是为大家提供一个简单易懂的介绍。Qt 是目前最先进、最完整的跨平台C开发工具。它不仅完全实现了一次编写&#xff0c;所有平台无差别运行&#xff0c;更提供了几乎所有开发过程中需要用到的工具。如今&#xff…

java排错定位

1、检查有没有报错信息 日志文件中登记的错误&#xff0c;这个算是最简单的&#xff0c;在定位错误时&#xff0c;也最希望问题在这一步得到确认。在打印异常时&#xff0c;通常会打印异常的调用栈信息&#xff0c;通过调用栈信息就可以很便捷的定位问题了。 例如&#xff1a; …

【JavaScript】原型与原型链以及判断数据类型方式

&#x1f4bb; 【JavaScript】原型与原型链以及判断数据类型方式 &#x1f3e0;专栏&#xff1a;JavaScript &#x1f440;个人主页&#xff1a;繁星学编程&#x1f341; &#x1f9d1;个人简介&#xff1a;一个不断提高自我的平凡人&#x1f680; &#x1f50a;分享方向&#…

Redis对不起是我肤浅了(基础和应用篇):位图(Bitmaps)的妙用和深入分析每个命令的用法

一、前言 在Redis 4.0 版本之前&#xff0c;Redis是单线程程序&#xff0c;主要是指Redis的网络I/O线程。Redis的持久化、集群同步等操作&#xff0c;则是由另外的线程来执行的。但在Redis 4.0 版本之后&#xff0c;Redis添加了多线程的支持&#xff0c;这时的多线程主要体现在…

【IT互联网行业内,什么岗位工作更有前景?】

前言互联网及IT行业作为集技术与高薪于一身的新技术行业&#xff0c;不仅成为时下众多年轻人的首选行业&#xff0c;其本身也承载了社会、企业数字化发展转型的重担&#xff0c;从国家到社会、市场都非常重视行业技术的发展和渗透&#xff0c;其重要性不言而喻。作为普通人的小…

AcWing 1073. 树的中心(详解树形DP和换根DP)

AcWing 1073. 树的中心&#xff08;树形DP 换根DP&#xff09;一、问题二、思路1、暴力做法2、树形DP换根DP&#xff08;1&#xff09;思路分析&#xff08;2&#xff09;普通树形DP与换根DP的区别三、代码一、问题 二、思路 1、暴力做法 这道题其实暴力的做法很简单&#x…

【金融学】Economics of Money and Banking {暂时搁置,中级宏观和微观经济学未学}

Economics of Money and BankingClass1 The Big PicturePrerequisitesSome MaterialsCourse Material: https://www.coursera.org/learn/money-banking/lecture/8WXSW/the-big-picture Class1 The Big Picture Prerequisites intermediate macroeconomics 中级宏观经济学 int…

Java on VS Code 2023年1月更新|Spring 插件包、代码补全更新以及性能改进

作者&#xff1a;Nick Zhu - Senior Program Manager, Developer Division at Microsoft 排版&#xff1a;Alan Wang 大家好&#xff0c;欢迎来到我们 2023 年的第一篇博客&#xff01;我们想与您分享几个与 Spring 插件、代码编辑和性能相关的激动人心的更新&#xff0c;让我们…

XML方式—解决mybatis实体类属性名和数据库字段名不一致问题

数据库字段与类属性名称不一致&#xff0c;导致查询数据时数据没有封装上。 [Brand{id1, brandNamenull, companyNamenull}, Brand{id2, brandNamenull, companyNamenull}]解决方式一&#xff08;为表字段取别名&#xff09; <select id"selectAll" resultType&…

【大唐杯备考】——5G网络组网部署(学习笔记)

&#x1f4d6; 前言&#xff1a;本期介绍5G网络组网部署。 目录&#x1f552; 1. SA组网和NSA组网&#x1f558; 1.1 SA组网&#x1f558; 1.2 NSA组网&#x1f564; 1.2.1 Option 3系列&#x1f564; 1.2.2 Option 7系列&#x1f564; 1.2.3 Option 4系列&#x1f558; 1.3 组…