3D激光SLAM:ALOAM:激光雷达的运动畸变补偿代码解析

简介: **什么是激光雷达的运动畸变 ?**激光雷达的一帧数据是过去一周期内形成的所有数据,数据仅有一时间戳,而非某个时刻的数据,因此在这一帧时间内的激光雷达或者其载体通常会发生运动,因此,这一帧的原点不一致,会导致一些问题,这个问题就是**运动畸变**

前言

什么是激光雷达的运动畸变 ?
激光雷达的一帧数据是过去一周期内形成的所有数据,数据仅有一时间戳,而非某个时刻的数据,因此在这一帧时间内的激光雷达或者其载体通常会发生运动,因此,这一帧的原点不一致,会导致一些问题,这个问题就是运动畸变
图
所以需要去运动畸变,也叫畸变校正

如何进行运动补偿?

运动补偿的目的就是把所有的点云补偿到某一时刻,这样就可以把本身在过去100ms内收集的点云统一到一个时间点上去 这个时间点可以是起始时刻,也可以是结束时刻,也可以是中间的任意时刻

常见的是补偿到起始时刻

Pstart = T_start_current * Pcurrent

畸变校准方法
因此运动补偿需要知道每个点时刻对应的位姿 T_start_current 通常有几种做法

1 如果有高频里程计,可以比较方便的获取每个点相对起始扫描时刻的位姿

2 如果有imu,可以方便的求出每个点相对起始点的旋转

3 如果没有其它传感器,可以使用匀速模型假设,使用上一帧间里程计的结果,作为当前两帧之间的运动,同时假设当前帧也是匀速运动,也可以估计出每个点相对起始时刻的位姿
k-1 到 k 帧 和 k到k+1帧的运动是一至的,用k-1到k帧的位姿变换当做k到k+1帧的位姿变换, 可以求到k到k+1帧的每个点的位姿变换

ALOAM是使用的纯lidar的方式,所以使用的是第3种方式

Code

将帧中间的点转到起始点坐标系下

// undistort lidar point
void TransformToStart(PointType const *const pi, PointType *const po)
{

功能函数的名字 : TransformToStart
形参 传入的指针 pi是输入点的点云地址 po是转换后的输出点的点云地址

调用的的时候按下面这种方式用就可以了

 TransformToStart(&(cornerPointsSharp->points[i]), &pointSel);

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

    //interpolation ratio
    double s;
    //由于kitti数据集上的lidar已经做过了运动补偿,因此这里就不做具体补偿了
    if (DISTORTION)
        // intensity 实数部分存的是 scan上点的 id 虚数部分存的这一点相对这一帧起始点的时间差
        s = (pi->intensity - int(pi->intensity)) / SCAN_PERIOD;//求出了点占的时间比率
    else
        s = 1.0;  //s = 1  说明全部补偿到点云结束的时刻

s代表要转换的点在根据时间在这一帧里占的比率
SCAN_PERIOD是一帧的时间,10hz的lidar,那么周期就是0.1s
在前面的点的预处理时将 intensity 附了别的值 实数部分存的是 scan上点的 id 虚数部分存的这一点相对这一帧起始点的时间差
图
intensity的整体减去实数部分,就是时间差,那么除以周期,也就是时间占比了

有的lidar在内部做了去畸变,那么可以设置DISTORTION为1
则s为1,那么按照上面的定义,就是把全部补偿到点云结束的时刻

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

    // 这里相当于是一个匀速模型的假设
    // 把位姿变换分解成平移和旋转  
    Eigen::Quaterniond q_point_last = Eigen::Quaterniond::Identity().slerp(s, q_last_curr);
    Eigen::Vector3d t_point_last = s * t_last_curr;

做一个匀速模型假设,即上一帧的位姿变换,就是这帧的位姿变换
以此来求出输入点坐标系到起始时刻点坐标系的位姿变换,通过上面求的时间占比s

这里把位姿变换 分解成了 旋转 + 平移 的方式

由于四元数是一个超复数,不是简单的乘法,求它的占比用的 Eigen的slerp函数(球面线性插值)

在这里插入图片描述
线性插值和球面线性插值的对比,如上图
两个单位四元数之间进行插值,如左图的线性插值,得到的四元数一定不是单位四元数,我们期望对于旋转的插值应该是不改变长度的,所以显然右图球面(Slerp)插值更为合理。

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

    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;//赋值原的intensity
}

将求得的转换后的坐标赋值给输出点
赋值原的intensity

将帧中间的点转到起始点坐标系下

这个是通过反变换的思想,首先把点统一到起始时刻坐标系下,再通过反变换,得到结束时刻坐标系下的点

void TransformToEnd(PointType const *const pi, PointType *const po)
{

功能函数的名字 : TransformToEnd
形参 传入的指针 pi是输入点的点云地址 po是转换后的输出点的点云地址

调用的的时候按下面这种方式用就可以了

TransformToEnd(&cornerPointsLessSharp->points[i], &cornerPointsLessSharp->points[i]);

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

    // 首先先做畸变校正,都转到起始时刻
    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);//取出起始时刻坐标系下的点
    //q_last_curr  \ t_last_curr 是结束时刻坐标系转到起始时刻坐标系 的 旋转 和 平移  
    Eigen::Vector3d point_end = q_last_curr.inverse() * (un_point - t_last_curr);//通过反变换,求得转到 结束时刻坐标系下 的点坐标

q_last_curr \ t_last_curr 是结束时刻坐标系转到起始时刻坐标系 的 旋转 和 平移

代码的公式可以推导下:
在这里插入图片描述
两边乘 Re-s的逆
在这里插入图片描述
合并下
在这里插入图片描述
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    //将求得的转换后的坐标赋值给输出点
    po->x = point_end.x();
    po->y = point_end.y();
    po->z = point_end.z();

    //移除掉 intensity 相对时间的信息
    po->intensity = int(pi->intensity);
}

将求得的转换后的坐标赋值给输出点
移除掉 intensity 相对时间的信息
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

整体代码:

// undistort lidar point
void TransformToStart(PointType const *const pi, PointType *const po)
{
    //interpolation ratio
    double s;
    //由于kitti数据集上的lidar已经做过了运动补偿,因此这里就不做具体补偿了
    if (DISTORTION)
        // intensity 实数部分存的是 scan上点的 id 虚数部分存的这一点相对这一帧起始点的时间差
        s = (pi->intensity - int(pi->intensity)) / SCAN_PERIOD;//求出了点占的时间比率
    else
        s = 1.0;  //s = 1  说明全部补偿到点云结束的时刻
    //s = 1;
    //所有点的操作方式都是一致的,相当于从结束时刻补偿到起始时刻
    // 这里相当于是一个匀速模型的假设
    // 把位姿变换分解成平移和旋转  
    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;//赋值原的intensity
}

// transform all lidar points to the start of the next frame

void TransformToEnd(PointType const *const pi, PointType *const po)
{
    // 首先先做畸变校正,都转到起始时刻
    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);//取出起始时刻坐标系下的点
    //q_last_curr  \ t_last_curr 是结束时刻坐标系转到起始时刻坐标系 的 旋转 和 平移  
    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();

    //移除掉 intensity 相对时间的信息
    po->intensity = int(pi->intensity);
}

以上是 激光雷达的运动畸变以及补偿方式 的代码解析

相关文章
|
10月前
|
算法 PyTorch 算法框架/工具
昇腾 msmodelslim w8a8量化代码解析
msmodelslim w8a8量化算法原理和代码解析
823 5
|
12月前
|
搜索推荐 UED Python
实现一个带有昼夜背景切换的动态时钟:从代码到功能解析
本文介绍了一个使用Python和Tkinter库实现的动态时钟程序,具有昼夜背景切换、指针颜色随机变化及整点和半点报时功能。通过设置不同的背景颜色和随机变换指针颜色,增强视觉吸引力;利用多线程技术确保音频播放不影响主程序运行。该程序结合了Tkinter、Pygame、Pytz等库,提供了一个美观且实用的时间显示工具。欢迎点赞、关注、转发、收藏!
496 94
|
10月前
|
人工智能 小程序 前端开发
【一步步开发AI运动小程序】十九、运动识别中如何解析RGBA帧图片?
本文介绍了如何将相机抽取的RGBA帧图像解析为`.jpg`或`.png`格式,适用于体测、赛事等场景。首先讲解了RGBA图像结构,其为一维数组,每四个元素表示一个像素的颜色与透明度值。接着通过`uni.createOffscreenCanvas()`创建离屏画布以减少绘制干扰,并提供代码实现,将RGBA数据逐像素绘制到画布上生成图片。最后说明了为何不直接使用拍照API及图像转换的调用频率建议,强调应先暂存帧数据,运动结束后再进行转换和上传,以优化性能。
|
10月前
|
传感器 监控 Java
Java代码结构解析:类、方法、主函数(1分钟解剖室)
### Java代码结构简介 掌握Java代码结构如同拥有程序世界的建筑蓝图,类、方法和主函数构成“黄金三角”。类是独立的容器,承载成员变量和方法;方法实现特定功能,参数控制输入环境;主函数是程序入口。常见错误包括类名与文件名不匹配、忘记static修饰符和花括号未闭合。通过实战案例学习电商系统、游戏角色控制和物联网设备监控,理解类的作用、方法类型和主函数任务,避免典型错误,逐步提升编程能力。 **脑图速记法**:类如太空站,方法即舱段;main是发射台,static不能换;文件名对仗,括号要成双;参数是坐标,void不返航。
419 5
|
11月前
|
人工智能 文字识别 自然语言处理
保单AI识别技术及代码示例解析
车险保单包含基础信息、车辆信息、人员信息、保险条款及特别约定等关键内容。AI识别技术通过OCR、文档结构化解析和数据校验,实现对保单信息的精准提取。然而,版式多样性、信息复杂性、图像质量和法律术语解析是主要挑战。Python代码示例展示了如何使用PaddleOCR进行保单信息抽取,并提出了定制化训练、版式分析等优化方向。典型应用场景包括智能录入、快速核保、理赔自动化等。未来将向多模态融合、自适应学习和跨区域兼容性发展。
|
自然语言处理 搜索推荐 数据安全/隐私保护
鸿蒙登录页面好看的样式设计-HarmonyOS应用开发实战与ArkTS代码解析【HarmonyOS 5.0(Next)】
鸿蒙登录页面设计展示了 HarmonyOS 5.0(Next)的未来美学理念,结合科技与艺术,为用户带来视觉盛宴。该页面使用 ArkTS 开发,支持个性化定制和无缝智能设备连接。代码解析涵盖了声明式 UI、状态管理、事件处理及路由导航等关键概念,帮助开发者快速上手 HarmonyOS 应用开发。通过这段代码,开发者可以了解如何构建交互式界面并实现跨设备协同工作,推动智能生态的发展。
762 10
鸿蒙登录页面好看的样式设计-HarmonyOS应用开发实战与ArkTS代码解析【HarmonyOS 5.0(Next)】
|
12月前
|
SQL Java 数据库连接
如何在 Java 代码中使用 JSqlParser 解析复杂的 SQL 语句?
大家好,我是 V 哥。JSqlParser 是一个用于解析 SQL 语句的 Java 库,可将 SQL 解析为 Java 对象树,支持多种 SQL 类型(如 `SELECT`、`INSERT` 等)。它适用于 SQL 分析、修改、生成和验证等场景。通过 Maven 或 Gradle 安装后,可以方便地在 Java 代码中使用。
3790 11
|
PHP 开发者 容器
PHP命名空间深度解析:避免命名冲突与提升代码组织####
本文深入探讨了PHP中命名空间的概念、用途及最佳实践,揭示其在解决全局命名冲突、提高代码可维护性方面的重要性。通过生动实例和详尽分析,本文将帮助开发者有效利用命名空间来优化大型项目结构,确保代码的清晰与高效。 ####
198 20
|
机器学习/深度学习 存储 人工智能
强化学习与深度强化学习:深入解析与代码实现
本书《强化学习与深度强化学习:深入解析与代码实现》系统地介绍了强化学习的基本概念、经典算法及其在深度学习框架下的应用。从强化学习的基础理论出发,逐步深入到Q学习、SARSA等经典算法,再到DQN、Actor-Critic等深度强化学习方法,结合Python代码示例,帮助读者理解并实践这些先进的算法。书中还探讨了强化学习在无人驾驶、游戏AI等领域的应用及面临的挑战,为读者提供了丰富的理论知识和实战经验。
689 5
|
存储 安全 Java
系统安全架构的深度解析与实践:Java代码实现
【11月更文挑战第1天】系统安全架构是保护信息系统免受各种威胁和攻击的关键。作为系统架构师,设计一套完善的系统安全架构不仅需要对各种安全威胁有深入理解,还需要熟练掌握各种安全技术和工具。
532 10

热门文章

最新文章

推荐镜像

更多
  • DNS