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.navigation.LockedException;
19 import com.irurueta.navigation.NotReadyException;
20 import com.irurueta.navigation.inertial.calibration.CalibrationException;
21 import com.irurueta.navigation.inertial.calibration.StandardDeviationFrameBodyMagneticFluxDensity;
22 import com.irurueta.numerical.robust.RANSACRobustEstimator;
23 import com.irurueta.numerical.robust.RANSACRobustEstimatorListener;
24 import com.irurueta.numerical.robust.RobustEstimator;
25 import com.irurueta.numerical.robust.RobustEstimatorException;
26 import com.irurueta.numerical.robust.RobustEstimatorMethod;
27
28 import java.io.IOException;
29 import java.util.List;
30
31 /**
32 * Robustly estimates magnetometer hard-iron biases, soft-iron cross
33 * couplings and scaling factors using RANSAC algorithm.
34 * <p>
35 * To use this calibrator at least 4 measurements at different known
36 * frames must be provided. In other words, magnetometer samples must
37 * be obtained at 4 different positions or orientations.
38 * Notice that frame velocities are ignored by this calibrator.
39 * <p>
40 * Measured magnetic flux density is assumed to follow the model shown below:
41 * <pre>
42 * mBmeas = bm + (I + Mm) * mBtrue + w
43 * </pre>
44 * Where:
45 * - mBmeas is the measured magnetic flux density. This is a 3x1 vector.
46 * - bm is magnetometer hard-iron bias. Ideally, on a perfect magnetometer,
47 * this should be a 3x1 zero vector.
48 * - I is the 3x3 identity matrix.
49 * - Mm is the 3x3 soft-iron matrix containing cross-couplings and scaling
50 * factors. Ideally, on a perfect magnetometer, this should be a 3x3 zero
51 * matrix.
52 * - mBtrue is ground-truth magnetic flux density. This is a 3x1 vector.
53 * - w is measurement noise. This is a 3x1 vector.
54 */
55 public class RANSACRobustKnownFrameMagnetometerCalibrator extends RobustKnownFrameMagnetometerCalibrator {
56
57 /**
58 * Constant defining default threshold to determine whether samples are inliers or not.
59 */
60 public static final double DEFAULT_THRESHOLD = 500e-9;
61
62 /**
63 * Minimum value that can be set as threshold.
64 * Threshold must be strictly greater than 0.0.
65 */
66 public static final double MIN_THRESHOLD = 0.0;
67
68 /**
69 * Indicates that by default inliers will only be computed but not kept.
70 */
71 public static final boolean DEFAULT_COMPUTE_AND_KEEP_INLIERS = false;
72
73 /**
74 * Indicates that by default residuals will only be computed but not kept.
75 */
76 public static final boolean DEFAULT_COMPUTE_AND_KEEP_RESIDUALS = false;
77
78 /**
79 * Threshold to determine whether samples are inliers or not when testing possible solutions.
80 * The threshold refers to the amount of error on distance between estimated position and
81 * distances provided for each sample.
82 */
83 private double threshold = DEFAULT_THRESHOLD;
84
85 /**
86 * Indicates whether inliers must be computed and kept.
87 */
88 private boolean computeAndKeepInliers = DEFAULT_COMPUTE_AND_KEEP_INLIERS;
89
90 /**
91 * Indicates whether residuals must be computed and kept.
92 */
93 private boolean computeAndKeepResiduals = DEFAULT_COMPUTE_AND_KEEP_RESIDUALS;
94
95 /**
96 * Constructor.
97 */
98 public RANSACRobustKnownFrameMagnetometerCalibrator() {
99 super();
100 }
101
102 /**
103 * Constructor.
104 *
105 * @param listener listener to be notified of events such as when estimation
106 * starts, ends or its progress significantly changes.
107 */
108 public RANSACRobustKnownFrameMagnetometerCalibrator(final RobustKnownFrameMagnetometerCalibratorListener listener) {
109 super(listener);
110 }
111
112 /**
113 * Constructor.
114 *
115 * @param measurements list of body magnetic flux density measurements with standard
116 * deviations taken at different frames (positions and
117 * orientations).
118 */
119 public RANSACRobustKnownFrameMagnetometerCalibrator(
120 final List<StandardDeviationFrameBodyMagneticFluxDensity> measurements) {
121 super(measurements);
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 * @param listener listener to handle events raised by this calibrator.
131 */
132 public RANSACRobustKnownFrameMagnetometerCalibrator(
133 final List<StandardDeviationFrameBodyMagneticFluxDensity> measurements,
134 final RobustKnownFrameMagnetometerCalibratorListener listener) {
135 super(measurements, listener);
136 }
137
138 /**
139 * Constructor.
140 *
141 * @param commonAxisUsed indicates whether z-axis is assumed to be common
142 * for the accelerometer, gyroscope and magnetometer.
143 */
144 public RANSACRobustKnownFrameMagnetometerCalibrator(final boolean commonAxisUsed) {
145 super(commonAxisUsed);
146 }
147
148 /**
149 * Constructor.
150 *
151 * @param commonAxisUsed indicates whether z-axis is assumed to be common
152 * for the accelerometer, gyroscope and magnetometer.
153 * @param listener listener to handle events raised by this calibrator.
154 */
155 public RANSACRobustKnownFrameMagnetometerCalibrator(
156 final boolean commonAxisUsed, final RobustKnownFrameMagnetometerCalibratorListener listener) {
157 super(commonAxisUsed, listener);
158 }
159
160 /**
161 * Constructor.
162 *
163 * @param measurements list of body magnetic flux density measurements with standard
164 * deviations taken at different frames (positions and
165 * orientations).
166 * @param commonAxisUsed indicates whether z-axis is assumed to be common
167 * for the accelerometer, gyroscope and magnetometer.
168 */
169 public RANSACRobustKnownFrameMagnetometerCalibrator(
170 final List<StandardDeviationFrameBodyMagneticFluxDensity> measurements, final boolean commonAxisUsed) {
171 super(measurements, commonAxisUsed);
172 }
173
174 /**
175 * Constructor.
176 *
177 * @param measurements list of body magnetic flux density measurements with standard
178 * deviations taken at different frames (positions and
179 * orientations).
180 * @param commonAxisUsed indicates whether z-axis is assumed to be common
181 * for the accelerometer, gyroscope and magnetometer.
182 * @param listener listener to handle events raised by this calibrator.
183 */
184 public RANSACRobustKnownFrameMagnetometerCalibrator(
185 final List<StandardDeviationFrameBodyMagneticFluxDensity> measurements, final boolean commonAxisUsed,
186 final RobustKnownFrameMagnetometerCalibratorListener listener) {
187 super(measurements, commonAxisUsed, listener);
188 }
189
190 /**
191 * Gets threshold to determine whether samples are inliers or not when testing possible solutions.
192 * The threshold refers to the amount of error on norm between measured specific forces and the
193 * ones generated with estimated calibration parameters provided for each sample.
194 *
195 * @return threshold to determine whether samples are inliers or not.
196 */
197 public double getThreshold() {
198 return threshold;
199 }
200
201 /**
202 * Sets threshold to determine whether samples are inliers or not when testing possible solutions.
203 * The threshold refers to the amount of error on norm between measured specific forces and the
204 * ones generated with estimated calibration parameters provided for each sample.
205 *
206 * @param threshold threshold to determine whether samples are inliers or not.
207 * @throws IllegalArgumentException if provided value is equal or less than zero.
208 * @throws LockedException if calibrator is currently running.
209 */
210 public void setThreshold(final double threshold) throws LockedException {
211 if (running) {
212 throw new LockedException();
213 }
214 if (threshold <= MIN_THRESHOLD) {
215 throw new IllegalArgumentException();
216 }
217 this.threshold = threshold;
218 }
219
220 /**
221 * Indicates whether inliers must be computed and kept.
222 *
223 * @return true if inliers must be computed and kept, false if inliers
224 * only need to be computed but not kept.
225 */
226 public boolean isComputeAndKeepInliersEnabled() {
227 return computeAndKeepInliers;
228 }
229
230 /**
231 * Specifies whether inliers must be computed and kept.
232 *
233 * @param computeAndKeepInliers true if inliers must be computed and kept,
234 * false if inliers only need to be computed but not kept.
235 * @throws LockedException if calibrator is currently running.
236 */
237 public void setComputeAndKeepInliersEnabled(final boolean computeAndKeepInliers) throws LockedException {
238 if (running) {
239 throw new LockedException();
240 }
241 this.computeAndKeepInliers = computeAndKeepInliers;
242 }
243
244 /**
245 * Indicates whether residuals must be computed and kept.
246 *
247 * @return true if residuals must be computed and kept, false if residuals
248 * only need to be computed but not kept.
249 */
250 public boolean isComputeAndKeepResiduals() {
251 return computeAndKeepResiduals;
252 }
253
254 /**
255 * Specifies whether residuals must be computed and kept.
256 *
257 * @param computeAndKeepResiduals true if residuals must be computed and kept,
258 * false if residuals only need to be computed but not kept.
259 * @throws LockedException if calibrator is currently running.
260 */
261 public void setComputeAndKeepResidualsEnabled(final boolean computeAndKeepResiduals) throws LockedException {
262 if (running) {
263 throw new LockedException();
264 }
265 this.computeAndKeepResiduals = computeAndKeepResiduals;
266 }
267
268 /**
269 * Estimates magnetometer calibration parameters containing hard-iron
270 * bias and soft-iron scale factors and cross-coupling errors.
271 *
272 * @throws LockedException if calibrator is currently running.
273 * @throws NotReadyException if calibrator is not ready.
274 * @throws CalibrationException if estimation fails for numerical reasons.
275 */
276 @SuppressWarnings("DuplicatedCode")
277 @Override
278 public void calibrate() throws LockedException, NotReadyException, CalibrationException {
279 if (running) {
280 throw new LockedException();
281 }
282 if (!isReady()) {
283 throw new NotReadyException();
284 }
285
286 final var innerEstimator = new RANSACRobustEstimator<>(new RANSACRobustEstimatorListener<PreliminaryResult>() {
287 @Override
288 public double getThreshold() {
289 return threshold;
290 }
291
292 @Override
293 public int getTotalSamples() {
294 return measurements.size();
295 }
296
297 @Override
298 public int getSubsetSize() {
299 return preliminarySubsetSize;
300 }
301
302 @Override
303 public void estimatePreliminarSolutions(
304 final int[] samplesIndices, final List<PreliminaryResult> solutions) {
305 computePreliminarySolutions(samplesIndices, solutions);
306 }
307
308 @Override
309 public double computeResidual(final PreliminaryResult currentEstimation, final int i) {
310 return computeError(measurements.get(i), currentEstimation);
311 }
312
313 @Override
314 public boolean isReady() {
315 return RANSACRobustKnownFrameMagnetometerCalibrator.super.isReady();
316 }
317
318 @Override
319 public void onEstimateStart(final RobustEstimator<PreliminaryResult> estimator) {
320 // no action needed
321 }
322
323 @Override
324 public void onEstimateEnd(final RobustEstimator<PreliminaryResult> estimator) {
325 // no action needed
326 }
327
328 @Override
329 public void onEstimateNextIteration(
330 final RobustEstimator<PreliminaryResult> estimator, final int iteration) {
331 if (listener != null) {
332 listener.onCalibrateNextIteration(
333 RANSACRobustKnownFrameMagnetometerCalibrator.this, iteration);
334 }
335 }
336
337 @Override
338 public void onEstimateProgressChange(
339 final RobustEstimator<PreliminaryResult> estimator, final float progress) {
340 if (listener != null) {
341 listener.onCalibrateProgressChange(
342 RANSACRobustKnownFrameMagnetometerCalibrator.this, progress);
343 }
344 }
345 });
346
347 try {
348 running = true;
349
350 if (listener != null) {
351 listener.onCalibrateStart(this);
352 }
353
354 inliersData = null;
355
356 setupWmmEstimator();
357
358 innerEstimator.setComputeAndKeepInliersEnabled(computeAndKeepInliers || refineResult);
359 innerEstimator.setComputeAndKeepResidualsEnabled(computeAndKeepResiduals || refineResult);
360 innerEstimator.setConfidence(confidence);
361 innerEstimator.setMaxIterations(maxIterations);
362 innerEstimator.setProgressDelta(progressDelta);
363 final var preliminaryResult = innerEstimator.estimate();
364 inliersData = innerEstimator.getInliersData();
365
366 attemptRefine(preliminaryResult);
367
368 if (listener != null) {
369 listener.onCalibrateEnd(this);
370 }
371
372 } catch (final com.irurueta.numerical.LockedException e) {
373 throw new LockedException(e);
374 } catch (final com.irurueta.numerical.NotReadyException e) {
375 throw new NotReadyException(e);
376 } catch (final RobustEstimatorException | IOException e) {
377 throw new CalibrationException(e);
378 } finally {
379 running = false;
380 }
381 }
382
383 /**
384 * Returns method being used for robust estimation.
385 *
386 * @return method being used for robust estimation.
387 */
388 @Override
389 public RobustEstimatorMethod getMethod() {
390 return RobustEstimatorMethod.RANSAC;
391 }
392
393 /**
394 * Indicates whether this calibrator requires quality scores for each
395 * measurement or not.
396 *
397 * @return true if quality scores are required, false otherwise.
398 */
399 @Override
400 public boolean isQualityScoresRequired() {
401 return false;
402 }
403 }