FastLIO代码阅读之一:IMU数据处理

2023-10-22 10:20

本文主要是介绍FastLIO代码阅读之一:IMU数据处理,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

FastLIO代码阅读之一:IMU数据处理

文章目录

  • FastLIO代码阅读之一:IMU数据处理
    • 1 对IMU数据的前向传播
    • 2 后向传播
    • 3 IMU数据处理函数

SLAM小白,刚开始学习,请各位大佬抱着批判的眼光看待博客。有错误还请重喷,随时修改~

这两部分在代码中主要对应到IMU_Processing.hpp中的UndistortPcl函数。

void ImuProcess::UndistortPcl(const MeasureGroup &meas, esekfom::esekf<state_ikfom, 12, input_ikfom> &kf_state, PointCloudXYZI &pcl_out);

1 对IMU数据的前向传播

这个函数主要实现了论文中的前向传播和反向传播。

  • 前向传播:用于获得一次LiDAR_Scan中的IMU位姿和误差,从接收到IMU输入开始,用:

    • x ^ i + 1 = x ^ i ⊞ ( Δ t f ( x ^ i , u i , 0 ) ) , x 0 = x ‾ k − 1 ( 4 ) \hat{x}_{i+1}=\hat{x}_i\boxplus(\Delta tf(\hat{x}_i, u_i, 0)), x_0=\overline{x}_{k-1} \space\space\space(4) x^i+1=x^i(Δtf(x^i,ui,0)),x0=xk1   (4)传播状态;

    • x ~ i + 1 = x i + 1 ⊟ x ^ i + 1 = ( x i ⊞ ( Δ t f ( x i , u i , w i ) ) ⊟ ( x ^ i ⊞ ( Δ t f ( x ^ i , u i , 0 ) ) ) ( 5 ) \begin{array}{lcl} \widetilde{x}_{i+1}=x_{i+1}\boxminus \hat{x}_{i+1}\\ \space\space\space\space\space\space\space=(x_i\boxplus(\Delta tf(x_i,u_i,w_i))\boxminus(\hat{x}_i\boxplus(\Delta tf(\hat{x}_i,u_i,0)))\space\space\space \end{array} (5) x i+1=xi+1x^i+1       =(xi(Δtf(xi,ui,wi))(x^i(Δtf(x^i,ui,0)))   (5)传播协方差;

​ 一直到这一次扫描结束,会得到一个传播结果: x ^ k , P ^ k \hat{x}_k, \hat{P}_k x^k,P^k a ^ \hat{a} a^表示IKEF中的先验信息)。 P ^ k = x k ⊟ x ^ k \hat{P}_k=x_k\boxminus\hat{x}_k P^k=xkx^k.

入参为

  • meas:测量数据,主要包括imu和lidar两部分。
    meas的数据结构
struct MeasureGroup     // Lidar data and imu dates for the curent process
{MeasureGroup(){lidar_beg_time = 0.0;// typedef pcl::PointCloud<PointType> PointCloudXYZI;// 这里本质上是新生成一个PointCloud对象指针并调用boost::shared_ptr<T>::swap和当前的雷达互换内容this->lidar.reset(new PointCloudXYZI());	};double lidar_beg_time;double lidar_end_time;// 这里的lidar是一个点云的指针,也可以理解为是一个数组的起始位置。PointCloudXYZI::Ptr lidar;// imu是一个双向队列,相比于vector,不需要整块空间且可以从头部和尾部执行插入删除。deque<sensor_msgs::Imu::ConstPtr> imu;
};
  • kf_state
  • pcl_out:未加处理的点云信息
void ImuProcess::UndistortPcl(const MeasureGroup &meas, esekfom::esekf<state_ikfom, 12, input_ikfom> &kf_state, PointCloudXYZI &pcl_out)
{/*** add the imu of the last frame-tail to the of current frame-head ***/auto v_imu = meas.imu;	// imu数据,队列v_imu.push_front(last_imu_);	// 将上一次的imu数据加入队列const double &imu_beg_time = v_imu.front()->header.stamp.toSec();const double &imu_end_time = v_imu.back()->header.stamp.toSec();    const double &pcl_beg_time = meas.lidar_beg_time;const double &pcl_end_time = meas.lidar_end_time;/*** sort point clouds by offset time ***/pcl_out = *(meas.lidar);	// 取值,拿到lidar数据sort(pcl_out.points.begin(), pcl_out.points.end(), time_list);	// 对雷达数据按照时间先后进行排序// cout<<"[ IMU Process ]: Process lidar from "<<pcl_beg_time<<" to "<<pcl_end_time<<", " \//          <<meas.imu.size()<<" imu msgs from "<<imu_beg_time<<" to "<<imu_end_time<<endl;/*** Initialize IMU pose ***/state_ikfom imu_state = kf_state.get_x();		// 获取先验imu姿态 \hat{x}_kIMUpose.clear();IMUpose.push_back(set_pose6d(0.0, acc_s_last, angvel_last, imu_state.vel, imu_state.pos, imu_state.rot.toRotationMatrix()));	// 存储IMU信息[时间间隔、上一帧加速度、角速度、速度、位置、旋转矩阵],论文中提到初始值为上一时刻的最优值。/*** forward propagation at each imu point ***/V3D angvel_avr, acc_avr, acc_imu, vel_imu, pos_imu;M3D R_imu;	// imu旋转矩阵double dt = 0;input_ikfom in;// 用iterator的方式迭代处理imu信息。for (auto it_imu = v_imu.begin(); it_imu < (v_imu.end() - 1); it_imu++){auto &&head = *(it_imu);	// *(it_imu)其实就是拿到存放imu的deque,deque中存放的是shared_ptr,shared_ptr的类型是IMU信息的空间地址,所以这里的&&head拿到的就是imu数据的地址。auto &&tail = *(it_imu + 1); // 下一个imu测量数据// TODO:这里的last_lidar_end_time_是上一次scan结束的雷达还是上一次雷达特征点时间?if (tail->header.stamp.toSec() < last_lidar_end_time_)    continue; 	// 判断时间先后顺序。// 取两帧速度的中值angvel_avr<<0.5 * (head->angular_velocity.x + tail->angular_velocity.x),0.5 * (head->angular_velocity.y + tail->angular_velocity.y),0.5 * (head->angular_velocity.z + tail->angular_velocity.z);acc_avr   <<0.5 * (head->linear_acceleration.x + tail->linear_acceleration.x),0.5 * (head->linear_acceleration.y + tail->linear_acceleration.y),0.5 * (head->linear_acceleration.z + tail->linear_acceleration.z);// fout_imu << setw(10) << head->header.stamp.toSec() - first_lidar_time << " " << angvel_avr.transpose() << " " << acc_avr.transpose() << endl;// TODO: 平均加速度*重力加速度/加速度均值的标准差含义?acc_avr  = acc_avr * G_m_s2 / mean_acc.norm(); // - state_inout.ba;// 计算时间间隔if(head->header.stamp.toSec() < last_lidar_end_time_){dt = tail->header.stamp.toSec() - last_lidar_end_time_;// dt = tail->header.stamp.toSec() - pcl_beg_time;}else{dt = tail->header.stamp.toSec() - head->header.stamp.toSec();}// 原始测量值的中值in.acc = acc_avr;in.gyro = angvel_avr;Q.block<3, 3>(0, 0).diagonal() = cov_gyr;Q.block<3, 3>(3, 3).diagonal() = cov_acc;Q.block<3, 3>(6, 6).diagonal() = cov_bias_gyr;Q.block<3, 3>(9, 9).diagonal() = cov_bias_acc;kf_state.predict(dt, Q, in);/* save the poses at each IMU measurements */imu_state = kf_state.get_x();// imu_state这里的流形相关代码暂时看不下去,太绕了,一层套一层的宏定义==,后续再来看吧,先过代码。 angvel_last = angvel_avr - imu_state.bg;	// 计算得到的角速度与预测的角速度的差。// TODO:确认acc_s_last中x,y,z的顺序acc_s_last  = imu_state.rot * (acc_avr - imu_state.ba); // 计算得到的加速度与预测的加速度的差,并转换到预测imu坐标系下。// 三个方向的加速度都加上重力,重力加速度矩阵为[9.809, 0, 0],这里感觉维度顺序对不上,还要仔细看看。for(int i=0; i<3; i++){acc_s_last[i] += imu_state.grav[i];    }double &&offs_t = tail->header.stamp.toSec() - pcl_beg_time; // 后一个IMU时刻距离此次雷达开始的时间间隔IMUpose.push_back(set_pose6d(offs_t, acc_s_last, angvel_last, imu_state.vel, imu_state.pos, imu_state.rot.toRotationMatrix()));   }/*** calculated the pos and attitude prediction at the frame-end ***/double note = pcl_end_time > imu_end_time ? 1.0 : -1.0;dt = note * (pcl_end_time - imu_end_time);kf_state.predict(dt, Q, in);    // IMU前向传播,用中值作为更新状态/*save the poses at each IMU measurements*/imu_state = kf_state.get_x();  	// 获取到更新后的IMU姿态last_imu_ = meas.imu.back();		// 上一帧imu数据为IMU数列的最后一个对象last_lidar_end_time_ = pcl_end_time;	// 设置上一帧雷达结束时间

2 后向传播

  • 反向传播:用于对齐雷达测量时坐标系与扫描结束时坐标系。

    • Lidar数据的相对误差( ρ j ≤ t k \rho_j\le t_k ρjtk),会加剧里程计的误差。因此需要修正位姿映射到k时刻。对于两次IMU测量期间的所有LiDAR特征点,都用左侧的IMU测量值作为反向传播输入。
    • x ˘ j − 1 = x ˘ j ⊞ ( − Δ t f ( x ˘ j , u j , 0 ) ) 式 1 \breve{x}_{j-1}=\breve{x}_j\boxplus(-\Delta tf(\breve{x}_j,u_j,0)) 式1 x˘j1=x˘j(Δtf(x˘j,uj,0))1. j-1 时刻的最优估计由 j 时刻的最优估计减去 j-1 时刻的运动量。

    image-20220902141217064

    反向传播中的运动方程:

    • 上一时刻IMU的位置,为这一时刻IMU位置减去位移。
    • 上一时刻IMU的速度,为这一时刻IMU速度减去速度(线性加速度+重力加速度)变化量
    • 上一时刻IMU的旋转矩阵为这一时刻旋转矩阵乘以旋转矩阵(角速度*时间)

    反向传播会生成一个相对位姿 I k T ˘ I j ^{I_k}\breve{T}_{I_j} IkT˘Ij

    通过 L k P f j = I T L − 1 I k T ˘ I j I T L L j P f j ( 10 ) ^{L_k}P_{f_j}={^IT^{-1}_{L}}{^{I_k}\breve{T}_{I_j}}{^IT_L}{^{L_j}P_{f_j}}\space\space\space(10) LkPfj=ITL1IkT˘IjITLLjPfj   (10)将特征点从该点采样时的雷达坐标系映射到一次Scan扫描结束时的雷达坐标系。

  // 后向传播/*** undistort each lidar point (backward propagation) ***/if (pcl_out.points.begin() == pcl_out.points.end()) return;	// 如果存放点云的vector为空(无点)则退出auto it_pcl = pcl_out.points.end() - 1;  // 拿到最后一个雷达点云数据// 倒序遍历IMU数据for (auto it_kp = IMUpose.end() - 1; it_kp != IMUpose.begin(); it_kp--){// 取相邻两帧IMU数据作为head和tail,这里是要做运动补偿,将数据从雷达采样时间映射到IMU帧时间auto head = it_kp - 1;	// 当前雷达点的左侧IMU帧auto tail = it_kp;		// 当前雷达点的右侧IMU帧R_imu<<MAT_FROM_ARRAY(head->rot);	// 将左侧IMU数据的旋转矩阵赋值给R_imu// cout<<"head imu acc: "<<acc_imu.transpose()<<endl;vel_imu<<VEC_FROM_ARRAY(head->vel);	// 左侧IMU的速度pos_imu<<VEC_FROM_ARRAY(head->pos);	// 左侧IMU的位置acc_imu<<VEC_FROM_ARRAY(tail->acc);	// 右侧IMU的加速度angvel_avr<<VEC_FROM_ARRAY(tail->gyr);	// 右侧IMU的角速度// 在两帧IMU之间去畸变(按照两帧IMU之间的雷达采样点倒序处理,和外层的倒序IMU嵌套得到对一次Scan的运动补偿),要求时间要大于左侧IMU时间。(offset_time,IMU采样时间)for(; it_pcl->curvature / double(1000) > head->offset_time; it_pcl --){dt = it_pcl->curvature / double(1000) - head->offset_time;	// 左侧IMU采样时间到雷达采样时间的时间间隔(论文中有提到对两帧IMU之间的所有雷达点,都用左侧IMU的测量值作为反向传播的输入值)/* Transform to the 'end' frame, using only the rotation* Note: Compensation direction is INVERSE of Frame's moving direction   * So if we want to compensate a point at timestamp-i to the frame-e* P_compensate = R_imu_e ^ T * (R_i * P_i + T_ei) where T_ei is represented in global frame */M3D R_i(R_imu * Exp(angvel_avr, dt));	// R_i代表在左侧IMU基础上做了一个dt的旋转量的旋转矩阵V3D P_i(it_pcl->x, it_pcl->y, it_pcl->z);	// 点在雷达坐标系下的位置V3D T_ei(pos_imu + vel_imu * dt + 0.5 * acc_imu * dt * dt - imu_state.pos);   // 平移量为上一帧imu位置+变化量 - 当前imu的位置// 这里对应着论文中的公式(10)和式1,四元数的共轭代表着同轴,旋转角相反。/*TODO:确认imu_status到底是什么?1 从lidar系映射到imu系 P_compensate = offset_R_L_I * P_i + offset_T_L_I2 展现这个点在dt时间内运动 P_compensate = R_i * P_compensate + T_ei3 反向运动,与上一时刻相比,旋转角度、位移量一致,方向相反。 P_compensate = rot.conjugate() * P_compensate - offset_T_L_I4 将数据转回Lidar坐标系 offset_R_L_I.conjugate() * p_compensate*/V3D P_compensate = imu_state.offset_R_L_I.conjugate() * (imu_state.rot.conjugate() * (R_i * (imu_state.offset_R_L_I * P_i + imu_state.offset_T_L_I) + T_ei) - imu_state.offset_T_L_I);// not accurate!// save Undistorted points and their rotationit_pcl->x = P_compensate(0);it_pcl->y = P_compensate(1);it_pcl->z = P_compensate(2);if (it_pcl == pcl_out.points.begin()) break;}}
}

实现李代数指数映射部分

template<typename T, typename Ts>
Eigen::Matrix<T, 3, 3> Exp(const Eigen::Matrix<T, 3, 1> &ang_vel, const Ts &dt)
{T ang_vel_norm = ang_vel.norm();    // 三轴角速度的二范数,就是角速度的标量,表示旋转的有多快。Eigen::Matrix<T, 3, 3> Eye3 = Eigen::Matrix<T, 3, 3>::Identity(); // 得到三维单位阵if (ang_vel_norm > 0.0000001)       // 确实是在旋转的。{Eigen::Matrix<T, 3, 1> r_axis = ang_vel / ang_vel_norm; // 等价于normalized()得到一个模长为1的向量,这里是旋转轴的方向向量。Eigen::Matrix<T, 3, 3> K;   // 斜对称矩阵KK << SKEW_SYM_MATRX(r_axis);T r_ang = ang_vel_norm * dt;    // 旋转角/// Roderigous Tranformation. 推导过程:return Eye3 + std::sin(r_ang) * K + (1.0 - std::cos(r_ang)) * K * K;    // 罗德里格斯公式,将轴角转为旋转矩阵}else{return Eye3;}
}

这里的罗德里格斯公式和14讲里面的有区别(推导过程可以看后面的链接)

  • 这里的: R = I + s i n θ ⋅ K + ( 1 − c o s θ ) K 2 R=I+sin\theta\cdot K+(1-cos\theta)K^2 R=I+sinθK+(1cosθ)K2.罗德里格斯公式
  • 14讲的: R = c o s θ I + ( 1 − c o s θ ) n n T + s i n θ n R=cos\theta I + (1-cos\theta)nn^T+sin\theta n R=cosθI+(1cosθ)nnT+sinθn.罗德里格斯公式

3 IMU数据处理函数

void ImuProcess::Process(const MeasureGroup &meas,  esekfom::esekf<state_ikfom, 12, input_ikfom> &kf_state, PointCloudXYZI::Ptr cur_pcl_un_)
{double t1,t2,t3;t1 = omp_get_wtime();if(meas.imu.empty()) {return;};		// 判断imu数据是否为空,为空退出ROS_ASSERT(meas.lidar != nullptr);	// IMU初始化if (imu_need_init_){// The very first lidar frameIMU_init(meas, kf_state, init_iter_num);imu_need_init_ = true;last_imu_   = meas.imu.back();		// 最新imu为imu的最后一帧// state_ikfom imu_state = kf_state.get_x();	// 拿到imu状态数据if (init_iter_num > MAX_INI_COUNT){cov_acc *= pow(G_m_s2 / mean_acc.norm(), 2);	// 加速度协方差*重力加速度的平方,意欲何为?imu_need_init_ = false;cov_acc = cov_acc_scale;cov_gyr = cov_gyr_scale;ROS_INFO("IMU Initial Done");// ROS_INFO("IMU Initial Done: Gravity: %.4f %.4f %.4f %.4f; state.bias_g: %.4f %.4f %.4f; acc covarience: %.8f %.8f %.8f; gry covarience: %.8f %.8f %.8f",\//          imu_state.grav[0], imu_state.grav[1], imu_state.grav[2], mean_acc.norm(), cov_bias_gyr[0], cov_bias_gyr[1], cov_bias_gyr[2], cov_acc[0], cov_acc[1], cov_acc[2], cov_gyr[0], cov_gyr[1], cov_gyr[2]);fout_imu.open(DEBUG_FILE_DIR("imu.txt"),ios::out);}return;}UndistortPcl(meas, kf_state, *cur_pcl_un_);	// 前向传播imu状态,后向去畸变t2 = omp_get_wtime();t3 = omp_get_wtime();// cout<<"[ IMU Process ]: Time: "<<t3 - t1<<endl;
}

参考链接

FastLIO官方github

一位师兄的FastLIO2代码笔记

这篇关于FastLIO代码阅读之一:IMU数据处理的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/260818

相关文章

springboot循环依赖问题案例代码及解决办法

《springboot循环依赖问题案例代码及解决办法》在SpringBoot中,如果两个或多个Bean之间存在循环依赖(即BeanA依赖BeanB,而BeanB又依赖BeanA),会导致Spring的... 目录1. 什么是循环依赖?2. 循环依赖的场景案例3. 解决循环依赖的常见方法方法 1:使用 @La

使用C#代码在PDF文档中添加、删除和替换图片

《使用C#代码在PDF文档中添加、删除和替换图片》在当今数字化文档处理场景中,动态操作PDF文档中的图像已成为企业级应用开发的核心需求之一,本文将介绍如何在.NET平台使用C#代码在PDF文档中添加、... 目录引言用C#添加图片到PDF文档用C#删除PDF文档中的图片用C#替换PDF文档中的图片引言在当

C#使用SQLite进行大数据量高效处理的代码示例

《C#使用SQLite进行大数据量高效处理的代码示例》在软件开发中,高效处理大数据量是一个常见且具有挑战性的任务,SQLite因其零配置、嵌入式、跨平台的特性,成为许多开发者的首选数据库,本文将深入探... 目录前言准备工作数据实体核心技术批量插入:从乌龟到猎豹的蜕变分页查询:加载百万数据异步处理:拒绝界面

用js控制视频播放进度基本示例代码

《用js控制视频播放进度基本示例代码》写前端的时候,很多的时候是需要支持要网页视频播放的功能,下面这篇文章主要给大家介绍了关于用js控制视频播放进度的相关资料,文中通过代码介绍的非常详细,需要的朋友可... 目录前言html部分:JavaScript部分:注意:总结前言在javascript中控制视频播放

Spring Boot 3.4.3 基于 Spring WebFlux 实现 SSE 功能(代码示例)

《SpringBoot3.4.3基于SpringWebFlux实现SSE功能(代码示例)》SpringBoot3.4.3结合SpringWebFlux实现SSE功能,为实时数据推送提供... 目录1. SSE 简介1.1 什么是 SSE?1.2 SSE 的优点1.3 适用场景2. Spring WebFlu

java之Objects.nonNull用法代码解读

《java之Objects.nonNull用法代码解读》:本文主要介绍java之Objects.nonNull用法代码,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐... 目录Java之Objects.nonwww.chinasem.cnNull用法代码Objects.nonN

SpringBoot实现MD5加盐算法的示例代码

《SpringBoot实现MD5加盐算法的示例代码》加盐算法是一种用于增强密码安全性的技术,本文主要介绍了SpringBoot实现MD5加盐算法的示例代码,文中通过示例代码介绍的非常详细,对大家的学习... 目录一、什么是加盐算法二、如何实现加盐算法2.1 加盐算法代码实现2.2 注册页面中进行密码加盐2.

python+opencv处理颜色之将目标颜色转换实例代码

《python+opencv处理颜色之将目标颜色转换实例代码》OpenCV是一个的跨平台计算机视觉库,可以运行在Linux、Windows和MacOS操作系统上,:本文主要介绍python+ope... 目录下面是代码+ 效果 + 解释转HSV: 关于颜色总是要转HSV的掩膜再标注总结 目标:将红色的部分滤

在C#中调用Python代码的两种实现方式

《在C#中调用Python代码的两种实现方式》:本文主要介绍在C#中调用Python代码的两种实现方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录C#调用python代码的方式1. 使用 Python.NET2. 使用外部进程调用 Python 脚本总结C#调

Java时间轮调度算法的代码实现

《Java时间轮调度算法的代码实现》时间轮是一种高效的定时调度算法,主要用于管理延时任务或周期性任务,它通过一个环形数组(时间轮)和指针来实现,将大量定时任务分摊到固定的时间槽中,极大地降低了时间复杂... 目录1、简述2、时间轮的原理3. 时间轮的实现步骤3.1 定义时间槽3.2 定义时间轮3.3 使用时