当前位置: 首页 > news >正文

《自动驾驶与机器人中的SLAM技术》ch4:基于预积分和图优化的 GINS

前言:预积分图优化的结构

                       

         1 预积分的图优化顶点

        这里使用 《自动驾驶与机器人中的SLAM技术》ch4:预积分学 中提到的散装的形式来实现预积分的顶点部分,所以每个状态被分为位姿(R,p)、速度、陀螺零偏、加计零偏四种顶点(共 15 维)。后三者实际上都是 \mathbb{R}^3 的变量,可以直接使用继承来实现。g2o 部分可参考 G2O (General Graph Optimization) 。

        下文中提到的顶点的维度为 优化变量的维度

        1.1 旋转在前,平移在后的位姿顶点(6维)

        位姿顶点的顺序为旋转在前,平移在后,雅可比矩阵的顺序要与之对应。

/*** 旋转在前,平移在后的的SO3+t类型pose,6自由度,存储时伪装为g2o::VertexSE3,供g2o_viewer查看*/
class VertexPose : public g2o::BaseVertex<6, SE3> {public:EIGEN_MAKE_ALIGNED_OPERATOR_NEWVertexPose() {}// 读写函数是为了保存顶点信息到 .g2o 文件中的,该文件可使用 g2o_viewer 可视化bool read(std::istream& is) override {double data[7];for (int i = 0; i < 7; i++) {is >> data[i];}setEstimate(SE3(Quatd(data[6], data[3], data[4], data[5]), Vec3d(data[0], data[1], data[2])));return true;}bool write(std::ostream& os) const override {os << "VERTEX_SE3:QUAT ";os << id() << " ";Quatd q = _estimate.unit_quaternion();os << _estimate.translation().transpose() << " ";os << q.coeffs()[0] << " " << q.coeffs()[1] << " " << q.coeffs()[2] << " " << q.coeffs()[3] << std::endl;return true;}// 重置。这里未实现该函数的功能virtual void setToOriginImpl() {}// 更新virtual void oplusImpl(const double* update_) {// Eigen::Map<const Vec3d>(&update_[0]) :使用 &update_[0] 获取指向数组中第 1 个元素的地址,从该地址开始,将 update_ 数组连续的 3 个元素映射为一个 Eigen::Vector3d 对象// Eigen::Map<const Vec3d>(&update_[3]) :使用 &update_[3] 获取指向数组中第 4 个元素的地址,从该地址开始,将 update_ 数组连续的 3 个元素映射为一个 Eigen::Vector3d 对象// Eigen::Map: 这是一个模板类,用于将现有的内存块映射为 Eigen 对象。这种映射是零开销的,因为它不会复制数据,而是直接使用提供的内存。_estimate.so3() = _estimate.so3() * SO3::exp(Eigen::Map<const Vec3d>(&update_[0]));  // 旋转部分,右乘更新_estimate.translation() += Eigen::Map<const Vec3d>(&update_[3]);                     // 平移部分updateCache();}
};

        1.2 速度顶点(3维)

/*** 速度顶点,单纯的Vec3d*/
class VertexVelocity : public g2o::BaseVertex<3, Vec3d> {public:EIGEN_MAKE_ALIGNED_OPERATOR_NEWVertexVelocity() {}virtual bool read(std::istream& is) { return false; }virtual bool write(std::ostream& os) const { return false; }virtual void setToOriginImpl() { _estimate.setZero(); }virtual void oplusImpl(const double* update_) { _estimate += Eigen::Map<const Vec3d>(update_); }
};

        1.3 陀螺仪和加速度计零偏顶点(3维)

        陀螺仪零偏顶点和加速度计零偏顶点均继承自速度顶点。

/*** 陀螺零偏顶点,亦为Vec3d,从速度顶点继承*/
class VertexGyroBias : public VertexVelocity {public:EIGEN_MAKE_ALIGNED_OPERATOR_NEWVertexGyroBias() {}
};/*** 加计零偏顶点,Vec3d,亦从速度顶点继承*/
class VertexAccBias : public VertexVelocity {public:EIGEN_MAKE_ALIGNED_OPERATOR_NEWVertexAccBias() {}
};

        2 预积分的图优化边

        在基于预积分和图优化的 GINS 系统中,边主要有以下几种:

  • 1. 预积分的边,约束上一时刻的 15 维状态与下一时刻的旋转、平移、速度;
  • 2. 零偏随机游走的边,共两种,连接两个时刻的零偏状态;
  • 3. GNSS 的观测边。因为我们使用六自由度观测,所以它关联单个时刻的位姿;
  • 4. 先验信息,刻画上一时刻的状态分布,关联下一时刻的 15 维状态;
  • 5. 轮速计的观测边。关联上一时刻的速度顶点。

        下文中提到的边的维度为 观测值维度

        2.1 预积分边(9维多元边)

       注意:预积分边需要从外部传入 预积分对象、重力矢量和权重因子(为信息矩阵添加的乘积因子)。其中需要从预积分类中获取 整体预积分时间、预积分的协方差矩阵(其逆乘以权重因子作为信息矩阵)、预积分陀螺仪零偏(计算 \delta\boldsymbol{b}_{g,i})、雅可比矩阵(\frac{\partial\Delta\tilde{\boldsymbol{R}}_{ij}}{\partial\boldsymbol{b}_{g,i}}, \frac{\partial\Delta\tilde{\boldsymbol{v}}_{ij}}{\partial\boldsymbol{b}_{g,i}}, \frac{\partial\Delta\tilde{\boldsymbol{v}}_{ij}}{\partial\boldsymbol{b}_{a,i}}, \frac{\partial\Delta\tilde{\boldsymbol{p}}_{ij}}{\partial\boldsymbol{b}_{g,i}}, \frac{\partial\Delta\tilde{\boldsymbol{p}}_{ij}}{\partial\boldsymbol{b}_{a,i}})和零偏更新时修正后的预积分观测量。

        其中残差的定义残差对状态变量的雅可比矩阵见 《自动驾驶与机器人中的SLAM技术》ch4:预积分学 中的推导。

        计算 预积分残差对状态变量的雅可比矩阵 的结构如下图所示:

         代码实现如下: 

/// 与预积分相关的vertex, edge
/*** 预积分边(多元边)* 连接6个顶点:上一帧的pose, v, bg, ba,下一帧的pose, v* 观测量为9维,即预积分残差, 顺序:R, v, p* information从预积分类中获取,构造函数中计算* 旋转在前,平移在后*/
class EdgeInertial : public g2o::BaseMultiEdge<9, Vec9d> {public:EIGEN_MAKE_ALIGNED_OPERATOR_NEW/*** 构造函数中需要指定预积分类对象* @param preinteg  预积分对象指针* @param gravity   重力矢量* @param weight    权重。为信息矩阵添加的乘积因子*/EdgeInertial(std::shared_ptr<IMUPreintegration> preinteg, const Vec3d& gravity, double weight = 1.0);bool read(std::istream& is) override { return false; }bool write(std::ostream& os) const override { return false; }// 计算边的误差,即预积分残差void computeError() override;// 计算雅可比矩阵void linearizeOplus() override;// 计算 Hessian 矩阵Eigen::Matrix<double, 24, 24> GetHessian() {linearizeOplus();Eigen::Matrix<double, 9, 24> J;J.block<9, 6>(0, 0) = _jacobianOplus[0];J.block<9, 3>(0, 6) = _jacobianOplus[1];J.block<9, 3>(0, 9) = _jacobianOplus[2];J.block<9, 3>(0, 12) = _jacobianOplus[3];J.block<9, 6>(0, 15) = _jacobianOplus[4];J.block<9, 3>(0, 21) = _jacobianOplus[5];return J.transpose() * information() * J;}private:const double dt_;std::shared_ptr<IMUPreintegration> preint_ = nullptr;Vec3d grav_;
};
EdgeInertial::EdgeInertial(std::shared_ptr<IMUPreintegration> preinteg, const Vec3d& gravity, double weight): preint_(preinteg), dt_(preinteg->dt_) {resize(6);  // 6个关联顶点grav_ = gravity;setInformation(preinteg->cov_.inverse() * weight);
}void EdgeInertial::computeError() {auto* p1 = dynamic_cast<const VertexPose*>(_vertices[0]);auto* v1 = dynamic_cast<const VertexVelocity*>(_vertices[1]);auto* bg1 = dynamic_cast<const VertexGyroBias*>(_vertices[2]);auto* ba1 = dynamic_cast<const VertexAccBias*>(_vertices[3]);auto* p2 = dynamic_cast<const VertexPose*>(_vertices[4]);auto* v2 = dynamic_cast<const VertexVelocity*>(_vertices[5]);Vec3d bg = bg1->estimate();Vec3d ba = ba1->estimate();// 零偏的更新(假设预积分的测量值是随零偏线性变化的,当零偏更新时,在原先预积分测量值的基础上进行修正得到新的预积分测量值)const SO3 dR = preint_->GetDeltaRotation(bg);const Vec3d dv = preint_->GetDeltaVelocity(bg, ba);const Vec3d dp = preint_->GetDeltaPosition(bg, ba);/// 预积分误差项 p112 (4.41)// 预积分残差项const Vec3d er = (dR.inverse() * p1->estimate().so3().inverse() * p2->estimate().so3()).log();Mat3d RiT = p1->estimate().so3().inverse().matrix();const Vec3d ev = RiT * (v2->estimate() - v1->estimate() - grav_ * dt_) - dv;const Vec3d ep = RiT * (p2->estimate().translation() - p1->estimate().translation() - v1->estimate() * dt_ -grav_ * dt_ * dt_ / 2) -dp;_error << er, ev, ep;
}void EdgeInertial::linearizeOplus() {// 在 setVertex() 函数中设置 void setVertex(size_t i, Vertex* v) {_vertices[i]=v;}// 顺序为 v0_pose、v0_vel、v0_bg、v0_ba、v1_pose、v1_vel(这里体现了 旋转在前,平移在后)auto* p1 = dynamic_cast<const VertexPose*>(_vertices[0]);auto* v1 = dynamic_cast<const VertexVelocity*>(_vertices[1]);auto* bg1 = dynamic_cast<const VertexGyroBias*>(_vertices[2]);auto* ba1 = dynamic_cast<const VertexAccBias*>(_vertices[3]);auto* p2 = dynamic_cast<const VertexPose*>(_vertices[4]);auto* v2 = dynamic_cast<const VertexVelocity*>(_vertices[5]);Vec3d bg = bg1->estimate();Vec3d ba = ba1->estimate();Vec3d dbg = bg - preint_->bg_;// 一些中间符号const SO3 R1 = p1->estimate().so3();const SO3 R1T = R1.inverse();const SO3 R2 = p2->estimate().so3();auto dR_dbg = preint_->dR_dbg_;auto dv_dbg = preint_->dV_dbg_;auto dp_dbg = preint_->dP_dbg_;auto dv_dba = preint_->dV_dba_;auto dp_dba = preint_->dP_dba_;// 估计值Vec3d vi = v1->estimate();Vec3d vj = v2->estimate();Vec3d pi = p1->estimate().translation();Vec3d pj = p2->estimate().translation();const SO3 dR = preint_->GetDeltaRotation(bg);const SO3 eR = SO3(dR).inverse() * R1T * R2;const Vec3d er = eR.log();// 修复 bug const Mat3d invJr = SO3::jr_inv(er);/// 雅可比矩阵/// 注意有3个index, 顶点的,自己误差的,顶点内部变量的/// 变量顺序:pose1(R1,p1), v1, bg1, ba1, pose2(R2,p2), v2/// 残差顺序:eR, ev, ep,残差顺序为行(row),变量顺序为列(col)//       | R1 | p1 | v1 | bg1 | ba1 | R2 | p2 | v2 |//  vert | 0       | 1  | 2   | 3   | 4       | 5  |//  col  | 0    3  | 0  | 0   | 0   | 0    3  | 0  |//    row//  eR 0 |//  ev 3 |//  ep 6 |/// 残差对R1, 9x3_jacobianOplus[0].setZero();// dR/dR1, p113 (4.42)_jacobianOplus[0].block<3, 3>(0, 0) = -invJr * (R2.inverse() * R1).matrix();// dv/dR1, p115 (4.47)_jacobianOplus[0].block<3, 3>(3, 0) = SO3::hat(R1T * (vj - vi - grav_ * dt_));// dp/dR1, p115 (4.48d)_jacobianOplus[0].block<3, 3>(6, 0) = SO3::hat(R1T * (pj - pi - vi * dt_ - 0.5 * grav_ * dt_ * dt_));/// 残差对p1, 9x3// dp/dp1, p115 (4.48a)_jacobianOplus[0].block<3, 3>(6, 3) = -R1T.matrix();/// 残差对v1, 9x3_jacobianOplus[1].setZero();// dv/dv1, p114 (4.46a)_jacobianOplus[1].block<3, 3>(3, 0) = -R1T.matrix();// dp/dv1, p115 (4.48c)_jacobianOplus[1].block<3, 3>(6, 0) = -R1T.matrix() * dt_;/// 残差对bg1_jacobianOplus[2].setZero();// dR/dbg1, p114 (4.45)_jacobianOplus[2].block<3, 3>(0, 0) = -invJr * eR.inverse().matrix() * SO3::jr((dR_dbg * dbg).eval()) * dR_dbg;// dv/dbg1  p111 (4.38)前添加负号_jacobianOplus[2].block<3, 3>(3, 0) = -dv_dbg;// dp/dbg1  p111 (4.38)前添加负号_jacobianOplus[2].block<3, 3>(6, 0) = -dp_dbg;/// 残差对ba1_jacobianOplus[3].setZero();// dv/dba1  p111 (4.38)前添加负号_jacobianOplus[3].block<3, 3>(3, 0) = -dv_dba;// dp/dba1  p111 (4.38)前添加负号_jacobianOplus[3].block<3, 3>(6, 0) = -dp_dba;/// 残差对pose2_jacobianOplus[4].setZero();// dr/dr2, p114 (4.43)_jacobianOplus[4].block<3, 3>(0, 0) = invJr;// dp/dp2, p115 (4.48b)_jacobianOplus[4].block<3, 3>(6, 3) = R1T.matrix();/// 残差对v2_jacobianOplus[5].setZero();// dv/dv2, p114 (4,46b)_jacobianOplus[5].block<3, 3>(3, 0) = R1T.matrix();  // OK
}

        2.2 零偏随机游走边(3维双元边)

        2.2.1 陀螺仪随机游走边

        残差定义:

r_{ b_{g,ij}}=b_{g,j}-b_{g,i}

        残差对状态变量的雅可比矩阵:

\begin{aligned} & \frac{\partial r_{ b_{g,ij}}}{\partial b_{g,i}}=-I, \\ & \frac{\partial r_{ b_{g,ij}}}{\partial b_{g,j}}=I . \end{aligned}

/*** 陀螺仪随机游走边(双元边)*/
class EdgeGyroRW : public g2o::BaseBinaryEdge<3, Vec3d, VertexGyroBias, VertexGyroBias> {public:EIGEN_MAKE_ALIGNED_OPERATOR_NEWEdgeGyroRW() {}virtual bool read(std::istream& is) { return false; }virtual bool write(std::ostream& os) const { return false; }void computeError() {// 在 setVertex() 函数中设置 void setVertex(size_t i, Vertex* v) {_vertices[i]=v;}// 顺序为 v0_bg、v1_bgconst auto* VG1 = dynamic_cast<const VertexGyroBias*>(_vertices[0]);const auto* VG2 = dynamic_cast<const VertexGyroBias*>(_vertices[1]);_error = VG2->estimate() - VG1->estimate();}virtual void linearizeOplus() {// 残差对 bg1, 3x3_jacobianOplusXi = -Mat3d::Identity();// 残差对 bg2, 3x3_jacobianOplusXj.setIdentity();}Eigen::Matrix<double, 6, 6> GetHessian() {linearizeOplus();Eigen::Matrix<double, 3, 6> J;J.block<3, 3>(0, 0) = _jacobianOplusXi;J.block<3, 3>(0, 3) = _jacobianOplusXj;return J.transpose() * information() * J;}
};

        2.2.2 加速度计随机游走边

        残差定义:

r_{ b_{a,ij}}=b_{a,j}-b_{a,i}

        残差对状态变量的雅可比矩阵:

\begin{aligned} & \frac{\partial r_{ b_{a,ij}}}{\partial b_{a,i}}=-I, \\ & \frac{\partial r_{ b_{a,ij}}}{\partial b_{a,j}}=I . \end{aligned}

/*** 加速度计随机游走边(双元边)*/
class EdgeAccRW : public g2o::BaseBinaryEdge<3, Vec3d, VertexAccBias, VertexAccBias> {public:EIGEN_MAKE_ALIGNED_OPERATOR_NEWEdgeAccRW() {}virtual bool read(std::istream& is) { return false; }virtual bool write(std::ostream& os) const { return false; }void computeError() {// 在 setVertex() 函数中设置 void setVertex(size_t i, Vertex* v) {_vertices[i]=v;}// 顺序为 v0_ba、v1_baconst auto* VA1 = dynamic_cast<const VertexAccBias*>(_vertices[0]);const auto* VA2 = dynamic_cast<const VertexAccBias*>(_vertices[1]);_error = VA2->estimate() - VA1->estimate();}virtual void linearizeOplus() {// 残差对 ba1, 3x3_jacobianOplusXi = -Mat3d::Identity();// 残差对 ba2, 3x3_jacobianOplusXj.setIdentity();}Eigen::Matrix<double, 6, 6> GetHessian() {linearizeOplus();Eigen::Matrix<double, 3, 6> J;J.block<3, 3>(0, 0) = _jacobianOplusXi;J.block<3, 3>(0, 3) = _jacobianOplusXj;return J.transpose() * information() * J;}
};

        2.3 GNSS 观测边(单边)

        GNSS 方案分为双天线方案和单天线方案。

        2.3.1 双天线方案(6维 旋转+位移)

        注意:双天线 GNSS 边需要从外部传入 GNSS 测量值(T_{WB})。

         残差定义:

\begin{aligned} & r_{ R_{i}}=\mathrm{Log}\left(R_{gnss}^{T}R_{i}\right), \\&r_{ p_{i}}=p_{i}-p_{gnss}. \end{aligned}

        

        残差对状态变量的雅可比矩阵:

\begin{aligned} & \frac{\partial r_{ R_{i}}}{\partial R_{i}}=J_r^{-1}\left({ R_{gnss}^{T}R_{i}}\right), \\ &\frac{\partial r_{ p_{i}}}{\partial p_{i}}=I. \end{aligned}

        j 时刻的 GNSS 残差和雅可比矩阵同上。

/*** 6 自由度的GNSS* 误差的旋转在前,平移在后*/
class EdgeGNSS : public g2o::BaseUnaryEdge<6, SE3, VertexPose> {public:EIGEN_MAKE_ALIGNED_OPERATOR_NEW;EdgeGNSS() = default;EdgeGNSS(VertexPose* v, const SE3& obs) {setVertex(0, v);setMeasurement(obs);}// 这里使用的是 bag包或者 .txt数据,其中的 GNSS 数据已经被转换到车体坐标系下了,是直接对 R,p 的观测void computeError() override {// 在 setVertex() 函数中设置 void setVertex(size_t i, Vertex* v) {_vertices[i]=v;}// 顺序为 p_gnss_0 或者 p_gnss_1VertexPose* v = (VertexPose*)_vertices[0];_error.head<3>() = (_measurement.so3().inverse() * v->estimate().so3()).log();_error.tail<3>() = v->estimate().translation() - _measurement.translation();};void linearizeOplus() override {VertexPose* v = (VertexPose*)_vertices[0];// jacobian 6x6_jacobianOplusXi.setZero();_jacobianOplusXi.block<3, 3>(0, 0) = (_measurement.so3().inverse() * v->estimate().so3()).jr_inv();  // dR/dR_jacobianOplusXi.block<3, 3>(3, 3) = Mat3d::Identity();                                              // dp/dp}Mat6d GetHessian() {linearizeOplus();return _jacobianOplusXi.transpose() * information() * _jacobianOplusXi;}virtual bool read(std::istream& in) { return true; }virtual bool write(std::ostream& out) const { return true; }private:
};

        2.3.2 单天线方案(3维 位移)

        注意:单天线 GNSS 边需要从外部传入 传感器外参(T_{BG}) 和 GNSS 测量值(此时是 T_{WG} 而不是双天线的 T_{WB})。

        残差定义:

\begin{aligned} &r_{ p_{i}}=p_{i}*T_{BG}-p_{gnss}. \end{aligned}

        

        残差对状态变量的雅可比矩阵:

\begin{aligned} & \frac{\partial r_{ p_{i}}}{\partial p_{i}}=I. \end{aligned}        

         j 时刻的 GNSS 残差和雅可比矩阵同上。 

/*** 只有平移的GNSS* 此时需要提供RTK外参 TBG,才能正确施加约束*/
class EdgeGNSSTransOnly : public g2o::BaseUnaryEdge<3, Vec3d, VertexPose> {public:EIGEN_MAKE_ALIGNED_OPERATOR_NEW;/*** 指定位姿顶点、RTK观测 t_WG、外参TGB* @param v* @param obs*/EdgeGNSSTransOnly(VertexPose* v, const Vec3d& obs, const SE3& TBG) : TBG_(TBG) {setVertex(0, v);setMeasurement(obs);}void computeError() override {// 在 setVertex() 函数中设置 void setVertex(size_t i, Vertex* v) {_vertices[i]=v;}// 顺序为 p_gnss_0 或者 p_gnss_1VertexPose* v = (VertexPose*)_vertices[0];// RTK 读数为 T_WG_error = (v->estimate() * TBG_).translation() - _measurement;};// 可以由用户提供雅可比矩阵的解析解;或者省略 linearizeOplus() 函数使用 g2o 默认的数值方式自动计算雅可比矩阵的数值解,但效率较低并且可能会引入误差// void linearizeOplus() override {//     VertexPose* v = (VertexPose*)_vertices[0];//     // jacobian 6x6//     _jacobianOplusXi.setZero();//     _jacobianOplusXi.block<3, 3>(0, 0) = (_measurement.so3().inverse() * v->estimate().so3()).jr_inv();  // dR/dR//     _jacobianOplusXi.block<3, 3>(3, 3) = Mat3d::Identity();                                              // dp/dp// }virtual bool read(std::istream& in) { return true; }virtual bool write(std::ostream& out) const { return true; }private:SE3 TBG_;
};

        2.4 先验因子边(15维多元边)

        注意:先验信息边需要从外部传入 上一轮优化i-n 时刻到 i 时刻)得到的 i 时刻关键帧状态(R,p,v,b_g,b_a,这里加上下标 last\underline{~}state 区分)和 先验信息矩阵(上一轮优化完成后,利用边缘化(对 Hessian 矩阵)的方法,求出的 i 时刻关键帧状态的协方差矩阵,作为当前轮优化i 时刻状态的先验因子使用)。

        残差定义:

\begin{aligned} & r_{ R_{i}}=\mathrm{Log}\left(R_{last\underline{~}state}^{T}R_{i}\right), \\&r_{ p_{i}}=p_{i}-p_{last\underline{~}state}, \\&r_{ v_{i}}=v_{i}-v_{last\underline{~}state}, \\& r_{ b_{g,i}}=b_{g,i}-b_{g,last\underline{~}state}, \\& r_{ b_{a,i}}=b_{a,i}-b_{a,last\underline{~}state}. \end{aligned}

        

        残差对状态变量的雅可比矩阵:

\begin{aligned} & \frac{\partial r_{ R_{i}}}{\partial R_{i}}=J_r^{-1}(r_{ R_{i}}), \\ &\frac{\partial r_{ p_{i}}}{\partial p_{i}}=I, \\ &\frac{\partial r_{ v_{i}}}{\partial v_{i}}=I, \\ &\frac{\partial r_{ b_{g,i}}}{\partial b_{g,i}}=I, \\ & \frac{\partial r_{ b_{a,i}}}{\partial b_{a,i}}=I . \end{aligned} 

/*** 先验信息边(多元变)* 对上一帧IMU pvq bias的先验* info 由外部指定,通过时间窗口边缘化给出** 顶点顺序:pose, v, bg, ba* 残差顺序:R, p, v, bg, ba,15维* 残差的旋转在前,平移在后*/
class EdgePriorPoseNavState : public g2o::BaseMultiEdge<15, Vec15d> {public:EIGEN_MAKE_ALIGNED_OPERATOR_NEWEdgePriorPoseNavState(const NavStated& state, const Mat15d& info);virtual bool read(std::istream& is) { return false; }virtual bool write(std::ostream& os) const { return false; }void computeError();virtual void linearizeOplus();Eigen::Matrix<double, 15, 15> GetHessian() {linearizeOplus();Eigen::Matrix<double, 15, 15> J;J.block<15, 6>(0, 0) = _jacobianOplus[0];J.block<15, 3>(0, 6) = _jacobianOplus[1];J.block<15, 3>(0, 9) = _jacobianOplus[2];J.block<15, 3>(0, 12) = _jacobianOplus[3];return J.transpose() * information() * J;}NavStated state_;
};
EdgePriorPoseNavState::EdgePriorPoseNavState(const NavStated& state, const Mat15d& info) {resize(4);state_ = state;setInformation(info);
}void EdgePriorPoseNavState::computeError() {// 在 setVertex() 函数中设置 void setVertex(size_t i, Vertex* v) {_vertices[i]=v;}// 顺序为 v0_pose、v0_vel、v0_bg、v0_baauto* vp = dynamic_cast<const VertexPose*>(_vertices[0]);auto* vv = dynamic_cast<const VertexVelocity*>(_vertices[1]);auto* vg = dynamic_cast<const VertexGyroBias*>(_vertices[2]);auto* va = dynamic_cast<const VertexAccBias*>(_vertices[3]);const Vec3d er = SO3(state_.R_.matrix().transpose() * vp->estimate().so3().matrix()).log();const Vec3d ep = vp->estimate().translation() - state_.p_;const Vec3d ev = vv->estimate() - state_.v_;const Vec3d ebg = vg->estimate() - state_.bg_;const Vec3d eba = va->estimate() - state_.ba_;_error << er, ep, ev, ebg, eba;
}void EdgePriorPoseNavState::linearizeOplus() {const auto* vp = dynamic_cast<const VertexPose*>(_vertices[0]);const Vec3d er = SO3(state_.R_.matrix().transpose() * vp->estimate().so3().matrix()).log();//       | R1 | p1 | v1 | bg1 | ba1 |//  vert | 0       | 1  | 2   | 3   |//  col  | 0    3  | 0  | 0   | 0   |//    row//  eR 0 |//  ep 3 |//  ev 6 |// ebg 9 |// eba 12|/// 注意有3个index, 顶点的,自己误差的,顶点内部变量的_jacobianOplus[0].setZero();_jacobianOplus[0].block<3, 3>(0, 0) = SO3::jr_inv(er);    // dr/dr_jacobianOplus[0].block<3, 3>(3, 3) = Mat3d::Identity();  // dp/dp_jacobianOplus[1].setZero();_jacobianOplus[1].block<3, 3>(6, 0) = Mat3d::Identity();  // dv/dv_jacobianOplus[2].setZero();_jacobianOplus[2].block<3, 3>(9, 0) = Mat3d::Identity();  // dbg/dbg_jacobianOplus[3].setZero();_jacobianOplus[3].block<3, 3>(12, 0) = Mat3d::Identity();  // dba/dba
}

        2.5 轮速计边(3维单元边)

        注意:轮速计边需要从外部传入 经过转换后的 v_{odom}(世界坐标系下),ODOM 观测的是车体坐标系下的车头朝向的速度 v_{b,x} 。

        残差定义:

\begin{aligned} &r_{ v_{j}}=v_{j}*-v_{odom}. \end{aligned}

        

        残差对状态变量的雅可比矩阵:

\begin{aligned} & \frac{\partial r_{ v_{j}}}{\partial v_{j}}=I. \end{aligned}   

/*** 3维 轮速计观测边* 轮速观测世界速度在自车坐标系下矢量, 3维情况下假设自车不会有y和z方向速度*/
class EdgeEncoder3D : public g2o::BaseUnaryEdge<3, Vec3d, VertexVelocity> {public:EIGEN_MAKE_ALIGNED_OPERATOR_NEW;EdgeEncoder3D() = default;/*** 构造函数需要知道世界系下速度* @param v0* @param speed*/EdgeEncoder3D(VertexVelocity* v0, const Vec3d& speed) {setVertex(0, v0);setMeasurement(speed);}void computeError() override {VertexVelocity* v0 = (VertexVelocity*)_vertices[0];_error = v0->estimate() - _measurement;}void linearizeOplus() override { _jacobianOplusXi.setIdentity(); }virtual bool read(std::istream& in) { return true; }virtual bool write(std::ostream& out) const { return true; }
};

        3 实现基于预积分和图优化的 GINS

        最后我们利用前面定义的图优化顶点和边,来实现一个类似于 ESKF 的 GNSS 惯导融合定位。读者也可借这个实验来更深入地考察图优化与滤波器之间的异同。这个基于图优化的 GINS 系统逻辑和 ESKF 大体相同,一样需要静态的 IMU 初始化来确定初始的 IMU 零偏与重力方向。我们把这些逻辑处理放到一个单独的类中,重点关注这个图优化模型是如何构建的。它的基本逻辑如下:

  • 1. 使用 IMU 静止初始化算法来获取初始的零偏(b_g,b_a)和重力方向(g),随后初始化各类信息矩阵(b_gb_a、GNSS、ODOM、先验因子的信息矩阵),然后使用首个带姿态的 GNSS 数据来获取初始位置与姿态,初始速度为 0,初始的零偏等于预积分类中的零偏,即 IMU 静止初始化得到的零偏。如果 IMU 和 GNSS 都有效,就开始进行预测和优化过程。
  • 2. 当 IMU 数据到达时,使用预积分器来累计 IMU 的积分信息。
  • 3. 当 ODOM 数据到达时,我们将它记录为最近时刻的速度观测并保留它的读数。
  • 4. 在 GNSS 数据到达时,先使用 last_imu_ 数据预积分到 GNSS 时刻。
  • 5. 使用 IMU 预积分的预测值来作为优化的初始值j 时刻优化的初始值,代码中为 this_frame_),构建前一个时刻的 GNSS 与当前时刻的 GNSS 间的图优化问题。当然也可以用 GNSS 的观测值来计算,两种方式的优化初值会不太一样,但在本例中结果相似。该问题的节点和边定义如下:

                • 节点:前一时刻与当前时刻的位姿、速度、两个零偏,共 8 个顶点。

                • 边:两个时刻间的预积分观测边,两个时刻的 GNSS 的观测边,前一个时刻的先验边,两个零偏随机游走边,速度观测边。这样一共有 7 个边。

        3.1 代码实现

  • 1. 使用 IMU 静止初始化算法来获取初始的零偏(b_g,b_a)和重力方向(g),随后初始化各类信息矩阵(b_gb_a、GNSS、ODOM、先验因子的信息矩阵),然后使用首个带姿态的 GNSS 数据来获取初始位置与姿态,初始速度为 0,初始的零偏等于预积分类中的零偏,即 IMU 静止初始化得到的零偏。如果 IMU 和 GNSS 都有效,就开始进行预测和优化过程。

        IMU 静止初始化代码如下: 

run_gins_pre_integ.cc 主函数/// IMU 处理函数// IMU 静止初始化if (!imu_init.InitSuccess()) {imu_init.AddIMU(imu);return;}

        IMU 静止初始化完成后 ,使用静止初始化得到的 b_g,b_a,g 初始化预积分类的 b_g,b_a,g,然后初始化各类信息矩阵(b_gb_a、GNSS、ODOM、先验因子):

run_gins_pre_integ.cc 主函数/// 需要IMU初始化if (!imu_inited) {// 读取初始零偏,设置GINSsad::GinsPreInteg::Options options;options.preinteg_options_.init_bg_ = imu_init.GetInitBg();options.preinteg_options_.init_ba_ = imu_init.GetInitBa();options.gravity_ = imu_init.GetGravity();gins.SetOptions(options);imu_inited = true;std::cout << "imu_inited = true" <<std::endl;std::cout << "imu time:" << std::fixed << std::setprecision(8) << imu.timestamp_ << std::endl;return;}

         初始化各类信息矩阵的代码如下(b_gb_a、GNSS、ODOM、先验因子),其中预积分边的信息矩阵由预积分类中的协方差矩阵之逆初始化:

void GinsPreInteg::SetOptions(sad::GinsPreInteg::Options options) {options_ = options;// 设置 bg 的信息矩阵double bg_rw2 = 1.0 / (options_.bias_gyro_var_ * options_.bias_gyro_var_);options_.bg_rw_info_.diagonal() << bg_rw2, bg_rw2, bg_rw2;// 设置 ba 的信息矩阵double ba_rw2 = 1.0 / (options_.bias_acce_var_ * options_.bias_acce_var_);options_.ba_rw_info_.diagonal() << ba_rw2, ba_rw2, ba_rw2;// 设置 GNSS 的信息矩阵double gp2 = options_.gnss_pos_noise_ * options_.gnss_pos_noise_;double gh2 = options_.gnss_height_noise_ * options_.gnss_height_noise_;double ga2 = options_.gnss_ang_noise_ * options_.gnss_ang_noise_;options_.gnss_info_.diagonal() << 1.0 / ga2, 1.0 / ga2, 1.0 / ga2, 1.0 / gp2, 1.0 / gp2, 1.0 / gh2;// 初始化 IMU预积分类 的 实例对象pre_integ_ = std::make_shared<IMUPreintegration>(options_.preinteg_options_);// 设置 ODOM 的信息矩阵double o2 = 1.0 / (options_.odom_var_ * options_.odom_var_);options_.odom_info_.diagonal() << o2, o2, o2;// 先验因子的信息矩阵prior_info_.block<6, 6>(9, 9) = Mat6d ::Identity() * 1e6;if (this_frame_) {this_frame_->bg_ = options_.preinteg_options_.init_bg_;this_frame_->ba_ = options_.preinteg_options_.init_ba_;}
}

        当首个带姿态的 GNSS 数据到来时,使用该数据初始化初始位姿,初始速度为 0,初始的零偏等于预积分类中的零偏,即 IMU 静止初始化得到的零偏:

void GinsPreInteg::AddGnss(const GNSS& gnss) {this_frame_ = std::make_shared<NavStated>(current_time_);this_gnss_ = gnss;if (!first_gnss_received_) {if (!gnss.heading_valid_) {// 要求首个GNSS必须有航向return;}std::cout << "first gnss time:" << std::fixed << std::setprecision(8) << gnss.unix_time_ << std::endl;// 首个gnss信号,将初始pose设置为该gnss信号this_frame_->timestamp_ = gnss.unix_time_;this_frame_->p_ = gnss.utm_pose_.translation();this_frame_->R_ = gnss.utm_pose_.so3();this_frame_->v_.setZero();this_frame_->bg_ = options_.preinteg_options_.init_bg_;this_frame_->ba_ = options_.preinteg_options_.init_ba_;pre_integ_ = std::make_shared<IMUPreintegration>(options_.preinteg_options_);last_frame_ = this_frame_;last_gnss_ = this_gnss_;first_gnss_received_ = true;current_time_ = gnss.unix_time_;return;}//* 省略 *// 
}
  • 2. 当 IMU 数据到达时,使用预积分器来累计 IMU 的积分信息。
void GinsPreInteg::AddImu(const IMU& imu) {if (first_gnss_received_ && first_imu_received_) {std::cout << "---IMU 预积分" << std::endl;pre_integ_->Integrate(imu, imu.timestamp_ - last_imu_.timestamp_);}first_imu_received_ = true;last_imu_ = imu;current_time_ = imu.timestamp_;
}
  • 3. 当 ODOM 数据到达时,我们将它记录为最近时刻的速度观测并保留它的读数。
void GinsPreInteg::AddOdom(const sad::Odom& odom) {last_odom_ = odom;last_odom_set_ = true;
}
  • 4. 在 GNSS 数据到达时,先使用 last_imu_ 数据预积分到 GNSS 时刻。这里 last_imu_ 比较特殊,该数据使用了两次,这是第二次使用(第一次是前面的预积分),使用该数据预积分到当前 GNSS 的时刻
void GinsPreInteg::AddGnss(const GNSS& gnss) {this_frame_ = std::make_shared<NavStated>(current_time_);this_gnss_ = gnss;//* 省略 *//// 积分到GNSS时刻// 这部分为什么可以积分到GNSS时刻 ???// 这个特殊的 last_imu_ 数据使用了两次,这是第二次使用(第一次是前面的预积分)。预积分到当前GNSS的时刻pre_integ_->Integrate(last_imu_, gnss.unix_time_ - current_time_);current_time_ = gnss.unix_time_;// last_frame_ 和 this_frame_,使用 IMU 预积分的预测值来作为优化的初始值(this_frame_)*this_frame_ = pre_integ_->Predict(*last_frame_, options_.gravity_);Optimize();last_frame_ = this_frame_;last_gnss_ = this_gnss_;
}
  • 5. 使用 IMU 预积分的预测值来作为优化的初始值j 时刻优化的初始值,代码中为 this_frame_),构建前一个时刻的 GNSS 与当前时刻的 GNSS 间的图优化问题。

        注意:①优化完成后将优化后的值重新赋值给 last_frame_ 和 this_frame_,并且使用优化后的 b_g,b_a 重置预积分实例对象。②这里没有先验因子的信息矩阵进行更新处理,后续第 8 章会探究先验因子信息矩阵的设置及更新。

void GinsPreInteg::Optimize() {if (pre_integ_->dt_ < 1e-3) {// 未得到积分return;}// 创建可变尺寸的 BlockSolverusing BlockSolverType = g2o::BlockSolverX;using LinearSolverType = g2o::LinearSolverEigen<BlockSolverType::PoseMatrixType>;auto* solver = new g2o::OptimizationAlgorithmLevenberg(g2o::make_unique<BlockSolverType>(g2o::make_unique<LinearSolverType>()));g2o::SparseOptimizer optimizer;optimizer.setAlgorithm(solver);// 上时刻顶点, pose, v, bg, baauto v0_pose = new VertexPose();v0_pose->setId(0);v0_pose->setEstimate(last_frame_->GetSE3());optimizer.addVertex(v0_pose);auto v0_vel = new VertexVelocity();v0_vel->setId(1);v0_vel->setEstimate(last_frame_->v_);optimizer.addVertex(v0_vel);auto v0_bg = new VertexGyroBias();v0_bg->setId(2);v0_bg->setEstimate(last_frame_->bg_);optimizer.addVertex(v0_bg);auto v0_ba = new VertexAccBias();v0_ba->setId(3);v0_ba->setEstimate(last_frame_->ba_);optimizer.addVertex(v0_ba);// 本时刻顶点,pose, v, bg, ba// this_frame_ 是从 起始点last_frame_ 开始通过 IMU预积分 得到的预测值,在这里作为图优化的初始值。auto v1_pose = new VertexPose();v1_pose->setId(4);v1_pose->setEstimate(this_frame_->GetSE3());optimizer.addVertex(v1_pose);auto v1_vel = new VertexVelocity();v1_vel->setId(5);v1_vel->setEstimate(this_frame_->v_);optimizer.addVertex(v1_vel);auto v1_bg = new VertexGyroBias();v1_bg->setId(6);v1_bg->setEstimate(this_frame_->bg_);optimizer.addVertex(v1_bg);auto v1_ba = new VertexAccBias();v1_ba->setId(7);v1_ba->setEstimate(this_frame_->ba_);optimizer.addVertex(v1_ba);// 预积分边auto edge_inertial = new EdgeInertial(pre_integ_, options_.gravity_);edge_inertial->setVertex(0, v0_pose);edge_inertial->setVertex(1, v0_vel);edge_inertial->setVertex(2, v0_bg);edge_inertial->setVertex(3, v0_ba);edge_inertial->setVertex(4, v1_pose);edge_inertial->setVertex(5, v1_vel);auto* rk = new g2o::RobustKernelHuber();rk->setDelta(200.0);edge_inertial->setRobustKernel(rk);optimizer.addEdge(edge_inertial);// 两个零偏随机游走auto* edge_gyro_rw = new EdgeGyroRW();edge_gyro_rw->setVertex(0, v0_bg);edge_gyro_rw->setVertex(1, v1_bg);edge_gyro_rw->setInformation(options_.bg_rw_info_);optimizer.addEdge(edge_gyro_rw);auto* edge_acc_rw = new EdgeAccRW();edge_acc_rw->setVertex(0, v0_ba);edge_acc_rw->setVertex(1, v1_ba);edge_acc_rw->setInformation(options_.ba_rw_info_);optimizer.addEdge(edge_acc_rw);// 上时刻先验auto* edge_prior = new EdgePriorPoseNavState(*last_frame_, prior_info_);edge_prior->setVertex(0, v0_pose);edge_prior->setVertex(1, v0_vel);edge_prior->setVertex(2, v0_bg);edge_prior->setVertex(3, v0_ba);optimizer.addEdge(edge_prior);// GNSS边auto edge_gnss0 = new EdgeGNSS(v0_pose, last_gnss_.utm_pose_);edge_gnss0->setInformation(options_.gnss_info_);optimizer.addEdge(edge_gnss0);auto edge_gnss1 = new EdgeGNSS(v1_pose, this_gnss_.utm_pose_);edge_gnss1->setInformation(options_.gnss_info_);optimizer.addEdge(edge_gnss1);// Odom边EdgeEncoder3D* edge_odom = nullptr;Vec3d vel_world = Vec3d::Zero();Vec3d vel_odom = Vec3d::Zero();if (last_odom_set_) {// velocity obsdouble velo_l =options_.wheel_radius_ * last_odom_.left_pulse_ / options_.circle_pulse_ * 2 * M_PI / options_.odom_span_;double velo_r =options_.wheel_radius_ * last_odom_.right_pulse_ / options_.circle_pulse_ * 2 * M_PI / options_.odom_span_;double average_vel = 0.5 * (velo_l + velo_r);vel_odom = Vec3d(average_vel, 0.0, 0.0);vel_world = this_frame_->R_ * vel_odom;edge_odom = new EdgeEncoder3D(v1_vel, vel_world);edge_odom->setInformation(options_.odom_info_);optimizer.addEdge(edge_odom);// 重置odom数据到达标志位,等待最新的odom数据last_odom_set_ = false;}optimizer.setVerbose(options_.verbose_);optimizer.initializeOptimization();optimizer.optimize(20);if (options_.verbose_) {// 获取结果,统计各类误差LOG(INFO) << "chi2/error: ";LOG(INFO) << "preintegration: " << edge_inertial->chi2() << "/" << edge_inertial->error().transpose();// LOG(INFO) << "gnss0: " << edge_gnss0->chi2() << ", " << edge_gnss0->error().transpose();LOG(INFO) << "gnss1: " << edge_gnss1->chi2() << ", " << edge_gnss1->error().transpose();LOG(INFO) << "bias: " << edge_gyro_rw->chi2() << "/" << edge_acc_rw->error().transpose();LOG(INFO) << "prior: " << edge_prior->chi2() << "/" << edge_prior->error().transpose();if (edge_odom) {LOG(INFO) << "body vel: " << (v1_pose->estimate().so3().inverse() * v1_vel->estimate()).transpose();LOG(INFO) << "meas: " << vel_odom.transpose();LOG(INFO) << "odom: " << edge_odom->chi2() << "/" << edge_odom->error().transpose();}}last_frame_->R_ = v0_pose->estimate().so3();last_frame_->p_ = v0_pose->estimate().translation();last_frame_->v_ = v0_vel->estimate();last_frame_->bg_ = v0_bg->estimate();last_frame_->ba_ = v0_ba->estimate();this_frame_->R_ = v1_pose->estimate().so3();this_frame_->p_ = v1_pose->estimate().translation();this_frame_->v_ = v1_vel->estimate();this_frame_->bg_ = v1_bg->estimate();this_frame_->ba_ = v1_ba->estimate();// 重置integ// 更新 bg、ba,重置预积分options_.preinteg_options_.init_bg_ = this_frame_->bg_;options_.preinteg_options_.init_ba_ = this_frame_->ba_;pre_integ_ = std::make_shared<IMUPreintegration>(options_.preinteg_options_);
}

        最后运行基于图优化的GINS:

    /// 设置各类回调函数io.SetIMUProcessFunc([&](const sad::IMU& imu) {if (imu_init.InitSuccess()){std::cout << "IMU 处理函数!    " << "imu_inited:" << imu_inited << "    gnss_inited:" << gnss_inited << std::endl;}/// IMU 处理函数// IMU 静止初始化if (!imu_init.InitSuccess()) {imu_init.AddIMU(imu);return;}/// 需要IMU初始化if (!imu_inited) {// 读取初始零偏,设置GINSsad::GinsPreInteg::Options options;options.preinteg_options_.init_bg_ = imu_init.GetInitBg();options.preinteg_options_.init_ba_ = imu_init.GetInitBa();options.gravity_ = imu_init.GetGravity();gins.SetOptions(options);imu_inited = true;std::cout << "imu_inited = true" <<std::endl;std::cout << "imu time:" << std::fixed << std::setprecision(8) << imu.timestamp_ << std::endl;return;}if (!gnss_inited) {/// 等待有效的RTK数据return;}// 与 ESKF 不同之处!!!  /// GNSS 也接收到之后,再开始进行预测gins.AddImu(imu);auto state = gins.GetState();save_result(fout, state);if (ui) {ui->UpdateNavState(state);usleep(5e2);}}).SetGNSSProcessFunc([&](const sad::GNSS& gnss) {if (imu_init.InitSuccess()){std::cout << "GNSS 处理函数!    " << "imu_inited:" << imu_inited << "    gnss_inited:" << gnss_inited << std::endl;}/// GNSS 处理函数if (!imu_inited) {return;}// heading_valid_ 数据由 GNSS 构造函数确定sad::GNSS gnss_convert = gnss;if (!sad::ConvertGps2UTM(gnss_convert, antenna_pos, FLAGS_antenna_angle) || !gnss_convert.heading_valid_) {return;}/// 去掉原点if (!first_gnss_set) {origin = gnss_convert.utm_pose_.translation();first_gnss_set = true;std::cout << "gnss_inited = true" <<std::endl;std::cout << "gnss time:" << std::fixed << std::setprecision(8) << gnss_convert.unix_time_ << std::endl;}gnss_convert.utm_pose_.translation() -= origin;// 与 ESKF 不同之处!!!gins.AddGnss(gnss_convert);auto state = gins.GetState();save_result(fout, state);if (ui) {ui->UpdateNavState(state);usleep(1e3);}gnss_inited = true;}).SetOdomProcessFunc([&](const sad::Odom& odom) {if (imu_init.InitSuccess()){std::cout << "ODOM 处理函数!    " << "imu_inited:" << imu_inited << "    gnss_inited:" << gnss_inited << std::endl;}imu_init.AddOdom(odom);if (imu_inited && gnss_inited) {// 与 ESKF 不同之处!!!gins.AddOdom(odom);}}).Go();

        3.2 运行结果 

        IMU 静止初始化结果:

I0118 16:14:15.636049 40021 static_imu_init.cc:86] mean acce: -0.612774 0.0203702 009.80743
I0118 16:14:15.636137 40021 static_imu_init.cc:109] IMU 初始化成功,初始化时间= 9.99001, bg = -0.000306222 00.000156394 -8.89245e-06, ba = -0.00103346 3.43548e-05 000.0165404, gyro sq = 05.5781e-07 4.46347e-07 7.20772e-07, acce sq = 03.5829e-05 3.60427e-05 4.34496e-05, grav = 000.611741 -0.0203359 00-9.79089, norm: 9.81
I0118 16:14:15.636152 40021 static_imu_init.cc:113] mean gyro: -0.000306222 00.000156394 -8.89245e-06 acce: -0.00103346 3.43548e-05 000.0165404

        某一轮优化过程:

GNSS 处理函数!    imu_inited:1    gnss_inited:1
iteration= 0	 chi2= 11.939758	 time= 2.537e-05	 cumTime= 2.537e-05	 edges= 7	 schur= 0	 lambda= 6666674.355996	 levenbergIter= 1
iteration= 1	 chi2= 11.937747	 time= 1.0831e-05	 cumTime= 3.6201e-05	 edges= 7	 schur= 0	 lambda= 4444449.570664	 levenbergIter= 1
iteration= 2	 chi2= 11.934778	 time= 1.069e-05	 cumTime= 4.6891e-05	 edges= 7	 schur= 0	 lambda= 2962966.380443	 levenbergIter= 1
iteration= 3	 chi2= 11.930420	 time= 1.0512e-05	 cumTime= 5.7403e-05	 edges= 7	 schur= 0	 lambda= 1975310.920295	 levenbergIter= 1
iteration= 4	 chi2= 11.924066	 time= 1.0654e-05	 cumTime= 6.8057e-05	 edges= 7	 schur= 0	 lambda= 1213094.557098	 levenbergIter= 1
iteration= 5	 chi2= 11.914094	 time= 1.074e-05	 cumTime= 7.8797e-05	 edges= 7	 schur= 0	 lambda= 549789.382063	 levenbergIter= 1
iteration= 6	 chi2= 11.893194	 time= 1.048e-05	 cumTime= 8.9277e-05	 edges= 7	 schur= 0	 lambda= 183263.127354	 levenbergIter= 1
iteration= 7	 chi2= 11.834372	 time= 1.0341e-05	 cumTime= 9.9618e-05	 edges= 7	 schur= 0	 lambda= 61087.709118	 levenbergIter= 1
iteration= 8	 chi2= 11.669598	 time= 1.0288e-05	 cumTime= 0.000109906	 edges= 7	 schur= 0	 lambda= 20362.569706	 levenbergIter= 1
iteration= 9	 chi2= 11.239074	 time= 1.0601e-05	 cumTime= 0.000120507	 edges= 7	 schur= 0	 lambda= 6787.523235	 levenbergIter= 1
iteration= 10	 chi2= 10.292144	 time= 1.0354e-05	 cumTime= 0.000130861	 edges= 7	 schur= 0	 lambda= 2262.507745	 levenbergIter= 1
iteration= 11	 chi2= 8.562370	 time= 1.046e-05	 cumTime= 0.000141321	 edges= 7	 schur= 0	 lambda= 754.169248	 levenbergIter= 1
iteration= 12	 chi2= 5.795209	 time= 1.0682e-05	 cumTime= 0.000152003	 edges= 7	 schur= 0	 lambda= 251.389749	 levenbergIter= 1
iteration= 13	 chi2= 3.136536	 time= 1.0396e-05	 cumTime= 0.000162399	 edges= 7	 schur= 0	 lambda= 83.796583	 levenbergIter= 1
iteration= 14	 chi2= 2.276110	 time= 1.071e-05	 cumTime= 0.000173109	 edges= 7	 schur= 0	 lambda= 27.932194	 levenbergIter= 1
iteration= 15	 chi2= 2.216088	 time= 1.0856e-05	 cumTime= 0.000183965	 edges= 7	 schur= 0	 lambda= 9.310731	 levenbergIter= 1
iteration= 16	 chi2= 2.215426	 time= 1.0525e-05	 cumTime= 0.00019449	 edges= 7	 schur= 0	 lambda= 6.207154	 levenbergIter= 1
iteration= 17	 chi2= 2.215425	 time= 1.051e-05	 cumTime= 0.000205	 edges= 7	 schur= 0	 lambda= 4.138103	 levenbergIter= 1
iteration= 18	 chi2= 2.215425	 time= 1.0608e-05	 cumTime= 0.000215608	 edges= 7	 schur= 0	 lambda= 2.758735	 levenbergIter= 1
iteration= 19	 chi2= 2.215425	 time= 1.0528e-05	 cumTime= 0.000226136	 edges= 7	 schur= 0	 lambda= 1.839157	 levenbergIter= 1
I0118 16:15:22.336377 40077 gins_pre_integ.cc:240] chi2/error: 
I0118 16:15:22.336380 40077 gins_pre_integ.cc:241] preintegration: 0.00249247/05.70395e-08 -2.69427e-08 -6.17484e-07 -0.000170989 -9.10803e-05 09.26133e-07 -1.31586e-05 -6.98776e-06 07.65849e-08
I0118 16:15:22.336386 40077 gins_pre_integ.cc:243] gnss1: 0.0543654, 00.00120472 -0.00234341 00.00125066 000.0156347 -0.00404241 -0.00189687
I0118 16:15:22.336392 40077 gins_pre_integ.cc:244] bias: 0/0 0 0
I0118 16:15:22.336395 40077 gins_pre_integ.cc:245] prior: 1.69277/000.00167301 0-0.00260842 00-0.0103468 -7.47095e-05 00.000265061 00.000486168 000-0.128167 00-0.0195928 00.000358022 -1.37342e-07 0001.857e-07 06.22816e-07 01.71906e-06 09.29327e-07 00-9.148e-09
I0118 16:15:22.336407 40077 gins_pre_integ.cc:247] body vel: 0000.612737 000.0214819 -0.00161771
I0118 16:15:22.336410 40077 gins_pre_integ.cc:248] meas: 0.584907 00000000 00000000
I0118 16:15:22.336413 40077 gins_pre_integ.cc:249] odom: 0.405799/00000.031448 000.00505169 -1.74854e-05

         实时运行结果:

        3.3 总结

        3.3.1 图优化和 ESKF 的区别

  • 1. 相比 ESKF,基于预积分的图优化方案可以累积 IMU 读数。累积多少时间,或者每次迭代优化取多少次,都可以人为选择。而 ESKF 默认只能迭代一次,预测也只依据单个时刻的IMU 数据。
  • 2. 预积分边(或者用因子图优化的方法,称 IMU 因子或预积分因子)是一个很灵活的因子。它关联的六个顶点都可以发生变化。为了保持状态不发生随意改变,预积分因子通常要配合其他因子一起使用。在我们的案例中,两端的 GNSS 因子可以限制位姿的变化,ODOM 因子可以限制速度的改变,两个零偏因子会限制零偏的变化量,但不限制零偏的绝对值
  • 3. 先验因子会让整个估计变得更平滑。严格来说,先验因子的协方差矩阵还需要使用边缘化来操作。因为本章主要介绍预积分原理,所以我们给先验因子设定了固定大小的信息矩阵,来简化程序中的一些实现。本书的第 8 章中,我们会谈论先验因子信息矩阵的设定和代码实现方式。读者可以尝试去除本因子,看看轨迹估计会产生什么影响。
  • 4. 图优化让我们很方便地设置核函数,回顾各个因子占据的误差大小,进而确定优化过程主要受哪一部分影响。例如,我们可以分析正常情况下 RTK 观测应该产生多少残差,而异常情况下应该产生多少残差,从而确定 GNSS 是否给出了正确的位姿读数。后面我们还会向读者介绍如何来控制优化流程以实现更鲁棒的效果。读者可以打开本程序的调试输出,查看这些信息。
  • 5. 由于引入了更多计算,图优化的耗时明显要高于滤波器方案。不过,智能汽车的算力相比以往有了明显的增加,目前图优化在一些实时计算里也可以很好地使用了。

        3.3.2 总结

        本章介绍了预积分的基本原理,包括它的观测模型、噪声模型、雅可比推导方式以及针对零偏的处理方式。读者在实践当中也可以灵活应用预积分:

  • 不考虑优化,那么预积分和直接积分完全等同;预积分可以用于预测后续状态
  • 用于优化时,预积分可以方便地建模两帧间的相对运动。如果固定 IMU 零偏,还可以大幅简化预积分模型。如果考虑零偏,那么需要针对零偏的更新,来更新预积分的观测;
  • 预积分模型可以很容易地与其他图优化模型进行融合,在同一个问题中进行优化。也可以很方便地设置积分时间、优化帧数等参数,相比于滤波器方案更加自由。        

相关文章:

《自动驾驶与机器人中的SLAM技术》ch4:基于预积分和图优化的 GINS

前言&#xff1a;预积分图优化的结构 1 预积分的图优化顶点 这里使用 《自动驾驶与机器人中的SLAM技术》ch4&#xff1a;预积分学 中提到的散装的形式来实现预积分的顶点部分&#xff0c;所以每个状态被分为位姿&#xff08;&#xff09;、速度、陀螺零偏、加计零偏四种顶点&am…...

「2024·我的成长之路」:年终反思与展望

文章目录 1. 前言2.创作历程2.1 摆烂期2.2 转变期3. 上升期 2. 个人收获3.经验分享4. 展望未来 1. 前言 2025年1月16日&#xff0c;2024年博客之星入围公布&#xff0c;很荣幸获得了这次入围的机会。2024年对我个人是里程碑的一年&#xff0c;是意义非凡的一年&#xff0c;是充…...

P8738 [蓝桥杯 2020 国 C] 天干地支

两种方法 #include<bits/stdc.h> using namespace std;int main(){int year;cin>>year;string tg[10] {"geng", "xin", "ren", "gui","jia", "yi", "bing", "ding", "wu&…...

Linux网络 TCP socket

TCP简介 TCP&#xff08;Transmission Control Protocol&#xff09;是一种面向连接的、可靠的、基于字节流的传输层通信协议。它位于OSI模型的第四层&#xff0c;主要为应用层提供数据传输服务。TCP通过三次握手建立连接&#xff0c;确保数据在发送和接收过程中的准确性和顺序…...

IntelliJ IDEA 2023.3 中配置 Spring Boot 项目的热加载

IntelliJ IDEA 2023.3 中配置 Spring Boot 项目的热加载 在 IntelliJ IDEA 2023.3 中配置 Spring Boot 项目的热加载&#xff0c;可以让你在不重启应用的情况下看到代码修改的效果。以下是详细的配置步骤&#xff1a; 添加 spring-boot-devtools 依赖 在 pom.xml 文件中添加 …...

【网络协议】【http】【https】RSA+AES-TLS1.2

【网络协议】【http】【https】RSAAES-TLS1.2 https并不是一个协议 而是在传输层之间添加了SSL/TLS协议 TLS 协议用于应用层协议&#xff08;如 HTTP&#xff09;和传输层&#xff08;如 TCP&#xff09;之间&#xff0c;增加了一层安全性来解决 HTTP 存在的问题&#xff0c;H…...

Java中如何安全地停止线程?

大家好&#xff0c;我是锋哥。今天分享关于【Java中如何安全地停止线程?】面试题。希望对大家有帮助&#xff1b; Java中如何安全地停止线程? 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 在Java中&#xff0c;安全地停止线程是一项重要的任务&#xff0c;尤其…...

01.04、回文排序

01.04、[简单] 回文排序 1、题目描述 给定一个字符串&#xff0c;编写一个函数判定其是否为某个回文串的排列之一。回文串是指正反两个方向都一样的单词或短语。排列是指字母的重新排列。回文串不一定是字典当中的单词。 2、解题思路 回文串的特点&#xff1a; 一个回文串在…...

[深度学习]多层神经网络

多层神经网络 文章目录 多层神经网络单个神经元人类大脑神经与神经元神经元与矩阵神经元的串联激活函数激活函数位置神经网络的三种表现形式神经网络的参数&#xff08;可训练的&#xff09; 深度学习的训练过程全连接网络过拟合和欠拟合 单个神经元 一个神经元实际表示的数据公…...

JavaScript语言的正则表达式

JavaScript语言的正则表达式详解 正则表达式&#xff08;Regular Expression&#xff0c;简称Regex或RegExp&#xff09;是一种强大的文本处理工具&#xff0c;可以在字符串中执行模式匹配和替换操作。在JavaScript中&#xff0c;正则表达式是处理字符串时不可或缺的部分&…...

yolo系列模型为什么坚持使用CNN网络?

在深度学习领域&#xff0c;目标检测是一项至关重要的任务&#xff0c;而YOLO&#xff08;You Only Look Once&#xff09;系列模型无疑是这一领域的佼佼者。YOLO以其高效、准确的特点&#xff0c;在实时目标检测任务中占据了重要地位。然而&#xff0c;随着Transformer模型在自…...

Kotlin语言的数据结构

Kotlin语言的数据结构导论 Kotlin是一种现代化的编程语言&#xff0c;具有简洁、安全和高效的特点。Kotlin不仅支持面向对象编程&#xff0c;还融入了函数式编程的概念&#xff0c;使得开发者能够以更优雅的方式处理数据。在构建复杂应用时&#xff0c;数据结构的选择及其实现…...

光纤接口、GTX高速收发器基础知识学习、光口眼图测试--FPGA学习笔记28

----素材来源原子哥 一、光纤接口简介 光纤接口是用来连接光纤线缆的物理接口&#xff0c;简称为光口。其原理是利用了光从光密介质进入光疏介质从而发生了全反射。通常有 FC、 SC、 ST、 LC、 D4、 DIN、 MU、 MT 等等各种形式接口。 &#xff08;1&#xff09; SC 型光纤接…...

【k8s】k8s部署Argo CD

1、创建 Argo CD 命名空间&#xff1a; 先创建一个专用的命名空间 argocd 用于部署 Argo CD。 kubectl create namespace argocd 2、安装 Argo CD&#xff1a; 使用 kubectl 从 Argo CD 官方 GitHub 仓库安装它。运行以下命令来安装所有的 Argo CD 组件&#xff1a; kubectl a…...

PHP礼品兑换系统小程序

&#x1f381; 礼品兑换系统&#xff1a;革新企业礼品管理&#xff0c;专属神器来袭&#xff01; &#x1f4bb; 一款专为追求高效与个性化的现代企业量身打造的礼品兑换系统&#xff0c;它基于强大的ThinkPHP框架与前沿的Uniapp技术栈深度融合&#xff0c;不仅完美适配礼品卡…...

【SSH端口转发:实现安全的远程端口映射】

SSH端口转发&#xff1a;实现安全的远程端口映射 在网络应用开发和运维过程中&#xff0c;我们经常需要进行端口转发来实现各种网络访问需求。今天我要分享一个使用SSH进行端口转发的实用脚本&#xff0c;并详细讲解其工作原理。 脚本内容 免密 ssh-copy-id -p 20080 rootxx…...

2024年第十五届蓝桥杯青少组国赛(c++)真题—快速分解质因数

快速分解质因数 完整题目和在线测评可点击下方链接前往&#xff1a; 快速分解质因数_C_少儿编程题库学习中心-嗨信奥https://www.hixinao.com/tiku/cpp/show-3781.htmlhttps://www.hixinao.com/tiku/cpp/show-3781.html 若如其他赛事真题可自行前往题库中心查找&#xff0c;题…...

为什么你的 Qt 应用程序会出现 xcb 插件错误

有朋友咨询为什么他们的 Qt 应用程序在统信 UOS ARM 版本下运行&#xff0c;提示如下错误&#xff1a; qt.qpa.plugin: Could not find the Qt platform plugin "xcb" in "" This application failed to start because no Qt platform plugin could be i…...

ANSYS HFSS 中的相控天线阵列仿真方法

概述 相控天线阵列系统广泛使用&#xff0c;从国防雷达应用到商业 5G 应用。设计这些天线阵列涉及复杂的数学运算&#xff0c;需要全波仿真。Ansys HFSS 全场 3D 电磁仿真软件可以在合理的时间内以较低的计算成本仿真复杂的相控阵天线系统&#xff0c;同时考虑复杂激励、环境&…...

【记录】Jenkins版本及JDK关系介绍的官网地址

Redhat Jenkins Packages...

66,【6】buuctf web [HarekazeCTF2019]Avatar Uploader 1

进入靶场 习惯性输入admin 还想用桌面上的123.png 发现不行 看看给的源码 <?php // 关闭错误报告&#xff0c;可能会隐藏一些错误信息&#xff0c;在开发阶段可考虑开启&#xff08;例如 error_reporting(E_ALL)&#xff09; error_reporting(0); // 引入配置文件&#x…...

MECD+: 视频推理中事件级因果图推理--VLM长视频因果推理

论文链接&#xff1a;https://arxiv.org/pdf/2501.07227v1 1. 摘要及主要贡献点 摘要&#xff1a; 视频因果推理旨在从因果角度对视频内容进行高层次的理解。然而&#xff0c;目前的研究存在局限性&#xff0c;主要表现为以问答范式执行&#xff0c;关注包含孤立事件和基本因…...

pycharm+pyside6+desinger实现查询汉字笔顺GIF动图

一、引言 这学期儿子语文期末考试有一道这样的题目&#xff1a; 这道题答案是B&#xff0c;儿子做错了选了C。我告诉他“车字旁”和“车”的笔顺是不一样的&#xff0c;因为二者有一个笔画是不一样的&#xff0c;“车字旁”下边那笔是“提”&#xff0c;而“车”字是“横”&am…...

拟合算法 (matlab工具箱)

拟合算法&#xff1a; 1线性最小二乘法拟合 使用matlab进行求解 拟合优度&#xff1a;R^2 拟合优度的matlab代码&#xff1a; 2,Matlab工具箱的教学 一些函数: 拟合算法&#xff1a; 插值算法中&#xff0c;得到的多项式f(x)要经过所有样本点。但是如果样本点太多&#…...

联想电脑怎么用u盘装系统_联想电脑用u盘装win10系统教程

联想电脑怎么重装系统&#xff1f;在当今科技发展迅猛的时代&#xff0c;联想电脑已经成为了人们生活中不可或缺的一部分。然而&#xff0c;随着时间的推移&#xff0c;我们可能会遇到一些问题&#xff0c;例如系统崩溃或者需要更换操作系统。这时&#xff0c;使用U盘来重新安装…...

WPF2-在xaml为对象的属性赋值

1. AttributeValue方式 1.1. 简单属性赋值1.2. 对象属性赋值 2. 属性标签的方式给属性赋值3. 标签扩展 (Markup Extensions) 3.1. StaticResource3.2. Binding 3.2.1. 普通 Binding3.2.2. ElementName Binding3.2.3. RelativeSource Binding3.2.4. StaticResource Binding (带参…...

什么是报文的大端和小端,有没有什么记忆口诀?

在计算机科学中&#xff0c;**大端&#xff08;Big-Endian&#xff09;和小端&#xff08;Little-Endian&#xff09;**是两种不同的字节序&#xff08;即多字节数据在内存中的存储顺序&#xff09;。理解这两种字节序对于网络通信、文件格式解析以及跨平台编程等非常重要。 1…...

【2024 博客之星评选】请继续保持Passion

我尝试复盘自己2024年走的路&#xff0c;希望能给诸君一些借鉴。 文章目录 回头望感想与收获成长与教训今年计划感恩一些体己话 回头望 回望我的2024年&#xff0c;年初拿高绩效&#xff0c;但感觉逐渐被公司一点点剥离出中心&#xff1b;年中一直在学习防患于未然&#xff1b…...

网络通信---MCU移植LWIP

使用的MCU型号为STM32F429IGT6&#xff0c;PHY为LAN7820A 目标是通过MCU的ETH给LWIP提供输入输出从而实现基本的Ping应答 OK废话不多说我们直接开始 下载源码 LWIP包源码&#xff1a;lwip源码 -在这里下载 ST官方支持的ETH包&#xff1a;ST-ETH支持包 这里下载 创建工程 …...

Redis源码-redisObject

解释 redis中&#xff0c;所有的数据类型最终都转换成了redisObject&#xff0c;该结构体的定义&#xff0c;在文件server.h中。 参数说明 参数名说明unsigned type:4对象对应的数据类型unsigned encoding:4对象的编码方式unsigned lru:LRU_BITSLRU算法清空对象&#xff0c…...

YOLOv10-1.1部分代码阅读笔记-tuner.py

tuner.py ultralytics\engine\tuner.py 目录 tuner.py 1.所需的库和模块 2.class Tuner: 1.所需的库和模块 # Ultralytics YOLO &#x1f680;, AGPL-3.0 license# 此模块提供用于对象检测、实例分割、图像分类、姿势估计和多对象跟踪的 Ultralytics YOLO 模型的超参数调…...

【数据结构】二分查找

&#x1f6a9; WRITE IN FRONT &#x1f6a9; &#x1f50e; 介绍&#xff1a;"謓泽"正在路上朝着"攻城狮"方向"前进四" &#x1f50e;&#x1f3c5; 荣誉&#xff1a;2021|2022年度博客之星物联网与嵌入式开发TOP5|TOP4、2021|2222年获评…...

iOS-支付相关

支付宝支付 #import <AlipaySDK/AlipaySDK.h> //orderStrAliPay为服务端传的订单信息 //fromScheme为应用配置的schemeUrl标识&#xff0c;用户支付包支付成功后跳转会本应用内 //callback回调需要在- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url 中调…...

ubuntu16.04 VSCode下cmake+clang+lldb调试c++

VSCode下cmakeclanglldb调试c Ubuntu16.04 安装OpenCV4.5.4 文章目录 VSCode下cmakeclanglldb调试c1.安装clangclangdcmake2、打开VSCode&#xff0c;安装扩展插件3、编译4、Debug4.1 创建launch.json。4.2 配置setting.json 5. vscode安装配置clang-format插件5.1 Linux系统安…...

学Python的人…

学Python的人… 一、Python能干什么&#xff1f; 1.爬虫&#xff1a;前几年&#xff0c;深度学习还没发展起来的时候&#xff0c;书店里Python就和爬虫挂钩&#xff0c;因为Python写爬虫确实方便。 2.数据分析&#xff1a;Python有各种的数据分析库可以方便使用&#xff0…...

GDB相比IDE有什么优点

GDB(GNU Debugger)相比于集成开发环境(IDE)具有一些独特的优点,主要体现在其灵活性、可定制性和低级控制能力。具体来说,GDB有以下几个优点: 1. 轻量级且无依赖 GDB是一个命令行工具,不依赖于任何复杂的图形界面或大型库,这使得它非常适合在资源受限的环境中使用,比…...

Docker 镜像加速的配置

解决拉取镜像报错&#xff1a;Error response from daemon: Get "https://registry-1.docker.io/v2/": net/http: request canceled while 在使用 Docker 过程中&#xff0c;拉取镜像的速度常常会受到网络状况的影响&#xff0c;尤其是在国内网络环境下&#xff0c;…...

分布式多卡训练(DDP)踩坑

多卡训练最近在跑yolov10版本的RT-DETR&#xff0c;用来进行目标检测。 单卡训练语句&#xff08;正常运行&#xff09;&#xff1a; python main.py多卡训练语句&#xff1a; 需要通过torch.distributed.launch来启动&#xff0c;一般是单节点&#xff0c;其中CUDA_VISIBLE…...

MFC程序设计(一)MFC入门

本MFC教程使用VS2022实现 MFC基本概念 微软基础类库&#xff08;英语&#xff1a;Microsoft Foundation Classes&#xff0c;简称MFC&#xff09;是一个微软公司提供的类库&#xff08;class libraries&#xff09;&#xff0c;以C类的形式封装了Windows API&#xff0c;并且…...

swift Actor并发处理

preconcurrency 使用 /*在 Swift 5.5 引入并发模型后&#xff0c;编译器会对潜在的数据竞争和不安全的并发代码发出警告或错误。然而&#xff0c;某些旧代码或第三方库可能尚未完全适配这些新规则。preconcurrency 提供了一种临时解决方案&#xff0c;允许你在不修改代码的情况…...

网络编程 | UDP套接字通信及编程实现经验教程

1、UDP基础 传输层主要应用的协议模型有两种&#xff0c;一种是TCP协议&#xff0c;另外一种则是UDP协议。在上一篇博客文章中&#xff0c;已经对TCP协议及如何编程实现进行了详细的梳理讲解&#xff0c;在本文中&#xff0c;主要讲解与TCP一样广泛使用了另一种协议&#xff1a…...

Hadoop•搭建完全分布式集群

听说这里是目录哦 一、安装Hadoop&#x1f955;二、配置Hadoop系统环境变量&#x1f96e;三、验证Hadoop系统环境变量是否配置成功&#x1f9c1;四、修改Hadoop配置文件&#x1f36d;五、分发Hadoop安装目录&#x1f9cb;六、分发系统环境变量文件&#x1f368;七、格式化HDFS文…...

代码中使用 Iterable<T> 作为方法参数的解释

/*** 根据课程 id 集合查询课程简单信息* param ids id 集合* return 课程简单信息的列表*/ GetMapping("/courses/simpleInfo/list") List<CourseSimpleInfoDTO> getSimpleInfoList(RequestParam("ids") Iterable<Long> ids); 一、代码解释&…...

web前端1--基础

&#xff08;时隔数月我又来写笔记啦~&#xff09; 1、下载vscode 1、官网下载&#xff1a;Visual Studio Code - Code Editing. Redefined 2、步骤&#xff1a; 1、点击同意 一直下一步 勾一个创建桌面快捷方式 在一直下一步 2、在桌面新建文件夹 拖到vscode图标上 打开v…...

关于opensips的帮助命令的解释

opensips -help以下是 opensips 命令及其选项的中文解释&#xff08;基于 3.6.0-dev 版本&#xff09;&#xff1a; 命令用法 opensips -l 地址 [-l 地址 ...] [选项]选项说明 选项功能-f 文件指定配置文件&#xff08;默认为 /usr/local//etc/opensips/opensips.cfg&#x…...

你还在用idea吗

从VIM、Emacs&#xff0c;到eclipse、Jetbrains, 再到VSCode&#xff0c;过去的三十年时间&#xff0c;出现了这三代IDE产品。现在属于AI的时代来了&#xff0c;最新一代的产品像Cursor、Windsurf&#xff0c;就在昨天&#xff0c;字节跳动发布了最新的IDE&#xff0c;就叫Trae…...

安装wxFormBuilder

1. 网址&#xff1a;GitHub - wxFormBuilder/wxFormBuilder: A wxWidgets GUI Builder 2. 安装MSYS2 MSYS2可以在GitHub的内容中找到&#xff0c;这个版本是32位64位的 3. 在程序中打开MINGW64 shell 4. 在MSYS2 MINGW64 shell中输入 pacman -Syu pacman -S ${MINGW_PACKAGE…...

【大数据2025】Hadoop 万字讲解

文章目录 一、大数据通识大数据诞生背景与基本概念大数据技术定义与特征大数据生态架构概述数据存储数据计算与易用性框架分布式协调服务和任务调度组件数仓架构流处理架构 二、HDFSHDFS 原理总结一、系统架构二、存储机制三、数据写入流程四、心跳机制与集群管理 安全模式&…...

HTML语言的计算机基础

HTML语言的计算机基础 引言 在当今信息技术迅猛发展的时代&#xff0c;网页设计和开发已成为计算机科学中不可或缺的一部分。而HTML&#xff08;超文本标记语言&#xff09;作为构建网页的基础语言&#xff0c;承载着网页上所有内容的结构&#xff0c;帮助开发者创建和展示信…...

Cannot resolve symbol ‘XXX‘ Maven 依赖问题的解决过程

一、问题描述 在使用 Maven 管理项目依赖时&#xff0c;遇到了一个棘手的问题。具体表现为&#xff1a;在 pom.xml 文件中导入了所需的依赖&#xff0c;并且在 IDE 中导入语句没有显示为红色&#xff08;表示 IDE 没有提示依赖缺失&#xff09;&#xff0c;但是在实际使用这些依…...