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