《自动驾驶与机器人中的SLAM技术》ch4:预积分学
目录
1 预积分的定义
2 预积分的测量模型 ( 预积分的测量值可由 IMU 的测量值积分得到 )
2.1 旋转部分
2.2 速度部分
2.3 平移部分
2.4 将预积分测量和误差式代回最初的定义式
3 预积分的噪声模型和协方差矩阵
3.1 旋转部分
3.2 速度部分
3.3 平移部分
3.4 噪声项合并
4 零偏的更新
5 图优化中的预积分边
5.1 预积分边的残差
5.2 预积分残差相对于状态变量的雅可比矩阵
5.2.1 旋转部分
5.2.2 速度部分
5.2.3 平移部分
6 预积分流程
7 预积分的程序实现
7.1 程序实现
7.2 测试
1 测试在恒定角速度运转下的预积分情况
2. 测试在恒定加速度运行下的预积分情况
在基于 ESKF 的 GINS 中,我们将两个 GNSS 观测之间的 IMU 数据进行积分,作为 ESKF 的预测过程。这种做法把 IMU 数据看成某种一次性的使用方式:将它们积分到当前状态估计值上,然后用观测数据更新当时的状态估计值。这种做法和此时的状态估计值有关。但是,如果状态量发生了改变,能否重复利用这些 IMU 数据呢?从物理意义上看,IMU 反映的是两个时刻间车辆的角度变化量和速度变化量。如果我们希望 IMU 的计算与当时的状态估计值无关,在算法上应该如何处理呢?这就是本章要讨论的内容。
本章介绍一种十分常见的 IMU 数据处理方法:预积分(Pre-integraion)。与传统 IMU 的运动学积分不同,预积分可以将一段时间内的 IMU 测量数据累计起来,建立预积分测量,同时还能保证测量值与状态变量无关。如果以吃饭来比喻的话,ESKF 像是一口口地吃菜,而预积分则是从锅里先把菜一块块夹到碗里,然后再把碗里的菜一口气吃掉。至于用多大的碗,每次夹多少次菜再一起吃,形式上就比较自由了。无论是 LIO 系统还是 VIO 系统,预积分已经成为诸多与 IMU 紧耦合的标准方法,但是原理相对传统 ESKF 的预测过程会更加复杂一些。下面我们来推导其基本原理,然后实现一个预积分系统。
1 预积分的定义
在一个 IMU 系统里,我们考虑它的五个变量:旋转 R、平移 p、角速度 ω、线速度 v 与加速度 a。根据第 2 章介绍的运动学,这些变量的运动学关系可以写成如下运动学方程:
在 到
时间内对运动学方程进行欧拉积分得:
IMU 测量方程(其中 ,
为 IMU 测量的高斯噪声 )如下:
IMU 测量方程带入积分后的运动学方程(其中 ,
为离散化后的 IMU 测量噪声 )如下:
其中噪声项满足:
以上过程与我们在 IMU 测量方程和噪声方程(第 3 章)中已有描述。当然,我们完全可以用这种约束来构建图优化,对 IMU 相关的问题进行求解。但是这组方程刻画的时间太短,仅包含单个 IMU 数据。或者说,IMU 的测量频率太高。我们并不希望优化过程随着 IMU 数据进行调用,那样太浪费计算资源。我们更希望将这些 IMU 测量值组合在一起处理,即在关键帧之间对 IMU 数据进行预积分,然后按照 关键帧 的频率调用优化过程。
假设从离散时间 和
之间的 IMU 数据被累计起来,这个过程可以持续若干秒钟。这种被累计起来的观测被称为预积分。如果我们使用不同形式的运动学(第 2 章中介绍的运动学),得到的预积分形式也是不同的 。本书主要使用 SO(3) + t 的方式来推导预积分。那么,累积离散时间
和
之间的 IMU 数据,令累积的时间为
,得到:
上述式子只是累积形式,是传统意义上的直接积分。直接积分的缺点是其描述的过程和状态量有关。如果我们对 i 时刻的状态进行优化,那么,
, . . . ,
时刻的状态也会跟着发生改变,这个积分就必须重新计算,这是非常不便的。为此,定义状态变化量
如下:
对上述累积式稍加改变,尽量将 IMU 读数放在一侧,状态量放到另一侧,可得如下式子,该式称为预积分的定义式:
- 1. 我们不妨考虑从
时刻出发,此时这三个量都为零。在
时刻,我们计算出
,
和
。而在
时刻时,由于这三个式子都是累乘或累加的形式,只需在
,
时刻的结果之上,加上第
时刻的测量值即可。这在计算层面带来了很大的便利。进一步,我们还会发现这种性质对后续计算各种雅可比矩阵都非常方便。
- 2. 从等号最右侧来看,上述所有计算都和
的取值无关。即使它们的状态估计值发生改变,IMU 积分量也无需重新计算。
- 3. 不过,如果零偏
或
发生变化,那么上述式子理论上还是需要重新计算。然而,我们也可以通过“修正”而非“重新计算”的思路,来调整我们的预积分量。
- 4. 请注意,预积分量并没有直接的物理含义。尽管符号上用了
,
之类的样子,但它并不表示某两个速度或位置上的偏差。它只是如此定义而已。当然,从量纲上来说,应该与角度、速度、位移对应。
- 5. 同样地,由于预积分量不是直接的物理量,这种“预积分测量模型”的噪声也必须从原始的 IMU 噪声推导而来。
2 预积分的测量模型 ( 预积分的测量值可由 IMU 的测量值积分得到 )
由前面的讨论可见,预积分内部带有 IMU 的零偏量,因此不可避免地会依赖此时的零偏量估计。为了处理这种依赖,我们对预积分定义作一些工程上的调整:
- 1. 我们首先认为
时刻的零偏是固定的,并且在整个预积分计算过程中也都是固定的。
- 2. 假设预积分的测量值是随零偏线性变化的(一阶线性化模型),舍弃对零偏量的高阶项。
- 3. 当零偏估计发生改变时,在原先预积分测量值的基础上使用线性模型进行修正得到新的预积分测量值。
首先,我们固定 时刻的零偏估计,来分析预积分的噪声。无论是图优化还是滤波器技术,都需要知道某个测量量究竟含有多大的噪声。
2.1 旋转部分
定义预积分旋转测量值 如下:
预积分定义式中的旋转部分经过变换可得如下式,其中 为预积分旋转的测量噪声:
上式中的 ,可根据《自动驾驶与机器人中的SLAM技术》ch2:基础数学知识 中公式计算得到
2.2 速度部分
定义预积分速度测量值 如下:
预积分定义式中的速度部分经过变换可得如下式,其中 为预积分速度的测量噪声:
2.3 平移部分
定义预积分平移测量值 如下:
预积分定义式中的平移部分经过变换可得如下式,其中 为预积分平移的测量噪声:
2.4 将预积分测量和误差式代回最初的定义式
将上面的预积分测量及误差式:
重新代入最初的预积分定义式中,得:
这个式子归纳了前面我们讨论的内容,显示了预积分的几大优点:
- 1. 它的左侧是可以通过 IMU 的测量值积分得到的预积分测量值,右侧是根据状态变量推断出来的预测值,再加上(或乘上)一个随机噪声。
- 2. 左侧变量的定义方式非常适合程序实现。
可以通过
时刻 IMU 读数得到,
可以由
时刻 IMU 读数和
算得,而
又可以通过
时刻 IMU 读数和
和
计算结果得到。另一方面,如果知道了
时刻的预积分测量值,又很容易根据
时刻 IMU 读数,计算出
时刻的预积分测量值。这是由预积分测量值的累加/累乘定义方式决定的。
- 3. 从右侧看来,也很容易根据
和
时刻的状态变量来推测预积分测量值的大小,从而写出误差公式,形成最小二乘。
将上式变化可以得到预测公式。通过预测公式可以使用 时刻到
时刻的预积分测量值和
时刻状态推测
时刻状态:
忽略测量噪声得:
接下来继续推导预积分的噪声模型。
3 预积分的噪声模型和协方差矩阵
由于噪声项的定义比较复杂,本节会使用同样的思路来处理各种噪声项。我们会将复杂的噪声项线性化,保留一阶项系数,然后推导线性模型下的协方差矩阵变化。这是一种非常常见的处理思路,对许多复杂模型都很有效。
3.1 旋转部分
旋转部分的测量噪声如下,其中 为
时刻离散化后的 IMU 陀螺仪测量噪声:
由上式可知,作为随机变量的 只和随机变量
有关,而其他的都是确定的观测量。
对上式两侧取 得:
我们将其变换为递推形式,即从 时刻噪声推断得到
时刻噪声:
注意上式中 。
3.2 速度部分
速度部分的测量噪声如下,其中 为
时刻离散化后的 IMU 加速度计测量噪声:
我们将其变换为递推形式,即从 时刻噪声推断得到
时刻噪声:
3.3 平移部分
平移部分的测量噪声如下,其中 为
时刻离散化后的 IMU 加速度计测量噪声:
我们将其变换为递推形式,即从 时刻噪声推断得到
时刻噪声:
3.4 噪声项合并
为了方便表示,将 3 种噪声项的递推形式合并为一个。令 为预积分测量噪声、
为
时刻离散化后的 IMU 测量噪声:
那么,从 到
的递推形式为:
其中,系数矩阵 为:
如果我们以协方差的形式来记录噪声,那么每次增加 IMU 观测时,噪声应该呈现出逐渐增大的关系:
这里的 阵接近单位矩阵
,因此可以看成将预积分测量噪声累加起来。陀螺仪的噪声通过
矩阵进入到旋转的观测量中,而加速度计的噪声则主要进入速度与平移估计中。这种累加关系很容易在程序中实现出来。后面我们会在实验章节中看到它们的实现。注意,如果预积分定义的残差项顺序发生改变,我们也需要调整这里的系统矩阵行列关系以保持一致性。
4 零偏的更新
先前的讨论都假设了在 时刻的 IMU 零偏固定不变,当然这都是为了方便后续的计算。然而在实际的图优化中,我们经常会对状态变量(优化变量)进行更新,导致零偏发生改变。理论上来讲,如果 IMU零偏发生了变化,预积分就应该重新计算,因为预积分的每一步都用到了
时刻的 IMU 零偏。但是实际操作过程中,我们也可以选用一种取巧的做法:假设预积分的测量值是随零偏线性变化的,当零偏估计发生改变时,在原先预积分测量值的基础上使用线性模型进行修正得到新的预积分测量值。具体来说,我们把预积分测量值看成
的函数,那么,当
更新了
之后,预积分观测应作如下的修正,下式即为预积分测量值的修正公式:
只需求出上式中 预积分观测值 相对于
的雅可比矩阵即可在零偏更新时对预积分观测值进行修正。
预积分观测值 相对于
的雅可比矩阵如下:
将其变换为递推形式,即从 时刻雅可比矩阵推断得到
时刻雅可比矩阵:
5 图优化中的预积分边
现在我们定义了预积分的测量模型,推导了它的噪声模型和协方差矩阵,并说明了随着零偏量更新,预积分观测值应该怎么更新。事实上,我们已经可以把预积分观测作为图优化的因子(Factor)或者边(Edge)了。
在 IMU 相关的应用中,通常把每个时刻的状态建模为包含旋转、平移、线速度、IMU 零偏的变量,构成状态变量集合 :
预积分模型构建了关键帧 与关键帧
之间的相对运动约束。
5.1 预积分边的残差
不同文献当中对残差的具体定义并不完全一致,残差的实际定义是相当灵活的。注意更换残差定义时,对应的噪声协方差可能会发生改变。
预积分本身的观测模型已经在预积分定义式中介绍,我们可以用 时刻、
时刻的状态变量值与预积分的观测量作差,得到残差的定义公式。
通常我们会把 统一写成一个 9 维的残差变量。它表面上关联两个时刻的旋转、平移、线速度,但由于预积分观测内部含有 IMU 零偏,所以实际也和
时刻两个零偏有关。在优化过程中,如果对
时刻的零偏进行更新,那么预积分观测量也应该线性地发生改变,从而影响残差项的取值。所以,尽管在这个残差项里似乎不含有
,它们显然是和残差相关的。因此,如果我们把预积分残差看作同一个,那么它与状态顶点的关联应该如下图所示。除了预积分因子本身之外,还需要约束 IMU 的随机游走,因此在 IMU 的不同时刻,零偏会存在一个约束因子。
关于图优化中顶点的形式,有以下两种:
- 把所有状态变量放在同一个顶点
- 选择“散装的形式”,即对旋转、平移、线速度和两个零偏分别构造顶点,共构造 5 个顶点,然后求解这几个顶点之间的雅可比。如果采用这种做法,那么雅可比矩阵的数量会变多,但单个雅可比矩阵的维度可以降低(单个雅可比通常为 3 × 3,而预积分观测量对状态变量的雅可比会变为 9 × 15,且有很多零块)。由于视觉和激光的观测约束通常只和
相关,如果需要构造视觉和激光雷达的紧耦合 LIO 系统,则可以避免雅可比矩阵当中的一些零矩阵块。
总而言之,两种做法各有各的好处,本书的代码实现部分使用了散装做法,即把各状态量写成单独的顶点,分别来约束它们。
5.2 预积分残差相对于状态变量的雅可比矩阵
最后我们来讨论预积分残差相对状态变量的雅可比矩阵。
5.2.1 旋转部分
旋转部分的残差如下,其与状态变量 有关:
假设优化初始的零偏为 ,在某一步迭代时,我们当前估计出来的零偏修正为
,而当前修正得到的预积分旋转观测量为
,残差为
。预积分旋转残差相对状态变量的雅可比矩阵如下:
上式中的 为
,
为
。
5.2.2 速度部分
速度部分的残差如下,其与状态变量 有关:
预积分速度残差相对状态变量的雅可比矩阵如下:
5.2.3 平移部分
平移部分的残差如下,其与状态变量 有关:
预积分平移残差相对状态变量的雅可比矩阵如下:
6 预积分流程
在一个关键帧组成的系统中,我们可以从任意一个时刻的关键帧出发开始预积分,并且在任一时刻停止预积分过程。之后,我们可以把预积分的观测量、噪声以及各种累计雅可比取出来,用于约束两个关键帧的状态。按照先前的讨论,在开始预积分之后,当 时刻一个新的 IMU 数据到来时,我们的程序应该完成以下任务:
- 1. 在上一个数据基础上,利用下式,计算三个预积分观测量(其中
右乘更新):
;
- 2. 计算三个噪声量的协方差矩阵,作为后续图优化的信息矩阵;
其中,
。
- 3. 计算预积分观测量相对于零偏的雅可比矩阵,当零偏更新时进行预积分测量值的修正;
- 4.增量积分时间
。
- 5.可以使用
时刻到
时刻的预积分测量值
推测
时刻状态;
7 预积分的程序实现
7.1 程序实现
一个预积分类应该存储以下数据:
- • 预积分的观测量
;
- • 预积分开始时的 IMU 零偏
,
;
- • 在积分时期内的测量噪声
。
- • 各积分量对 IMU 零偏的雅可比矩阵。
- • 整个积分时间
。
/*** IMU 预积分器** 调用Integrate来插入新的IMU读数,然后通过Get函数得到预积分的值* 雅可比也可以通过本类获得,可用于构建g2o的边类*/
class IMUPreintegration {public:EIGEN_MAKE_ALIGNED_OPERATOR_NEW/// 参数配置项/// 初始的零偏需要设置,其他可以不改struct Options {Options() {}Vec3d init_bg_ = Vec3d::Zero(); // 初始零偏Vec3d init_ba_ = Vec3d::Zero(); // 初始零偏double noise_gyro_ = 1e-2; // 陀螺噪声,标准差double noise_acce_ = 1e-1; // 加计噪声,标准差};public:double dt_ = 0; // 整体预积分时间Mat9d cov_ = Mat9d::Zero(); // 累计噪声矩阵Mat6d noise_gyro_acce_ = Mat6d::Zero(); // 测量噪声矩阵// 零偏Vec3d bg_ = Vec3d::Zero();Vec3d ba_ = Vec3d::Zero();// 预积分观测量SO3 dR_;Vec3d dv_ = Vec3d::Zero();Vec3d dp_ = Vec3d::Zero();// 雅可比矩阵Mat3d dR_dbg_ = Mat3d::Zero();Mat3d dV_dbg_ = Mat3d::Zero();Mat3d dV_dba_ = Mat3d::Zero();Mat3d dP_dbg_ = Mat3d::Zero();Mat3d dP_dba_ = Mat3d::Zero();
};
注意 IMU 零偏相关的噪声项并不直接和预积分类有关,我们将它们挪到优化类当中。
单个 IMU 的积分函数实现如下:
void IMUPreintegration::Integrate(const IMU &imu, double dt) {// 去掉零偏的测量Vec3d gyr = imu.gyro_ - bg_; // 陀螺Vec3d acc = imu.acce_ - ba_; // 加计// 更新dv, dp, 见p105 (4.9), (4.13), (4.16) // 等号右侧 dp_ dv_ 为 i 时刻预积分观测量, 等号左侧 dp_ dv_ 为 j 时刻预积分观测量Vec3d omega = gyr * dt; // 转动量SO3 deltaR = SO3::exp(omega); // exp后SO3 new_dR = dR_ * deltaR;Vec3d new_dv = dv_ + dR_ * acc * dt;Vec3d new_dp = dp_ + dv_ * dt + 0.5f * dR_.matrix() * acc * dt * dt;Mat3d rightJ = SO3::jr(omega); // 右雅可比Mat3d acc_hat = SO3::hat(acc);double dt2 = dt * dt;// 运动方程雅可比矩阵系数,A,B阵,见p108 (4.29)Eigen::Matrix<double, 9, 9> A;A.setIdentity();Eigen::Matrix<double, 9, 6> B;B.setZero();A.block<3, 3>(0, 0) = deltaR.matrix().transpose(); // A.block<3, 3>(3, 0) = -dR_.matrix() * dt * acc_hat;A.block<3, 3>(6, 0) = -0.5f * dR_.matrix() * acc_hat * dt2;A.block<3, 3>(6, 3) = dt * Mat3d::Identity();B.block<3, 3>(0, 0) = rightJ * dt;B.block<3, 3>(3, 3) = dR_.matrix() * dt;B.block<3, 3>(6, 3) = 0.5f * dR_.matrix() * dt2;// 更新噪声项cov_ = A * cov_ * A.transpose() + B * noise_gyro_acce_ * B.transpose(); // 更新各雅可比,见式p111 (4.39)dR_dbg_ = deltaR.matrix().transpose() * dR_dbg_ - rightJ * dt; // p111 (4.39a)dV_dba_ = dV_dba_ - dR_.matrix() * dt; // (4.39b)dV_dbg_ = dV_dbg_ - dR_.matrix() * dt * acc_hat * dR_dbg_; // (4.39c)dP_dba_ = dP_dba_ + dV_dba_ * dt - 0.5f * dR_.matrix() * dt2; // (4.39d)dP_dbg_ = dP_dbg_ + dV_dbg_ * dt - 0.5f * dR_.matrix() * dt2 * acc_hat * dR_dbg_; // (4.39e)// 更新预积分测量值dR_ = new_dR; dv_ = new_dv; dp_ = new_dp; // my 增量积分时间dt_ += dt;
}
注意:如果不进行优化,预积分和直接积分的效果是完全一致的,都是将 IMU 的数据积分起来。
在预积分之后,我们也可以像 ESKF 一样,从起始状态向最终状态进行预测。预测函数实现是非常简单的:
NavStated IMUPreintegration::Predict(const sad::NavStated &start, const Vec3d &grav) const {// grav 存在默认值// std::cout << "grav: " << grav.transpose() << std::endl;// p105 (4.18) 变形版, 忽略噪声后 i 时刻状态和 j 时刻状态的关系式, 这里的 dt_ 是整体预积分时间SO3 Rj = start.R_ * dR_;Vec3d vj = start.R_ * dv_ + start.v_ + grav * dt_;// pj 有点问题Vec3d pj = start.R_ * dp_ + start.p_ + start.v_ * dt_ + 0.5f * grav * dt_ * dt_;auto state = NavStated(start.timestamp_ + dt_, Rj, pj, vj);state.bg_ = bg_;state.ba_ = ba_;return state;
}
与 ESKF 不同的是:
- 预积分可以对多个 IMU 数据进行预测,可以从任意起始时刻向后预测
- ESKF 通常只在当前状态下,针对单个 IMU 数据,向下一个时刻预测
7.2 测试
1 测试在恒定角速度运转下的预积分情况
TEST(PREINTEGRATION_TEST, ROTATION_TEST) {// 测试在恒定角速度运转下的预积分情况double imu_time_span = 0.01; // IMU测量间隔Vec3d constant_omega(0, 0, M_PI); // 角速度为180度/s,转1秒应该等于转180度Vec3d gravity(0, 0, -9.8); // Z 向上,重力方向为负sad::NavStated start_status(0), end_status(1.0);sad::IMUPreintegration pre_integ;// 对比直接积分Sophus::SO3d R;Vec3d t = Vec3d::Zero();Vec3d v = Vec3d::Zero();for (int i = 1; i <= 100; ++i) {double time = imu_time_span * i;Vec3d acce = -gravity; // 水平放置时加速度计应该测量到一个向上的力,为 -gpre_integ.Integrate(sad::IMU(time, constant_omega, acce), imu_time_span);sad::NavStated this_status = pre_integ.Predict(start_status, gravity);// p101 (4.4)t = t + v * imu_time_span + 0.5 * gravity * imu_time_span * imu_time_span +0.5 * (R * acce) * imu_time_span * imu_time_span;v = v + gravity * imu_time_span + (R * acce) * imu_time_span;R = R * Sophus::SO3d::exp(constant_omega * imu_time_span);// 验证在简单情况下,直接积分和预积分结果相等// Google Test框架中的断言(assertions),用于比较预期值和实际值是否在允许的误差范围内。如果不满足条件,则报 FailureEXPECT_NEAR(t[0], this_status.p_[0], 1e-2);EXPECT_NEAR(t[1], this_status.p_[1], 1e-2);EXPECT_NEAR(t[2], this_status.p_[2], 1e-2);EXPECT_NEAR(v[0], this_status.v_[0], 1e-2);EXPECT_NEAR(v[1], this_status.v_[1], 1e-2);EXPECT_NEAR(v[2], this_status.v_[2], 1e-2);EXPECT_NEAR(R.unit_quaternion().x(), this_status.R_.unit_quaternion().x(), 1e-4);EXPECT_NEAR(R.unit_quaternion().y(), this_status.R_.unit_quaternion().y(), 1e-4);EXPECT_NEAR(R.unit_quaternion().z(), this_status.R_.unit_quaternion().z(), 1e-4);EXPECT_NEAR(R.unit_quaternion().w(), this_status.R_.unit_quaternion().w(), 1e-4);}end_status = pre_integ.Predict(start_status, gravity);LOG(INFO) << "preinteg result: ";LOG(INFO) << "end rotation: \n" << end_status.R_.matrix();LOG(INFO) << "end trans: \n" << end_status.p_.transpose();LOG(INFO) << "end v: \n" << end_status.v_.transpose();LOG(INFO) << "direct integ result: ";LOG(INFO) << "end rotation: \n" << R.matrix();LOG(INFO) << "end trans: \n" << t.transpose();LOG(INFO) << "end v: \n" << v.transpose();SUCCEED();
}
测试结果:
./test_preintegration --gtest_filter=PREINTEGRATION_TEST.ROTATION_TESTNote: Google Test filter = PREINTEGRATION_TEST.ROTATION_TEST
[==========] Running 1 test from 1 test suite.
[----------] Global test environment set-up.
[----------] 1 test from PREINTEGRATION_TEST
[ RUN ] PREINTEGRATION_TEST.ROTATION_TEST
I0116 22:22:12.217562 490210 test_preintegration.cc:74] preinteg result:
I0116 22:22:12.217624 490210 test_preintegration.cc:75] end rotation:
000000000-1 03.1225e-16 00000000000
-3.1225e-16 000000000-1 00000000000
00000000000 00000000000 00000000001
I0116 22:22:12.217639 490210 test_preintegration.cc:76] end trans:
000000000000 000000000000 -4.44089e-15
I0116 22:22:12.217643 490210 test_preintegration.cc:77] end v:
000000000000 000000000000 -1.77636e-15
I0116 22:22:12.217648 490210 test_preintegration.cc:79] direct integ result:
I0116 22:22:12.217648 490210 test_preintegration.cc:80] end rotation:
000000000-1 03.1225e-16 00000000000
-3.1225e-16 000000000-1 00000000000
00000000000 00000000000 00000000001
I0116 22:22:12.217655 490210 test_preintegration.cc:81] end trans:
0 0 0
I0116 22:22:12.217658 490210 test_preintegration.cc:82] end v:
0 0 0
[ OK ] PREINTEGRATION_TEST.ROTATION_TEST (0 ms)
[----------] 1 test from PREINTEGRATION_TEST (0 ms total)[----------] Global test environment tear-down
[==========] 1 test from 1 test suite ran. (0 ms total)
[ PASSED ] 1 test.
2. 测试在恒定加速度运行下的预积分情况
TEST(PREINTEGRATION_TEST, ACCELERATION_TEST) {// 测试在恒定加速度运行下的预积分情况double imu_time_span = 0.01; // IMU测量间隔Vec3d gravity(0, 0, -9.8); // Z 向上,重力方向为负Vec3d constant_acce(0.1, 0, 0); // x 方向上的恒定加速度sad::NavStated start_status(0), end_status(1.0);sad::IMUPreintegration pre_integ;// 对比直接积分Sophus::SO3d R;Vec3d t = Vec3d::Zero();Vec3d v = Vec3d::Zero();for (int i = 1; i <= 100; ++i) {double time = imu_time_span * i;Vec3d acce = constant_acce - gravity;pre_integ.Integrate(sad::IMU(time, Vec3d::Zero(), acce), imu_time_span);sad::NavStated this_status = pre_integ.Predict(start_status, gravity);t = t + v * imu_time_span + 0.5 * gravity * imu_time_span * imu_time_span +0.5 * (R * acce) * imu_time_span * imu_time_span;v = v + gravity * imu_time_span + (R * acce) * imu_time_span;// 验证在简单情况下,直接积分和预积分结果相等EXPECT_NEAR(t[0], this_status.p_[0], 1e-2);EXPECT_NEAR(t[1], this_status.p_[1], 1e-2);EXPECT_NEAR(t[2], this_status.p_[2], 1e-2);EXPECT_NEAR(v[0], this_status.v_[0], 1e-2);EXPECT_NEAR(v[1], this_status.v_[1], 1e-2);EXPECT_NEAR(v[2], this_status.v_[2], 1e-2);EXPECT_NEAR(R.unit_quaternion().x(), this_status.R_.unit_quaternion().x(), 1e-4);EXPECT_NEAR(R.unit_quaternion().y(), this_status.R_.unit_quaternion().y(), 1e-4);EXPECT_NEAR(R.unit_quaternion().z(), this_status.R_.unit_quaternion().z(), 1e-4);EXPECT_NEAR(R.unit_quaternion().w(), this_status.R_.unit_quaternion().w(), 1e-4);}end_status = pre_integ.Predict(start_status, gravity);LOG(INFO) << "preinteg result: ";LOG(INFO) << "end rotation: \n" << end_status.R_.matrix();LOG(INFO) << "end trans: \n" << end_status.p_.transpose();LOG(INFO) << "end v: \n" << end_status.v_.transpose();LOG(INFO) << "direct integ result: ";LOG(INFO) << "end rotation: \n" << R.matrix();LOG(INFO) << "end trans: \n" << t.transpose();LOG(INFO) << "end v: \n" << v.transpose();SUCCEED();
}
测试结果:
./test_preintegration --gtest_filter=PREINTEGRATION_TEST.ACCELERATION_TESTNote: Google Test filter = PREINTEGRATION_TEST.ACCELERATION_TEST
[==========] Running 1 test from 1 test suite.
[----------] Global test environment set-up.
[----------] 1 test from PREINTEGRATION_TEST
[ RUN ] PREINTEGRATION_TEST.ACCELERATION_TEST
I0116 22:26:44.789176 490325 test_preintegration.cc:126] preinteg result:
I0116 22:26:44.789251 490325 test_preintegration.cc:127] end rotation:
1 0 0
0 1 0
0 0 1
I0116 22:26:44.789265 490325 test_preintegration.cc:128] end trans:
000000000.05 000000000000 -4.44089e-15
I0116 22:26:44.789271 490325 test_preintegration.cc:129] end v:
0000000000.1 000000000000 -1.77636e-15
I0116 22:26:44.789274 490325 test_preintegration.cc:131] direct integ result:
I0116 22:26:44.789276 490325 test_preintegration.cc:132] end rotation:
1 0 0
0 1 0
0 0 1
I0116 22:26:44.789283 490325 test_preintegration.cc:133] end trans:
0.05 0000 0000
I0116 22:26:44.789286 490325 test_preintegration.cc:134] end v:
0.1 000 000
[ OK ] PREINTEGRATION_TEST.ACCELERATION_TEST (0 ms)
[----------] 1 test from PREINTEGRATION_TEST (0 ms total)[----------] Global test environment tear-down
[==========] 1 test from 1 test suite ran. (0 ms total)
[ PASSED ] 1 test.
相关文章:
《自动驾驶与机器人中的SLAM技术》ch4:预积分学
目录 1 预积分的定义 2 预积分的测量模型 ( 预积分的测量值可由 IMU 的测量值积分得到 ) 2.1 旋转部分 2.2 速度部分 2.3 平移部分 2.4 将预积分测量和误差式代回最初的定义式 3 预积分的噪声模型和协方差矩阵 3.1 旋转部分 3.2 速度部分 3.3 平移部分 3.4 噪声项合并 4 零偏的…...

Docker部署MySQL 5.7:持久化数据的实战技巧
在生产环境中使用Docker启动MySQL 5.7时,需要考虑数据持久化、配置文件管理、安全性等多个方面。以下是一个详细的步骤指南。 1. 准备工作 (1)创建挂载目录 在宿主机上创建用于挂载的目录,以便持久化数据和配置文件。 sudo mkdi…...

Spring框架 了解
深入浅出Spring框架:为初学者量身定制的入门指南 引言 在现代Java开发中,Spring框架无疑是构建企业级应用的核心技术之一。无论是初学者还是经验丰富的开发者,掌握Spring都能极大地提升你的编程技能和项目开发效率。本文将带你深入了解Spri…...

低代码独特架构带来的编译难点及多线程解决方案
前言 在当今软件开发领域,低代码平台以其快速构建应用的能力,吸引了众多开发者与企业的目光。然而,低代码平台独特的架构在带来便捷的同时,也给编译过程带来了一系列棘手的难点。 一,低代码编译的难点 (1…...

如何使用Ultralytics训练自己的yolo5 yolo8 yolo10 yolo11等目标检测模型
Ultralytics正在以惊人的速度吸收优秀的CV算法,之前Ultralytics定位于YOLOV8,但逐渐地扩展到支持其他版本的YOLO,最新版本的ultralytics全面支持yolo5 yolo7 yolo8 yolo9 yolo10 yolo11,包含模型的训练、验证、预测、部署等。毫无…...

Java技术栈 —— Andorid开发入门
Java技术栈 —— Andorid开发入门 一、搭建开发环境二、HelloWorld三、将Andorid项目打包成APK文件,并安装至手机上四、开发常见问题 一、搭建开发环境 不用Intellij,而是用Andorid Studio(免费),这是专门给Andorid的IDE。 参考文章或视频链…...

Qt天气预报系统获取天气数据
Qt天气预报系统获取天气数据 1、获取天气数据1.1添加天气类头文件1.2定义今天和未来几天天气数据类1.3定义一个解析JSON数据的函数1.4在mainwindow中添加weatherData.h1.5创建今天天气数据和未来几天天气数据对象1.6添加parseJson定义1.7把解析JSON数据添加进去1.8添加错误1.9解…...

力扣 搜索二维矩阵
二分查找,闭区间与开区间的不同解法。 题目 乍一看,不是遍历一下找到元素就可以了。 class Solution {public boolean searchMatrix(int[][] matrix, int target) {for (int[] ints : matrix) {for (int ans : ints) {if (ans target) return true;}}…...

JavaScript 操作符与表达式
Hi, 我是布兰妮甜,编写流畅、愉悦用户体验的程序员。JavaScript 是一种功能强大且灵活的编程语言,广泛应用于前端和后端开发。它提供了一系列丰富的操作符和表达式来处理数据、执行逻辑判断以及控制程序流程。理解这些概念对于编写高效、可读性强的代码至…...

深度学习 Pytorch 张量(Tensor)的创建和常用方法
1 张量的基本创建及其类型 和Numpy中的array一样,张量的本质也是结构化地组织了大量的数据。 并且在实际操作中,张量的创建和基本功能也与其非常类似。 1.1 张量(Tensor)函数创建方法 张量的最基本创建方法和Numpy中创建Array的格式一致。 # Numpy创建…...

在VMwareFusion中使用Ubuntu
在VMwareFusion使用Ubuntu 在VMwareFusion使用Ubuntu背景在VMwareFusion虚拟机里使用Ubuntu1、集成桌面工具2、主机和虚拟机之间共享剪贴板内容3、设置root用户密码4、设置静态ip4.1、静态ip和动态ip的区别4.2、查看当前ip4.2、linux网络配置文件所在位置4.3、基于ubuntu22.04.…...

%.*s——C语言中printf 函数中的一种格式化输出方式
在C语言中,%.*s 是 printf 函数中的一种格式化输出方式,用于控制字符串的输出长度。具体来说,%.*s 中的 * 表示输出宽度(即最多输出的字符数)是一个变量,这个变量的值在运行时通过 printf 函数的参数传递。…...

基于微信小程序的摄影竞赛系统设计与实现(LW+源码+讲解)
专注于大学生项目实战开发,讲解,毕业答疑辅导,欢迎高校老师/同行前辈交流合作✌。 技术范围:SpringBoot、Vue、SSM、HLMT、小程序、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容:…...

hydra破解密码
hydra九头蛇是常用的密码破解工具 1、破解centos ssh密码 hydra -l root -P password.txt ssh://192.168.1.107:2222 hydra -l root -P password.txt -s 2222 192.168.1.107 ssh2、破解ftp hydra -l allen -P e:\aa.txt ftp://127.0.0.1 hydra -l allen -P e:\aa.txt ftp:…...

JAVA之外观模式
外观模式,又称门面模式,是一种结构型设计模式,旨在为复杂的子系统提供一个统一且简化的接口。通过这一模式,客户端可以更加便捷地与子系统交互,而无需深入了解其内部结构和实现细节。外观模式不仅简化了客户端的使用&a…...

如何选择合适的服务器?服务器租赁市场趋势分析
服务器租赁市场概览 服务器租赁 market可以分为两种类型:按小时、按月和按年,每种模式都有其特点和适用场景,按小时租赁是最经济实惠的选择,适用于短期需求;按月租赁则适合中长期使用;而按年租赁则是最灵活…...

CentOS 下载软件时报Error: Failed to synchronize cache for repo ‘AppStream‘解决方法
下载软件时出现以下问题 直接把CentOS-AppStream.repo改个名字就行 cd /etc/yum.repos.d/ mv CentOS-AppStream.repo CentOS-AppStream.repo.bak就可以了 解决思路 把AI问遍,无人会,解决法 想要下载软件通通失败了,解决方法当然是问AI&am…...

鲍厚霖:引领AI广告创新,搭建中美合作桥梁
2024年是鲍厚霖和她领导的超能S咨询公司(Triple S AI)收获颇丰的一年。这一年中,她以卓越的战略眼光和创新能力,为中美教育、文化与技术的深度融合注入了新的活力。2025年,Triple S AI计划推出全新2.0版本平台,进一步深化人工智能驱动的营销与文化合作领域,推动产业变革与社会福…...

学习记录1
[SUCTF 2019]EasyWeb 直接给了源代码,分析一下 <?php function get_the_flag(){// webadmin will remove your upload file every 20 min!!!! $userdir "upload/tmp_".md5($_SERVER[REMOTE_ADDR]);if(!file_exists($userdir)){mkdir($userdir);}if…...

【Gossip 协议】Golang的实现库Memberlist 库简介
Gossip 协议简介 Gossip 协议是一种分布式协议,用于在节点之间传播信息,常用于成员管理、故障检测、服务发现等场景。在这个协议中,每个节点定期与其他节点交换信息,最终保证所有节点达到一致的状态。它的工作原理类似于人群中的…...

LDD3学习7--硬件接口I/O端口(以short为例)
1 理论 1.1 基本概念 目前对外设的操作,都是通过寄存器。寄存器的概念,其实就是接口,访问硬件接口,有I/O端口通信和内存映射I/O (Memory-Mapped I/O),I/O端口通信是比较老的那种,都是老的串口并口设备&am…...

openharmony电源管理子系统
电源管理子系统 简介目录使用说明相关仓 简介 电源管理子系统提供如下功能: 重启服务:系统重启和下电。系统电源管理服务:系统电源状态管理和休眠运行锁管理。显示相关的能耗调节:包括根据环境光调节背光亮度,和根…...

【Rust自学】13.4. 闭包 Pt.4:使用闭包捕获环境
13.4.0. 写在正文之前 Rust语言在设计过程中收到了很多语言的启发,而函数式编程对Rust产生了非常显著的影响。函数式编程通常包括通过将函数作为值传递给参数、从其他函数返回它们、将它们分配给变量以供以后执行等等。 在本章中,我们会讨论 Rust 的一…...

在 macOS 上,用命令行连接 MySQL(/usr/local/mysql/bin/mysql -u root -p)
根据你提供的文件内容,MySQL 的安装路径是 /usr/local/mysql。要直接使用 mysql 命令,你需要找到 mysql 可执行文件的路径。 在 macOS 上,mysql 客户端通常位于 MySQL 安装目录的 bin 子目录中。因此,完整的路径应该是࿱…...

mono3d汇总
lidar坐标系 lidar坐标系可以简单归纳为标准lidar坐标系和nucense lidar坐标系,参考链接。这个坐标系和车辆的ego坐标系是一致的。 标准lidar坐标系 opendet3d,mmdetection3d和kitt都i使用了该坐标系 up z^ x front| /| /left y <------ 0kitti采…...

K8S 节点选择器
今天我们来实验 pod 调度的 nodeName 与 nodeSelector。官网描述如下: 假设有如下三个节点的 K8S 集群: k8s31master 是控制节点 k8s31node1、k8s31node2 是工作节点 容器运行时是 containerd 一、镜像准备 1.1、镜像拉取 docker pull tomcat:8.5-jre8…...

【2024年华为OD机试】 (C卷,200分)- 反射计数(Java JS PythonC/C++)
一、问题描述 题目解析 题目描述 给定一个包含 0 和 1 的二维矩阵,一个物体从给定的初始位置出发,在给定的速度下进行移动。遇到矩阵的边缘时会发生镜面反射。无论物体经过 0 还是 1,都不影响其速度。请计算并给出经过 t 时间单位后&#…...

AI编程工具使用技巧——通义灵码
活动介绍通义灵码1. 理解通义灵码的基本概念示例代码生成 2. 使用明确的描述示例代码生成 3. 巧妙使用注释示例代码生成 4. 注意迭代与反馈原始代码反馈后生成优化代码 5. 结合生成的代码进行调试示例测试代码 其他功能定期优化生成的代码合作与分享结合其他工具 总结 活动介绍…...

挖掘机检测数据集,准确识别率91.0%,4327张原始图片,支持YOLO,COCO JSON,PASICAL VOC XML等多种格式标注
挖掘机检测数据集,准确识别率91.0%,4327张图片,支持YOLO,COCO JSON,PASICAL VOC XML等多种格式标注 数据集详情 数据集分割 训练组70% 3022图片 有效集20% 870图片 测试集10&…...

使用Docker部署postgresql
使用Docker部署postgresql postgresql数据库在Docker中的镜像的名称为postgres,可以从DockerHub中pull下来,如果pull不下来那么很大概率是网络问题导致的,这时候你可能需要在网上找一些能用的镜像源,以成功拉取postgres镜像。 有…...