1
$\begingroup$

I'm currently trying to estimate orientation from imu data under ros2 humble. I've created a ros2 node "imu_publisher" which get imu data, compute orientation using madwick filter and publish an imu message.

After converting the orientation output of madwick filter (quaternions) into euler angles, I can see rotations up to 180° and down to -180°, which seems correct. However i have another node which listen to imu message and publish a tf where the rotation component is the same as the orientation quaternion of the imu msg.

The problem is when I visualize the tf on rviz, the tf can't have bigger rotation than 90° or lower rotation than -90°. I heard a bit about gimbal lock but i thought quaternions were not affected by this phenomenon :'(

Here is the code of my node :

#include "imu_node.hpp"


IMU_PUBLISHER::IMU_PUBLISHER(char i2c_slave_addr, char i2c_port_num, uint16_t nb_ech_calibration)
: Node("IMU_PUBLISHER"),
  mpu6050(MPU6050(i2c_slave_addr,i2c_port_num,nb_ech_calibration)),
  madwick(MADWICK((std::abs(mpu6050.gyro_offset_x)+std::abs(mpu6050.gyro_offset_y)+std::abs(mpu6050.gyro_offset_z))/3)),
  alpha_(1.0),
  theta_(0.0),
  phi_(0.0),
  psi_(0.0),
  prev_theta_(0.0),
  prev_phi_(0.0),
  prev_psi_(0.0),
  //current_orientation(1.0, 0.0, 0.0, 0.0),
  file("imu_data_rectified.log")
{

    imu_data.header.frame_id="imu_link";
    
    RCLCPP_INFO(this->get_logger(), "\n===============================================================\nIMU CALIBRATION SUCCESS\n===============================================================\n");

    

    prev_time = this->now();
    imu_publisher_ = this->create_publisher<sensor_msgs::msg::Imu>("/utils/imu", 1);
    imu_timer_ = this->create_wall_timer(100ms, std::bind(&IMU_PUBLISHER::timer_callback, this));
}

IMU_PUBLISHER::~IMU_PUBLISHER(){
    file.close();
}

void IMU_PUBLISHER::timer_callback()
{
    
    auto current_time = this->now();
    double dt = ((double)(current_time - prev_time).nanoseconds())/(1000000000);

    madwick.DELTA_T = (float)dt;
    //Récupération des données acceléromètres et gyromètres

    //std::cout << "dt : " << dt << std::endl;
    std::cout << "DELTA_T : " << madwick.DELTA_T << std::endl;
    fflush(stdout);
    //IMUData data = mpu6050.get_filtered_data();

    
/*
    double accel_x = data.ax;
    double accel_y = data.ay;
    double accel_z = data.az;

    double gyro_x = -data.gx;
    double gyro_y = -data.gy;
    double gyro_z = data.gz;
*/
    double accel_x = mpu6050.get_accel_x_g();
    double accel_y = mpu6050.get_accel_y_g();
    double accel_z = mpu6050.get_accel_z_g();

    std::cout << std::fixed << std::setprecision(14) << "Accel before normalization : " << accel_x << ", " << accel_y << ", " << accel_z << std::endl;
    normalize_accel(accel_x,accel_y,accel_z);
    std::cout << std::fixed << std::setprecision(14) <<  "Accel after normalization : " << accel_x << ", " << accel_y << ", " << accel_z << std::endl;

    double gyro_x = mpu6050.get_gyro_x_rad();
    double gyro_y = mpu6050.get_gyro_y_rad();
    double gyro_z = mpu6050.get_gyro_z_rad();

    quaternion q = madwick.filter(accel_x, accel_y, accel_z, gyro_x, gyro_y, gyro_z);

    //quaternion q = madwick.filter(data.ax, data.ay, data.az, data.gx, data.gy, data.gz);


    file << accel_x << " " << accel_y << " " << accel_z << " " << gyro_x << " " << gyro_y << " " << gyro_z << std::endl;

    imu_data.linear_acceleration.x=accel_x;
    imu_data.linear_acceleration.y=accel_y;
    imu_data.linear_acceleration.z=accel_z;


    imu_data.angular_velocity.x=gyro_x;
    imu_data.angular_velocity.y=gyro_y;
    imu_data.angular_velocity.z=gyro_z;

    imu_data.orientation.x = q.x;
    imu_data.orientation.y = q.y;
    imu_data.orientation.z = q.z;
    imu_data.orientation.w = q.w;

    imu_publisher_->publish(imu_data);

    prev_time = current_time;
}

Here is the code of the madwick class :

#include "madwick_filter.hpp"

MADWICK::MADWICK(float offset_gyro) : GYRO_MEAN_ERROR(offset_gyro), DELTA_T(0.1), BETA(std::sqrt(3.0f/4.0f) * GYRO_MEAN_ERROR)
{
    q_est = quaternion(1.0, 0.0, 0.0, 0.0);
    std::cout << "GYRO_MEAN_ERROR : " << GYRO_MEAN_ERROR << std::endl;
    std::cout << "BETA : " << BETA << std::endl;

}

MADWICK::~MADWICK()
{

}

quaternion MADWICK::filter(double ax, double ay, double az, double gx, double gy, double gz){

    //Variables and constants
    quaternion q_est_prev = q_est;
    quaternion q_est_dot = {0,0,0,0};            // used as a place holder in equations 42 and 43
    //const struct quaternion q_g_ref = {0, 0, 0, 1};// equation (23), reference to field of gravity for gradient descent optimization (not needed because I used eq 25 instead of eq 21
    quaternion q_a = {0, ax, ay, az};    // equation (24) raw acceleration values, needs to be normalized
    q_a.normalize();              // normalize the acceleration quaternion to be a unit quaternion
    
    double F_g [3] = {0};                        // equation(15/21/25) objective function for gravity
    double J_g [3][4] = {0};                     // jacobian matrix for gravity
    
    quaternion gradient = {0,0,0,0};
    
    /* Integrate angluar velocity to obtain position in angles. */
    quaternion q_w;                   // equation (10), places gyroscope readings in a quaternion
    q_w.w = 0;                              // the real component is zero, which the Madgwick uses to simplfy quat. mult.
    q_w.x = gx;
    q_w.y = gy;
    q_w.z = gz;
    
    q_w = q_w*0.5;                  // equation (12) dq/dt = (1/2)q*w
    q_w = q_est_prev * q_w;        // equation (12)
    
    
    //Compute the objective function for gravity, equation(15), simplified to equation (25) due to the 0's in the acceleration reference quaternion
    F_g[0] = 2*(q_est_prev.x * q_est_prev.z - q_est_prev.w * q_est_prev.y) - q_a.x;
    F_g[1] = 2*(q_est_prev.w * q_est_prev.x + q_est_prev.y* q_est_prev.z) - q_a.y;
    F_g[2] = 2*(0.5 - q_est_prev.x * q_est_prev.x - q_est_prev.y * q_est_prev.y) - q_a.z;
    
    //Compute the Jacobian matrix, equation (26), for gravity
    J_g[0][0] = -2 * q_est_prev.y;
    J_g[0][1] =  2 * q_est_prev.z;
    J_g[0][2] = -2 * q_est_prev.w;
    J_g[0][3] =  2 * q_est_prev.x;
    
    J_g[1][0] = 2 * q_est_prev.x;
    J_g[1][1] = 2 * q_est_prev.w;
    J_g[1][2] = 2 * q_est_prev.z;
    J_g[1][3] = 2 * q_est_prev.y;
    
    J_g[2][0] = 0;
    J_g[2][1] = -4 * q_est_prev.x;
    J_g[2][2] = -4 * q_est_prev.y;
    J_g[2][3] = 0;
    
    // now computer the gradient, equation (20), gradient = J_g'*F_g
    gradient.w = J_g[0][0] * F_g[0] + J_g[1][0] * F_g[1] + J_g[2][0] * F_g[2];
    gradient.x = J_g[0][1] * F_g[0] + J_g[1][1] * F_g[1] + J_g[2][1] * F_g[2];
    gradient.y = J_g[0][2] * F_g[0] + J_g[1][2] * F_g[1] + J_g[2][2] * F_g[2];
    gradient.z = J_g[0][3] * F_g[0] + J_g[1][3] * F_g[1] + J_g[2][3] * F_g[2];
    
    // Normalize the gradient, equation (44)
    gradient.normalize();
  
    /* This is the sensor fusion part of the algorithm.
     Combining Gyroscope position angles calculated in the beginning, with the quaternion orienting the accelerometer to gravity created above.
     Noticably this new quaternion has not be created yet, I have only calculated the gradient in equation (19).
     Madgwick however uses assumptions with the step size and filter gains to optimize the gradient descent,
        combining it with the sensor fusion in equations (42-44).
     He says the step size has a var alpha, which he assumes to be very large.
     This dominates the previous estimation in equation (19) to the point you can ignore it.
     Eq. 36 has the filter gain Gamma, which is related to the step size and thus alpha. With alpha being very large,
        you can make assumptions to simplify the fusion equatoin of eq.36.
     Combining the simplification of the gradient descent equation with the simplification of the fusion equation gets you eq.
     41 which can be subdivided into eqs 42-44.
    */
    gradient = gradient * BETA;             // multiply normalized gradient by beta
    q_est_dot = q_w - gradient;        // subtract above from q_w, the integrated gyro quaternion
    q_est_dot = q_est_dot * DELTA_T;
    q_est = q_est_prev + q_est_dot;     // Integrate orientation rate to find position
    q_est.normalize();                 // normalize the orientation of the estimate
                                                //(shown in diagram, plus always use unit quaternions for orientation)
    
    quaternion res = q_est;
    to_euler_angles(q_est,roll,pitch,yaw);
    std::cout << "orientation (RPY °) : [" << roll << ", " << pitch << ", " << yaw << "]" << std::endl;
    fflush(stdout);
    return res;
}

quaternion MADWICK::filter(float ax, float ay, float az, float gx, float gy, float gz){

    //Variables and constants
    quaternion q_est_prev = q_est;
    quaternion q_est_dot = {0,0,0,0};            // used as a place holder in equations 42 and 43
    //const struct quaternion q_g_ref = {0, 0, 0, 1};// equation (23), reference to field of gravity for gradient descent optimization (not needed because I used eq 25 instead of eq 21
    quaternion q_a = {0, ax, ay, az};    // equation (24) raw acceleration values, needs to be normalized
    q_a.normalize();              // normalize the acceleration quaternion to be a unit quaternion
    
    float F_g [3] = {0};                        // equation(15/21/25) objective function for gravity
    float J_g [3][4] = {0};                     // jacobian matrix for gravity
    
    quaternion gradient = {0,0,0,0};
    
    /* Integrate angluar velocity to obtain position in angles. */
    quaternion q_w;                   // equation (10), places gyroscope readings in a quaternion
    q_w.w = 0;                              // the real component is zero, which the Madgwick uses to simplfy quat. mult.
    q_w.x = gx;
    q_w.y = gy;
    q_w.z = gz;
    
    q_w = q_w*0.5;                  // equation (12) dq/dt = (1/2)q*w
    q_w = q_est_prev * q_w;        // equation (12)

    /* NOTE
    * Page 10 states equation (40) substitutes equation (13) into it. This seems false, as he actually
    * substitutes equation (12), q_se_dot_w, not equation (13), q_se_w.
    * 
    * // quat_scalar(&q_w, deltaT);               // equation (13) integrates the angles velocity to position
    * // quat_add(&q_w, q_w, q_est_prev);         // addition part of equation (13)
    */

    /* Compute the gradient by multiplying the jacobian matrix by the objective function. This is equation 20.
     The Jacobian matrix, J, is a 3x4 matrix of partial derivatives for each quaternion component in the x y z axes
     The objective function, F, is a 3x1 matrix for x y and z.
     To multiply these together, the inner dimensions must match, so use J'.
     I calculated "by hand" the transpose of J, so I will be using "hard coordinates" to get those values from J.
     The matrix multiplcation can also be done hard coded to reduce code.
     
     Note: it is possible to compute the objective function with quaternion multiplcation functions, but it does not take into account the many zeros that cancel terms out and is not optimized like the paper shows
     */
    
    
    //Compute the objective function for gravity, equation(15), simplified to equation (25) due to the 0's in the acceleration reference quaternion
    F_g[0] = 2*(q_est_prev.x * q_est_prev.z - q_est_prev.w * q_est_prev.y) - q_a.x;
    F_g[1] = 2*(q_est_prev.w * q_est_prev.x + q_est_prev.y* q_est_prev.z) - q_a.y;
    F_g[2] = 2*(0.5 - q_est_prev.x * q_est_prev.x - q_est_prev.y * q_est_prev.y) - q_a.z;
    
    //Compute the Jacobian matrix, equation (26), for gravity
    J_g[0][0] = -2 * q_est_prev.y;
    J_g[0][1] =  2 * q_est_prev.z;
    J_g[0][2] = -2 * q_est_prev.w;
    J_g[0][3] =  2 * q_est_prev.x;
    
    J_g[1][0] = 2 * q_est_prev.x;
    J_g[1][1] = 2 * q_est_prev.w;
    J_g[1][2] = 2 * q_est_prev.z;
    J_g[1][3] = 2 * q_est_prev.y;
    
    J_g[2][0] = 0;
    J_g[2][1] = -4 * q_est_prev.x;
    J_g[2][2] = -4 * q_est_prev.y;
    J_g[2][3] = 0;
    
    // now computer the gradient, equation (20), gradient = J_g'*F_g
    gradient.w = J_g[0][0] * F_g[0] + J_g[1][0] * F_g[1] + J_g[2][0] * F_g[2];
    gradient.x = J_g[0][1] * F_g[0] + J_g[1][1] * F_g[1] + J_g[2][1] * F_g[2];
    gradient.y = J_g[0][2] * F_g[0] + J_g[1][2] * F_g[1] + J_g[2][2] * F_g[2];
    gradient.z = J_g[0][3] * F_g[0] + J_g[1][3] * F_g[1] + J_g[2][3] * F_g[2];
    
    // Normalize the gradient, equation (44)
    gradient.normalize();
  
    gradient = gradient * BETA;             // multiply normalized gradient by beta
    q_est_dot = q_w - gradient;        // subtract above from q_w, the integrated gyro quaternion
    q_est_dot = q_est_dot * DELTA_T;
    q_est = q_est_prev + q_est_dot;     // Integrate orientation rate to find position
    q_est.normalize();                 // normalize the orientation of the estimate
                                                //(shown in diagram, plus always use unit quaternions for orientation)
    
    quaternion res = q_est;
    to_euler_angles(q_est,roll,pitch,yaw);
    std::cout << "orientation (RPY °) : [" << roll << ", " << pitch << ", " << yaw << "]" << std::endl;
    fflush(stdout);
    return res;
}

void MADWICK::to_euler_angles(quaternion q, float& roll, float& pitch, float& yaw) {
    // Roll (x-axis rotation)
    float sinr_cosp = 2 * (q.w * q.x + q.y * q.z);
    float cosr_cosp = 1 - 2 * (q.x * q.x + q.y * q.y);
    roll = std::atan2(sinr_cosp, cosr_cosp);

    // Pitch (y-axis rotation)
    float sinp = 2 * (q.w * q.y - q.z * q.x);
    if (std::abs(sinp) >= 1)
        pitch = std::copysign(M_PI / 2, sinp); // Use 90 degrees if out of range
    else
        pitch = std::asin(sinp);

    // Yaw (z-axis rotation)
    float siny_cosp = 2 * (q.w * q.z + q.x * q.y);
    float cosy_cosp = 1 - 2 * (q.y * q.y + q.z * q.z);
    yaw = std::atan2(siny_cosp, cosy_cosp);

    // Convert radians to degrees
    roll *= (180.0f / M_PI);
    pitch *= (180.0f / M_PI);
    yaw *= (180.0f / M_PI);
}

Finally, here is the code of the node which is porting the imu msg into a tf :

#include <rclcpp/rclcpp.hpp>
#include "sensor_msgs/msg/imu.hpp"
#include <geometry_msgs/msg/transform_stamped.hpp>

#include <tf2_geometry_msgs/tf2_geometry_msgs.hpp>
#include <tf2_ros/transform_broadcaster.h>
#include <tf2/LinearMath/Quaternion.h>
#include <tf2/LinearMath/Matrix3x3.h>

class ImuToTFNode : public rclcpp::Node {
public:
    ImuToTFNode() : Node("imu_to_tf_node") {
        imu_subscriber_ = this->create_subscription<sensor_msgs::msg::Imu>(
            "/utils/imu", 10, std::bind(&ImuToTFNode::imu_callback, this, std::placeholders::_1));
        tf_broadcaster_ = std::make_shared<tf2_ros::TransformBroadcaster>(this);
    }

private:
    void imu_callback(const sensor_msgs::msg::Imu::SharedPtr imu_msg) {
        // Créer une transformation entre les cadres "imu" et "map"
        geometry_msgs::msg::TransformStamped tf_msg;
        tf_msg.header.stamp = this->now();
        tf_msg.header.frame_id = "map";  
        tf_msg.child_frame_id = "imu_test";

        // Récupérer l'orientation de l'IMU
        /*
        tf2::Quaternion orientation;
        tf2::fromMsg(imu_msg->orientation, orientation);
*/
        tf_msg.transform.rotation.x = imu_msg->orientation.x;
        tf_msg.transform.rotation.y = imu_msg->orientation.y;
        tf_msg.transform.rotation.z = imu_msg->orientation.z;
        tf_msg.transform.rotation.w = imu_msg->orientation.w;

        tf_msg.transform.translation.x = 0.0;  // Remplacez par votre valeur de translation en X
        tf_msg.transform.translation.y = 0.0;  // Remplacez par votre valeur de translation en Y
        tf_msg.transform.translation.z = 0.25;

        // Publier la transformation
        tf_broadcaster_->sendTransform(tf_msg);
    }

    rclcpp::Subscription<sensor_msgs::msg::Imu>::SharedPtr imu_subscriber_;
    std::shared_ptr<tf2_ros::TransformBroadcaster> tf_broadcaster_;
};

int main(int argc, char** argv) {
    rclcpp::init(argc, argv);
    rclcpp::spin(std::make_shared<ImuToTFNode>());
    rclcpp::shutdown();
    return 0;
}


As you can see below, when converted to RPY, the madwick filter quaternion output can have value up to -180 or 180.

[ros2-1] Accel avant normalisation : -0.11059570312500, -0.24145507812500, 0.86450195312500
[ros2-1] Accel apres normalisation : -0.12228954255449, -0.26698533683529, 0.95591008870958
[ros2-1] orientation (RPY °) : [-13.40970325469971, 9.04709053039551, -154.49398803710938]
[ros2-1] DELTA_T : 0.09999807178974
[ros2-1] Accel avant normalisation : -0.13452148437500, -0.23388671875000, 0.86865234375000
[ros2-1] Accel apres normalisation : -0.14789226529545, -0.25713392042294, 0.95499215956662
[ros2-1] orientation (RPY °) : [-12.85645866394043, 9.29825305938721, -154.39842224121094]
[ros2-1] DELTA_T : 0.10001067817211
[ros2-1] Accel avant normalisation : -0.12597656250000, -0.20751953125000, 0.84399414062500
[ros2-1] Accel apres normalisation : -0.14344624761253, -0.23629711331521, 0.96103425968317
[ros2-1] orientation (RPY °) : [-12.73875904083252, 9.16974925994873, -154.41403198242188]
[ros2-1] DELTA_T : 0.10000046342611
[ros2-1] Accel avant normalisation : -0.14404296875000, -0.21655273437500, 0.85522460937500
[ros2-1] Accel apres normalisation : -0.16114037880915, -0.24225680678596, 0.95673685926858
[ros2-1] orientation (RPY °) : [-12.44879531860352, 8.99049091339111, -154.54493713378906]
[ros2-1] DELTA_T : 0.10000327974558
[ros2-1] Accel avant normalisation : -0.11718750000000, -0.21630859375000, 0.87304687500000
[ros2-1] Accel apres normalisation : -0.12919680907614, -0.23847577675305, 0.96251622761727
[ros2-1] orientation (RPY °) : [-12.36839866638184, 9.10458660125732, -154.67362976074219]
[ros2-1] DELTA_T : 0.10000317543745
[ros2-1] Accel avant normalisation : -0.12304687500000, -0.21142578125000, 0.86767578125000
[ros2-1] Accel apres normalisation : -0.13649123048320, -0.23452659840962, 0.96247982765334
[ros2-1] orientation (RPY °) : [-12.38699245452881, 9.06555271148682, -154.70242309570312]

I'm sorry to bother you but i'm stuck on this since a while and can't understand what's going on. If anyone can help me I would be glad :)

$\endgroup$

1 Answer 1

2
$\begingroup$

Found it ! the problem was coming from an intermediate node which was converting imu data from one frame to another. In this node, I convert imu data with a static tf like this :

tf2::Vector3 orientation_inter(msg.orientation.x, msg.orientation.y, msg.orientation.z);
orientation_inter = this->tf_intermediaire * orientation_inter;

tf2::Quaternion orientation_quat(orientation_inter.x(), orientation_inter.y(), orientation_inter.z(), msg.orientation.w);
        
orientation_quat.normalize(); 

new_msg.orientation.x = orientation_quat.x();
new_msg.orientation.y = orientation_quat.y();
new_msg.orientation.z = orientation_quat.z();
new_msg.orientation.w = orientation_quat.w();

I had forgotten to normalize the obtained quaternion.

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.