mag_cal.cpp
Go to the documentation of this file.
1 /*
2  * Copyright (c) 2017 Daniel Koch and James Jackson, BYU MAGICC Lab.
3  * All rights reserved.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions are met:
7  *
8  * * Redistributions of source code must retain the above copyright notice, this
9  * list of conditions and the following disclaimer.
10  *
11  * * Redistributions in binary form must reproduce the above copyright notice,
12  * this list of conditions and the following disclaimer in the documentation
13  * and/or other materials provided with the distribution.
14  *
15  * * Neither the name of the copyright holder nor the names of its
16  * contributors may be used to endorse or promote products derived from
17  * this software without specific prior written permission.
18  *
19  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22  * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
23  * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
24  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
25  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
26  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
27  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29  * POSSIBILITY OF SUCH DAMAGE.
30  */
31 
38 #include <rosflight/mag_cal.h>
39 #include <stdio.h>
40 
41 namespace rosflight
42 {
43 
45  calibrating_(false),
46  nh_private_("~"),
47  reference_field_strength_(1.0),
48  mag_subscriber_(nh_, "/magnetometer", 1)
49 {
50  A_ = Eigen::MatrixXd::Zero(3, 3);
51  b_ = Eigen::MatrixXd::Zero(3, 1);
52  ransac_iters_ = 100;
53  inlier_thresh_ = 200;
54 
55  calibration_time_ = nh_private_.param<double>("calibration_time", 60.0);
56  measurement_skip_ = nh_private_.param<int>("measurement_skip", 20);
57 
58  param_set_client_ = nh_.serviceClient<rosflight_msgs::ParamSet>("param_set");
60 }
61 
63 {
64  // reset calibration parameters
65  bool success = true;
66  // reset soft iron parameters
67  success = success && set_param("MAG_A11_COMP", 1.0f);
68  success = success && set_param("MAG_A12_COMP", 0.0f);
69  success = success && set_param("MAG_A13_COMP", 0.0f);
70  success = success && set_param("MAG_A21_COMP", 0.0f);
71  success = success && set_param("MAG_A22_COMP", 1.0f);
72  success = success && set_param("MAG_A23_COMP", 0.0f);
73  success = success && set_param("MAG_A31_COMP", 0.0f);
74  success = success && set_param("MAG_A32_COMP", 0.0f);
75  success = success && set_param("MAG_A33_COMP", 1.0f);
76 
77  // reset hard iron parameters
78  success = success && set_param("MAG_X_BIAS", 0.0f);
79  success = success && set_param("MAG_Y_BIAS", 0.0f);
80  success = success && set_param("MAG_Z_BIAS", 0.0f);
81 
82  if (!success)
83  {
84  ROS_FATAL("Failed to reset calibration parameters");
85  return;
86  }
87 
89 
90  // wait for data to arrive
91  ros::Duration timeout(3.0);
92  ros::Time start = ros::Time::now();
93  while (ros::Time::now() - start < timeout && first_time_ && ros::ok())
94  {
95  ros::spinOnce();
96  }
97 
98  if (first_time_)
99  {
100  ROS_FATAL("No messages on magnetometer topic, unable to calibrate");
101  return;
102  }
103 
104  while(calibrating_ && ros::ok())
105  {
106  ros::spinOnce();
107  }
108 
109  if (! calibrating_)
110  {
111  //compute calibration
113 
114  // set calibration parameters
115  // set soft iron parameters
116  success = success && set_param("MAG_A11_COMP", a11());
117  success = success && set_param("MAG_A12_COMP", a12());
118  success = success && set_param("MAG_A13_COMP", a13());
119  success = success && set_param("MAG_A21_COMP", a21());
120  success = success && set_param("MAG_A22_COMP", a22());
121  success = success && set_param("MAG_A23_COMP", a23());
122  success = success && set_param("MAG_A31_COMP", a31());
123  success = success && set_param("MAG_A32_COMP", a32());
124  success = success && set_param("MAG_A33_COMP", a33());
125 
126  // set hard iron parameters
127  success = success && set_param("MAG_X_BIAS", bx());
128  success = success && set_param("MAG_Y_BIAS", by());
129  success = success && set_param("MAG_Z_BIAS", bz());
130  }
131 
132 }
133 
134 void CalibrateMag::set_reference_magnetic_field_strength(double reference_magnetic_field)
135 {
136  reference_field_strength_ = reference_magnetic_field;
137 }
138 
140 {
141  calibrating_ = true;
142 
143  first_time_ = true;
144  start_time_ = 0;
145 
146  measurement_prev_ = Eigen::Vector3d::Zero();
147  measurements_.clear();
148 }
149 
151 {
152  // fit ellipsoid to measurements according to Li paper but in RANSAC form
153  ROS_INFO("Collected %u measurements. Fitting ellipsoid.", (uint32_t)measurements_.size());
155 
156  // magnetometer calibration parameters according to Renaudin paper
157  ROS_INFO("Computing calibration parameters.");
158  magCal(u, A_, b_);
159 }
160 
161 bool CalibrateMag::mag_callback(const sensor_msgs::MagneticField::ConstPtr& mag)
162 {
163 
164  if (calibrating_)
165  {
166  if (first_time_)
167  {
168  first_time_ = false;
169  ROS_WARN_ONCE("Calibrating Mag, do the mag dance for %g seconds!", calibration_time_);
171  }
172 
173 
174  double elapsed = ros::Time::now().toSec() - start_time_;
175 
176  printf("\r%.1f seconds remaining", calibration_time_ - elapsed);
177 
178  // if still in calibration mode
179  if (elapsed < calibration_time_)
180  {
182  {
183  Eigen::Vector3d measurement;
184  measurement << mag->magnetic_field.x, mag->magnetic_field.y, mag->magnetic_field.z;
185 
186  if (measurement != measurement_prev_)
187  {
188  measurements_.push_back(measurement);
189  }
190  measurement_prev_ = measurement;
191  }
193  }
194  else
195  {
196  ROS_WARN("\rdone!");
197  calibrating_ = false;
198  }
199  }
200 
201 }
202 
203 Eigen::MatrixXd CalibrateMag::ellipsoidRANSAC(EigenSTL::vector_Vector3d meas, int iters, double inlier_thresh)
204 {
205  // initialize random number generator
206  std::random_device random_dev;
207  std::mt19937 generator(random_dev());
208 
209  // RANSAC to find a good ellipsoid fit
210  int inlier_count_best = 0; // number of inliers for best fit
211  EigenSTL::vector_Vector3d inliers_best; // container for inliers to best fit
212  double dist_sum = 0; // sum distances of all measurements from ellipsoid surface
213  int dist_count = 0; // count number distances of all measurements from ellipsoid surface
214  for (unsigned i = 0; i < iters; i++)
215  {
216  // pick 9 random, unique measurements by shuffling measurements
217  // and grabbing the first 9
218  std::shuffle(meas.begin(), meas.end(), generator);
219  EigenSTL::vector_Vector3d meas_sample;
220  for (unsigned j = 0; j < 9; j++)
221  {
222  meas_sample.push_back(meas[j]);
223  }
224 
225  // fit ellipsoid to 9 random points
226  Eigen::MatrixXd u = ellipsoidLS(meas_sample);
227 
228  // check if LS fit is actually an ellipsoid (paragraph of Li following eq. 2-4)
229  // unpack needed parts of u
230  double a = u(0);
231  double b = u(1);
232  double c = u(2);
233  double f = u(3);
234  double g = u(4);
235  double h = u(5);
236  double p = u(6);
237  double q = u(7);
238  double r = u(8);
239  double d = u(9);
240 
241  double I = a + b + c;
242  double J = a * b + b * c + a *c - f * f - g * g - h * h;
243 
244  if (4 * J - I * I <= 0)
245  {
246  continue;
247  }
248 
249  // eq. 15 of Renaudin and eqs. 1 and 4 of Li
250  Eigen::MatrixXd Q(3, 3);
251  Q << a, h, g,
252  h, b, f,
253  g, f, c;
254 
255  Eigen::MatrixXd ub(3, 1);
256  ub << 2 * p,
257  2 * q,
258  2 * r;
259  double k = d;
260 
261  // eq. 21 of Renaudin (should be negative according to eq. 16)
262  // this is the vector to the ellipsoid center
263  Eigen::MatrixXd bb = -0.5 * (Q.inverse() * ub);
264  Eigen::Vector3d r_e; r_e << bb(0), bb(1), bb(2);
265 
266  // count inliers and store inliers
267  int inlier_count = 0;
269  for (unsigned j = 0; j < meas.size(); j++)
270  {
271  // compute the vector from ellipsoid center to surface along
272  // measurement vector and a one from the perturbed measurement
273  Eigen::Vector3d perturb = Eigen::Vector3d::Ones() * 0.1;
274  Eigen::Vector3d r_int = intersect(meas[j], r_e, Q, ub, k);
275  Eigen::Vector3d r_int_prime = intersect(meas[j] + perturb, r_e, Q, ub, k);
276 
277  // now compute the vector normal to the surface
278  Eigen::Vector3d r_align = r_int_prime - r_int;
279  Eigen::Vector3d r_normal = r_align.cross(r_int.cross(r_int_prime));
280  Eigen::Vector3d i_normal = r_normal / r_normal.norm();
281 
282  // get vector from surface to measurement and take dot product
283  // with surface normal vector to find distance from ellipsoid fit
284  Eigen::Vector3d r_sm = meas[j] - r_e - r_int;
285  double dist = r_sm.dot(i_normal);
286  dist_sum += dist;
287  dist_count++;
288 
289  // check measurement distance against inlier threshold
290  if (fabs(dist) < inlier_thresh)
291  {
292  inliers.push_back(meas[j]);
293  inlier_count++;
294  }
295  }
296 
297  // save best inliers and their count
298  if (inlier_count > inlier_count_best)
299  {
300  inliers_best.clear();
301  for (unsigned j = 0; j < inliers.size(); j++)
302  {
303  inliers_best.push_back(inliers[j]);
304  }
305  inlier_count_best = inlier_count;
306  }
307  }
308 
309  // check average measurement distance from surface
310  double dist_avg = dist_sum / dist_count;
311  if (inlier_thresh > fabs(dist_avg))
312  {
313  ROS_WARN("Inlier threshold is greater than average measurement distance from surface. Reduce inlier threshold.");
314  ROS_INFO("Inlier threshold = %7.1f, Average measurement distance = %7.1f", inlier_thresh_, dist_avg);
315  }
316 
317  // perform LS on set of best inliers
318  Eigen::MatrixXd u_final = ellipsoidLS(inliers_best);
319 
320  return u_final;
321 }
322 
323 Eigen::Vector3d CalibrateMag::intersect(Eigen::Vector3d r_m, Eigen::Vector3d r_e, Eigen::MatrixXd Q, Eigen::MatrixXd ub, double k)
324 {
325  // form unit vector from ellipsoid center (r_e) pointing to measurement
326  Eigen::Vector3d r_em = r_m - r_e;
327  Eigen::Vector3d i_em = r_em / r_em.norm();
328 
329  // solve quadratic equation for alpha, which determines how much to scale
330  // i_em to intersect the ellipsoid surface (this is in Jerel's notebook)
331  double A = (i_em.transpose() * Q * i_em)(0);
332  double B = (2 * (i_em.transpose() * Q * r_e + ub.transpose() * i_em))(0);
333  double C = (ub.transpose() * r_e + r_e.transpose() * Q * r_e)(0) + k;
334  double alpha = (-B + sqrt(B * B - 4 * A * C)) / (2 * A);
335 
336  // compute vector from ellipsoid center to its surface along measurement vector
337  Eigen::Vector3d r_int = r_e + alpha * i_em;
338 
339  return r_int;
340 }
341 
342 void CalibrateMag::eigSort(Eigen::MatrixXd &w, Eigen::MatrixXd &v)
343 {
344  // create index array
345  int idx[w.cols()];
346  for (unsigned i = 0; i < w.cols(); i++)
347  {
348  idx[i] = i;
349  }
350 
351  // do a bubble sort and keep track of where values go with the index array
352  int has_changed = 1; // true
353  Eigen::MatrixXd w_sort = w;
354  while (has_changed == 1)
355  {
356  has_changed = 0; // false
357  for (unsigned i = 0; i < w.cols()-1; i++)
358  {
359  if (w_sort(i) < w_sort(i+1))
360  {
361  // switch values
362  double tmp = w_sort(i);
363  w_sort(i) = w_sort(i+1);
364  w_sort(i+1) = tmp;
365 
366  // switch indices
367  tmp = idx[i];
368  idx[i] = idx[i+1];
369  idx[i+1] = tmp;
370 
371  has_changed = 1; // true
372  }
373  }
374  }
375 
376  // add sorted eigenvalues/eigenvectors to output
377  Eigen::MatrixXd w1 = w;
378  Eigen::MatrixXd v1 = v;
379  for (unsigned i = 0; i < w.cols(); i++)
380  {
381  w1(i) = w(idx[i]);
382  v1.col(i) = v.col(idx[i]);
383  }
384  w = w1;
385  v = v1;
386 }
387 
388 /*
389  This function gets ellipsoid parameters via least squares on ellipsoidal data
390  according to the paper: Li, Qingde, and John G. Griffiths. "Least squares ellipsoid
391  specific fitting." Geometric modeling and processing, 2004. proceedings. IEEE, 2004.
392  */
394 {
395  // form D matrix from eq. 6
396  Eigen::MatrixXd D(10, meas.size());
397  for (unsigned i = 0; i < D.cols(); i++)
398  {
399  // unpack measurement components
400  double x = meas[i](0);
401  double y = meas[i](1);
402  double z = meas[i](2);
403 
404  // fill in the D matrix
405  D(0, i) = x * x;
406  D(1, i) = y * y;
407  D(2, i) = z * z;
408  D(3, i) = 2 * y * z;
409  D(4, i) = 2 * x * z;
410  D(5, i) = 2 * x * y;
411  D(6, i) = 2 * x;
412  D(7, i) = 2 * y;
413  D(8, i) = 2 * z;
414  D(9, i) = 1;
415  }
416 
417  // form the C1 matrix from eq. 7
418  double k = 4;
419  Eigen::MatrixXd C1 = Eigen::MatrixXd::Zero(6, 6);
420  C1(0, 0) = -1;
421  C1(0, 1) = k / 2 - 1;
422  C1(0, 2) = k / 2 - 1;
423  C1(1, 0) = k / 2 - 1;
424  C1(1, 1) = -1;
425  C1(1, 2) = k / 2 - 1;
426  C1(2, 0) = k / 2 - 1;
427  C1(2, 1) = k / 2 - 1;
428  C1(2, 2) = -1;
429  C1(3, 3) = -k;
430  C1(4, 4) = -k;
431  C1(5, 5) = -k;
432 
433  // decompose D*D^T according to eq. 11
434  Eigen::MatrixXd DDt = D * D.transpose();
435  Eigen::MatrixXd S11 = DDt.block(0, 0, 6, 6);
436  Eigen::MatrixXd S12 = DDt.block(0, 6, 6, 4);
437  Eigen::MatrixXd S22 = DDt.block(6, 6, 4, 4);
438 
439  // solve eigensystem in eq. 15
440  Eigen::MatrixXd ES = C1.inverse() * (S11 - S12 * S22.inverse() * S12.transpose());
441  Eigen::EigenSolver<Eigen::MatrixXd> eigensolver(ES);
442  if (eigensolver.info() != Eigen::Success)
443  {
444  abort();
445  }
446  Eigen::MatrixXd w = eigensolver.eigenvalues().real().transpose();
447  Eigen::MatrixXd V = eigensolver.eigenvectors().real();
448 
449  // sort eigenvalues and eigenvectors from most positive to most negative
450  eigSort(w, V);
451 
452  // compute solution vector defined in paragraph below eq. 15
453  Eigen::MatrixXd u1 = V.col(0);
454  Eigen::MatrixXd u2 = -(S22.inverse() * S12.transpose() * u1);
455  Eigen::MatrixXd u(10, 1);
456  u.block(0, 0, 6, 1) = u1;
457  u.block(6, 0, 4, 1) = u2;
458 
459  return u;
460 }
461 
462 /*
463  This function compute magnetometer calibration parameters according to Section 5.3 of the
464 paper: Renaudin, Valérie, Muhammad Haris Afzal, and Gérard Lachapelle. "Complete triaxis
465 magnetometer calibration in the magnetic domain." Journal of sensors 2010 (2010).
466 */
467 void CalibrateMag::magCal(Eigen::MatrixXd u, Eigen::MatrixXd &A, Eigen::MatrixXd &bb)
468 {
469  // unpack coefficients
470  double a = u(0);
471  double b = u(1);
472  double c = u(2);
473  double f = u(3);
474  double g = u(4);
475  double h = u(5);
476  double p = u(6);
477  double q = u(7);
478  double r = u(8);
479  double d = u(9);
480 
481  // compute Q, u, and k according to eq. 15 of Renaudin and eqs. 1 and 4 of Li
482  Eigen::MatrixXd Q(3, 3);
483  Q << a, h, g,
484  h, b, f,
485  g, f, c;
486 
487  Eigen::MatrixXd ub(3, 1);
488  ub << 2 * p,
489  2 * q,
490  2 * r;
491  double k = d;
492 
493  // extract bb according to eq. 21 of Renaudin (should be negative according to eq. 16)
494  bb = -0.5 * (Q.inverse() * ub);
495 
496  // eigendecomposition of Q according to eq. 22 of Renaudin
497  Eigen::EigenSolver<Eigen::MatrixXd> eigensolver(Q);
498  if (eigensolver.info() != Eigen::Success)
499  {
500  abort();
501  }
502  Eigen::MatrixXd D = eigensolver.eigenvalues().real().asDiagonal();
503  Eigen::MatrixXd V = eigensolver.eigenvectors().real();
504 
505  // compute alpha according to eq. 27 of Renaudin (the denominator needs to be multiplied by -1)
506  double Hm = reference_field_strength_; // (uT) Provo, UT magnetic field magnitude
507  Eigen::MatrixXd utVDiVtu = ub.transpose() * V * D.inverse() * V.transpose() * ub;
508  double alpha = (4. * Hm * Hm) / (utVDiVtu(0) - 4 * k);
509 
510  // now compute A from eq. 8 and 28 of Renaudin
511  A = V * (alpha * D).cwiseSqrt() * V.transpose();
512 }
513 
514 bool CalibrateMag::set_param(std::string name, double value)
515 {
516  rosflight_msgs::ParamSet srv;
517  srv.request.name = name;
518  srv.request.value = value;
519 
520  if (param_set_client_.call(srv))
521  {
522  return srv.response.exists;
523  }
524  else
525  {
526  return false;
527  }
528 
529 }
530 
531 } // namespace mag_cal
d
ServiceClient serviceClient(const std::string &service_name, bool persistent=false, const M_string &header_values=M_string())
ros::NodeHandle nh_
Definition: mag_cal.h:109
const double a23() const
Definition: mag_cal.h:98
#define ROS_FATAL(...)
Eigen::Vector3d intersect(Eigen::Vector3d r_m, Eigen::Vector3d r_e, Eigen::MatrixXd Q, Eigen::MatrixXd ub, double k)
Definition: mag_cal.cpp:323
message_filters::Subscriber< sensor_msgs::MagneticField > mag_subscriber_
Definition: mag_cal.h:112
Eigen::MatrixXd b_
Definition: mag_cal.h:117
std::vector< Eigen::Vector3d, Eigen::aligned_allocator< Eigen::Vector3d > > vector_Vector3d
const double a21() const
Definition: mag_cal.h:96
f
double calibration_time_
seconds to record data for temperature compensation
Definition: mag_cal.h:123
bool calibrating_
whether a temperature calibration is in progress
Definition: mag_cal.h:121
void set_reference_magnetic_field_strength(double reference_magnetic_field)
Definition: mag_cal.cpp:134
bool call(MReq &req, MRes &res)
ros::NodeHandle nh_private_
Definition: mag_cal.h:110
bool first_time_
waiting for first measurement for calibration
Definition: mag_cal.h:122
const double bz() const
Definition: mag_cal.h:104
int ransac_iters_
number of ransac iterations to fit ellipsoid to mag measurements
Definition: mag_cal.h:125
Eigen::MatrixXd A_
Definition: mag_cal.h:117
const double a31() const
Definition: mag_cal.h:99
TFSIMD_FORCE_INLINE const tfScalar & y() const
#define ROS_WARN(...)
Eigen::MatrixXd ellipsoidRANSAC(EigenSTL::vector_Vector3d meas, int iters, double inlier_thresh)
Definition: mag_cal.cpp:203
const double a32() const
Definition: mag_cal.h:100
bool set_param(std::string name, double value)
Definition: mag_cal.cpp:514
#define ROS_WARN_ONCE(...)
#define ROS_INFO(...)
bool param(const std::string &param_name, T &param_val, const T &default_val) const
const double a13() const
Definition: mag_cal.h:95
const double by() const
Definition: mag_cal.h:103
ROSCPP_DECL bool ok()
ros::ServiceClient param_set_client_
Definition: mag_cal.h:115
TFSIMD_FORCE_INLINE const tfScalar & x() const
Eigen::Vector3d measurement_prev_
Definition: mag_cal.h:129
const double a33() const
Definition: mag_cal.h:101
const double a11() const
The const stuff is to make it read-only.
Definition: mag_cal.h:93
TFSIMD_FORCE_INLINE const tfScalar & z() const
double inlier_thresh_
threshold to consider measurement an inlier in ellipsoidRANSAC
Definition: mag_cal.h:128
double start_time_
timestamp of first calibration measurement
Definition: mag_cal.h:124
TFSIMD_FORCE_INLINE const tfScalar & w() const
void magCal(Eigen::MatrixXd u, Eigen::MatrixXd &A, Eigen::MatrixXd &bb)
Definition: mag_cal.cpp:467
Eigen::MatrixXd ellipsoidLS(EigenSTL::vector_Vector3d meas)
Definition: mag_cal.cpp:393
void start_mag_calibration()
Begin the magnetometer calibration routine.
Definition: mag_cal.cpp:139
static Time now()
bool mag_callback(const sensor_msgs::MagneticField::ConstPtr &mag)
set_refence_magnetic_field_strength
Definition: mag_cal.cpp:161
EigenSTL::vector_Vector3d measurements_
Definition: mag_cal.h:130
const double a12() const
Definition: mag_cal.h:94
ROSCPP_DECL void spinOnce()
double reference_field_strength_
the strength of earth&#39;s magnetic field at your location
Definition: mag_cal.h:119
void eigSort(Eigen::MatrixXd &w, Eigen::MatrixXd &v)
Definition: mag_cal.cpp:342
const double bx() const
Definition: mag_cal.h:102
const double a22() const
Definition: mag_cal.h:97
Connection registerCallback(const C &callback)


rosflight
Author(s): Daniel Koch , James Jackson
autogenerated on Wed Jul 3 2019 20:00:12