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