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 }