Calibration and integration of imu’s stationary bias noise

The Imu used in the example is Lunqu Technology n100 mini. The coordinate system of the data output by imu is based on the ROS coordinate system.

Eigen::Quaterniond q_ahrs(ahrs_frame_.frame.data.data_pack.Qw,
                          ahrs_frame_.frame.data.data_pack.Qx,
                          ahrs_frame_.frame.data.data_pack.Qy,
                          ahrs_frame_.frame.data.data_pack.Qz);

Eigen::Quaterniond q_r =
    Eigen::AngleAxisd(PI, Eigen::Vector3d::UnitZ()) *
    Eigen::AngleAxisd(PI, Eigen::Vector3d::UnitY()) *
    Eigen::AngleAxisd(0.00000, Eigen::Vector3d::UnitX());

Eigen::Quaterniond q_rr =
    Eigen::AngleAxisd(0.00000, Eigen::Vector3d::UnitZ()) *
    Eigen::AngleAxisd(0.00000, Eigen::Vector3d::UnitY()) *
    Eigen::AngleAxisd(PI, Eigen::Vector3d::UnitX());

Eigen::Quaterniond q_out = q_r * q_ahrs * q_rr

Why do we need to right multiply q_rr?

Answer: You can remove *q_rr. After recompiling, you can see through TF conversion that the coordinates are actually upside down and need to be rotated 180° around the X axis to be the coordinate system used in our ROS.

The coordinate transformation of TF can be completed by just multiplying q_r to the left. However, the coordinate system we usually use in ros is the front left and upper left, so we have to correct the pose of the coordinate system by multiplying q_rr to the right.

Note: This IMU is a nine-axis IMU, integrated with a magnetometer, that is, the original yaw angle refers to north is 0 degrees, and the range is 0-360°, increasing clockwise. After converting to the coordinates under the imu single product ROS standard, the heading should be specially processed as the northeast sky coordinate system.

Input single frame Imu example

 seq: 90498
  stamp:
    secs: 1698127594
    nsecs: 155961838
  frame_id: "gyro_link"
orientation:
  x: 0.0049992394633591695
  y: 0.002799155190586991
  z: -0.1353550255298614
  w: -0.9907805919647217
orientation_covariance: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
angular_velocity:
  x: -0.0013670891057699919
  y: 0.0030183307826519012
  z: -0.002960443962365389
angular_velocity_covariance: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
linear_acceleration:
  x: 0.04501450061798096
  y: -0.09672745317220688
  z: 9.795211791992188
linear_acceleration_covariance: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]

imu_integration.h

#ifndef SLAM_IN_AUTO_DRIVING_IMU_INTEGRATION_H
#define SLAM_IN_AUTO_DRIVING_IMU_INTEGRATION_H

#include "eigen_types.h"
#include "imu.h"
#include "nav_state.h"

namespace sad {<!-- -->

/**
 * This program demonstration relies solely on the points of the IMU
 */
class IMUIntegration {<!-- -->
   public:
    IMUIntegration(const Vec3d & amp; gravity, const Vec3d & amp; init_bg, const Vec3d & amp; init_ba)
        : gravity_(gravity), bg_(init_bg), ba_(init_ba) {<!-- -->}

    // Increase imu reading
    void AddIMU(const IMU & amp; imu) {<!-- -->
        double dt = imu.timestamp_ - timestamp_;
        if (dt > 0 & amp; & amp; dt < 0.1)
        {<!-- -->
            // Assume that the IMU time interval is within 0 to 0.1
            p_ = p_ + v_ * dt + 0.5 * gravity_ * dt * dt + 0.5 * (R_ * (imu.acce_ - ba_)) * dt * dt;
            v_ = v_ + R_ * (imu.acce_ - ba_) * dt + gravity_ * dt;
            R_ = R_ * Sophus::SO3d::exp((imu.gyro_ - bg_) * dt);
        }
        //update time
        timestamp_ = imu.timestamp_;
    }

    /// Compose NavState
    NavStated GetNavState() const {<!-- --> return NavStated(timestamp_, R_, p_, v_, bg_, ba_); }

    SO3 GetR() const {<!-- --> return R_; }
    Vec3d GetV() const {<!-- --> return v_; }
    Vec3d GetP() const {<!-- --> return p_; }

   private:
    // Cumulative amount
    SO3 R_;
    Vec3d v_ = Vec3d::Zero();
    Vec3d p_ = Vec3d::Zero();

    double timestamp_ = 0.0;

    // Zero offset, set externally
    Vec3d bg_ = Vec3d::Zero();
    Vec3d ba_ = Vec3d::Zero();

    Vec3d gravity_ = Vec3d(0, 0, -9.8); // Gravity
};

} // namespace sad

#endif // SLAM_IN_AUTO_DRIVING_IMU_INTEGRATION_H

static_imu_init.h

#ifndef SLAM_IN_AUTO_DRIVING_STATIC_IMU_INIT_H
#define SLAM_IN_AUTO_DRIVING_STATIC_IMU_INIT_H

#include "eigen_types.h"
#include "imu.h"
#include "odom.h"

#include <deque>

namespace sad {<!-- -->

/**
 * IMU horizontal static state initializer
 * Usage: Call AddIMU, AddOdom to add data, and use InitSuccess to get whether the initialization is successful.
 * After success, use each Get function to obtain internal parameters
 *
 * The initializer attempts to initialize the system each time AddIMU is called. When there is odom, initialization requires that the odom wheel speed reading is close to zero; when there is no odom, it is assumed that the vehicle is initially stationary.
 * The initializer collects IMU readings over a period of time, estimates the initial zero bias and noise parameters according to section 3.5.4 of the book, and provides them to ESKF or other filters
 */
class StaticIMUInit {<!-- -->
   public:
    struct Options
    {<!-- -->
        Options() {<!-- -->}
        double init_time_seconds_ = 10.0; // inactivity time
        int init_imu_queue_max_size_ = 2000; // Initialize the maximum length of the IMU queue
        int static_odom_pulse_ = 5; // Wheel speedometer output noise when stationary
        double max_static_gyro_var = 0.5; // Static gyro measurement variance
        double max_static_acce_var = 0.05; // Static aggregation measurement variance
        double gravity_norm_ = 9.81; // Gravity size
        bool use_speed_for_static_checking_ = false; // Whether to use odom to determine if the vehicle is stationary (some data sets do not have the odom option)
    };

    /// Constructor
    StaticIMUInit(Options options = Options()) : options_(options) {<!-- -->}

    /// Add IMU data
    bool AddIMU(const IMU & amp; imu);
    /// Add wheel speed data
    bool AddOdom(const Odom & amp; odom);

    /// Determine whether initialization is successful
    bool InitSuccess() const {<!-- --> return init_success_; }

    /// Get each Cov, bias, gravity
    Vec3d GetCovGyro() const {<!-- --> return cov_gyro_; }
    Vec3d GetCovAcce() const {<!-- --> return cov_acce_; }
    Vec3d GetInitBg() const {<!-- --> return init_bg_; }
    Vec3d GetInitBa() const {<!-- --> return init_ba_; }
    Vec3d GetGravity() const {<!-- --> return gravity_; }

   private:
    /// Try to initialize the system
    bool TryInit();

    Options options_; // option information
    bool init_success_ = false; // Whether initialization is successful
    Vec3d cov_gyro_ = Vec3d::Zero(); // Gyro measurement noise covariance (evaluated during initialization)
    Vec3d cov_acce_ = Vec3d::Zero(); // Added measurement noise covariance (evaluated at initialization)
    Vec3d init_bg_ = Vec3d::Zero(); // gyro initial zero bias
    Vec3d init_ba_ = Vec3d::Zero(); // Add initial zero offset
    Vec3d gravity_ = Vec3d::Zero(); // Gravity
    bool is_static_ = false; // Indicates whether the vehicle is stationary
    std::deque<IMU> init_imu_deque_; // Data for initialization
    double current_time_ = 0.0; // current time
    double init_start_time_ = 0.0; // Initial time of inactivity
};

} // namespace sad

#endif // SLAM_IN_AUTO_DRIVING_STATIC_IMU_INIT_H

static_imu_init.cc

#include "math_utils.h"


namespace sad {

bool StaticIMUInit::AddIMU(const IMU & amp; imu)
{
    if (init_success_)
    {
        return true;
    }

    if (options_.use_speed_for_static_checking_ & amp; & amp; !is_static_)
    {
        LOG(WARNING) << "Wait for the vehicle to be stationary";
        init_imu_deque_.clear();
        return false;
    }

    if (init_imu_deque_.empty())
    {
        //Record initial rest time
        init_start_time_ = imu.timestamp_;
    }

    //Enter into the initialization queue
    init_imu_deque_.push_back(imu);

    double init_time = imu.timestamp_ - init_start_time_; // Initialization elapsed time
    if (init_time > options_.init_time_seconds_)
    {
        //Try initialization logic
        TryInit();
    }

    // Maintain initial queue length
    while (init_imu_deque_.size() > options_.init_imu_queue_max_size_)
    {
        init_imu_deque_.pop_front();
    }

    current_time_ = imu.timestamp_;
    return false;
}

bool StaticIMUInit::AddOdom(const Odom & amp; odom) {
    // Determine whether the vehicle is stationary
    if (init_success_) {
        return true;
    }

    if (odom.left_pulse_ < options_.static_odom_pulse_ & amp; & amp; odom.right_pulse_ < options_.static_odom_pulse_) {
        is_static_ = true;
    } else {
        is_static_ = false;
    }

    current_time_ = odom.timestamp_;
    return true;
}

bool StaticIMUInit::TryInit()
{
    if (init_imu_deque_.size() < 10) {
        return false;
    }

    // Calculate mean and variance
    Vec3d mean_gyro, mean_acce;
    math::ComputeMeanAndCovDiag(init_imu_deque_, mean_gyro, cov_gyro_, [](const IMU & amp; imu) { return imu.gyro_; });
    math::ComputeMeanAndCovDiag(init_imu_deque_, mean_acce, cov_acce_, [this](const IMU & amp; imu) { return imu.acce_; });

    // Take the ace mean as the direction, and take the length of 9.8 as the gravity
    LOG(INFO) << "mean acce: " << mean_acce.transpose();
    gravity_ = -mean_acce / mean_acce.norm() * options_.gravity_norm_;

    // Recalculate the covariance of the summation
    math::ComputeMeanAndCovDiag(init_imu_deque_, mean_acce, cov_acce_,
                                [this](const IMU & amp; imu) { return imu.acce_ + gravity_; });

    // Check IMU noise
    if (cov_gyro_.norm() > options_.max_static_gyro_var) {
        LOG(ERROR) << "The gyroscope measurement is too noisy" << cov_gyro_.norm() << " > " << options_.max_static_gyro_var;
        return false;
    }

    if (cov_acce_.norm() > options_.max_static_acce_var) {
        LOG(ERROR) << "The total measurement noise is too noisy" << cov_acce_.norm() << " > " << options_.max_static_acce_var;
        return false;
    }

    // Estimate measurement noise and bias
    init_bg_ = mean_gyro;
    init_ba_ = mean_acce;

    LOG(INFO) << "IMU initialization successful, initialization time = " << current_time_ - init_start_time_ << ", bg = " << init_bg_.transpose()
              << ", ba = " << init_ba_.transpose() << ", gyro sq = " << cov_gyro_.transpose()
              << ", acce sq = " << cov_acce_.transpose() << ", grav = " << gravity_.transpose()
              << ", norm: " << gravity_.norm();
    LOG(INFO) << "mean gyro: " << mean_gyro.transpose() << " acce: " << mean_acce.transpose();
    init_success_ = true;
    return true;
}

} // namespace sad

main.cpp

#include<ros/ros.h>
#include<imu_integration.h>
#include "sensor_msgs/Imu.h"
#include "tf/tf.h"
#include<nav_msgs/Odometry.h>
#include<static_imu_init.h>
// In this experiment, we assume that the zero bias is known
Vec3d gravity(-0.050622,0.102474,-9.809334); // Gravity direction
Vec3d init_bg(-0.005136,-0.000981,-0.003482);
Vec3d init_ba(-0.000118,0.000239,-0.022851);
sad::IMUIntegration imu_integ1(gravity, init_bg, init_ba);

ros::Publisher pub_;

sad::StaticIMUInit imu_init; // Use default configuration



sad::IMU imu_format(const sensor_msgs::Imu & amp;imu_msg)
{<!-- -->

    sad::IMU imu;
    imu.timestamp_=imu_msg.header.stamp.toSec();
    imu.acce_.x()=imu_msg.linear_acceleration.x;
    imu.acce_.y()=imu_msg.linear_acceleration.y;
    imu.acce_.z()=imu_msg.linear_acceleration.z;
    imu.gyro_.x()=imu_msg.angular_velocity.x;
    imu.gyro_.y()=imu_msg.angular_velocity.y;
    imu.gyro_.z()=imu_msg.angular_velocity.z;
    return imu;

}
void IMUCallback(const sensor_msgs::Imu & amp;imu_msg)
{<!-- -->
    sad::IMU imu_out=imu_format(imu_msg);

    /// IMU processing function
    if (!imu_init.InitSuccess())
    {<!-- -->
        imu_init.AddIMU(imu_out);
        return;
    }
    //imu_init.GetInitBg(), imu_init.GetInitBa(), imu_init.GetGravity()
    static bool imu_inited = false;

    if(!imu_inited)
    {<!-- -->
    
        ROS_INFO("bg:%f,%f,%f",imu_init.GetInitBg().x(),imu_init.GetInitBg().y(),imu_init.GetInitBg().z());
        ROS_INFO("ba:%f,%f,%f",imu_init.GetInitBa().x(),imu_init.GetInitBa().y(),imu_init.GetInitBa().z());
        ROS_INFO("g:%f,%f,%f",imu_init.GetGravity().x(),imu_init.GetGravity().y(),imu_init.GetGravity().z());
        imu_inited=true;
    }

    imu_integ1.AddIMU(imu_out);
    Eigen::Matrix3d R=imu_integ1.GetR().matrix();
    nav_msgs::Odometry odom;
    odom.header.frame_id="odom";
    odom.header.stamp=imu_msg.header.stamp;
    odom.child_frame_id="base_link";
    odom.pose.pose.position.x=imu_integ1.GetP().x();
    odom.pose.pose.position.y=imu_integ1.GetP().y();
    odom.pose.pose.position.z=imu_integ1.GetP().z();
    Eigen::Quaterniond q;
    q=R.block<3,3>(0,0);
    odom.pose.pose.orientation.x=q.x();
    odom.pose.pose.orientation.y=q.y();
    odom.pose.pose.orientation.z=q.z();
    odom.pose.pose.orientation.w=q.w();
    odom.twist.twist.linear.x=imu_integ1.GetV().x();
    odom.twist.twist.linear.y=imu_integ1.GetV().y();
    odom.twist.twist.linear.z=imu_integ1.GetV().z();
    pub_.publish(odom);
}

int main(int argc, char** argv)
{<!-- -->
    ros::init(argc, argv, "imu_integration");
    ros::NodeHandle n;
    ros::Subscriber sub = n.subscribe("/imu", 100, IMUCallback);
    pub_=n.advertise<nav_msgs::Odometry>("/imu_integration_odom",10);
    ros::spin();
    return 0;
}