View Javadoc
1   /*
2    * Copyright (C) 2020 Alberto Irurueta Carro (alberto@irurueta.com)
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  package com.irurueta.navigation.inertial.calibration.magnetometer;
17  
18  import com.irurueta.algebra.Matrix;
19  import com.irurueta.navigation.LockedException;
20  import com.irurueta.navigation.NotReadyException;
21  import com.irurueta.navigation.inertial.calibration.CalibrationException;
22  import com.irurueta.navigation.inertial.calibration.StandardDeviationFrameBodyMagneticFluxDensity;
23  import com.irurueta.numerical.robust.PROMedSRobustEstimator;
24  import com.irurueta.numerical.robust.PROMedSRobustEstimatorListener;
25  import com.irurueta.numerical.robust.RobustEstimator;
26  import com.irurueta.numerical.robust.RobustEstimatorException;
27  import com.irurueta.numerical.robust.RobustEstimatorMethod;
28  
29  import java.io.IOException;
30  import java.util.List;
31  
32  /**
33   * Robustly estimates magnetometer soft-iron cross
34   * couplings and scaling factors using PROMedS algorithm.
35   * <p>
36   * To use this calibrator at least 4 measurements at different known
37   * frames must be provided. In other words, magnetometer samples must
38   * be obtained at 4 different positions or orientations.
39   * Notice that frame velocities are ignored by this calibrator.
40   * <p>
41   * Measured magnetic flux density is assumed to follow the model shown below:
42   * <pre>
43   *     mBmeas = bm + (I + Mm) * mBtrue + w
44   * </pre>
45   * Where:
46   * - mBmeas is the measured magnetic flux density. This is a 3x1 vector.
47   * - bm is magnetometer hard-iron bias. Ideally, on a perfect magnetometer,
48   * this should be a 3x1 zero vector.
49   * - I is the 3x3 identity matrix.
50   * - Mm is the 3x3 soft-iron matrix containing cross-couplings and scaling
51   * factors. Ideally, on a perfect magnetometer, this should be a 3x3 zero
52   * matrix.
53   * - mBtrue is ground-truth magnetic flux density. This is a 3x1 vector.
54   * - w is measurement noise. This is a 3x1 vector.
55   */
56  public class PROMedSRobustKnownHardIronAndFrameMagnetometerCalibrator extends
57          RobustKnownHardIronAndFrameMagnetometerCalibrator {
58  
59      /**
60       * Default value to be used for stop threshold. Stop threshold can be used to
61       * avoid keeping the algorithm unnecessarily iterating in case that best
62       * estimated threshold using median of residuals is not small enough. Once a
63       * solution is found that generates a threshold below this value, the
64       * algorithm will stop.
65       * The stop threshold can be used to prevent the LMedS algorithm iterating
66       * too many times in cases where samples have a very similar accuracy.
67       * For instance, in cases where proportion of outliers is very small (close
68       * to 0%), and samples are very accurate (i.e. 1e-6), the algorithm would
69       * iterate for a long time trying to find the best solution when indeed
70       * there is no need to do that if a reasonable threshold has already been
71       * reached.
72       * Because of this behaviour the stop threshold can be set to a value much
73       * lower than the one typically used in RANSAC, and yet the algorithm could
74       * still produce even smaller thresholds in estimated results.
75       */
76      public static final double DEFAULT_STOP_THRESHOLD = 500e-9;
77  
78      /**
79       * Minimum allowed stop threshold value.
80       */
81      public static final double MIN_STOP_THRESHOLD = 0.0;
82  
83      /**
84       * Threshold to be used to keep the algorithm iterating in case that best
85       * estimated threshold using median of residuals is not small enough. Once
86       * a solution is found that generates a threshold below this value, the
87       * algorithm will stop.
88       * The stop threshold can be used to prevent the LMedS algorithm iterating
89       * too many times in cases where samples have a very similar accuracy.
90       * For instance, in cases where proportion of outliers is very small (close
91       * to 0%), and samples are very accurate (i.e. 1e-6), the algorithm would
92       * iterate for a long time trying to find the best solution when indeed
93       * there is no need to do that if a reasonable threshold has already been
94       * reached.
95       * Because of this behaviour the stop threshold can be set to a value much
96       * lower than the one typically used in RANSAC, and yet the algorithm could
97       * still produce even smaller thresholds in estimated results.
98       */
99      private double stopThreshold = DEFAULT_STOP_THRESHOLD;
100 
101     /**
102      * Quality scores corresponding to each provided sample.
103      * The larger the score value the better the quality of the sample.
104      */
105     private double[] qualityScores;
106 
107     /**
108      * Constructor.
109      */
110     public PROMedSRobustKnownHardIronAndFrameMagnetometerCalibrator() {
111     }
112 
113     /**
114      * Constructor.
115      *
116      * @param listener listener to be notified of events such as when estimation
117      *                 starts, ends or its progress significantly changes.
118      */
119     public PROMedSRobustKnownHardIronAndFrameMagnetometerCalibrator(
120             final RobustKnownHardIronAndFrameMagnetometerCalibratorListener listener) {
121         super(listener);
122     }
123 
124     /**
125      * Constructor.
126      *
127      * @param measurements list of body magnetic flux density measurements with standard
128      *                     deviations taken at different frames (positions and
129      *                     orientations).
130      */
131     public PROMedSRobustKnownHardIronAndFrameMagnetometerCalibrator(
132             final List<StandardDeviationFrameBodyMagneticFluxDensity> measurements) {
133         super(measurements);
134     }
135 
136     /**
137      * Constructor.
138      *
139      * @param measurements list of body magnetic flux density measurements with standard
140      *                     deviations taken at different frames (positions and
141      *                     orientations).
142      * @param listener     listener to handle events raised by this calibrator.
143      */
144     public PROMedSRobustKnownHardIronAndFrameMagnetometerCalibrator(
145             final List<StandardDeviationFrameBodyMagneticFluxDensity> measurements,
146             final RobustKnownHardIronAndFrameMagnetometerCalibratorListener listener) {
147         super(measurements, listener);
148     }
149 
150     /**
151      * Constructor.
152      *
153      * @param commonAxisUsed indicates whether z-axis is assumed to be common
154      *                       for the accelerometer, gyroscope and magnetometer.
155      */
156     public PROMedSRobustKnownHardIronAndFrameMagnetometerCalibrator(final boolean commonAxisUsed) {
157         super(commonAxisUsed);
158     }
159 
160     /**
161      * Constructor.
162      *
163      * @param commonAxisUsed indicates whether z-axis is assumed to be common
164      *                       for the accelerometer, gyroscope and magnetometer.
165      * @param listener       listener to handle events raised by this calibrator.
166      */
167     public PROMedSRobustKnownHardIronAndFrameMagnetometerCalibrator(
168             final boolean commonAxisUsed, final RobustKnownHardIronAndFrameMagnetometerCalibratorListener listener) {
169         super(commonAxisUsed, listener);
170     }
171 
172     /**
173      * Constructor.
174      *
175      * @param measurements   list of body magnetic flux density measurements with standard
176      *                       deviations taken at different frames (positions and
177      *                       orientations).
178      * @param commonAxisUsed indicates whether z-axis is assumed to be common
179      *                       for the accelerometer, gyroscope and magnetometer.
180      */
181     public PROMedSRobustKnownHardIronAndFrameMagnetometerCalibrator(
182             final List<StandardDeviationFrameBodyMagneticFluxDensity> measurements, final boolean commonAxisUsed) {
183         super(measurements, commonAxisUsed);
184     }
185 
186     /**
187      * Constructor.
188      *
189      * @param measurements   list of body magnetic flux density measurements with standard
190      *                       deviations taken at different frames (positions and
191      *                       orientations).
192      * @param commonAxisUsed indicates whether z-axis is assumed to be common
193      *                       for the accelerometer, gyroscope and magnetometer.
194      * @param listener       listener to handle events raised by this calibrator.
195      */
196     public PROMedSRobustKnownHardIronAndFrameMagnetometerCalibrator(
197             final List<StandardDeviationFrameBodyMagneticFluxDensity> measurements, final boolean commonAxisUsed,
198             final RobustKnownHardIronAndFrameMagnetometerCalibratorListener listener) {
199         super(measurements, commonAxisUsed, listener);
200     }
201 
202     /**
203      * Constructor.
204      *
205      * @param qualityScores quality scores corresponding to each provided
206      *                      measurement. The larger the score value the better
207      *                      the quality of the sample.
208      * @throws IllegalArgumentException if provided quality scores length
209      *                                  is smaller than 3 samples.
210      */
211     public PROMedSRobustKnownHardIronAndFrameMagnetometerCalibrator(final double[] qualityScores) {
212         super();
213         internalSetQualityScores(qualityScores);
214     }
215 
216     /**
217      * Constructor.
218      *
219      * @param qualityScores quality scores corresponding to each provided
220      *                      measurement. The larger the score value the better
221      *                      the quality of the sample.
222      * @param listener      listener to be notified of events such as when estimation
223      *                      starts, ends or its progress significantly changes.
224      * @throws IllegalArgumentException if provided quality scores length
225      *                                  is smaller than 3 samples.
226      */
227     public PROMedSRobustKnownHardIronAndFrameMagnetometerCalibrator(
228             final double[] qualityScores, final RobustKnownHardIronAndFrameMagnetometerCalibratorListener listener) {
229         super(listener);
230         internalSetQualityScores(qualityScores);
231     }
232 
233     /**
234      * Constructor.
235      *
236      * @param qualityScores quality scores corresponding to each provided
237      *                      measurement. The larger the score value the better
238      *                      the quality of the sample.
239      * @param measurements  list of body magnetic flux density measurements with standard
240      *                      deviations taken at different frames (positions and
241      *                      orientations).
242      * @throws IllegalArgumentException if provided quality scores length
243      *                                  is smaller than 3 samples.
244      */
245     public PROMedSRobustKnownHardIronAndFrameMagnetometerCalibrator(
246             final double[] qualityScores, final List<StandardDeviationFrameBodyMagneticFluxDensity> measurements) {
247         super(measurements);
248         internalSetQualityScores(qualityScores);
249     }
250 
251     /**
252      * Constructor.
253      *
254      * @param qualityScores quality scores corresponding to each provided
255      *                      measurement. The larger the score value the better
256      *                      the quality of the sample.
257      * @param measurements  list of body magnetic flux density measurements with standard
258      *                      deviations taken at different frames (positions and
259      *                      orientations).
260      * @param listener      listener to handle events raised by this calibrator.
261      * @throws IllegalArgumentException if provided quality scores length
262      *                                  is smaller than 3 samples.
263      */
264     public PROMedSRobustKnownHardIronAndFrameMagnetometerCalibrator(
265             final double[] qualityScores, final List<StandardDeviationFrameBodyMagneticFluxDensity> measurements,
266             final RobustKnownHardIronAndFrameMagnetometerCalibratorListener listener) {
267         super(measurements, listener);
268         internalSetQualityScores(qualityScores);
269     }
270 
271     /**
272      * Constructor.
273      *
274      * @param qualityScores  quality scores corresponding to each provided
275      *                       measurement. The larger the score value the better
276      *                       the quality of the sample.
277      * @param commonAxisUsed indicates whether z-axis is assumed to be common
278      *                       for the accelerometer, gyroscope and magnetometer.
279      * @throws IllegalArgumentException if provided quality scores length
280      *                                  is smaller than 3 samples.
281      */
282     public PROMedSRobustKnownHardIronAndFrameMagnetometerCalibrator(
283             final double[] qualityScores, final boolean commonAxisUsed) {
284         super(commonAxisUsed);
285         internalSetQualityScores(qualityScores);
286     }
287 
288     /**
289      * Constructor.
290      *
291      * @param qualityScores  quality scores corresponding to each provided
292      *                       measurement. The larger the score value the better
293      *                       the quality of the sample.
294      * @param commonAxisUsed indicates whether z-axis is assumed to be common
295      *                       for the accelerometer, gyroscope and magnetometer.
296      * @param listener       listener to handle events raised by this calibrator.
297      * @throws IllegalArgumentException if provided quality scores length
298      *                                  is smaller than 3 samples.
299      */
300     public PROMedSRobustKnownHardIronAndFrameMagnetometerCalibrator(
301             final double[] qualityScores, final boolean commonAxisUsed,
302             final RobustKnownHardIronAndFrameMagnetometerCalibratorListener listener) {
303         super(commonAxisUsed, listener);
304         internalSetQualityScores(qualityScores);
305     }
306 
307     /**
308      * Constructor.
309      *
310      * @param qualityScores  quality scores corresponding to each provided
311      *                       measurement. The larger the score value the better
312      *                       the quality of the sample.
313      * @param measurements   list of body magnetic flux density measurements with standard
314      *                       deviations taken at different frames (positions and
315      *                       orientations).
316      * @param commonAxisUsed indicates whether z-axis is assumed to be common
317      *                       for the accelerometer, gyroscope and magnetometer.
318      * @throws IllegalArgumentException if provided quality scores length
319      *                                  is smaller than 3 samples.
320      */
321     public PROMedSRobustKnownHardIronAndFrameMagnetometerCalibrator(
322             final double[] qualityScores, final List<StandardDeviationFrameBodyMagneticFluxDensity> measurements,
323             final boolean commonAxisUsed) {
324         super(measurements, commonAxisUsed);
325         internalSetQualityScores(qualityScores);
326     }
327 
328     /**
329      * Constructor.
330      *
331      * @param qualityScores  quality scores corresponding to each provided
332      *                       measurement. The larger the score value the better
333      *                       the quality of the sample.
334      * @param measurements   list of body magnetic flux density measurements with standard
335      *                       deviations taken at different frames (positions and
336      *                       orientations).
337      * @param commonAxisUsed indicates whether z-axis is assumed to be common
338      *                       for the accelerometer, gyroscope and magnetometer.
339      * @param listener       listener to handle events raised by this calibrator.
340      * @throws IllegalArgumentException if provided quality scores length
341      *                                  is smaller than 3 samples.
342      */
343     public PROMedSRobustKnownHardIronAndFrameMagnetometerCalibrator(
344             final double[] qualityScores, final List<StandardDeviationFrameBodyMagneticFluxDensity> measurements,
345             final boolean commonAxisUsed, final RobustKnownHardIronAndFrameMagnetometerCalibratorListener listener) {
346         super(measurements, commonAxisUsed, listener);
347         internalSetQualityScores(qualityScores);
348     }
349 
350     /**
351      * Returns threshold to be used to keep the algorithm iterating in case that
352      * best estimated threshold using median of residuals is not small enough.
353      * Once a solution is found that generates a threshold below this value, the
354      * algorithm will stop.
355      * The stop threshold can be used to prevent the LMedS algorithm to iterate
356      * too many times in cases where samples have a very similar accuracy.
357      * For instance, in cases where proportion of outliers is very small (close
358      * to 0%), and samples are very accurate (i.e. 1e-6), the algorithm would
359      * iterate for a long time trying to find the best solution when indeed
360      * there is no need to do that if a reasonable threshold has already been
361      * reached.
362      * Because of this behaviour the stop threshold can be set to a value much
363      * lower than the one typically used in RANSAC, and yet the algorithm could
364      * still produce even smaller thresholds in estimated results.
365      *
366      * @return stop threshold to stop the algorithm prematurely when a certain
367      * accuracy has been reached.
368      */
369     public double getStopThreshold() {
370         return stopThreshold;
371     }
372 
373     /**
374      * Sets threshold to be used to keep the algorithm iterating in case that
375      * best estimated threshold using median of residuals is not small enough.
376      * Once a solution is found that generates a threshold below this value,
377      * the algorithm will stop.
378      * The stop threshold can be used to prevent the LMedS algorithm to iterate
379      * too many times in cases where samples have a very similar accuracy.
380      * For instance, in cases where proportion of outliers is very small (close
381      * to 0%), and samples are very accurate (i.e. 1e-6), the algorithm would
382      * iterate for a long time trying to find the best solution when indeed
383      * there is no need to do that if a reasonable threshold has already been
384      * reached.
385      * Because of this behaviour the stop threshold can be set to a value much
386      * lower than the one typically used in RANSAC, and yet the algorithm could
387      * still produce even smaller thresholds in estimated results.
388      *
389      * @param stopThreshold stop threshold to stop the algorithm prematurely
390      *                      when a certain accuracy has been reached.
391      * @throws IllegalArgumentException if provided value is zero or negative.
392      * @throws LockedException          if calibrator is currently running.
393      */
394     public void setStopThreshold(final double stopThreshold) throws LockedException {
395         if (running) {
396             throw new LockedException();
397         }
398         if (stopThreshold <= MIN_STOP_THRESHOLD) {
399             throw new IllegalArgumentException();
400         }
401 
402         this.stopThreshold = stopThreshold;
403     }
404 
405     /**
406      * Returns quality scores corresponding to each provided sample.
407      * The larger the score value the better the quality of the sample.
408      *
409      * @return quality scores corresponding to each sample.
410      */
411     @Override
412     public double[] getQualityScores() {
413         return qualityScores;
414     }
415 
416     /**
417      * Sets quality scores corresponding to each provided sample.
418      * The larger the score value the better the quality of the sample.
419      *
420      * @param qualityScores quality scores corresponding to each sample.
421      * @throws IllegalArgumentException if provided quality scores length
422      *                                  is smaller than minimum required samples.
423      * @throws LockedException          if calibrator is currently running.
424      */
425     @Override
426     public void setQualityScores(final double[] qualityScores) throws LockedException {
427         if (running) {
428             throw new LockedException();
429         }
430         internalSetQualityScores(qualityScores);
431     }
432 
433     /**
434      * Indicates whether solver is ready to find a solution.
435      *
436      * @return true if solver is ready, false otherwise.
437      */
438     @Override
439     public boolean isReady() {
440         return super.isReady() && qualityScores != null && qualityScores.length == measurements.size();
441     }
442 
443     /**
444      * Estimates magnetometer calibration parameters containing soft-iron
445      * scale factors and cross-coupling errors.
446      *
447      * @throws LockedException      if calibrator is currently running.
448      * @throws NotReadyException    if calibrator is not ready.
449      * @throws CalibrationException if estimation fails for numerical reasons.
450      */
451     @SuppressWarnings("DuplicatedCode")
452     @Override
453     public void calibrate() throws LockedException, NotReadyException, CalibrationException {
454         if (running) {
455             throw new LockedException();
456         }
457         if (!isReady()) {
458             throw new NotReadyException();
459         }
460 
461         final var innerEstimator = new PROMedSRobustEstimator<>(new PROMedSRobustEstimatorListener<Matrix>() {
462             @Override
463             public double[] getQualityScores() {
464                 return qualityScores;
465             }
466 
467             @Override
468             public double getThreshold() {
469                 return stopThreshold;
470             }
471 
472             @Override
473             public int getTotalSamples() {
474                 return measurements.size();
475             }
476 
477             @Override
478             public int getSubsetSize() {
479                 return preliminarySubsetSize;
480             }
481 
482             @Override
483             public void estimatePreliminarSolutions(final int[] samplesIndices, final List<Matrix> solutions) {
484                 computePreliminarySolutions(samplesIndices, solutions);
485             }
486 
487             @Override
488             public double computeResidual(final Matrix currentEstimation, final int i) {
489                 return computeError(measurements.get(i), currentEstimation);
490             }
491 
492             @Override
493             public boolean isReady() {
494                 return PROMedSRobustKnownHardIronAndFrameMagnetometerCalibrator.this.isReady();
495             }
496 
497             @Override
498             public void onEstimateStart(final RobustEstimator<Matrix> estimator) {
499                 // no action needed
500             }
501 
502             @Override
503             public void onEstimateEnd(final RobustEstimator<Matrix> estimator) {
504                 // no action needed
505             }
506 
507             @Override
508             public void onEstimateNextIteration(final RobustEstimator<Matrix> estimator, final int iteration) {
509                 if (listener != null) {
510                     listener.onCalibrateNextIteration(
511                             PROMedSRobustKnownHardIronAndFrameMagnetometerCalibrator.this, iteration);
512                 }
513             }
514 
515             @Override
516             public void onEstimateProgressChange(final RobustEstimator<Matrix> estimator, final float progress) {
517                 if (listener != null) {
518                     listener.onCalibrateProgressChange(
519                             PROMedSRobustKnownHardIronAndFrameMagnetometerCalibrator.this, progress);
520                 }
521             }
522         });
523 
524         try {
525             running = true;
526 
527             if (listener != null) {
528                 listener.onCalibrateStart(this);
529             }
530 
531             inliersData = null;
532 
533             setupWmmEstimator();
534 
535             innerEstimator.setUseInlierThresholds(true);
536             innerEstimator.setConfidence(confidence);
537             innerEstimator.setMaxIterations(maxIterations);
538             innerEstimator.setProgressDelta(progressDelta);
539             final var preliminaryResult = innerEstimator.estimate();
540             inliersData = innerEstimator.getInliersData();
541 
542             attemptRefine(preliminaryResult);
543 
544             if (listener != null) {
545                 listener.onCalibrateEnd(this);
546             }
547 
548         } catch (final com.irurueta.numerical.LockedException e) {
549             throw new LockedException(e);
550         } catch (final com.irurueta.numerical.NotReadyException e) {
551             throw new NotReadyException(e);
552         } catch (final RobustEstimatorException | IOException e) {
553             throw new CalibrationException(e);
554         } finally {
555             running = false;
556         }
557     }
558 
559     /**
560      * Returns method being used for robust estimation.
561      *
562      * @return method being used for robust estimation.
563      */
564     @Override
565     public RobustEstimatorMethod getMethod() {
566         return RobustEstimatorMethod.PROMEDS;
567     }
568 
569     /**
570      * Indicates whether this calibrator requires quality scores for each
571      * measurement or not.
572      *
573      * @return true if quality scores are required, false otherwise.
574      */
575     @Override
576     public boolean isQualityScoresRequired() {
577         return true;
578     }
579 
580     /**
581      * Sets quality scores corresponding to each provided sample.
582      * This method is used internally and does not check whether instance is
583      * locked or not.
584      *
585      * @param qualityScores quality scores to be set.
586      * @throws IllegalArgumentException if provided quality scores length
587      *                                  is smaller than 3 samples.
588      */
589     private void internalSetQualityScores(final double[] qualityScores) {
590         if (qualityScores == null || qualityScores.length < MINIMUM_MEASUREMENTS) {
591             throw new IllegalArgumentException();
592         }
593 
594         this.qualityScores = qualityScores;
595     }
596 }