View Javadoc
1   /*
2    * Copyright (C) 2017 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.geometry.estimators;
17  
18  import com.irurueta.geometry.CoordinatesType;
19  import com.irurueta.geometry.EuclideanTransformation2D;
20  import com.irurueta.geometry.Point2D;
21  import com.irurueta.numerical.robust.LMedSRobustEstimator;
22  import com.irurueta.numerical.robust.LMedSRobustEstimatorListener;
23  import com.irurueta.numerical.robust.RobustEstimator;
24  import com.irurueta.numerical.robust.RobustEstimatorException;
25  import com.irurueta.numerical.robust.RobustEstimatorMethod;
26  
27  import java.util.ArrayList;
28  import java.util.List;
29  
30  /**
31   * Finds the best Euclidean 2D transformation for provided collections of
32   * matched 2D points using LMedS algorithm.
33   */
34  public class LMedSEuclideanTransformation2DRobustEstimator extends EuclideanTransformation2DRobustEstimator {
35  
36      /**
37       * Default value ot be used for stop threshold. Stop threshold can be used
38       * to keep the algorithm iterating in case that best estimated threshold
39       * using median of residuals is not small enough. Once a solution is found
40       * that generates a threshold below this value, the algorithm will stop.
41       * The stop threshold can be used to prevent the LMedS algorithm iterating
42       * too many times in cases where samples have a very similar accuracy.
43       * For instance, in cases where proportion of outliers is very small (close
44       * to 0%), and samples are very accurate (i.e. 1e-6), the algorithm would
45       * iterate for a long time trying to find the best solution when indeed
46       * there is no need to do that if a reasonable threshold has already been
47       * reached.
48       * Because of this behaviour the stop threshold can be set to a value much
49       * lower than the one typically used in RANSAC, and yet the algorithm could
50       * still produce even smaller thresholds in estimated results.
51       */
52      public static final double DEFAULT_STOP_THRESHOLD = 1.0;
53  
54      /**
55       * Minimum allowed stop threshold value.
56       */
57      public static final double MIN_STOP_THRESHOLD = 0.0;
58  
59      /**
60       * Threshold to be used to keep the algorithm iterating in case that best
61       * estimated threshold using median of residuals is not small enough. Once
62       * a solution is found that generates a threshold below this value, the
63       * algorithm will stop.
64       * The stop threshold can be used to prevent the LMedS algorithm iterating
65       * too many times in cases where samples have a very similar accuracy.
66       * For instance, in cases where proportion of outliers is very small (close
67       * to 0%), and samples are very accurate (i.e. 1e-6), the algorithm would
68       * iterate for a long time trying to find the best solution when indeed
69       * there is no need to do that if a reasonable threshold has already been
70       * reached.
71       * Because of this behaviour the stop threshold can be set to a value much
72       * lower than the one typically used in RANSAC, and yet the algorithm could
73       * still produce even smaller thresholds in estimated results.
74       */
75      private double stopThreshold;
76  
77      /**
78       * Constructor.
79       */
80      public LMedSEuclideanTransformation2DRobustEstimator() {
81          super();
82          stopThreshold = DEFAULT_STOP_THRESHOLD;
83      }
84  
85      /**
86       * Constructor with lists of points to be used to estimate an Euclidean 2D
87       * transformation.
88       * Points in the list located at the same position are considered to be
89       * matched. Hence, both lists must have the same size, and their size must
90       * be greater or equal than MINIMUM_SIZE.
91       *
92       * @param inputPoints  list of input points to be used to estimate an
93       *                     Euclidean 2D transformation.
94       * @param outputPoints list of output points to be used to estimate an
95       *                     Euclidean 2D transformation.
96       * @throws IllegalArgumentException if provided lists of points don't have
97       *                                  the same size or their size is smaller than MINIMUM_SIZE.
98       */
99      public LMedSEuclideanTransformation2DRobustEstimator(
100             final List<Point2D> inputPoints, final List<Point2D> outputPoints) {
101         super(inputPoints, outputPoints);
102         stopThreshold = DEFAULT_STOP_THRESHOLD;
103     }
104 
105     /**
106      * Constructor.
107      *
108      * @param listener listener to be notified of events such as when estimation
109      *                 starts, ends or its progress significantly changes.
110      */
111     public LMedSEuclideanTransformation2DRobustEstimator(
112             final EuclideanTransformation2DRobustEstimatorListener listener) {
113         super(listener);
114         stopThreshold = DEFAULT_STOP_THRESHOLD;
115     }
116 
117     /**
118      * Constructor with listener and lists of points to be used to estimate an
119      * Euclidean 2D transformation.
120      * Points in the list located at the same position are considered to be
121      * matched. Hence, both lists must have the same size, and their size must
122      * be greater or equal than MINIMUM_SIZE.
123      *
124      * @param listener     listener to be notified of events such as when estimation
125      *                     starts, ends or its progress significantly changes.
126      * @param inputPoints  list of input points to be used to estimate an
127      *                     Euclidean 2D transformation.
128      * @param outputPoints list of output points to be used to estimate an
129      *                     Euclidean 2D transformation.
130      * @throws IllegalArgumentException if provided lists of points don't have
131      *                                  the same size or their size is smaller than MINIMUM_SIZE.
132      */
133     public LMedSEuclideanTransformation2DRobustEstimator(
134             final EuclideanTransformation2DRobustEstimatorListener listener,
135             final List<Point2D> inputPoints, final List<Point2D> outputPoints) {
136         super(listener, inputPoints, outputPoints);
137         stopThreshold = DEFAULT_STOP_THRESHOLD;
138     }
139 
140     /**
141      * Constructor.
142      *
143      * @param weakMinimumSizeAllowed true allows 2 points, false requires 3.
144      */
145     public LMedSEuclideanTransformation2DRobustEstimator(final boolean weakMinimumSizeAllowed) {
146         super(weakMinimumSizeAllowed);
147         stopThreshold = DEFAULT_STOP_THRESHOLD;
148     }
149 
150     /**
151      * Constructor with lists of points to be used to estimate an Euclidean 2D
152      * transformation.
153      * Points in the list located at the same position are considered to be
154      * matched. Hence, both lists must have the same size, and their size must
155      * be greater or equal than MINIMUM_SIZE.
156      *
157      * @param inputPoints            list of input points to be used to estimate an
158      *                               Euclidean 2D transformation.
159      * @param outputPoints           list of output points to be used to estimate an
160      *                               Euclidean 2D transformation.
161      * @param weakMinimumSizeAllowed true allows 2 points, false requires 3.
162      * @throws IllegalArgumentException if provided lists of points don't have
163      *                                  the same size or their size is smaller than MINIMUM_SIZE.
164      */
165     public LMedSEuclideanTransformation2DRobustEstimator(
166             final List<Point2D> inputPoints, final List<Point2D> outputPoints, final boolean weakMinimumSizeAllowed) {
167         super(inputPoints, outputPoints, weakMinimumSizeAllowed);
168         stopThreshold = DEFAULT_STOP_THRESHOLD;
169     }
170 
171     /**
172      * Constructor.
173      *
174      * @param listener               listener to be notified of events such as when estimation
175      *                               starts, ends or its progress significantly changes.
176      * @param weakMinimumSizeAllowed true allows 2 points, false requires 3.
177      */
178     public LMedSEuclideanTransformation2DRobustEstimator(
179             final EuclideanTransformation2DRobustEstimatorListener listener, final boolean weakMinimumSizeAllowed) {
180         super(listener, weakMinimumSizeAllowed);
181         stopThreshold = DEFAULT_STOP_THRESHOLD;
182     }
183 
184     /**
185      * Constructor with listener and lists of points to be used to estimate an
186      * Euclidean 2D transformation.
187      * Points in the list located at the same position are considered to be
188      * matched. Hence, both lists must have the same size, and their size must
189      * be greater or equal than MINIMUM_SIZE.
190      *
191      * @param listener               listener to be notified of events such as when estimation
192      *                               starts, ends or its progress significantly changes.
193      * @param inputPoints            list of input points to be used to estimate an
194      *                               Euclidean 2D transformation.
195      * @param outputPoints           list of output points to be used to estimate an
196      *                               Euclidean 2D transformation.
197      * @param weakMinimumSizeAllowed true allows 2 points, false requires 3.
198      * @throws IllegalArgumentException if provided lists of points don't have
199      *                                  the same size or their size is smaller than MINIMUM_SIZE.
200      */
201     public LMedSEuclideanTransformation2DRobustEstimator(
202             final EuclideanTransformation2DRobustEstimatorListener listener,
203             final List<Point2D> inputPoints, final List<Point2D> outputPoints, final boolean weakMinimumSizeAllowed) {
204         super(listener, inputPoints, outputPoints, weakMinimumSizeAllowed);
205         stopThreshold = DEFAULT_STOP_THRESHOLD;
206     }
207 
208     /**
209      * Returns threshold to be used to keep the algorithm iterating in case that
210      * best estimated threshold using median of residuals is not small enough.
211      * Once a solution is found that generates a threshold below this value, the
212      * algorithm will stop.
213      * The stop threshold can be used to prevent the LMedS algorithm iterating
214      * too many times in cases where samples have a very similar accuracy.
215      * For instance, in cases where proportion of outliers is very small (close
216      * to 0%), and samples are very accurate (i.e. 1e-6), the algorithm would
217      * iterate for a long time trying to find the best solution when indeed
218      * there is no need to do that if a reasonable threshold has already been
219      * reached.
220      * Because of this behaviour the stop threshold can be set to a value much
221      * lower than the one typically used in RANSAC, and yet the algorithm could
222      * still produce even smaller thresholds in estimated results.
223      *
224      * @return stop threshold to stop the algorithm prematurely when a certain
225      * accuracy has been reached.
226      */
227     public double getStopThreshold() {
228         return stopThreshold;
229     }
230 
231     /**
232      * Sets threshold to be used to keep the algorithm iterating in case that
233      * best estimated threshold using median of residuals is not small enough.
234      * Once a solution is found that generates a threshold below this value, the
235      * algorithm will stop.
236      * The stop threshold can be used to prevent the LMedS algorithm iterating
237      * too many times in cases where samples have a very similar accuracy.
238      * For instance, in cases where proportion of outliers is very small (close
239      * to 0%), and samples are very accurate (i.e. 1e-6), the algorithm would
240      * iterate for a long time trying to find the best solution when indeed
241      * there is no need to do that if a reasonable threshold has already been
242      * reached.
243      * Because of this behaviour the stop threshold can be set to a value much
244      * lower than the one typically used in RANSAC, and yet the algorithm could
245      * still produce even smaller thresholds in estimated results
246      *
247      * @param stopThreshold stop threshold to stop the algorithm prematurely
248      *                      when a certain accuracy has been reached
249      * @throws IllegalArgumentException if provided value is zero or negative
250      * @throws LockedException          if robust estimator is locked because an
251      *                                  estimation is already in progress
252      */
253     public void setStopThreshold(final double stopThreshold) throws LockedException {
254         if (isLocked()) {
255             throw new LockedException();
256         }
257         if (stopThreshold <= MIN_STOP_THRESHOLD) {
258             throw new IllegalArgumentException();
259         }
260 
261         this.stopThreshold = stopThreshold;
262     }
263 
264     /**
265      * Estimates an Euclidean 2D transformation using a robust estimator and
266      * the best set of matched 2d point correspondences found using the robust
267      * estimator.
268      *
269      * @return an Euclidean 2D transformation.
270      * @throws LockedException          if robust estimator is locked because an
271      *                                  estimation is already in progress.
272      * @throws NotReadyException        if provided input data is not enough to start
273      *                                  the estimation.
274      * @throws RobustEstimatorException if estimation fails for any reason
275      *                                  (i.e. numerical instability, no solution available, etc).
276      */
277     @Override
278     public EuclideanTransformation2D estimate() throws LockedException, NotReadyException, RobustEstimatorException {
279         if (isLocked()) {
280             throw new LockedException();
281         }
282         if (!isReady()) {
283             throw new NotReadyException();
284         }
285 
286         final var innerEstimator = new LMedSRobustEstimator<>(
287                 new LMedSRobustEstimatorListener<EuclideanTransformation2D>() {
288 
289                     // point to be reused when computing residuals
290                     private final Point2D testPoint = Point2D.create(CoordinatesType.HOMOGENEOUS_COORDINATES);
291 
292                     private final EuclideanTransformation2DEstimator nonRobustEstimator =
293                             new EuclideanTransformation2DEstimator(isWeakMinimumSizeAllowed());
294 
295                     private final List<Point2D> subsetInputPoints = new ArrayList<>();
296                     private final List<Point2D> subsetOutputPoints = new ArrayList<>();
297 
298                     @Override
299                     public int getTotalSamples() {
300                         return inputPoints.size();
301                     }
302 
303                     @Override
304                     public int getSubsetSize() {
305                         return nonRobustEstimator.getMinimumPoints();
306                     }
307 
308                     @SuppressWarnings("DuplicatedCode")
309                     @Override
310                     public void estimatePreliminarSolutions(
311                             final int[] samplesIndices, final List<EuclideanTransformation2D> solutions) {
312                         subsetInputPoints.clear();
313                         subsetOutputPoints.clear();
314                         for (final var samplesIndex : samplesIndices) {
315                             subsetInputPoints.add(inputPoints.get(samplesIndex));
316                             subsetOutputPoints.add(outputPoints.get(samplesIndex));
317                         }
318 
319                         try {
320                             nonRobustEstimator.setPoints(subsetInputPoints, subsetOutputPoints);
321                             solutions.add(nonRobustEstimator.estimate());
322                         } catch (final Exception e) {
323                             // if points are coincident, no solution is added
324                         }
325                     }
326 
327                     @Override
328                     public double computeResidual(final EuclideanTransformation2D currentEstimation, final int i) {
329                         final var inputPoint = inputPoints.get(i);
330                         final var outputPoint = outputPoints.get(i);
331 
332                         // transform input point and store result in mTestPoint
333                         currentEstimation.transform(inputPoint, testPoint);
334 
335                         return outputPoint.distanceTo(testPoint);
336                     }
337 
338                     @Override
339                     public boolean isReady() {
340                         return LMedSEuclideanTransformation2DRobustEstimator.this.isReady();
341                     }
342 
343                     @Override
344                     public void onEstimateStart(final RobustEstimator<EuclideanTransformation2D> estimator) {
345                         if (listener != null) {
346                             listener.onEstimateStart(LMedSEuclideanTransformation2DRobustEstimator.this);
347                         }
348                     }
349 
350                     @Override
351                     public void onEstimateEnd(final RobustEstimator<EuclideanTransformation2D> estimator) {
352                         if (listener != null) {
353                             listener.onEstimateEnd(LMedSEuclideanTransformation2DRobustEstimator.this);
354                         }
355                     }
356 
357                     @Override
358                     public void onEstimateNextIteration(
359                             final RobustEstimator<EuclideanTransformation2D> estimator, final int iteration) {
360                         if (listener != null) {
361                             listener.onEstimateNextIteration(
362                                     LMedSEuclideanTransformation2DRobustEstimator.this, iteration);
363                         }
364                     }
365 
366                     @Override
367                     public void onEstimateProgressChange(
368                             final RobustEstimator<EuclideanTransformation2D> estimator, final float progress) {
369                         if (listener != null) {
370                             listener.onEstimateProgressChange(
371                                     LMedSEuclideanTransformation2DRobustEstimator.this, progress);
372                         }
373                     }
374                 });
375 
376         try {
377             locked = true;
378             inliersData = null;
379             innerEstimator.setConfidence(confidence);
380             innerEstimator.setMaxIterations(maxIterations);
381             innerEstimator.setProgressDelta(progressDelta);
382             innerEstimator.setStopThreshold(stopThreshold);
383             final var transformation = innerEstimator.estimate();
384             inliersData = innerEstimator.getInliersData();
385             return attemptRefine(transformation);
386         } catch (final com.irurueta.numerical.LockedException e) {
387             throw new LockedException(e);
388         } catch (final com.irurueta.numerical.NotReadyException e) {
389             throw new NotReadyException(e);
390         } finally {
391             locked = false;
392         }
393     }
394 
395     /**
396      * Returns method being used for robust estimation.
397      *
398      * @return method being used for robust estimation.
399      */
400     @Override
401     public RobustEstimatorMethod getMethod() {
402         return RobustEstimatorMethod.LMEDS;
403     }
404 
405     /**
406      * Gets standard deviation used for Levenberg-Marquardt fitting during
407      * refinement.
408      * Returned value gives an indication of how much variance each residual
409      * has.
410      * Typically this value is related to the threshold used on each robust
411      * estimation, since residuals of found inliers are within the range of
412      * such threshold.
413      *
414      * @return standard deviation used for refinement.
415      */
416     @Override
417     protected double getRefinementStandardDeviation() {
418         final var inliersData = (LMedSRobustEstimator.LMedSInliersData) getInliersData();
419         return inliersData.getEstimatedThreshold();
420     }
421 }