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.EuclideanTransformation3D;
20  import com.irurueta.geometry.Point3D;
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 3D transformation for provided collections of
32   * matched 3D points using RANSAC algorithm.
33   */
34  @SuppressWarnings("DuplicatedCode")
35  public class RANSACEuclideanTransformation3DRobustEstimator extends EuclideanTransformation3DRobustEstimator {
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 RANSACEuclideanTransformation3DRobustEstimator() {
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       *                     affine 3D transformation.
98       * @param outputPoints list of output points to be used to estimate an
99       *                     affine 3D 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 RANSACEuclideanTransformation3DRobustEstimator(
104             final List<Point3D> inputPoints, final List<Point3D> 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 RANSACEuclideanTransformation3DRobustEstimator(
118             final EuclideanTransformation3DRobustEstimatorListener 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 3D 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      *                     affine 3D transformation.
136      * @param outputPoints list of output points to be used to estimate an
137      *                     affine 3D 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 RANSACEuclideanTransformation3DRobustEstimator(
142             final EuclideanTransformation3DRobustEstimatorListener listener,
143             final List<Point3D> inputPoints, final List<Point3D> 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 RANSACEuclideanTransformation3DRobustEstimator(final boolean weakMinimumSizeAllowed) {
156         super(weakMinimumSizeAllowed);
157         threshold = DEFAULT_THRESHOLD;
158         computeAndKeepInliers = DEFAULT_COMPUTE_AND_KEEP_INLIERS;
159         computeAndKeepResiduals = DEFAULT_COMPUTE_AND_KEEP_RESIDUALS;
160     }
161 
162     /**
163      * Constructor with lists of points to be used to estimate an Euclidean 2D
164      * transformation.
165      * Points in the list located at the same position are considered to be
166      * matched. Hence, both lists must have the same size, and their size must
167      * be greater or equal than MINIMUM_SIZE.
168      *
169      * @param inputPoints            list of input points to be used to estimate an
170      *                               affine 3D transformation.
171      * @param outputPoints           list of output points to be used to estimate an
172      *                               affine 3D transformation.
173      * @param weakMinimumSizeAllowed true allows 3 points, false requires 4.
174      * @throws IllegalArgumentException if provided lists of points don't have
175      *                                  the same size or their size is smaller than MINIMUM_SIZE.
176      */
177     public RANSACEuclideanTransformation3DRobustEstimator(
178             final List<Point3D> inputPoints, final List<Point3D> outputPoints, final boolean weakMinimumSizeAllowed) {
179         super(inputPoints, outputPoints, weakMinimumSizeAllowed);
180         threshold = DEFAULT_THRESHOLD;
181         computeAndKeepInliers = DEFAULT_COMPUTE_AND_KEEP_INLIERS;
182         computeAndKeepResiduals = DEFAULT_COMPUTE_AND_KEEP_RESIDUALS;
183     }
184 
185     /**
186      * Constructor.
187      *
188      * @param listener               listener to be notified of events such as when estimation
189      *                               starts, ends or its progress significantly changes.
190      * @param weakMinimumSizeAllowed true allows 3 points, false requires 4.
191      */
192     public RANSACEuclideanTransformation3DRobustEstimator(
193             final EuclideanTransformation3DRobustEstimatorListener listener, final boolean weakMinimumSizeAllowed) {
194         super(listener, weakMinimumSizeAllowed);
195         threshold = DEFAULT_THRESHOLD;
196         computeAndKeepInliers = DEFAULT_COMPUTE_AND_KEEP_INLIERS;
197         computeAndKeepResiduals = DEFAULT_COMPUTE_AND_KEEP_RESIDUALS;
198     }
199 
200     /**
201      * Constructor with listener and lists of points to be used to estimate an
202      * Euclidean 3D transformation.
203      * Points in the list located at the same position are considered to be
204      * matched. Hence, both lists must have the same size, and their size must
205      * be greater or equal than MINIMUM_SIZE.
206      *
207      * @param listener               listener to be notified of events such as when estimation
208      *                               starts, ends or its progress significantly changes.
209      * @param inputPoints            list of input points to be used to estimate an
210      *                               affine 3D transformation.
211      * @param outputPoints           list of output points to be used to estimate an
212      *                               affine 3D transformation.
213      * @param weakMinimumSizeAllowed true allows 3 points, false requires 4.
214      * @throws IllegalArgumentException if provided lists of points don't have
215      *                                  the same size or their size is smaller than MINIMUM_SIZE.
216      */
217     public RANSACEuclideanTransformation3DRobustEstimator(
218             final EuclideanTransformation3DRobustEstimatorListener listener,
219             final List<Point3D> inputPoints, final List<Point3D> outputPoints, final boolean weakMinimumSizeAllowed) {
220         super(listener, inputPoints, outputPoints, weakMinimumSizeAllowed);
221         threshold = DEFAULT_THRESHOLD;
222         computeAndKeepInliers = DEFAULT_COMPUTE_AND_KEEP_INLIERS;
223         computeAndKeepResiduals = DEFAULT_COMPUTE_AND_KEEP_RESIDUALS;
224     }
225 
226     /**
227      * Returns threshold to determine whether points are inliers or not when
228      * testing possible estimation solutions.
229      * The threshold refers to the amount of error (i.e. Euclidean distance) a
230      * possible solution has on a matched pair of points.
231      *
232      * @return threshold to determine whether points are inliers or not when
233      * testing possible estimation solutions.
234      */
235     public double getThreshold() {
236         return threshold;
237     }
238 
239     /**
240      * Sets threshold to determine whether points are inliers or not when
241      * testing possible estimation solutions.
242      * The threshold refers to the amount of error (i.e. Euclidean distance) a
243      * possible solution has on a matched pair of points.
244      *
245      * @param threshold threshold to be set.
246      * @throws IllegalArgumentException if provided value is equal or less than
247      *                                  zero.
248      * @throws LockedException          if robust estimator is locked because an
249      *                                  estimation is already in progress.
250      */
251     public void setThreshold(final double threshold) throws LockedException {
252         if (isLocked()) {
253             throw new LockedException();
254         }
255         if (threshold <= MIN_THRESHOLD) {
256             throw new IllegalArgumentException();
257         }
258         this.threshold = threshold;
259     }
260 
261     /**
262      * Indicates whether inliers must be computed and kept.
263      *
264      * @return true if inliers must be computed and kept, false if inliers only
265      * need to be computed but not kept.
266      */
267     public boolean isComputeAndKeepInliersEnabled() {
268         return computeAndKeepInliers;
269     }
270 
271     /**
272      * Specifies whether inliers must be computed and kept.
273      *
274      * @param computeAndKeepInliers true if inliers must be computed and kept,
275      *                              false if inliers only need to be computed but not kept.
276      * @throws LockedException if estimator is locked.
277      */
278     public void setComputeAndKeepInliersEnabled(final boolean computeAndKeepInliers) throws LockedException {
279         if (isLocked()) {
280             throw new LockedException();
281         }
282         this.computeAndKeepInliers = computeAndKeepInliers;
283     }
284 
285     /**
286      * Indicates whether residuals must be computed and kept.
287      *
288      * @return true if residuals must be computed and kept, false if residuals
289      * only need to be computed but not kept.
290      */
291     public boolean isComputeAndKeepResidualsEnabled() {
292         return computeAndKeepResiduals;
293     }
294 
295     /**
296      * Specifies whether residuals must be computed and kept.
297      *
298      * @param computeAndKeepResiduals true if residuals must be computed and
299      *                                kept, false if residuals only need to be computed but not kept.
300      * @throws LockedException if estimator is locked.
301      */
302     public void setComputeAndKeepResidualsEnabled(final boolean computeAndKeepResiduals) throws LockedException {
303         if (isLocked()) {
304             throw new LockedException();
305         }
306         this.computeAndKeepResiduals = computeAndKeepResiduals;
307     }
308 
309     /**
310      * Estimates an affine 3D transformation using a robust estimator and
311      * the best set of matched 3D point correspondences found using the robust
312      * estimator.
313      *
314      * @return an affine 3D transformation.
315      * @throws LockedException          if robust estimator is locked because an
316      *                                  estimation is already in progress.
317      * @throws NotReadyException        if provided input data is not enough to start
318      *                                  the estimation.
319      * @throws RobustEstimatorException if estimation fails for any reason
320      *                                  (i.e. numerical instability, no solution available, etc).
321      */
322     @Override
323     public EuclideanTransformation3D estimate() throws LockedException, NotReadyException, RobustEstimatorException {
324         if (isLocked()) {
325             throw new LockedException();
326         }
327         if (!isReady()) {
328             throw new NotReadyException();
329         }
330 
331         final var innerEstimator = new RANSACRobustEstimator<>(
332                 new RANSACRobustEstimatorListener<EuclideanTransformation3D>() {
333 
334                     // point to be reused when computing residuals
335                     private final Point3D testPoint = Point3D.create(CoordinatesType.HOMOGENEOUS_COORDINATES);
336 
337                     private final EuclideanTransformation3DEstimator nonRobustEstimator =
338                             new EuclideanTransformation3DEstimator(isWeakMinimumSizeAllowed());
339 
340                     private final List<Point3D> subsetInputPoints = new ArrayList<>();
341                     private final List<Point3D> subsetOutputPoints = new ArrayList<>();
342 
343                     @Override
344                     public double getThreshold() {
345                         return threshold;
346                     }
347 
348                     @Override
349                     public int getTotalSamples() {
350                         return inputPoints.size();
351                     }
352 
353                     @Override
354                     public int getSubsetSize() {
355                         return nonRobustEstimator.getMinimumPoints();
356                     }
357 
358                     @Override
359                     public void estimatePreliminarSolutions(
360                             final int[] samplesIndices, final List<EuclideanTransformation3D> solutions) {
361                         subsetInputPoints.clear();
362                         subsetOutputPoints.clear();
363                         for (final var samplesIndex : samplesIndices) {
364                             subsetInputPoints.add(inputPoints.get(samplesIndex));
365                             subsetOutputPoints.add(outputPoints.get(samplesIndex));
366                         }
367 
368                         try {
369                             nonRobustEstimator.setPoints(subsetInputPoints, subsetOutputPoints);
370                             solutions.add(nonRobustEstimator.estimate());
371                         } catch (final Exception e) {
372                             // if points are coincident, no solution is added
373                         }
374                     }
375 
376                     @Override
377                     public double computeResidual(final EuclideanTransformation3D currentEstimation, final int i) {
378                         final var inputPoint = inputPoints.get(i);
379                         final var outputPoint = outputPoints.get(i);
380 
381                         // transform input point and store result in mTestPoint
382                         currentEstimation.transform(inputPoint, testPoint);
383 
384                         return outputPoint.distanceTo(testPoint);
385                     }
386 
387                     @Override
388                     public boolean isReady() {
389                         return RANSACEuclideanTransformation3DRobustEstimator.this.isReady();
390                     }
391 
392                     @Override
393                     public void onEstimateStart(final RobustEstimator<EuclideanTransformation3D> estimator) {
394                         if (listener != null) {
395                             listener.onEstimateStart(RANSACEuclideanTransformation3DRobustEstimator.this);
396                         }
397                     }
398 
399                     @Override
400                     public void onEstimateEnd(final RobustEstimator<EuclideanTransformation3D> estimator) {
401                         if (listener != null) {
402                             listener.onEstimateEnd(RANSACEuclideanTransformation3DRobustEstimator.this);
403                         }
404                     }
405 
406                     @Override
407                     public void onEstimateNextIteration(
408                             final RobustEstimator<EuclideanTransformation3D> estimator, final int iteration) {
409                         if (listener != null) {
410                             listener.onEstimateNextIteration(
411                                     RANSACEuclideanTransformation3DRobustEstimator.this, iteration);
412                         }
413                     }
414 
415                     @Override
416                     public void onEstimateProgressChange(
417                             final RobustEstimator<EuclideanTransformation3D> estimator, final float progress) {
418                         if (listener != null) {
419                             listener.onEstimateProgressChange(
420                                     RANSACEuclideanTransformation3DRobustEstimator.this, progress);
421                         }
422                     }
423                 });
424 
425         try {
426             locked = true;
427             inliersData = null;
428             innerEstimator.setComputeAndKeepInliersEnabled(computeAndKeepInliers || refineResult);
429             innerEstimator.setComputeAndKeepResidualsEnabled(computeAndKeepResiduals || refineResult);
430             innerEstimator.setConfidence(confidence);
431             innerEstimator.setMaxIterations(maxIterations);
432             innerEstimator.setProgressDelta(progressDelta);
433             final var transformation = innerEstimator.estimate();
434             inliersData = innerEstimator.getInliersData();
435             return attemptRefine(transformation);
436         } catch (final com.irurueta.numerical.LockedException e) {
437             throw new LockedException(e);
438         } catch (final com.irurueta.numerical.NotReadyException e) {
439             throw new NotReadyException(e);
440         } finally {
441             locked = false;
442         }
443     }
444 
445     /**
446      * Returns method being used for robust estimation.
447      *
448      * @return method being used for robust estimation.
449      */
450     @Override
451     public RobustEstimatorMethod getMethod() {
452         return RobustEstimatorMethod.RANSAC;
453     }
454 
455     /**
456      * Gets standard deviation used for Levenberg-Marquardt fitting during
457      * refinement.
458      * Returned value gives an indication of how much variance each residual
459      * has.
460      * Typically, this value is related to the threshold used on each robust
461      * estimation, since residuals of found inliers are within the range of
462      * such threshold.
463      *
464      * @return standard deviation used for refinement.
465      */
466     @Override
467     protected double getRefinementStandardDeviation() {
468         return threshold;
469     }
470 }