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.geometry.estimators;
17  
18  import com.irurueta.geometry.AffineTransformation2D;
19  import com.irurueta.geometry.CoincidentPointsException;
20  import com.irurueta.geometry.CoordinatesType;
21  import com.irurueta.geometry.Point2D;
22  import com.irurueta.numerical.robust.RANSACRobustEstimator;
23  import com.irurueta.numerical.robust.RANSACRobustEstimatorListener;
24  import com.irurueta.numerical.robust.RobustEstimator;
25  import com.irurueta.numerical.robust.RobustEstimatorException;
26  import com.irurueta.numerical.robust.RobustEstimatorMethod;
27  
28  import java.util.List;
29  
30  /**
31   * Finds the best affine 2D transformation for provided collections of matched
32   * 2D points using RANSAC algorithm.
33   */
34  public class RANSACPointCorrespondenceAffineTransformation2DRobustEstimator
35          extends PointCorrespondenceAffineTransformation2DRobustEstimator {
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 RANSACPointCorrespondenceAffineTransformation2DRobustEstimator() {
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 affine 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 2D transformation.
98       * @param outputPoints list of output points to be used to estimate an
99       *                     affine 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 RANSACPointCorrespondenceAffineTransformation2DRobustEstimator(
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 RANSACPointCorrespondenceAffineTransformation2DRobustEstimator(
118             final AffineTransformation2DRobustEstimatorListener 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      * affine 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      *                     affine 2D transformation.
136      * @param outputPoints list of output points to be used to estimate an
137      *                     affine 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 RANSACPointCorrespondenceAffineTransformation2DRobustEstimator(
142             final AffineTransformation2DRobustEstimatorListener 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      * Returns threshold to determine whether points are inliers or not when
152      * testing possible estimation solutions.
153      * The threshold refers to the amount of error (i.e. Euclidean distance) a
154      * possible solution has on a matched pair of points.
155      *
156      * @return threshold to determine whether points are inliers or not when
157      * testing possible estimation solutions.
158      */
159     public double getThreshold() {
160         return threshold;
161     }
162 
163     /**
164      * Sets threshold to determine whether points are inliers or not when
165      * testing possible estimation solutions.
166      * The threshold refers to the amount of error (i.e. Euclidean distance) a
167      * possible solution has on a matched pair of points.
168      *
169      * @param threshold threshold to be set.
170      * @throws IllegalArgumentException if provided value is equal or less than
171      *                                  zero.
172      * @throws LockedException          if robust estimator is locked because an
173      *                                  estimation is already in progress.
174      */
175     public void setThreshold(final double threshold) throws LockedException {
176         if (isLocked()) {
177             throw new LockedException();
178         }
179         if (threshold <= MIN_THRESHOLD) {
180             throw new IllegalArgumentException();
181         }
182         this.threshold = threshold;
183     }
184 
185     /**
186      * Indicates whether inliers must be computed and kept.
187      *
188      * @return true if inliers must be computed and kept, false if inliers only
189      * need to be computed but not kept.
190      */
191     public boolean isComputeAndKeepInliersEnabled() {
192         return computeAndKeepInliers;
193     }
194 
195     /**
196      * Specifies whether inliers must be computed and kept.
197      *
198      * @param computeAndKeepInliers true if inliers must be computed and kept,
199      *                              false if inliers only need to be computed but not kept.
200      * @throws LockedException if estimator is locked.
201      */
202     public void setComputeAndKeepInliersEnabled(final boolean computeAndKeepInliers) throws LockedException {
203         if (isLocked()) {
204             throw new LockedException();
205         }
206         this.computeAndKeepInliers = computeAndKeepInliers;
207     }
208 
209     /**
210      * Indicates whether residuals must be computed and kept.
211      *
212      * @return true if residuals must be computed and kept, false if residuals
213      * only need to be computed but not kept.
214      */
215     public boolean isComputeAndKeepResidualsEnabled() {
216         return computeAndKeepResiduals;
217     }
218 
219     /**
220      * Specifies whether residuals must be computed and kept.
221      *
222      * @param computeAndKeepResiduals true if residuals must be computed and
223      *                                kept, false if residuals only need to be computed but not kept.
224      * @throws LockedException if estimator is locked.
225      */
226     public void setComputeAndKeepResidualsEnabled(final boolean computeAndKeepResiduals) throws LockedException {
227         if (isLocked()) {
228             throw new LockedException();
229         }
230         this.computeAndKeepResiduals = computeAndKeepResiduals;
231     }
232 
233     /**
234      * Estimates an affine 2D transformation using a robust estimator and
235      * the best set of matched 2D point correspondences found using the robust
236      * estimator.
237      *
238      * @return an affine 2D transformation.
239      * @throws LockedException          if robust estimator is locked because an
240      *                                  estimation is already in progress.
241      * @throws NotReadyException        if provided input data is not enough to start
242      *                                  the estimation.
243      * @throws RobustEstimatorException if estimation fails for any reason
244      *                                  (i.e. numerical instability, no solution available, etc).
245      */
246     @SuppressWarnings("DuplicatedCode")
247     @Override
248     public AffineTransformation2D estimate() throws LockedException, NotReadyException, RobustEstimatorException {
249         if (isLocked()) {
250             throw new LockedException();
251         }
252         if (!isReady()) {
253             throw new NotReadyException();
254         }
255 
256         final var innerEstimator = new RANSACRobustEstimator<>(
257                 new RANSACRobustEstimatorListener<AffineTransformation2D>() {
258 
259                     // point to be reused when computing residuals
260                     private final Point2D testPoint = Point2D.create(CoordinatesType.HOMOGENEOUS_COORDINATES);
261 
262                     @Override
263                     public double getThreshold() {
264                         return threshold;
265                     }
266 
267                     @Override
268                     public int getTotalSamples() {
269                         return inputPoints.size();
270                     }
271 
272                     @Override
273                     public int getSubsetSize() {
274                         return AffineTransformation2DRobustEstimator.MINIMUM_SIZE;
275                     }
276 
277                     @Override
278                     public void estimatePreliminarSolutions(
279                             final int[] samplesIndices, final List<AffineTransformation2D> solutions) {
280                         final var inputPoint1 = inputPoints.get(samplesIndices[0]);
281                         final var inputPoint2 = inputPoints.get(samplesIndices[1]);
282                         final var inputPoint3 = inputPoints.get(samplesIndices[2]);
283 
284                         final var outputPoint1 = outputPoints.get(samplesIndices[0]);
285                         final var outputPoint2 = outputPoints.get(samplesIndices[1]);
286                         final var outputPoint3 = outputPoints.get(samplesIndices[2]);
287 
288                         try {
289                             final var transformation = new AffineTransformation2D(inputPoint1, inputPoint2, inputPoint3,
290                                     outputPoint1, outputPoint2, outputPoint3);
291                             solutions.add(transformation);
292                         } catch (final CoincidentPointsException e) {
293                             // if points are coincident, no solution is added
294                         }
295                     }
296 
297                     @Override
298                     public double computeResidual(final AffineTransformation2D currentEstimation, final int i) {
299                         final var inputPoint = inputPoints.get(i);
300                         final var outputPoint = outputPoints.get(i);
301 
302                         // transform input point and store result in mTestPoint
303                         currentEstimation.transform(inputPoint, testPoint);
304 
305                         return outputPoint.distanceTo(testPoint);
306                     }
307 
308                     @Override
309                     public boolean isReady() {
310                         return RANSACPointCorrespondenceAffineTransformation2DRobustEstimator.this.isReady();
311                     }
312 
313                     @Override
314                     public void onEstimateStart(final RobustEstimator<AffineTransformation2D> estimator) {
315                         if (mListener != null) {
316                             mListener.onEstimateStart(
317                                     RANSACPointCorrespondenceAffineTransformation2DRobustEstimator.this);
318                         }
319                     }
320 
321                     @Override
322                     public void onEstimateEnd(final RobustEstimator<AffineTransformation2D> estimator) {
323                         if (mListener != null) {
324                             mListener.onEstimateEnd(
325                                     RANSACPointCorrespondenceAffineTransformation2DRobustEstimator.this);
326                         }
327                     }
328 
329                     @Override
330                     public void onEstimateNextIteration(
331                             final RobustEstimator<AffineTransformation2D> estimator, final int iteration) {
332                         if (mListener != null) {
333                             mListener.onEstimateNextIteration(
334                                     RANSACPointCorrespondenceAffineTransformation2DRobustEstimator.this,
335                                     iteration);
336                         }
337                     }
338 
339                     @Override
340                     public void onEstimateProgressChange(
341                             final RobustEstimator<AffineTransformation2D> estimator, final float progress) {
342                         if (mListener != null) {
343                             mListener.onEstimateProgressChange(
344                                     RANSACPointCorrespondenceAffineTransformation2DRobustEstimator.this,
345                                     progress);
346                         }
347                     }
348                 });
349 
350         try {
351             locked = true;
352             inliersData = null;
353             innerEstimator.setComputeAndKeepInliersEnabled(computeAndKeepInliers || refineResult);
354             innerEstimator.setComputeAndKeepResidualsEnabled(computeAndKeepResiduals || refineResult);
355             innerEstimator.setConfidence(confidence);
356             innerEstimator.setMaxIterations(maxIterations);
357             innerEstimator.setProgressDelta(progressDelta);
358             final var transformation = innerEstimator.estimate();
359             inliersData = innerEstimator.getInliersData();
360             return attemptRefine(transformation);
361         } catch (final com.irurueta.numerical.LockedException e) {
362             throw new LockedException(e);
363         } catch (final com.irurueta.numerical.NotReadyException e) {
364             throw new NotReadyException(e);
365         } finally {
366             locked = false;
367         }
368     }
369 
370     /**
371      * Returns method being used for robust estimation.
372      *
373      * @return method being used for robust estimation.
374      */
375     @Override
376     public RobustEstimatorMethod getMethod() {
377         return RobustEstimatorMethod.RANSAC;
378     }
379 
380     /**
381      * Gets standard deviation used for Levenberg-Marquardt fitting during
382      * refinement.
383      * Returned value gives an indication of how much variance each residual
384      * has.
385      * Typically, this value is related to the threshold used on each robust
386      * estimation, since residuals of found inliers are within the range of
387      * such threshold.
388      *
389      * @return standard deviation used for refinement.
390      */
391     @Override
392     protected double getRefinementStandardDeviation() {
393         return threshold;
394     }
395 }