├── README.md
├── config
└── imu_compass.yaml
├── launch
└── imu_compass.launch
├── CMakeLists.txt
├── package.xml
├── CHANGELOG.rst
├── include
└── imu_compass
│ └── imu_compass.h
├── scripts
└── compute_calibration
└── src
└── imu_compass.cpp
/README.md:
--------------------------------------------------------------------------------
1 | imu_compass
2 | ===========
3 |
4 | Node to combine IMU data (accelerometers and gyroscopes) with Compass data (magnetometers) for a cleaner reading of a Vehicles Heading
5 |
--------------------------------------------------------------------------------
/config/imu_compass.yaml:
--------------------------------------------------------------------------------
1 | # Sample set of values. Please generate your own values using the compute_calibration script included in this package
2 | mag_bias:
3 | x: 0.183117
4 | y: 0.273127
5 | z: -0.761884
6 | radius: 0.271064
7 |
--------------------------------------------------------------------------------
/launch/imu_compass.launch:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | cmake_minimum_required(VERSION 2.8.3)
2 | project(imu_compass)
3 |
4 | find_package(catkin REQUIRED COMPONENTS geometry_msgs sensor_msgs std_msgs tf)
5 |
6 | catkin_package()
7 |
8 | if(NOT WIN32)
9 | set_directory_properties(PROPERTIES COMPILE_OPTIONS "-std=c++11")
10 | endif()
11 |
12 | include_directories(include
13 | ${catkin_INCLUDE_DIRS}
14 | )
15 |
16 | add_executable(imu_compass src/imu_compass.cpp)
17 | target_link_libraries(imu_compass ${catkin_LIBRARIES})
18 |
19 | install(
20 | TARGETS imu_compass
21 | RUNTIME DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
22 | )
23 |
24 | install(
25 | PROGRAMS scripts/compute_calibration
26 | DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
27 | )
28 |
29 | install(
30 | DIRECTORY launch config
31 | DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}
32 | )
33 |
--------------------------------------------------------------------------------
/package.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | imu_compass
4 | 0.0.5
5 | Node to combine IMU data (accelerometers and gyroscopes) with Compass data (magnetometers) for a cleaner reading of a Vehicles Heading
6 | Prasenjit Mukherjee
7 |
8 | BSD
9 |
10 |
11 |
12 |
13 | catkin
14 | geometry_msgs
15 | sensor_msgs
16 | std_msgs
17 | tf
18 | geometry_msgs
19 | sensor_msgs
20 | std_msgs
21 | tf
22 | python-scipy
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/CHANGELOG.rst:
--------------------------------------------------------------------------------
1 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2 | Changelog for package imu_compass
3 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
4 |
5 | 0.0.5 (2014-07-09)
6 | ------------------
7 | * removing bag references from sample hard-iron calibration output file
8 | * fixing example launch file to reflect changes in param intake in the node
9 | * Contributors: Prasenjit Mukherjee
10 |
11 | 0.0.4 (2014-07-02)
12 | ------------------
13 | * successful test with kingfisher June 30 2014
14 | * adding the publishing of calibrated magnetometer data for debug purposes
15 | * Contributors: Mike Purvis, Prasenjit Mukherjee
16 |
17 | 0.0.3 (2013-10-24)
18 | ------------------
19 | * Populate the curr_heading_float message with the declination-corrected heading value.
20 | * Add declination param and topic input to imu_compass node.
21 | * Add dependency on scipy for compute script.
22 | * Initialize curr_imu_reading_ with an empty message to avoid startup segfault.
23 |
24 | 0.0.2 (2013-10-04)
25 | ------------------
26 | * adding rosparams for critical variables
27 | * tuning covariance to rely more on gyro
28 |
29 | 0.0.1 (2013-09-23)
30 | ------------------
31 | * creating new package for imu_compass
32 | * added imu_compass.cpp, used to be called um6_compass when it came from https://github.com/clearpathrobotics/um6/tree/compass_cleanup/src
33 |
--------------------------------------------------------------------------------
/include/imu_compass/imu_compass.h:
--------------------------------------------------------------------------------
1 | /*imu_compass.cpp
2 | Authors: Prasenjit (pmukherj@clearpathrobotics.com)
3 | Copyright (c) 2013, Clearpath Robotics, Inc., All rights reserved.
4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
5 | following conditions are met:
6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following
7 | disclaimer.
8 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
9 | disclaimer in the documentation and/or other materials provided with the distribution.
10 | * Neither the name of Clearpath Robotics nor the names of its contributors may be used to endorse or promote products
11 | derived from this software without specific prior written permission.
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
13 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
14 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
15 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
16 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
17 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
18 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
19 |
20 | Description:
21 | Header file for IMU Compass Class that combines gyroscope and magnetometer data to get a clean estimate of yaw.
22 | */
23 |
24 | #include "ros/ros.h"
25 | #include "tf/tf.h"
26 | #include "tf/transform_listener.h"
27 |
28 | #include "sensor_msgs/Imu.h"
29 | #include "geometry_msgs/Vector3Stamped.h"
30 | #include "std_msgs/Float32.h"
31 |
32 | //typedef boost::shared_ptr ImuConstPtr;
33 |
34 | class IMUCompass {
35 |
36 | private:
37 | ros::NodeHandle node_;
38 | ros::Subscriber imu_sub_;
39 | ros::Subscriber mag_sub_;
40 | ros::Subscriber decl_sub_;
41 | ros::Publisher imu_pub_;
42 | ros::Publisher mag_pub_;
43 | ros::Publisher compass_pub_;
44 | ros::Publisher raw_compass_pub_;
45 |
46 | tf::TransformListener listener_;
47 | ros::Timer debug_timer_;
48 |
49 | void imuCallback(sensor_msgs::ImuPtr data);
50 | void declCallback(const std_msgs::Float32& data);
51 | void magCallback(const geometry_msgs::Vector3StampedConstPtr& data);
52 | void debugCallback(const ros::TimerEvent&);
53 | void repackageImuPublish(tf::StampedTransform);
54 |
55 | //Heading Filter functions
56 | void initFilter(double heading_meas); //initialize heading fiter
57 | bool first_mag_reading_; //signifies receiving the first magnetometer message
58 | bool first_gyro_reading_; //signifies receiving the first gyroscope message
59 | bool filter_initialized_; //after receiving the first measurement, make sure the filter is initialized
60 | bool gyro_update_complete_; //sigfnifies that a gyro update (motion model update) has gone through
61 |
62 | double mag_zero_x_, mag_zero_y_, mag_zero_z_;
63 |
64 | sensor_msgs::ImuPtr curr_imu_reading_;
65 |
66 | //Heading Filter Variables
67 | //State and Variance
68 | double curr_heading_;
69 | double curr_heading_variance_;
70 | double sensor_timeout_;
71 |
72 | //Motion Update Variables
73 | double heading_prediction_;
74 | double heading_variance_prediction_;
75 | double heading_prediction_variance_;
76 | double mag_declination_;
77 | double last_motion_update_time_;
78 | double last_measurement_update_time_;
79 |
80 | //Measurement Update Variables
81 | double yaw_meas_variance_;
82 |
83 | public:
84 | IMUCompass(ros::NodeHandle &n);
85 | ~IMUCompass() {
86 | }
87 | };
88 |
89 |
--------------------------------------------------------------------------------
/scripts/compute_calibration:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import rosbag
4 |
5 | import datetime
6 | from tf.msg import tfMessage
7 | from argparse import ArgumentParser
8 | from geometry_msgs.msg import Quaternion
9 | from numpy import mean, array, hypot, diff, convolve, arange, sin, cos, ones, pi, matrix
10 | from tf.transformations import euler_from_quaternion,quaternion_from_euler,quaternion_multiply,quaternion_matrix
11 | import tf
12 |
13 | # Prompt user if scipy is missing.
14 | try:
15 | from scipy import optimize
16 | except ImportError:
17 | print("This script requires scipy be available.")
18 | print("On Ubuntu: sudo apt-get install python-scipy")
19 | exit(1)
20 |
21 | # Plots are optional
22 | try:
23 | from matplotlib import pyplot
24 | from mpl_toolkits.mplot3d import Axes3D
25 | except ImportError:
26 | pyplot = None
27 |
28 | parser = ArgumentParser(description='Process UM6 bag file for compass calibration. Pass a bag containing /imu/rpy and /imu/mag topics, with the UM6 compass facing upright, being slowly rotated in a clockwise direction for 30-120 seconds.')
29 | parser.add_argument('bag', metavar='FILE', type=str, help='input bag file')
30 | parser.add_argument('outfile', metavar='OUTFILE', type=str, help='output yaml file',
31 | nargs="?", default="/tmp/calibration.yaml")
32 | parser.add_argument('--plots', type=bool, help='Show plots if matplotlib available.')
33 | args = parser.parse_args()
34 |
35 | if not args.plots:
36 | pyplot = None
37 |
38 | bag = rosbag.Bag(args.bag)
39 | imu_rot = Quaternion(0,0,0,1)
40 | transform_found = False
41 | for topic, msg, time in bag.read_messages(topics=("/tf", "/tf")):
42 | for c_trans in msg.transforms:
43 | if ("base_link" in c_trans.header.frame_id and "imu_link" in c_trans.child_frame_id):
44 | imu_rot = c_trans.transform.rotation
45 | transform_found = True
46 | break
47 | if (transform_found):
48 | break
49 |
50 | temp_q = (imu_rot.x, imu_rot.y, imu_rot.z, imu_rot.w)
51 | imu_rot = temp_q
52 | t_array = quaternion_matrix(imu_rot)
53 | t_mat = matrix(t_array)
54 | t_mat = t_mat.I
55 |
56 | if (not transform_found):
57 | print("Transform between base_link and imu_link not found, assuming unity")
58 |
59 | time_yaw_tuples = []
60 | for topic, msg, time in bag.read_messages(topics=("/imu/rpy", "imu/rpy")):
61 | o_msg = matrix([msg.vector.x, msg.vector.y, msg.vector.z, 0])
62 | o_msg = o_msg.T
63 | transformed_msg = t_mat*o_msg
64 | time_yaw_tuples.append((time.to_sec(), float(transformed_msg[2])))
65 |
66 |
67 | if len(time_yaw_tuples) < 100:
68 | print("Insufficient data or no data in bag file. Looking for topic /imu/rpy.")
69 | exit(1)
70 |
71 |
72 | time_yaw = zip(*time_yaw_tuples)
73 | time_yaw.append(diff(time_yaw[-1]))
74 | time_yaw_tuples_filtered = [tup for tup in zip(*time_yaw) if abs(tup[-1]) < 3.0]
75 | time_yaw = zip(*time_yaw_tuples_filtered)
76 | # apply smoothing as a new column
77 | w = [1.0 / 30.0] * 30
78 | time_yaw.append(convolve(time_yaw[-1], w, 'same'))
79 |
80 | # remove sections of no movement.
81 | time_yaw_tuples_movement = [tup for tup in zip(*time_yaw)] # if abs(tup[-1]) > 0.01]
82 | time_start = time_yaw_tuples_movement[50][0]
83 | time_end = time_yaw_tuples_movement[-50][0]
84 |
85 | if pyplot:
86 | fig = pyplot.figure()
87 | ax1 = fig.add_subplot(211)
88 | ax1.scatter(array(time_yaw[0]), time_yaw[-1])
89 | lines = pyplot.axvline(time_start)
90 | pyplot.axvline(time_end)
91 | fig.gca().add_artist(lines)
92 |
93 | vecs = []
94 | for topic, msg, time in bag.read_messages(topics=("/imu/mag", "imu/mag")):
95 | if time.to_sec() > time_start and time.to_sec() < time_end:
96 | o_msg = matrix([msg.vector.x, msg.vector.y, msg.vector.z, 0])
97 | o_msg = o_msg.T
98 | t_msg = t_mat*o_msg
99 | vecs.append((float(t_msg[0]), float(t_msg[1]), float(t_msg[2])))
100 |
101 | print ("Using data from " + str(time_start) +" to " + str (time_end) + "(" + str(time_end - time_start) + " seconds), or " + str(len(vecs)) + " samples.")
102 |
103 | def calc_R(xc, yc):
104 | """ calculate the distance of each 2D points from the center (xc, yc) """
105 | return hypot(x-xc, y-yc)
106 |
107 | def f_2(c):
108 | """ calculate the algebraic distance between the 2D points and the mean circle centered at c=(xc, yc) """
109 | Ri = calc_R(*c)
110 | return Ri - Ri.mean()
111 |
112 |
113 | x,y,z = zip(*vecs)
114 | center_estimate = mean(x), mean(y)
115 | center, ier = optimize.leastsq(f_2, center_estimate)
116 | radius = calc_R(*center).mean()
117 | center = (center[0], center[1], mean(z))
118 |
119 | a = arange(0, 2*pi + pi/50, pi/50)
120 | circle_points = (center[0] + cos(a) * radius,
121 | center[1] + sin(a) * radius,
122 | center[2] * ones(len(a)))
123 |
124 | print("Magnetic circle centered at " + str(center) + ", with radius " + str(radius))
125 |
126 | with open(args.outfile, "w") as f:
127 | f.write("# Generated from %s on %s.\n" % (args.bag, datetime.date.today()))
128 | f.write("mag_bias:\n")
129 | f.write(" x: %f\n" % center[0])
130 | f.write(" y: %f\n" % center[1])
131 | f.write(" z: %f\n" % center[2])
132 | f.write(" radius: %f\n" % radius)
133 |
134 | print("Calibration file written to " + args.outfile)
135 |
136 | if pyplot:
137 | ax2 = fig.add_subplot(212, projection='3d')
138 | ax2.view_init(elev=80, azim=5)
139 | ax2.scatter(x, y, z)
140 | ax2.plot(*circle_points, c="red")
141 | pyplot.show()
142 |
--------------------------------------------------------------------------------
/src/imu_compass.cpp:
--------------------------------------------------------------------------------
1 | /*
2 | imu_compass.cpp
3 | Authors: Prasenjit (pmukherj@clearpathrobotics.com)
4 | Copyright (c) 2013, Clearpath Robotics, Inc., All rights reserved.
5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
6 | following conditions are met:
7 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following
8 | disclaimer.
9 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
10 | disclaimer in the documentation and/or other materials provided with the distribution.
11 | * Neither the name of Clearpath Robotics nor the names of its contributors may be used to endorse or promote products
12 | derived from this software without specific prior written permission.
13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
14 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
16 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
17 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
18 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
19 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
20 |
21 | Description:
22 | CPP file for IMU Compass Class that combines gyroscope and magnetometer data to get a clean estimate of yaw.
23 | */
24 |
25 | #include "imu_compass/imu_compass.h"
26 |
27 | double magn(tf::Vector3 a) {
28 | return sqrt(a.x()*a.x() + a.y()*a.y() + a.z()*a.z());
29 | }
30 |
31 | IMUCompass::IMUCompass(ros::NodeHandle &n) :
32 | node_(n), curr_imu_reading_(new sensor_msgs::Imu()) {
33 | // Acquire Parameters
34 | ros::param::param("~mag_bias/x", mag_zero_x_, 0.0);
35 | ros::param::param("~mag_bias/y", mag_zero_y_, 0.0);
36 | ros::param::param("~mag_bias/z", mag_zero_z_, 0.0);
37 |
38 |
39 | ROS_INFO("Using magnetometer bias (x,y):%f,%f", mag_zero_x_, mag_zero_y_);
40 |
41 | ros::param::param("~compass/sensor_timeout", sensor_timeout_, 0.5);
42 | ros::param::param("~compass/yaw_meas_variance", yaw_meas_variance_, 10.0);
43 | ros::param::param("~compass/gyro_meas_variance", heading_prediction_variance_, 0.01);
44 | ROS_INFO("Using variance %f", yaw_meas_variance_);
45 |
46 | ros::param::param("~compass/mag_declination", mag_declination_, 0.0);
47 | ROS_INFO("Using magnetic declination %f (%f degrees)", mag_declination_, mag_declination_ * 180 / M_PI);
48 |
49 | // Setup Subscribers
50 | imu_sub_ = node_.subscribe("imu/data", 1000, &IMUCompass::imuCallback, this);
51 | mag_sub_ = node_.subscribe("imu/mag", 1000, &IMUCompass::magCallback, this);
52 | decl_sub_ = node_.subscribe("imu/declination", 1000, &IMUCompass::declCallback, this);
53 | imu_pub_ = node_.advertise("imu/data_compass", 1);
54 | compass_pub_ = node_.advertise("imu/compass_heading", 1);
55 | mag_pub_ = node_.advertise("imu/mag_calib", 1);
56 |
57 | raw_compass_pub_ = node_.advertise("imu/raw_compass_heading", 1);
58 |
59 | first_mag_reading_ = false;
60 | first_gyro_reading_ = false;
61 | gyro_update_complete_ = false;
62 | last_motion_update_time_ = ros::Time::now().toSec();
63 | debug_timer_ = node_.createTimer(ros::Duration(1), &IMUCompass::debugCallback, this);
64 |
65 | ROS_INFO("Compass Estimator Started");
66 | }
67 |
68 | void IMUCompass::debugCallback(const ros::TimerEvent&) {
69 | if (!first_gyro_reading_)
70 | ROS_WARN("Waiting for IMU data, no gyroscope data available)");
71 | if (!first_mag_reading_)
72 | ROS_WARN("Waiting for mag data, no magnetometer data available, Filter not initialized");
73 |
74 | if ((ros::Time::now().toSec() - last_motion_update_time_ > sensor_timeout_) && first_gyro_reading_) {
75 | // gyro data is coming in too slowly
76 | ROS_WARN("Gyroscope data being receieved too slow or not at all");
77 | first_gyro_reading_ = false;
78 | }
79 |
80 | if ((ros::Time::now().toSec() - last_measurement_update_time_ > sensor_timeout_) && first_mag_reading_) {
81 | // gyro data is coming in too slowly
82 | ROS_WARN("Magnetometer data being receieved too slow or not at all");
83 | filter_initialized_ = false;
84 | first_mag_reading_ = false;
85 | }
86 | }
87 |
88 | void IMUCompass::imuCallback(const sensor_msgs::ImuPtr data) {
89 | // Transform Data and get the yaw direction
90 | geometry_msgs::Vector3 gyro_vector;
91 | geometry_msgs::Vector3 gyro_vector_transformed;
92 | gyro_vector = data->angular_velocity;
93 |
94 | if(!first_gyro_reading_)
95 | first_gyro_reading_ = true;
96 |
97 | double dt = ros::Time::now().toSec() - last_motion_update_time_;
98 | last_motion_update_time_ = ros::Time::now().toSec();
99 | tf::StampedTransform transform;
100 |
101 | try {
102 | listener_.lookupTransform("base_link", data->header.frame_id, ros::Time(0), transform);
103 | } catch (tf::TransformException &ex) {
104 | ROS_WARN("Missed transform between base_link and %s", data->header.frame_id.c_str());
105 | return;
106 | }
107 |
108 | tf::Vector3 orig_bt;
109 | tf::Matrix3x3 transform_mat(transform.getRotation());
110 | tf::vector3MsgToTF(gyro_vector, orig_bt);
111 | tf::vector3TFToMsg(orig_bt * transform_mat, gyro_vector_transformed);
112 | double yaw_gyro_reading = gyro_vector_transformed.z;
113 |
114 | // Run Motion Update
115 | if (filter_initialized_) {
116 | heading_prediction_ = curr_heading_ + yaw_gyro_reading * dt; // xp = A*x + B*u
117 | heading_variance_prediction_ = curr_heading_variance_ + heading_prediction_variance_; // Sp = A*S*A' + R
118 |
119 | if (heading_prediction_ > 3.14159)
120 | heading_prediction_ -= 2 * 3.14159;
121 | else if(heading_prediction_ < -3.14159)
122 | heading_prediction_ += 2 * 3.14159;
123 | gyro_update_complete_ = true;
124 | }
125 | curr_imu_reading_ = data;
126 | }
127 |
128 | void IMUCompass::declCallback(const std_msgs::Float32& data) {
129 | mag_declination_ = data.data;
130 | ROS_INFO("Using magnetic declination %f (%f degrees)", mag_declination_, mag_declination_ * 180 / M_PI);
131 | }
132 |
133 | void IMUCompass::magCallback(const geometry_msgs::Vector3StampedConstPtr& data) {
134 | geometry_msgs::Vector3 imu_mag = data->vector;
135 | geometry_msgs::Vector3 imu_mag_transformed;
136 |
137 | // Check for nans and bail
138 | if ( std::isnan(data->vector.x) ||
139 | std::isnan(data->vector.y) ||
140 | std::isnan(data->vector.z) ) {
141 | return;
142 | }
143 |
144 | imu_mag.x = data->vector.x;
145 | imu_mag.y = data->vector.y;
146 | imu_mag.z = data->vector.z;
147 |
148 | last_measurement_update_time_ = ros::Time::now().toSec();
149 | tf::StampedTransform transform;
150 | try {
151 | listener_.lookupTransform("base_link", data->header.frame_id, ros::Time(0), transform);
152 | } catch (tf::TransformException &ex) {
153 | ROS_WARN("Missed transform between base_link and %s", data->header.frame_id.c_str());
154 | return;
155 | }
156 |
157 | tf::Vector3 orig_bt;
158 | tf::Matrix3x3 transform_mat(transform.getRotation());
159 | tf::vector3MsgToTF(imu_mag, orig_bt);
160 | tf::vector3TFToMsg(orig_bt * transform_mat, imu_mag_transformed);
161 |
162 | // Compensate for hard iron
163 | double mag_x = imu_mag_transformed.x - mag_zero_x_;
164 | double mag_y = imu_mag_transformed.y - mag_zero_y_;
165 | double mag_z = imu_mag_transformed.z; // calibration is purely 2D
166 |
167 | // Normalize vector
168 | tf::Vector3 calib_mag(mag_x, mag_y, mag_z);
169 | calib_mag = calib_mag / magn(calib_mag);
170 | mag_x = calib_mag.x();
171 | mag_y = calib_mag.y();
172 | mag_z = calib_mag.z();
173 |
174 | geometry_msgs::Vector3Stamped calibrated_mag;
175 | calibrated_mag.header.stamp = ros::Time::now();
176 | calibrated_mag.header.frame_id = "imu_link";
177 |
178 | calibrated_mag.vector.x = calib_mag.x();
179 | calibrated_mag.vector.y = calib_mag.y();
180 | calibrated_mag.vector.z = calib_mag.z();
181 |
182 | mag_pub_.publish(calibrated_mag);
183 |
184 | tf::Quaternion q;
185 | tf::quaternionMsgToTF(curr_imu_reading_->orientation, q);
186 | tf::Transform curr_imu_meas;
187 | curr_imu_meas = tf::Transform(q, tf::Vector3(0, 0, 0));
188 | curr_imu_meas = curr_imu_meas * transform;
189 | tf::Quaternion orig (transform.getRotation());
190 |
191 | // Till Compensation
192 | tf::Matrix3x3 temp(curr_imu_meas.getRotation());
193 |
194 | double c_r, c_p, c_y;
195 | temp.getRPY(c_r, c_p, c_y);
196 | double cos_pitch = cos(c_p);
197 | double sin_pitch = sin(c_p);
198 | double cos_roll = cos(c_r);
199 | double sin_roll = sin(c_r);
200 | double t_mag_x = mag_x * cos_pitch + mag_z * sin_pitch;
201 | double t_mag_y = mag_x * sin_roll * sin_pitch + mag_y * cos_roll - mag_z * sin_roll * cos_pitch;
202 | double head_x = t_mag_x;
203 | double head_y = t_mag_y;
204 |
205 | // Retrieve magnetometer heading
206 | double heading_meas = atan2(head_x, head_y);
207 |
208 | // If this is the first magnetometer reading, initialize filter
209 | if (!first_mag_reading_) {
210 | // Initialize filter
211 | initFilter(heading_meas);
212 | first_mag_reading_ = true;
213 | return;
214 | }
215 | // If gyro update (motion update) is complete, run measurement update and publish imu data
216 | if (gyro_update_complete_) {
217 | // K = Sp*C'*inv(C*Sp*C' + Q)
218 | double kalman_gain = heading_variance_prediction_ * (1 / (heading_variance_prediction_ + yaw_meas_variance_));
219 | double innovation = heading_meas - heading_prediction_;
220 | if (abs(innovation) > M_PI) // large change, signifies a wraparound. kalman filters don't like discontinuities like wraparounds, handle seperately.
221 | curr_heading_ = heading_meas;
222 | else
223 | curr_heading_ = heading_prediction_ + kalman_gain * (innovation); // mu = mup + K*(y-C*mup)
224 |
225 | curr_heading_variance_ = (1 - kalman_gain) * heading_variance_prediction_; // S = (1-K*C)*Sp
226 |
227 | std_msgs::Float32 raw_heading_float;
228 | raw_heading_float.data = heading_meas;
229 | raw_compass_pub_.publish(raw_heading_float);
230 |
231 | repackageImuPublish(transform);
232 | gyro_update_complete_ = false;
233 | }
234 | }
235 |
236 | void IMUCompass::repackageImuPublish(tf::StampedTransform transform) {
237 | // Get Current IMU reading and Compass heading
238 | tf::Quaternion imu_reading;
239 | tf::quaternionMsgToTF(curr_imu_reading_->orientation, imu_reading);
240 | double compass_heading = curr_heading_ - mag_declination_;
241 |
242 | // Transform curr_imu_reading to base_link
243 | tf::Transform o_imu_reading;
244 | o_imu_reading = tf::Transform(imu_reading, tf::Vector3(0, 0, 0));
245 | o_imu_reading = o_imu_reading * transform;
246 | imu_reading = o_imu_reading.getRotation();
247 |
248 | // Acquire Quaternion that is the difference between the two readings
249 | tf::Quaternion compass_yaw = tf::createQuaternionFromRPY(0.0, 0.0, compass_heading);
250 | tf::Quaternion diff_yaw = tf::createQuaternionFromRPY(0.0, 0.0, compass_heading - tf::getYaw(imu_reading));
251 |
252 | // Transform the imu reading by the difference
253 | tf::Quaternion new_quaternion = diff_yaw * imu_reading;
254 |
255 | // Transform the imu reading back into imu_link
256 | sensor_msgs::Imu publish_imu;
257 | o_imu_reading = tf::Transform(new_quaternion, tf::Vector3(0, 0, 0));
258 | o_imu_reading = o_imu_reading * (transform.inverse());
259 | tf::quaternionTFToMsg(o_imu_reading.getRotation(), curr_imu_reading_->orientation);
260 |
261 | // Publish all data
262 | std_msgs::Float32 curr_heading_float;
263 | curr_heading_float.data = compass_heading;
264 | compass_pub_.publish(curr_heading_float);
265 | imu_pub_.publish(curr_imu_reading_);
266 | }
267 |
268 | void IMUCompass::initFilter(double heading_meas) {
269 | curr_heading_ = heading_meas;
270 | curr_heading_variance_ = 1; // not very sure
271 | filter_initialized_ = true;
272 | ROS_INFO("Magnetometer data received. Compass estimator initialized");
273 | }
274 |
275 | int main(int argc, char **argv) {
276 | ros::init(argc, argv, "imu_compass");
277 | ros::NodeHandle node;
278 | IMUCompass imu_heading_estimator(node);
279 | ros::spin();
280 | return 0;
281 | }
282 |
--------------------------------------------------------------------------------