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.RANSACRobustEstimator;
22  import com.irurueta.numerical.robust.RANSACRobustEstimatorListener;
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 RANSAC algorithm.
33   */
34  @SuppressWarnings("DuplicatedCode")
35  public class RANSACEuclideanTransformation2DRobustEstimator extends EuclideanTransformation2DRobustEstimator {
36  
37      /**
38       * Constant defining default threshold to determine whether points are
39       * inliers or not.
40       * By default, 1.0 is considered a good value for cases where measures are
41       * done on pixels, since typically the minimum resolution is 1 pixel.
42       */
43      public static final double DEFAULT_THRESHOLD = 1.0;
44  
45      /**
46       * Minimum value that can be set as threshold.
47       * Threshold must be strictly greater than 0.0.
48       */
49      public static final double MIN_THRESHOLD = 0.0;
50  
51      /**
52       * Indicates that by default inliers will only be computed but not kept.
53       */
54      public static final boolean DEFAULT_COMPUTE_AND_KEEP_INLIERS = false;
55  
56      /**
57       * Indicates that by default residuals will only be computed but not kept.
58       */
59      public static final boolean DEFAULT_COMPUTE_AND_KEEP_RESIDUALS = false;
60  
61      /**
62       * Threshold to determine whether points are inliers or not when testing
63       * possible estimation solutions.
64       * The threshold refers to the amount of error (i.e. distance) a possible
65       * solution has on a matched pair of points.
66       */
67      private double threshold;
68  
69      /**
70       * Indicates whether inliers must be computed and kept.
71       */
72      private boolean computeAndKeepInliers;
73  
74      /**
75       * Indicates whether residuals must be computed and kept.
76       */
77      private boolean computeAndKeepResiduals;
78  
79      /**
80       * Constructor.
81       */
82      public RANSACEuclideanTransformation2DRobustEstimator() {
83          super();
84          threshold = DEFAULT_THRESHOLD;
85          computeAndKeepInliers = DEFAULT_COMPUTE_AND_KEEP_INLIERS;
86          computeAndKeepResiduals = DEFAULT_COMPUTE_AND_KEEP_RESIDUALS;
87      }
88  
89      /**
90       * Constructor with lists of points to be used to estimate an Euclidean 2D
91       * transformation.
92       * Points in the list located at the same position are considered to be
93       * matched. Hence, both lists must have the same size, and their size must
94       * be greater or equal than MINIMUM_SIZE.
95       *
96       * @param inputPoints  list of input points to be used to estimate an
97       *                     Euclidean 2D transformation.
98       * @param outputPoints list of output points to be used to estimate an
99       *                     Euclidean 2D transformation.
100      * @throws IllegalArgumentException if provided lists of points don't have
101      *                                  the same size or their size is smaller than MINIMUM_SIZE.
102      */
103     public RANSACEuclideanTransformation2DRobustEstimator(
104             final List<Point2D> inputPoints, final List<Point2D> outputPoints) {
105         super(inputPoints, outputPoints);
106         threshold = DEFAULT_THRESHOLD;
107         computeAndKeepInliers = DEFAULT_COMPUTE_AND_KEEP_INLIERS;
108         computeAndKeepResiduals = DEFAULT_COMPUTE_AND_KEEP_RESIDUALS;
109     }
110 
111     /**
112      * Constructor.
113      *
114      * @param listener listener to be notified of events such as when estimation
115      *                 starts, ends or its progress significantly changes.
116      */
117     public RANSACEuclideanTransformation2DRobustEstimator(
118             final EuclideanTransformation2DRobustEstimatorListener listener) {
119         super(listener);
120         threshold = DEFAULT_THRESHOLD;
121         computeAndKeepInliers = DEFAULT_COMPUTE_AND_KEEP_INLIERS;
122         computeAndKeepResiduals = DEFAULT_COMPUTE_AND_KEEP_RESIDUALS;
123     }
124 
125     /**
126      * Constructor with listener and lists of points to be used to estimate an
127      * Euclidean 2D transformation.
128      * Points in the list located at the same position are considered to be
129      * matched. Hence, both lists must have the same size, and their size must
130      * be greater or equal than MINIMUM_SIZE.
131      *
132      * @param listener     listener to be notified of events such as when estimation
133      *                     starts, ends or its progress significantly changes.
134      * @param inputPoints  list of input points to be used to estimate an
135      *                     Euclidean 2D transformation.
136      * @param outputPoints list of output points to be used to estimate an
137      *                     Euclidean 2D transformation.
138      * @throws IllegalArgumentException if provided lists of points don't have
139      *                                  the same size or their size is smaller than MINIMUM_SIZE.
140      */
141     public RANSACEuclideanTransformation2DRobustEstimator(
142             final EuclideanTransformation2DRobustEstimatorListener listener,
143             final List<Point2D> inputPoints, final List<Point2D> outputPoints) {
144         super(listener, inputPoints, outputPoints);
145         threshold = DEFAULT_THRESHOLD;
146         computeAndKeepInliers = DEFAULT_COMPUTE_AND_KEEP_INLIERS;
147         computeAndKeepResiduals = DEFAULT_COMPUTE_AND_KEEP_RESIDUALS;
148     }
149 
150     /**
151      * Constructor.
152      *
153      * @param weakMinimumSizeAllowed true allows 3 points, false requires 4.
154      */
155     public RANSACEuclideanTransformation2DRobustEstimator(
156             final boolean weakMinimumSizeAllowed) {
157         super(weakMinimumSizeAllowed);
158         threshold = DEFAULT_THRESHOLD;
159         computeAndKeepInliers = DEFAULT_COMPUTE_AND_KEEP_INLIERS;
160         computeAndKeepResiduals = DEFAULT_COMPUTE_AND_KEEP_RESIDUALS;
161     }
162 
163     /**
164      * Constructor with lists of points to be used to estimate an Euclidean 2D
165      * transformation.
166      * Points in the list located at the same position are considered to be
167      * matched. Hence, both lists must have the same size, and their size must
168      * be greater or equal than MINIMUM_SIZE.
169      *
170      * @param inputPoints            list of input points to be used to estimate an
171      *                               Euclidean 2D transformation.
172      * @param outputPoints           list of output points to be used to estimate an
173      *                               Euclidean 2D transformation.
174      * @param weakMinimumSizeAllowed true allows 3 points, false requires 4.
175      * @throws IllegalArgumentException if provided lists of points don't have
176      *                                  the same size or their size is smaller than MINIMUM_SIZE.
177      */
178     public RANSACEuclideanTransformation2DRobustEstimator(
179             final List<Point2D> inputPoints, List<Point2D> outputPoints, final boolean weakMinimumSizeAllowed) {
180         super(inputPoints, outputPoints, weakMinimumSizeAllowed);
181         threshold = DEFAULT_THRESHOLD;
182         computeAndKeepInliers = DEFAULT_COMPUTE_AND_KEEP_INLIERS;
183         computeAndKeepResiduals = DEFAULT_COMPUTE_AND_KEEP_RESIDUALS;
184     }
185 
186     /**
187      * Constructor.
188      *
189      * @param listener               listener to be notified of events such as when estimation
190      *                               starts, ends or its progress significantly changes.
191      * @param weakMinimumSizeAllowed true allows 3 points, false requires 4.
192      */
193     public RANSACEuclideanTransformation2DRobustEstimator(
194             final EuclideanTransformation2DRobustEstimatorListener listener, final boolean weakMinimumSizeAllowed) {
195         super(listener, weakMinimumSizeAllowed);
196         threshold = DEFAULT_THRESHOLD;
197         computeAndKeepInliers = DEFAULT_COMPUTE_AND_KEEP_INLIERS;
198         computeAndKeepResiduals = DEFAULT_COMPUTE_AND_KEEP_RESIDUALS;
199     }
200 
201     /**
202      * Constructor with listener and lists of points to be used to estimate an
203      * Euclidean 2D transformation.
204      * Points in the list located at the same position are considered to be
205      * matched. Hence, both lists must have the same size, and their size must
206      * be greater or equal than MINIMUM_SIZE.
207      *
208      * @param listener               listener to be notified of events such as when estimation
209      *                               starts, ends or its progress significantly changes.
210      * @param inputPoints            list of input points to be used to estimate an
211      *                               Euclidean 2D transformation.
212      * @param outputPoints           list of output points to be used to estimate an
213      *                               Euclidean 2D transformation.
214      * @param weakMinimumSizeAllowed true allows 3 points, false requires 4.
215      * @throws IllegalArgumentException if provided lists of points don't have
216      *                                  the same size or their size is smaller than MINIMUM_SIZE.
217      */
218     public RANSACEuclideanTransformation2DRobustEstimator(
219             final EuclideanTransformation2DRobustEstimatorListener listener,
220             final List<Point2D> inputPoints, final List<Point2D> outputPoints, final boolean weakMinimumSizeAllowed) {
221         super(listener, inputPoints, outputPoints, weakMinimumSizeAllowed);
222         threshold = DEFAULT_THRESHOLD;
223         computeAndKeepInliers = DEFAULT_COMPUTE_AND_KEEP_INLIERS;
224         computeAndKeepResiduals = DEFAULT_COMPUTE_AND_KEEP_RESIDUALS;
225     }
226 
227     /**
228      * Returns threshold to determine whether points are inliers or not when
229      * testing possible estimation solutions.
230      * The threshold refers to the amount of error (i.e. Euclidean distance) a
231      * possible solution has on a matched pair of points.
232      *
233      * @return threshold to determine whether points are inliers or not when
234      * testing possible estimation solutions.
235      */
236     public double getThreshold() {
237         return threshold;
238     }
239 
240     /**
241      * Sets threshold to determine whether points are inliers or not when
242      * testing possible estimation solutions.
243      * The threshold refers to the amount of error (i.e. Euclidean distance) a
244      * possible solution has on a matched pair of points.
245      *
246      * @param threshold threshold to be set.
247      * @throws IllegalArgumentException if provided value is equal or less than
248      *                                  zero.
249      * @throws LockedException          if robust estimator is locked because an
250      *                                  estimation is already in progress.
251      */
252     public void setThreshold(final double threshold) throws LockedException {
253         if (isLocked()) {
254             throw new LockedException();
255         }
256         if (threshold <= MIN_THRESHOLD) {
257             throw new IllegalArgumentException();
258         }
259         this.threshold = threshold;
260     }
261 
262     /**
263      * Indicates whether inliers must be computed and kept.
264      *
265      * @return true if inliers must be computed and kept, false if inliers only
266      * need to be computed but not kept.
267      */
268     public boolean isComputeAndKeepInliersEnabled() {
269         return computeAndKeepInliers;
270     }
271 
272     /**
273      * Specifies whether inliers must be computed and kept.
274      *
275      * @param computeAndKeepInliers true if inliers must be computed and kept,
276      *                              false if inliers only need to be computed but not kept.
277      * @throws LockedException if estimator is locked.
278      */
279     public void setComputeAndKeepInliersEnabled(final boolean computeAndKeepInliers) throws LockedException {
280         if (isLocked()) {
281             throw new LockedException();
282         }
283         this.computeAndKeepInliers = computeAndKeepInliers;
284     }
285 
286     /**
287      * Indicates whether residuals must be computed and kept.
288      *
289      * @return true if residuals must be computed and kept, false if residuals
290      * only need to be computed but not kept.
291      */
292     public boolean isComputeAndKeepResidualsEnabled() {
293         return computeAndKeepResiduals;
294     }
295 
296     /**
297      * Specifies whether residuals must be computed and kept.
298      *
299      * @param computeAndKeepResiduals true if residuals must be computed and
300      *                                kept, false if residuals only need to be computed but not kept.
301      * @throws LockedException if estimator is locked.
302      */
303     public void setComputeAndKeepResidualsEnabled(final boolean computeAndKeepResiduals) throws LockedException {
304         if (isLocked()) {
305             throw new LockedException();
306         }
307         this.computeAndKeepResiduals = computeAndKeepResiduals;
308     }
309 
310     /**
311      * Estimates an Euclidean 2D transformation using a robust estimator and
312      * the best set of matched 2D point correspondences found using the robust
313      * estimator.
314      *
315      * @return an Euclidean 2D transformation.
316      * @throws LockedException          if robust estimator is locked because an
317      *                                  estimation is already in progress.
318      * @throws NotReadyException        if provided input data is not enough to start
319      *                                  the estimation.
320      * @throws RobustEstimatorException if estimation fails for any reason
321      *                                  (i.e. numerical instability, no solution available, etc).
322      */
323     @Override
324     public EuclideanTransformation2D estimate() throws LockedException, NotReadyException, RobustEstimatorException {
325         if (isLocked()) {
326             throw new LockedException();
327         }
328         if (!isReady()) {
329             throw new NotReadyException();
330         }
331 
332         final var innerEstimator = new RANSACRobustEstimator<>(
333                 new RANSACRobustEstimatorListener<EuclideanTransformation2D>() {
334 
335                     // point to be reused when computing residuals
336                     private final Point2D testPoint = Point2D.create(CoordinatesType.HOMOGENEOUS_COORDINATES);
337 
338                     private final EuclideanTransformation2DEstimator nonRobustEstimator =
339                             new EuclideanTransformation2DEstimator(isWeakMinimumSizeAllowed());
340 
341                     private final List<Point2D> subsetInputPoints = new ArrayList<>();
342                     private final List<Point2D> subsetOutputPoints = new ArrayList<>();
343 
344                     @Override
345                     public double getThreshold() {
346                         return threshold;
347                     }
348 
349                     @Override
350                     public int getTotalSamples() {
351                         return inputPoints.size();
352                     }
353 
354                     @Override
355                     public int getSubsetSize() {
356                         return nonRobustEstimator.getMinimumPoints();
357                     }
358 
359                     @Override
360                     public void estimatePreliminarSolutions(
361                             final int[] samplesIndices, final List<EuclideanTransformation2D> solutions) {
362                         subsetInputPoints.clear();
363                         subsetOutputPoints.clear();
364                         for (final var samplesIndex : samplesIndices) {
365                             subsetInputPoints.add(inputPoints.get(samplesIndex));
366                             subsetOutputPoints.add(outputPoints.get(samplesIndex));
367                         }
368 
369                         try {
370                             nonRobustEstimator.setPoints(subsetInputPoints, subsetOutputPoints);
371                             solutions.add(nonRobustEstimator.estimate());
372                         } catch (final Exception e) {
373                             // if points are coincident, no solution is added
374                         }
375                     }
376 
377                     @Override
378                     public double computeResidual(final EuclideanTransformation2D currentEstimation, final int i) {
379                         final var inputPoint = inputPoints.get(i);
380                         final var outputPoint = outputPoints.get(i);
381 
382                         // transform input point and store result in mTestPoint
383                         currentEstimation.transform(inputPoint, testPoint);
384 
385                         return outputPoint.distanceTo(testPoint);
386                     }
387 
388                     @Override
389                     public boolean isReady() {
390                         return RANSACEuclideanTransformation2DRobustEstimator.this.isReady();
391                     }
392 
393                     @Override
394                     public void onEstimateStart(final RobustEstimator<EuclideanTransformation2D> estimator) {
395                         if (listener != null) {
396                             listener.onEstimateStart(RANSACEuclideanTransformation2DRobustEstimator.this);
397                         }
398                     }
399 
400                     @Override
401                     public void onEstimateEnd(final RobustEstimator<EuclideanTransformation2D> estimator) {
402                         if (listener != null) {
403                             listener.onEstimateEnd(RANSACEuclideanTransformation2DRobustEstimator.this);
404                         }
405                     }
406 
407                     @Override
408                     public void onEstimateNextIteration(
409                             final RobustEstimator<EuclideanTransformation2D> estimator, final int iteration) {
410                         if (listener != null) {
411                             listener.onEstimateNextIteration(
412                                     RANSACEuclideanTransformation2DRobustEstimator.this, iteration);
413                         }
414                     }
415 
416                     @Override
417                     public void onEstimateProgressChange(
418                             final RobustEstimator<EuclideanTransformation2D> estimator, final float progress) {
419                         if (listener != null) {
420                             listener.onEstimateProgressChange(
421                                     RANSACEuclideanTransformation2DRobustEstimator.this, progress);
422                         }
423                     }
424                 });
425 
426         try {
427             locked = true;
428             inliersData = null;
429             innerEstimator.setComputeAndKeepInliersEnabled(computeAndKeepInliers || refineResult);
430             innerEstimator.setComputeAndKeepResidualsEnabled(computeAndKeepResiduals || refineResult);
431             innerEstimator.setConfidence(confidence);
432             innerEstimator.setMaxIterations(maxIterations);
433             innerEstimator.setProgressDelta(progressDelta);
434             final var transformation = innerEstimator.estimate();
435             inliersData = innerEstimator.getInliersData();
436             return attemptRefine(transformation);
437         } catch (final com.irurueta.numerical.LockedException e) {
438             throw new LockedException(e);
439         } catch (final com.irurueta.numerical.NotReadyException e) {
440             throw new NotReadyException(e);
441         } finally {
442             locked = false;
443         }
444     }
445 
446     /**
447      * Returns method being used for robust estimation.
448      *
449      * @return method being used for robust estimation.
450      */
451     @Override
452     public RobustEstimatorMethod getMethod() {
453         return RobustEstimatorMethod.RANSAC;
454     }
455 
456     /**
457      * Gets standard deviation used for Levenberg-Marquardt fitting during
458      * refinement.
459      * Returned value gives an indication of how much variance each residual
460      * has.
461      * Typically, this value is related to the threshold used on each robust
462      * estimation, since residuals of found inliers are within the range of
463      * such threshold.
464      *
465      * @return standard deviation used for refinement.
466      */
467     @Override
468     protected double getRefinementStandardDeviation() {
469         return threshold;
470     }
471 }