magnetometer_calibration.cpp
Go to the documentation of this file.
1 /*
2  * Copyright (C) 2020 Michael Ferguson
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 // Author: Michael Ferguson
18 
19 #include <vector>
20 
22 
23 #include <ros/ros.h>
24 #include <rosbag/bag.h>
25 #include <rosbag/view.h>
26 #include <rosbag/query.h>
27 #include <geometry_msgs/Twist.h>
28 #include <geometry_msgs/Vector3Stamped.h>
29 #include <sensor_msgs/MagneticField.h>
30 
32 {
33 public:
34  MagnetometerCapture(const std::string& topic, ros::NodeHandle& nh)
35  {
36  ROS_INFO_STREAM("Subscribing to " << topic);
37  subscriber_ = nh.subscribe(topic,
38  1,
40  this);
41  }
42 
43  void getData(std::vector<sensor_msgs::MagneticField>& data)
44  {
45  data = data_;
46  }
47 
48 private:
49  void callback(const sensor_msgs::MagneticFieldConstPtr& msg)
50  {
51  data_.push_back(*msg);
52  }
53 
55  std::vector<sensor_msgs::MagneticField> data_;
56 };
57 
58 int main(int argc, char** argv)
59 {
60  ros::init(argc, argv,"magnetometer_calibration");
61  ros::NodeHandle nh("~");
62 
63  /*
64  * "hard iron" offset is the result of nearby permanent ferromagnetic
65  * elements (for instance, components on the PCB.
66  *
67  * "soft iron" effect is induced by the geomagnetic field onto normally
68  * unmagnetized ferromagnetic elements.
69  */
70  bool soft_iron = false;
71  nh.getParam("soft_iron", soft_iron);
72  if (soft_iron)
73  {
74  ROS_ERROR("Soft Iron Calibration Is Not Yet Available.");
75  }
76 
77  /*
78  * Speed and duration to rotate the robot.
79  * Setting either of these to 0.0 will skip rotation.
80  */
81  double rotation_velocity = 0.2; // rad/s
82  double rotation_duration = 60.0; // seconds
83  nh.getParam("rotation_velocity", rotation_velocity);
84  nh.getParam("rotation_duration", rotation_duration);
85 
86  /*
87  * If this is a handheld IMU, you can manually
88  * rotate the sensor and press ENTER when done.
89  */
90  bool rotation_manual = false;
91  nh.getParam("rotation_manual", rotation_manual);
92 
93  // Name of bagfile to create and/or use
94  std::string bag_file_name = "/tmp/magnetometer_calibration.bag";
95  nh.getParam("bag_file_name", bag_file_name);
96 
97  // Determine topic name (including remapping)
98  std::string mag_topic_name = "/imu/mag";
99  mag_topic_name = ros::names::resolve(mag_topic_name);
100 
101  // Get calibration data
102  std::vector<sensor_msgs::MagneticField> data;
103  if (rotation_velocity <= 0.0 && rotation_duration <= 0.0 && !rotation_manual)
104  {
105  // Load calibration data from bag file
106  rosbag::Bag bag;
107  try
108  {
109  bag.open(bag_file_name, rosbag::bagmode::Read);
110  }
111  catch (rosbag::BagException&)
112  {
113  ROS_FATAL_STREAM("Cannot open " << bag_file_name);
114  return -1;
115  }
116  rosbag::View bag_view(bag, rosbag::TopicQuery(mag_topic_name));
117 
118  for (auto m : bag_view)
119  {
120  sensor_msgs::MagneticField::ConstPtr msg = m.instantiate<sensor_msgs::MagneticField>();
121  if (msg == NULL)
122  {
123  // Try as Vector3Stamped
124  geometry_msgs::Vector3Stamped::ConstPtr vec = m.instantiate<geometry_msgs::Vector3Stamped>();
125  if (vec != NULL)
126  {
127  sensor_msgs::MagneticField field;
128  field.header = vec->header;
129  field.magnetic_field.x = vec->vector.x;
130  field.magnetic_field.y = vec->vector.y;
131  field.magnetic_field.z = vec->vector.z;
132  data.push_back(field);
133  }
134  }
135  else
136  {
137  data.push_back(*msg);
138  }
139  }
140 
141  ROS_INFO_STREAM("Loaded bag file with " << data.size() << " samples");
142  }
143  else
144  {
145  // Create calibration (and bag file)
146  MagnetometerCapture capture(mag_topic_name, nh);
147 
148  if (rotation_manual)
149  {
150  // Need to process messages in background
152  spinner.start();
153 
154  // Wait for keypress
155  ROS_INFO("Rotate the sensor to many orientations.");
156  ROS_INFO("Press key when done rotating");
157  std::string throwaway;
158  std::getline(std::cin, throwaway);
159  }
160  else
161  {
162  // Rotate the robot
163  ROS_INFO_STREAM("Publishing to " << ros::names::resolve("/cmd_vel"));
164  ros::Publisher pub = nh.advertise<geometry_msgs::Twist>("/cmd_vel", 1);
165 
166  geometry_msgs::Twist msg;
167  msg.angular.z = rotation_velocity;
168 
169  ROS_INFO_STREAM("Rotating robot for " << rotation_duration << " seconds.");
170 
171  ros::Time start = ros::Time::now();
172  while (ros::ok() && (ros::Time::now() - start).toSec() < rotation_duration)
173  {
174  pub.publish(msg);
175  ros::spinOnce();
176  ros::Duration(0.1).sleep();
177  }
178 
179  msg.angular.z = 0.0;
180  pub.publish(msg);
181  ros::spinOnce();
182 
183  ROS_INFO("Done rotating robot.");
184  }
185 
186  // Get data
187  capture.getData(data);
188 
189  // Save data to bag file
190  ROS_INFO_STREAM("Saving bag file with " << data.size() << " samples.");
191  rosbag::Bag bag;
192  bag.open(bag_file_name, rosbag::bagmode::Write);
193  for (auto msg : data)
194  {
195  bag.write(mag_topic_name, msg.header.stamp, msg);
196  }
197  }
198 
199  // Setup free parameters
200  size_t num_params = 4;
201  if (soft_iron)
202  {
203  num_params += 6;
204  }
205  double* free_params = new double[num_params];
206  // Set initial estimate of magnetic field strength
207  free_params[0] = 0.45;
208  // Setup initial estimates of hard iron offsets (bias)
209  {
210  double bias_x = 0.0;
211  double bias_y = 0.0;
212  double bias_z = 0.0;
213  for (auto msg : data)
214  {
215  bias_x += msg.magnetic_field.x;
216  bias_y += msg.magnetic_field.y;
217  bias_z += msg.magnetic_field.z;
218  }
219  bias_x /= data.size();
220  bias_y /= data.size();
221  bias_z /= data.size();
222 
223  free_params[1] = bias_x;
224  free_params[2] = bias_y;
225  free_params[3] = bias_z;
226 
227  ROS_INFO_STREAM("Initial estimate for hard iron offsets: [" <<
228  bias_x << ", " <<
229  bias_y << ", " <<
230  bias_z << "]");
231  }
232 
233  // Setup error blocks
234  ceres::Problem* problem = new ceres::Problem();
235  ceres::TrivialLoss loss;
236  for (auto msg : data)
237  {
238  auto m = msg.magnetic_field;
239  if (soft_iron)
240  {
241 
242  }
243  else
244  {
245  problem->AddResidualBlock(
246  HardIronOffsetError::Create(m.x, m.y, m.z),
247  &loss, // squared loss
248  free_params);
249  }
250  }
251 
252  // Run calibration
253  ceres::Solver::Options options;
254  options.use_nonmonotonic_steps = true;
255  options.function_tolerance = 1e-10;
256  options.linear_solver_type = ceres::DENSE_QR;
257  options.max_num_iterations = 1000;
258  options.minimizer_progress_to_stdout = false;
259 
260  ceres::Solver::Summary summary;
261  ceres::Solve(options, problem, &summary);
262  ROS_INFO_STREAM(summary.BriefReport());
263 
264  // Save results
265  ROS_INFO_STREAM("Estimated total magnetic field: " << free_params[0] << "T");
266  ROS_INFO("You can compare to expected values from");
267  ROS_INFO(" https://www.ngdc.noaa.gov/geomag/calculators/magcalc.shtml#igrfwmm");
268 
269  std::cout << "mag_bias_x: " << free_params[1] << std::endl;
270  std::cout << "mag_bias_y: " << free_params[2] << std::endl;
271  std::cout << "mag_bias_z: " << free_params[3] << std::endl;
272  delete[] free_params;
273 
274  return 0;
275 }
void publish(const boost::shared_ptr< M > &message) const
void open(std::string const &filename, uint32_t mode=bagmode::Read)
Subscriber subscribe(const std::string &topic, uint32_t queue_size, void(T::*fp)(M), T *obj, const TransportHints &transport_hints=TransportHints())
bool sleep() const
void getData(std::vector< sensor_msgs::MagneticField > &data)
ROSCPP_DECL void init(int &argc, char **argv, const std::string &name, uint32_t options=0)
ROSCPP_DECL std::string resolve(const std::string &name, bool remap=true)
void spinner()
MagnetometerCapture(const std::string &topic, ros::NodeHandle &nh)
#define ROS_FATAL_STREAM(args)
#define ROS_INFO(...)
options
static ceres::CostFunction * Create(double x, double y, double z)
ROSCPP_DECL bool ok()
Publisher advertise(const std::string &topic, uint32_t queue_size, bool latch=false)
void callback(const sensor_msgs::MagneticFieldConstPtr &msg)
int main(int argc, char **argv)
std::vector< sensor_msgs::MagneticField > data_
#define ROS_INFO_STREAM(args)
bool getParam(const std::string &key, std::string &s) const
static Time now()
ROSCPP_DECL void spinOnce()
#define ROS_ERROR(...)


robot_calibration
Author(s): Michael Ferguson
autogenerated on Tue Nov 3 2020 17:30:30