1 /*
2 * Copyright (C) 2015 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.numerical.robust;
17
18 import com.irurueta.numerical.LockedException;
19 import com.irurueta.numerical.NotReadyException;
20 import com.irurueta.sorting.Sorter;
21
22 import java.util.ArrayList;
23 import java.util.BitSet;
24
25 /**
26 * This class implements MSAC (Median SAmple Consensus) algorithm to robustly
27 * estimate a data model.
28 * MSAC is a mixture between LMedS and RANSAC, where a fixed threshold is
29 * used such as in RANSAC to determine the number of remaining iterations, and
30 * the least median of residuals is used to pick the best solution, rather than
31 * the one producing a higher number of inliers based on the fixed threshold,
32 * such as in RANSAC.
33 * This algorithm requires a threshold known beforehand such as RANSAC, but
34 * might get better accuracy if the inlier samples are very accurate, since
35 * the solution with the smallest median of residuals will be picked. In typical
36 * situations however, this algorithm will produce similar results to RANSAC in
37 * both terms of accuracy and computational cost, since typically inlier samples
38 * tend to have certain error.
39 *
40 * @param <T> type of object to be estimated.
41 */
42 public class MSACRobustEstimator<T> extends RobustEstimator<T> {
43
44 /**
45 * Constant defining default confidence of estimated result, which is 99%.
46 * This means that with a probability of 99% estimation will be accurate
47 * because chosen sub-samples will be inliers.
48 */
49 public static final double DEFAULT_CONFIDENCE = 0.99;
50
51 /**
52 * Default maximum allowed number of iterations.
53 */
54 public static final int DEFAULT_MAX_ITERATIONS = 5000;
55
56 /**
57 * Minimum allowed confidence value.
58 */
59 public static final double MIN_CONFIDENCE = 0.0;
60
61 /**
62 * Maximum allowed confidence value.
63 */
64 public static final double MAX_CONFIDENCE = 1.0;
65
66 /**
67 * Minimum allowed number of iterations.
68 */
69 public static final int MIN_ITERATIONS = 1;
70
71 /**
72 * Minimum allowed threshold to determine inliers.
73 */
74 public static final double MIN_THRESHOLD = 0.0;
75
76 /**
77 * Amount of confidence expressed as a value between 0 and 1.0 (which is
78 * equivalent to 100%). The amount of confidence indicates the probability
79 * that the estimated result is correct. Usually this value will be close
80 * to 1.0, but not exactly 1.0.
81 */
82 private double confidence;
83
84 /**
85 * Maximum allowed number of iterations. When the maximum number of
86 * iterations is exceeded, result will not be available, however an
87 * approximate result will be available for retrieval.
88 */
89 private int maxIterations;
90
91 /**
92 * Instance in charge of picking random subsets of samples.
93 */
94 private SubsetSelector subsetSelector;
95
96 /**
97 * Number of iterations to be done to obtain required confidence.
98 */
99 private int iters;
100
101 /**
102 * Best solution that has been found so far during an estimation.
103 */
104 private T bestResult;
105
106 /**
107 * Data related to inliers found for best result.
108 */
109 private MSACInliersData bestResultInliersData;
110
111 /**
112 * Data related to solution producing the largest number of inliers.
113 */
114 private MSACInliersData bestNumberInliersData;
115
116 /**
117 * Constructor.
118 */
119 public MSACRobustEstimator() {
120 super();
121 confidence = DEFAULT_CONFIDENCE;
122 maxIterations = DEFAULT_MAX_ITERATIONS;
123 iters = maxIterations;
124 bestResult = null;
125 bestResultInliersData = bestNumberInliersData = null;
126 }
127
128 /**
129 * Constructor.
130 *
131 * @param listener listener to handle events raised by this estimator.
132 */
133 public MSACRobustEstimator(final MSACRobustEstimatorListener<T> listener) {
134 super(listener);
135 confidence = DEFAULT_CONFIDENCE;
136 maxIterations = DEFAULT_MAX_ITERATIONS;
137 iters = maxIterations;
138 bestResult = null;
139 bestResultInliersData = bestNumberInliersData = null;
140 }
141
142 /**
143 * Returns amount of confidence expressed as a value between 0 and 1.0
144 * (which is equivalent to 100%). The amount of confidence indicates the
145 * probability that the estimated result is correct. Usually this value will
146 * be close to 1.0, but not exactly 1.0.
147 *
148 * @return amount of confidence as a value between 0.0 and 1.0.
149 */
150 public double getConfidence() {
151 return confidence;
152 }
153
154 /**
155 * Sets amount of confidence expressed as a value between 0 and 1.0 (which
156 * is equivalent to 100%). The amount of confidence indicates the
157 * probability that the estimated result is correct. Usually this value will
158 * be close to 1.0, but not exactly 1.0.
159 *
160 * @param confidence confidence to be set as a value between 0.0 and 1.0.
161 * @throws IllegalArgumentException if provided value is not between 0.0 and
162 * 1.0.
163 * @throws LockedException if this estimator is locked because an estimation
164 * is being computed.
165 */
166 public void setConfidence(final double confidence) throws LockedException {
167 if (isLocked()) {
168 throw new LockedException();
169 }
170 if (confidence < MIN_CONFIDENCE || confidence > MAX_CONFIDENCE) {
171 throw new IllegalArgumentException();
172 }
173 this.confidence = confidence;
174 }
175
176 /**
177 * Maximum allowed number of iterations. When the maximum number of
178 * iterations is exceeded, result will not be available, however an
179 * approximate result will be available for retrieval.
180 *
181 * @return maximum allowed number of iterations.
182 */
183 public int getMaxIterations() {
184 return maxIterations;
185 }
186
187 /**
188 * Sets maximum allowed number of iterations. When the maximum number of
189 * iterations is exceeded, result will not be available, however an
190 * approximate result will be available for retrieval.
191 *
192 * @param maxIterations maximum allowed number of iterations to be set.
193 * @throws IllegalArgumentException if provided value is less than 1.
194 * @throws LockedException if this estimator is locked because an estimation
195 * is being computed.
196 */
197 public void setMaxIterations(final int maxIterations) throws LockedException {
198 if (isLocked()) {
199 throw new LockedException();
200 }
201 if (maxIterations < MIN_ITERATIONS) {
202 throw new IllegalArgumentException();
203 }
204 this.maxIterations = maxIterations;
205 }
206
207 /**
208 * Returns number of iterations to be done to obtain required confidence.
209 *
210 * @return number of iterations to be done to obtain required confidence.
211 */
212 public int getNIters() {
213 return iters;
214 }
215
216 /**
217 * Returns best solution that has been found so far during an estimation.
218 *
219 * @return best solution that has been found so far during an estimation.
220 */
221 public T getBestResult() {
222 return bestResult;
223 }
224
225 /**
226 * Returns data related to the best inliers found for best result.
227 *
228 * @return data related to inliers found for best result.
229 */
230 public MSACInliersData getBestResultInliersData() {
231 return bestResultInliersData;
232 }
233
234 /**
235 * Returns data related to solution producing the largest number of inliers.
236 *
237 * @return data related to solution producing the largest number of inliers.
238 */
239 public MSACInliersData getBestNumberInliersData() {
240 return bestNumberInliersData;
241 }
242
243 /**
244 * Indicates if estimator is ready to start the estimation process.
245 *
246 * @return true if ready, false otherwise.
247 */
248 @Override
249 public boolean isReady() {
250 if (!super.isReady()) {
251 return false;
252 }
253 return (listener instanceof MSACRobustEstimatorListener);
254 }
255
256 /**
257 * Robustly estimates an instance of T.
258 *
259 * @return estimated object.
260 * @throws LockedException if robust estimator is locked.
261 * @throws NotReadyException if provided input data is not enough to start
262 * the estimation.
263 * @throws RobustEstimatorException if estimation fails for any reason
264 * (i.e. numerical instability, no solution available, etc).
265 */
266 @Override
267 @SuppressWarnings("Duplicates")
268 public T estimate() throws LockedException, NotReadyException, RobustEstimatorException {
269 if (isLocked()) {
270 throw new LockedException();
271 }
272 if (!isReady()) {
273 throw new NotReadyException();
274 }
275
276 try {
277 final var listener = (MSACRobustEstimatorListener<T>) this.listener;
278
279 locked = true;
280
281 listener.onEstimateStart(this);
282
283 final var totalSamples = listener.getTotalSamples();
284 final var subsetSize = listener.getSubsetSize();
285 final var threshold = listener.getThreshold();
286 // only positive thresholds are allowed
287 if (threshold < MIN_THRESHOLD) {
288 throw new RobustEstimatorException();
289 }
290
291 var bestNumInliers = 0;
292 var bestMedianResidual = Double.MAX_VALUE;
293 iters = Integer.MAX_VALUE;
294 int newNIters;
295 var currentIter = 0;
296 // reusable list that will contain preliminary solutions on each
297 // iteration
298 final var iterResults = new ArrayList<T>();
299 bestResult = null; // best result found so far
300 int currentInliers;
301 // progress and previous progress to determine when progress
302 // notification must occur
303 var previousProgress = 0.0f;
304 float progress;
305 // indices of subset picked in one iteration
306 final var subsetIndices = new int[subsetSize];
307 final var residualsTemp = new double[totalSamples];
308
309 if (subsetSelector == null) {
310 // create new subset selector
311 subsetSelector = SubsetSelector.create(totalSamples);
312 } else {
313 // set number of samples to current subset selector
314 subsetSelector.setNumSamples(totalSamples);
315 }
316
317 // data related to inliers
318 var inliersData = new MSACInliersData(totalSamples);
319 // sorter to compute medians
320 final var sorter = Sorter.<Double>create();
321
322 while ((iters > currentIter) && (currentIter < maxIterations)) {
323 // generate a random subset of samples
324 subsetSelector.computeRandomSubsets(subsetSize, subsetIndices);
325
326 // clear list of preliminary solutions before calling listener
327 iterResults.clear();
328 // compute solution for current iteration
329 listener.estimatePreliminarSolutions(subsetIndices, iterResults);
330
331 // iterate over all solutions that have been found
332 for (final var iterResult : iterResults) {
333 // compute inliers
334 computeInliers(iterResult, threshold, residualsTemp, listener, sorter, inliersData);
335
336 // save solution that minimizes the median residual
337 if (inliersData.isMedianResidualImproved()) {
338 // keep current solution
339 bestResult = iterResult;
340
341 // keep the best inliers data corresponding to best solution
342 // in case it can be useful along with the result
343 bestResultInliersData = inliersData;
344 bestMedianResidual = inliersData.getBestMedianResidual();
345 }
346
347 // if number of inliers have improved, update number of
348 // remaining iterations
349 currentInliers = inliersData.getNumInliers();
350 if (currentInliers > bestNumInliers) {
351 // update best number of inliers
352 bestNumInliers = currentInliers;
353
354 // keep inliers data corresponding to best number of
355 // inliers
356 bestNumberInliersData = inliersData;
357
358 // recompute number of times the algorithm needs to be
359 // executed depending on current number of inliers to
360 // achieve with probability mConfidence that we have
361 // inliers and probability 1 - mConfidence that we have
362 // outliers
363 final var probSubsetAllInliers = Math.pow((double) bestNumInliers / (double) totalSamples,
364 subsetSize);
365
366 if (Math.abs(probSubsetAllInliers) < Double.MIN_VALUE || Double.isNaN(probSubsetAllInliers)) {
367 newNIters = Integer.MAX_VALUE;
368 } else {
369 final var logProbSomeOutliers = Math.log(1.0 - probSubsetAllInliers);
370 if (Math.abs(logProbSomeOutliers) < Double.MIN_VALUE || Double.isNaN(logProbSomeOutliers)) {
371 newNIters = Integer.MAX_VALUE;
372 } else {
373 newNIters = (int) Math.ceil(Math.abs(Math.log(1.0 - confidence) / logProbSomeOutliers));
374 }
375 }
376 if (newNIters < iters) {
377 iters = newNIters;
378 }
379 }
380
381 // reset inliers data if either residual or number of inliers
382 // improved
383 if (inliersData.isMedianResidualImproved()) {
384 // create new inliers data instance until a new best solution
385 // is found
386 inliersData = new MSACInliersData(totalSamples);
387 // update the best median residual on new instance so that
388 // only better solutions that are found later can update
389 // inliers data
390 inliersData.update(bestMedianResidual, inliersData.getInliers(), inliersData.getResiduals(),
391 inliersData.getNumInliers(), false);
392 }
393 }
394
395 if (iters > 0) {
396 progress = Math.min((float) currentIter / (float) iters, 1.0f);
397 } else {
398 progress = 1.0f;
399 }
400 if (progress - previousProgress > progressDelta) {
401 previousProgress = progress;
402 listener.onEstimateProgressChange(this, progress);
403 }
404 currentIter++;
405
406 listener.onEstimateNextIteration(this, currentIter);
407 }
408
409 // no solution could be found after completing all iterations
410 if (bestResult == null) {
411 throw new RobustEstimatorException();
412 }
413
414 listener.onEstimateEnd(this);
415
416 return bestResult;
417 } catch (final SubsetSelectorException e) {
418 throw new RobustEstimatorException(e);
419 } finally {
420 locked = false;
421 }
422 }
423
424 /**
425 * Returns data about inliers once estimation has been done.
426 *
427 * @return data about inliers or null if estimation has not been done.
428 */
429 @Override
430 public InliersData getInliersData() {
431 return getBestNumberInliersData();
432 }
433
434 /**
435 * Returns method being used for robust estimation.
436 *
437 * @return method being used for robust estimation.
438 */
439 @Override
440 public RobustEstimatorMethod getMethod() {
441 return RobustEstimatorMethod.MSAC;
442 }
443
444 /**
445 * Computes inliers data for current iteration.
446 *
447 * @param <T> type of result to be estimated.
448 * @param iterResult result to be tested on current iteration.
449 * @param threshold threshold to determine whether samples are inliers or
450 * not.
451 * @param residualsTemp temporal array to store residuals, since median
452 * computation requires modifying the original array.
453 * @param listener listener to obtain residuals for samples.
454 * @param sorter sorter instance to compute median of residuals.
455 * @param inliersData inliers data to be reused on each iteration
456 */
457 private static <T> void computeInliers(
458 final T iterResult, final double threshold, final double[] residualsTemp,
459 final LMedSRobustEstimatorListener<T> listener, final Sorter<Double> sorter,
460 final MSACInliersData inliersData) {
461
462 final var residuals = inliersData.getResiduals();
463 final var inliers = inliersData.getInliers();
464 var bestMedianResidual = inliersData.getBestMedianResidual();
465 var medianResidualImproved = false;
466
467 final var totalSamples = residuals.length;
468 double residual;
469 var numInliers = 0;
470 // find residuals and inliers
471 for (var i = 0; i < totalSamples; i++) {
472 residual = Math.abs(listener.computeResidual(iterResult, i));
473 if (residual < threshold) {
474 residuals[i] = residual;
475 numInliers++;
476 inliers.set(i);
477 } else {
478 residuals[i] = threshold;
479 inliers.clear(i);
480 }
481 }
482
483 // compute median of residuals
484 System.arraycopy(residuals, 0, residualsTemp, 0, residuals.length);
485 final var medianResidual = sorter.median(residualsTemp);
486 if (medianResidual < bestMedianResidual) {
487 bestMedianResidual = medianResidual;
488 medianResidualImproved = true;
489 }
490
491
492 // store values in inliers data, only if residuals improve
493 if (medianResidualImproved) {
494 inliersData.update(bestMedianResidual, inliers, residuals, numInliers, true);
495 }
496 }
497
498 /**
499 * Contains data related to inliers estimated in one iteration.
500 */
501 public static class MSACInliersData extends InliersData {
502 /**
503 * Best median of error found so far taking into account all provided
504 * samples.
505 */
506 private double bestMedianResidual;
507
508 /**
509 * Efficiently stores which samples are considered inliers and which
510 * ones aren't.
511 */
512 private BitSet inliers;
513
514 /**
515 * Indicates whether median residual computed in current iteration has
516 * improved respect to previous iterations.
517 */
518 private boolean medianResidualImproved;
519
520 /**
521 * Constructor.
522 *
523 * @param totalSamples total number of samples.
524 */
525 protected MSACInliersData(final int totalSamples) {
526 bestMedianResidual = Double.MAX_VALUE;
527 inliers = new BitSet(totalSamples);
528 residuals = new double[totalSamples];
529 numInliers = 0;
530 medianResidualImproved = false;
531 }
532
533 /**
534 * Returns best median of error found so far taking into account all
535 * provided samples.
536 *
537 * @return best median of error found so far taking into account all
538 * provided samples.
539 */
540 public double getBestMedianResidual() {
541 return bestMedianResidual;
542 }
543
544 /**
545 * Returns efficient array indicating which samples are considered
546 * inliers and which ones aren't.
547 *
548 * @return array indicating which samples are considered inliers and
549 * which ones aren't.
550 */
551 @Override
552 public BitSet getInliers() {
553 return inliers;
554 }
555
556 /**
557 * Returns boolean indicating whether median residual computed in
558 * current iteration has improved respect to previous iterations.
559 *
560 * @return true if median residual improved, false otherwise.
561 */
562 public boolean isMedianResidualImproved() {
563 return medianResidualImproved;
564 }
565
566 /**
567 * Updates data contained in this instance.
568 *
569 * @param bestMedianResidual best median of error found so far taking
570 * into account all provided samples.
571 * @param inliers efficiently stores which samples are considered
572 * inliers and which ones aren't.
573 * @param residuals residuals obtained for each sample of data.
574 * @param numInliers number of inliers found on current iteration.
575 * @param medianResidualImproved indicates whether median residual
576 * computed in current iteration has improved respect to previous
577 * iteration.
578 */
579 protected void update(final double bestMedianResidual, final BitSet inliers,
580 final double[] residuals, final int numInliers, final boolean medianResidualImproved) {
581 this.bestMedianResidual = bestMedianResidual;
582 this.inliers = inliers;
583 this.residuals = residuals;
584 this.numInliers = numInliers;
585 this.medianResidualImproved = medianResidualImproved;
586 }
587 }
588 }