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