View Javadoc
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 }