IMU 积分进行航迹推算

1.0 递推方程推导

LSTM 做航迹预测时用啥激活函数_世界坐标系连续时间内的 IMU 运动学方程:
LSTM 做航迹预测时用啥激活函数_SLAM_02
LSTM 做航迹预测时用啥激活函数_世界坐标系这些物理量带上角标之后应该写作 LSTM 做航迹预测时用啥激活函数_SLAM_04,对应世界坐标系,它在求导之后就是车辆在世界坐标系下的速度与加速度 LSTM 做航迹预测时用啥激活函数_SLAM_05 。在不考虑地球自传的时候,也可以简单的将 车辆行驶的打的视为固定的世界坐标系,这时 IMU 的测量值 LSTM 做航迹预测时用啥激活函数_VIO_06 就是车辆本身的角速度,以及车体系下的加速度:
LSTM 做航迹预测时用啥激活函数_IMU_07
LSTM 做航迹预测时用啥激活函数_世界坐标系注意 LSTM 做航迹预测时用啥激活函数_世界坐标系_09 带下标之后就是 LSTM 做航迹预测时用啥激活函数_SLAM_10。它将世界系下的物理量转换到车体系。然而,实际的车辆、机器人都在地球表面运行。这些系统受到重力的影响,所以我们应该把重 力写在系统方程中。在绝大多数 IMU 系统中,我们可以忽略地球自转的干扰,从而把 IMU 测量 值写为:
LSTM 做航迹预测时用啥激活函数_SLAM_11
LSTM 做航迹预测时用啥激活函数_世界坐标系LSTM 做航迹预测时用啥激活函数_IMU_13 为地球的重力。当然,如果在无重力环境下测量物体加速度,就不会出现重力项。注意这里 LSTM 做航迹预测时用啥激活函数_IMU_13的符号和坐标系定义相关。我们的车体系和世界系都是 LSTM 做航迹预测时用啥激活函数_IMU_15 轴向上,于是 LSTM 做航迹预测时用啥激活函数_IMU_13 通常取 值 LSTM 做航迹预测时用啥激活函数_SLAM_17。假设有一个水平放置的IMU,其读数此时应当为 LSTM 做航迹预测时用啥激活函数_IMU_18,为什么呢?因为此时真正的加速度应该为 LSTM 做航迹预测时用啥激活函数_LSTM 做航迹预测时用啥激活函数_19,但是由于地球重力的影响,其输出结果会减去 LSTM 做航迹预测时用啥激活函数_IMU_13 ,所以输出结果就是LSTM 做航迹预测时用啥激活函数_IMU_18

LSTM 做航迹预测时用啥激活函数_世界坐标系在大多数系统中,我们认为 IMU 的噪声由两部分组成:测量噪声(measurement noise)与零偏(bias)。记陀螺仪和加速度计的测量噪声分别为 LSTM 做航迹预测时用啥激活函数_LSTM 做航迹预测时用啥激活函数_23, LSTM 做航迹预测时用啥激活函数_SLAM_24,同时记零偏为 LSTM 做航迹预测时用啥激活函数_LSTM 做航迹预测时用啥激活函数_25, LSTM 做航迹预测时用啥激活函数_LSTM 做航迹预测时用啥激活函数_26,下标 LSTM 做航迹预测时用啥激活函数_IMU_27 表示陀螺仪,LSTM 做航迹预测时用啥激活函数_SLAM_28 表示加速度计。那么这几个参数在测量方程中体现为:
LSTM 做航迹预测时用啥激活函数_LSTM 做航迹预测时用啥激活函数_29
LSTM 做航迹预测时用啥激活函数_世界坐标系于是,我们直接把测量模型代入运动学方程,忽略测量噪声影响,即可得到连续时间下的积分 模型:
LSTM 做航迹预测时用啥激活函数_SLAM_31
LSTM 做航迹预测时用啥激活函数_世界坐标系有时候我们也把 LSTM 做航迹预测时用啥激活函数_IMU_33 称为 LSTM 做航迹预测时用啥激活函数_世界坐标系_34 状态。该方程可以从时间 LSTM 做航迹预测时用啥激活函数_IMU_35积分至 LSTM 做航迹预测时用啥激活函数_SLAM_36,推出下一个时刻的状态情况:
LSTM 做航迹预测时用啥激活函数_世界坐标系_37
LSTM 做航迹预测时用啥激活函数_世界坐标系这里我们先不考虑白噪声 LSTM 做航迹预测时用啥激活函数_VIO_39,则IMU的测量方程有:
LSTM 做航迹预测时用啥激活函数_世界坐标系_40
LSTM 做航迹预测时用啥激活函数_世界坐标系速度的递推,我们知道 LSTM 做航迹预测时用啥激活函数_世界坐标系_42
LSTM 做航迹预测时用啥激活函数_LSTM 做航迹预测时用啥激活函数_43
LSTM 做航迹预测时用啥激活函数_世界坐标系位置的递推,我们知道 LSTM 做航迹预测时用啥激活函数_LSTM 做航迹预测时用啥激活函数_45,则有:
LSTM 做航迹预测时用啥激活函数_VIO_46

2.0 代码实现

2.1 数据集介绍

这里使用的高博书中带的数据集,数据集的格式为:

# timestamp gx gy gz ax ay az

2.2 具体代码实现

代码实现主要有三个文件

  • common.hpp 主要用户存放 IMU 数据结构体和读取和保存数据。
  • imu_integration.hpp 主要存放 IMU数据的处理和航迹推算实现类。
  • run_imu_integration.cpp 程序入口函数。

如下命令运行

./run_imu_integration --txt_file_path="../slam_in_auto_driving/chapter3/dataset/10.txt" --output_inter_trajectory_path="./output_trajectory.txt"

  • common.hpp
#ifndef COMMON_HPP
#define COMMON_HPP

#include <eigen3/Eigen/Core>
#include <eigen3/Eigen/Dense>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <string>
#include <vector>

struct IMUMsg
{
  IMUMsg() = default;
  IMUMsg(double timestamp, Eigen::Vector3d gyro, Eigen::Vector3d acc)
      : timestamp_(timestamp), acc_(acc), gyro_(gyro){};

  double timestamp_{0.0};
  Eigen::Vector3d acc_;
  Eigen::Vector3d gyro_;
};

struct IMUIntegrationResult
{
  IMUIntegrationResult() = default;
  IMUIntegrationResult(const double ×tamp, const Eigen::Vector3d &P,
                       const Eigen::Quaterniond &Q, const Eigen::Vector3d &V)
      : timestamp_(timestamp), P_(P), V_(V), Q_(Q){};

  double timestamp_{0.0};
  Eigen::Vector3d P_;
  Eigen::Vector3d V_;
  Eigen::Quaterniond Q_;
};

inline void ReadImuMsg(std::ifstream &fin, std::vector<IMUMsg> &imu_msg)
{
  if (!fin)
  {
    std::cerr << "Coule not find file\n";
    return;
  }
  while (!fin.eof())
  {
    std::string line;
    std::getline(fin, line);
    if (line.empty())
    {
      continue;
    }

    if (line[0] == '#')
    {
      continue;
    }

    std::stringstream ss;
    ss << line;
    std::string data_type;
    ss >> data_type;
    if (data_type == "IMU")
    {
      double time, gx, gy, gz, ax, ay, az;
      ss >> time >> gx >> gy >> gz >> ax >> ay >> az;
      imu_msg.push_back(IMUMsg(time, Eigen::Vector3d(gx, gy, gz),
                               Eigen::Vector3d(ax, ay, az)));
    }
  }
  std::cout << "Read IMU msgs success\n";
}

inline void SaveImuIntegrationResult(
    const std::string &file_path,
    const std::vector<IMUIntegrationResult> &imu_inte_result)
{
  std::ofstream fout(file_path);
  for (const auto &imu_traj : imu_inte_result)
  {
    fout << std::setprecision(18) << imu_traj.timestamp_ << " " << std::setprecision(9);
    fout << imu_traj.P_(0) << " " << imu_traj.P_(1) << " " << imu_traj.P_(2) << " ";
    fout << imu_traj.Q_.w() << " " << imu_traj.Q_.x() << " " << imu_traj.Q_.y() << " " << imu_traj.Q_.z() << " ";
    fout << imu_traj.V_(0) << " " << imu_traj.V_(1) << " " << imu_traj.V_(2) << " ";
    fout << std::endl;
  }
}

#endif  // COMMON_HPP
  • imu_integration.hpp
#include <eigen3/Eigen/Core>
#include <eigen3/Eigen/Dense>
#include <sophus/so3.hpp>

#include "common.hpp"

class ImuIntegration
{
 public:
  ImuIntegration() = default;
  ~ImuIntegration() = default;
  ImuIntegration(const Eigen::Vector3d &gravity, const Eigen::Vector3d &init_bg,
                 const Eigen::Vector3d &init_ba)
      : gravity_(gravity), init_ba_(init_ba), init_bg_(init_bg)
  {
  }

  void AddNewImgMessage(const IMUMsg &imu_msg)
  {
    // Final P: -3.38794e+06  5.73752e+06  -512933
    // 其实第一帧 IMU 数据也可以不判断,因为后面有 dt<0.1 的判断
    if (first_imu_)
    {
      first_imu_ = false;
      timestamp_ = imu_msg.timestamp_;
    }

    double dt = imu_msg.timestamp_ - timestamp_;

    if (dt > 0 && dt < 0.1)
    {
      P_ = P_ + V_ * dt + 0.5 * (R_ * (imu_msg.acc_ - init_ba_)) * dt * dt +
           0.5 * gravity_ * dt * dt;
      V_ = V_ + R_ * (imu_msg.acc_ - init_ba_) * dt + gravity_ * dt;
      R_ = R_ * Sophus::SO3d::exp((imu_msg.gyro_ - init_bg_) * dt);
    }

    timestamp_ = imu_msg.timestamp_;
  }

  Eigen::Vector3d GetPosition() const { return P_; }
  Eigen::Vector3d GetVelocity() const { return V_; }
  Eigen::Quaterniond GetRotation() const { return R_.unit_quaternion(); }

 private:
  Sophus::SO3d R_;
  Eigen::Quaterniond R_quaternion_ = Eigen::Quaterniond::UnitRandom();
  Eigen::Vector3d P_ = Eigen::Vector3d::Zero();
  Eigen::Vector3d V_ = Eigen::Vector3d::Zero();
  Eigen::Vector3d gravity_ = Eigen::Vector3d(0, 0, -9.81);
  Eigen::Vector3d init_ba_ = Eigen::Vector3d::Zero();
  Eigen::Vector3d init_bg_ = Eigen::Vector3d::Zero();
  double timestamp_{0.0};
  bool first_imu_{true};
};
  • run_imu_integration.cpp
#include <gflags/gflags.h>

#include <eigen3/Eigen/Core>
#include <eigen3/Eigen/Dense>
#include <iostream>
#include <ostream>

#include "common.hpp"
#include "imu_integration.hpp"

DEFINE_string(txt_file_path, "../slam_in_auto_driving/chapter3/dataset/10.txt",
              "Imu integration file");
DEFINE_string(output_inter_trajectory_path, "./output_trajectory.txt",
              "output trajectory file");

int main(int argc, char *argv[])
{
  google::ParseCommandLineFlags(&argc, &argv, true);
  std::ifstream fin(FLAGS_txt_file_path);
  std::vector<IMUMsg> imu_msgs;
  std::vector<IMUIntegrationResult> imu_inter_result;
  ReadImuMsg(fin, imu_msgs);

  // 该实验中,我们假设零偏已知
  Eigen::Vector3d gravity(0, 0, -9.8);  // 重力方向
  Eigen::Vector3d init_bg(00.000224886, -7.61038e-05, -0.000742259);
  Eigen::Vector3d init_ba(-0.165205, 0.0926887, 0.0058049);

  ImuIntegration imu_integration(gravity, init_bg, init_ba);

  for (auto &imu_msg : imu_msgs)
  {
    imu_integration.AddNewImgMessage(imu_msg);
    imu_inter_result.push_back(IMUIntegrationResult(
        imu_msg.timestamp_, imu_integration.GetPosition(),
        imu_integration.GetRotation(), imu_integration.GetVelocity()));
  }

  SaveImuIntegrationResult(FLAGS_output_inter_trajectory_path,
                           imu_inter_result);
  // 高博书中程序输出的结果
  // T: 1624429630.2702086
  // P : -3387943.36 5737523.81 -512933.307
  // Q : 0.982857044 -0.132676506 0.0940114453 0.0868954789
  // V : -572.166705 4626.10758 -496.605214
  std::cout << "Final P: " << imu_integration.GetPosition().transpose()
            << std::endl;
  std::cout << "Final V: " << imu_integration.GetVelocity().transpose()
            << std::endl;
  std::cout << "Final Q: " << imu_integration.GetRotation().coeffs().transpose()
            << std::endl;

  return 0;
}

输出结果可视化:

LSTM 做航迹预测时用啥激活函数_VIO_47

可视化程序,运行:

python3 draw_imu_integration.py ./output_trajectory.txt

# coding=UTF-8
import sys
import numpy as np
import matplotlib.pyplot as plt

if __name__ == "__main__":
    if len(sys.argv) != 2:
        print('Please input valid file')
        exit(1)
    else:
        path = sys.argv[1]
        path_data = np.loadtxt(path)
        plt.rcParams['figure.figsize'] = (16.0, 12.0)
        
        # 轨迹
        plt.subplot(121)
        plt.scatter(path_data[:, 1], path_data[:, 2], s=2)
        plt.xlabel('X')
        plt.ylabel('Y')
        plt.grid()
        plt.title('2D trajectory')
        
        # 姿态
        plt.subplot(222)
        plt.plot(path_data[:, 0], path_data[:, 4], 'r')
        plt.plot(path_data[:, 0], path_data[:, 5], 'g')
        plt.plot(path_data[:, 0], path_data[:, 6], 'b')
        plt.plot(path_data[:, 0], path_data[:, 7], 'k')
        plt.title('q')
        plt.legend(['qx', 'qy', 'qz', 'qw'])

        # 速度
        plt.subplot(224)
        plt.plot(path_data[:, 0], path_data[:, 8], 'r')
        plt.plot(path_data[:, 0], path_data[:, 9], 'g')
        plt.plot(path_data[:, 0], path_data[:, 10], 'b')
        plt.title('v')
        plt.legend(['vx', 'vy', 'vz'])

        plt.show()
        exit(1)