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.PROMedSRobustEstimator;
23  import com.irurueta.numerical.robust.PROMedSRobustEstimatorListener;
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 PROMedS algorithm.
33   */
34  @SuppressWarnings("DuplicatedCode")
35  public class PROMedSPointCorrespondenceAffineTransformation2DRobustEstimator
36          extends PointCorrespondenceAffineTransformation2DRobustEstimator {
37  
38      /**
39       * Default value to be used for stop threshold. Stop threshold can be used
40       * to keep the algorithm iterating in case that best estimated threshold
41       * using median of residuals is not small enough. Once a solution is found
42       * that generates a threshold below this value, the algorithm will stop.
43       * The stop threshold can be used to prevent the LMedS algorithm iterating
44       * too many times in cases where samples have a very similar accuracy.
45       * For instance, in cases where proportion of outliers is very small (close
46       * to 0%), and samples are very accurate (i.e. 1e-6), the algorithm would
47       * iterate for a long time trying to find the best solution when indeed
48       * there is no need to do that if a reasonable threshold has already been
49       * reached.
50       * Because of this behaviour the stop threshold can be set to a value much
51       * lower than the one typically used in RANSAC, and yet the algorithm could
52       * still produce even smaller thresholds in estimated results.
53       */
54      public static final double DEFAULT_STOP_THRESHOLD = 1.0;
55  
56      /**
57       * Minimum allowed stop threshold value.
58       */
59      public static final double MIN_STOP_THRESHOLD = 0.0;
60  
61      /**
62       * Threshold to be used to keep the algorithm iterating in case that best
63       * estimated threshold using median of residuals is not small enough. Once
64       * a solution is found that generates a threshold below this value, the
65       * algorithm will stop.
66       * The stop threshold can be used to prevent the LMedS algorithm iterating
67       * too many times in cases where samples have a very similar accuracy.
68       * For instance, in cases where proportion of outliers is very small (close
69       * to 0%), and samples are very accurate (i.e. 1e-6), the algorithm would
70       * iterate for a long time trying to find the best solution when indeed
71       * there is no need to do that if a reasonable threshold has already been
72       * reached.
73       * Because of this behaviour the stop threshold can be set to a value much
74       * lower than the one typically used in RANSAC, and yet the algorithm could
75       * still produce even smaller thresholds in estimated results.
76       */
77      private double stopThreshold;
78  
79      /**
80       * Quality scores corresponding to each pair of matched points.
81       * The larger the score value the better the quality of the matching.
82       */
83      private double[] qualityScores;
84  
85      /**
86       * Constructor.
87       */
88      public PROMedSPointCorrespondenceAffineTransformation2DRobustEstimator() {
89          super();
90          stopThreshold = DEFAULT_STOP_THRESHOLD;
91      }
92  
93      /**
94       * Constructor with lists of points to be used to estimate an affine 2D
95       * transformation.
96       * Points in the list located at the same position are considered to be
97       * matched. Hence, both lists must have the same size, and their size must
98       * be greater or equal than MINIMUM_SIZE.
99       *
100      * @param inputPoints  list of input points to be used to estimate an
101      *                     affine 2D transformation.
102      * @param outputPoints list of output points to be used to estimate an
103      *                     affine 2D transformation.
104      * @throws IllegalArgumentException if provided lists of points don't have
105      *                                  the same size or their size is smaller than MINIMUM_SIZE.
106      */
107     public PROMedSPointCorrespondenceAffineTransformation2DRobustEstimator(
108             final List<Point2D> inputPoints, final List<Point2D> outputPoints) {
109         super(inputPoints, outputPoints);
110         stopThreshold = DEFAULT_STOP_THRESHOLD;
111     }
112 
113     /**
114      * Constructor.
115      *
116      * @param listener listener to be notified of events such as when estimation
117      *                 starts, ends or its progress significantly changes.
118      */
119     public PROMedSPointCorrespondenceAffineTransformation2DRobustEstimator(
120             final AffineTransformation2DRobustEstimatorListener listener) {
121         super(listener);
122         stopThreshold = DEFAULT_STOP_THRESHOLD;
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      *                     stars, 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 PROMedSPointCorrespondenceAffineTransformation2DRobustEstimator(
142             final AffineTransformation2DRobustEstimatorListener listener,
143             final List<Point2D> inputPoints, final List<Point2D> outputPoints) {
144         super(listener, inputPoints, outputPoints);
145         stopThreshold = DEFAULT_STOP_THRESHOLD;
146     }
147 
148     /**
149      * Constructor.
150      *
151      * @param qualityScores quality scores corresponding to each pair of matched
152      *                      points.
153      * @throws IllegalArgumentException if provided quality scores length is
154      *                                  smaller than MINIMUM_SIZE (i.e. 3 samples).
155      */
156     public PROMedSPointCorrespondenceAffineTransformation2DRobustEstimator(final double[] qualityScores) {
157         super();
158         stopThreshold = DEFAULT_STOP_THRESHOLD;
159         internalSetQualityScores(qualityScores);
160     }
161 
162     /**
163      * Constructor with lists of points to be used to estimate an affine 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 2D transformation.
171      * @param outputPoints  list of output points to be used to estimate an
172      *                      affine 2D transformation.
173      * @param qualityScores quality scores corresponding to each pair of matched
174      *                      points.
175      * @throws IllegalArgumentException if provided lists of points and array
176      *                                  of quality scores don't have the same size or their size is smaller than
177      *                                  MINIMUM_SIZE.
178      */
179     public PROMedSPointCorrespondenceAffineTransformation2DRobustEstimator(
180             final List<Point2D> inputPoints, final List<Point2D> outputPoints, final double[] qualityScores) {
181         super(inputPoints, outputPoints);
182 
183         if (qualityScores.length != inputPoints.size()) {
184             throw new IllegalArgumentException();
185         }
186 
187         stopThreshold = DEFAULT_STOP_THRESHOLD;
188         internalSetQualityScores(qualityScores);
189     }
190 
191     /**
192      * Constructor.
193      *
194      * @param listener      listener to be notified of events such as when estimation
195      *                      starts, ends or its progress significantly changes.
196      * @param qualityScores quality scores corresponding to each pair of matched
197      *                      points.
198      * @throws IllegalArgumentException if provided quality scores length is
199      *                                  smaller than MINIMUM_SIZE (i.e. 3 samples).
200      */
201     public PROMedSPointCorrespondenceAffineTransformation2DRobustEstimator(
202             final AffineTransformation2DRobustEstimatorListener listener, final double[] qualityScores) {
203         super(listener);
204         stopThreshold = DEFAULT_STOP_THRESHOLD;
205         internalSetQualityScores(qualityScores);
206     }
207 
208     /**
209      * Constructor with listener and lists of points to be used to estimate an
210      * affine 2D transformation.
211      * Points in the list located at the same position are considered to be
212      * matched. Hence, both lists must have the same size, and their size must
213      * be greater or equal than MINIMUM_SIZE.
214      *
215      * @param listener      listener to be notified of events such as when estimation
216      *                      stars, ends or its progress significantly changes.
217      * @param inputPoints   list of input points to be used to estimate an
218      *                      affine 2D transformation.
219      * @param outputPoints  list of output points to be used to estimate an
220      *                      affine 2D transformation.
221      * @param qualityScores quality scores corresponding to each pair of matched
222      *                      points.
223      * @throws IllegalArgumentException if provided lists of points don't have
224      *                                  the same size or their size is smaller than MINIMUM_SIZE.
225      */
226     public PROMedSPointCorrespondenceAffineTransformation2DRobustEstimator(
227             final AffineTransformation2DRobustEstimatorListener listener,
228             final List<Point2D> inputPoints, final List<Point2D> outputPoints, final double[] qualityScores) {
229         super(listener, inputPoints, outputPoints);
230 
231         if (qualityScores.length != inputPoints.size()) {
232             throw new IllegalArgumentException();
233         }
234 
235         stopThreshold = DEFAULT_STOP_THRESHOLD;
236         internalSetQualityScores(qualityScores);
237     }
238 
239     /**
240      * Returns threshold to be used to keep the algorithm iterating in case that
241      * best estimated threshold using median of residuals is not small enough.
242      * Once a solution is found that generates a threshold below this value, the
243      * algorithm will stop.
244      * As in LMedS, the stop threshold can be used to prevent the PROMedS
245      * algorithm iterating too many times in cases where samples have a very
246      * similar accuracy.
247      * For instance, in cases where proportion of outliers is very small (close
248      * to 0%), and samples are very accurate (i.e. 1e-6), the algorithm would
249      * iterate for a long time trying to find the best solution when indeed
250      * there is no need to do that if a reasonable threshold has already been
251      * reached.
252      * Because of this behaviour the stop threshold can be set to a value much
253      * lower than the one typically used in RANSAC, and yet the algorithm could
254      * still produce even smaller thresholds in estimated results.
255      *
256      * @return stop threshold to stop the algorithm prematurely when a certain
257      * accuracy has been reached.
258      */
259     public double getStopThreshold() {
260         return stopThreshold;
261     }
262 
263     /**
264      * Sets threshold to be used to keep the algorithm iterating in case that
265      * best estimated threshold using median of residuals is not small enough.
266      * Once a solution is found that generates a threshold below this value, the
267      * algorithm will stop.
268      * As in LMedS, the stop threshold can be used to prevent the PROMedS
269      * algorithm iterating too many times in cases where samples have a very
270      * similar accuracy.
271      * For instance, in cases where proportion of outliers is very small (close
272      * to 0%), and samples are very accurate (i.e. 1e-6), the algorithm would
273      * iterate for a long time trying to find the best solution when indeed
274      * there is no need to do that if a reasonable threshold has already been
275      * reached.
276      * Because of this behaviour the stop threshold can be set to a value much
277      * lower than the one typically used in RANSAC, and yet the algorithm could
278      * still produce even smaller thresholds in estimated results.
279      *
280      * @param stopThreshold stop threshold to stop the algorithm prematurely
281      *                      when a certain accuracy has been reached.
282      * @throws IllegalArgumentException if provided value is zero or negative.
283      * @throws LockedException          if robust estimator is locked because an
284      *                                  estimation is already in progress.
285      */
286     public void setStopThreshold(final double stopThreshold) throws LockedException {
287         if (isLocked()) {
288             throw new LockedException();
289         }
290         if (stopThreshold <= MIN_STOP_THRESHOLD) {
291             throw new IllegalArgumentException();
292         }
293 
294         this.stopThreshold = stopThreshold;
295     }
296 
297     /**
298      * Returns quality scores corresponding to each pair of matched points.
299      * The larger the score value the better the quality of the matching.
300      *
301      * @return quality scores corresponding to each pair of matched points.
302      */
303     @Override
304     public double[] getQualityScores() {
305         return qualityScores;
306     }
307 
308     /**
309      * Sets quality scores corresponding to each pair of matched points.
310      * The larger the score value the better the quality of the matching.
311      *
312      * @param qualityScores quality scores corresponding to each pair of matched
313      *                      points.
314      * @throws LockedException          if robust estimator is locked because an
315      *                                  estimation is already in progress.
316      * @throws IllegalArgumentException if provided quality scores length is
317      *                                  smaller than MINIMUM_SIZE (i.e. 3 samples).
318      */
319     @Override
320     public void setQualityScores(final double[] qualityScores) throws LockedException {
321         if (isLocked()) {
322             throw new LockedException();
323         }
324         internalSetQualityScores(qualityScores);
325     }
326 
327     /**
328      * Indicates if estimator is ready to start the affine 2D transformation
329      * estimation.
330      * This is true when input data (i.e. lists of matched points and quality
331      * scores) are provided and a minimum of MINIMUM_SIZE points are available.
332      *
333      * @return true if estimator is ready, false otherwise.
334      */
335     @Override
336     public boolean isReady() {
337         return super.isReady() && qualityScores != null && qualityScores.length == inputPoints.size();
338     }
339 
340     /**
341      * Estimates an affine 2D transformation using a robust estimator and
342      * the best set of matched 2D point correspondences found using the robust
343      * estimator.
344      *
345      * @return an affine 2D transformation.
346      * @throws LockedException          if robust estimator is locked because an
347      *                                  estimation is already in progress.
348      * @throws NotReadyException        if provided input data is not enough to start
349      *                                  the estimation.
350      * @throws RobustEstimatorException if estimation fails for any reason
351      *                                  (i.e. numerical instability, no solution available, etc).
352      */
353     @Override
354     public AffineTransformation2D estimate() throws LockedException, NotReadyException, RobustEstimatorException {
355         if (isLocked()) {
356             throw new LockedException();
357         }
358         if (!isReady()) {
359             throw new NotReadyException();
360         }
361 
362         final var innerEstimator = new PROMedSRobustEstimator<>(
363                 new PROMedSRobustEstimatorListener<AffineTransformation2D>() {
364 
365                     // point to be reused when computing residuals
366                     private final Point2D testPoint = Point2D.create(CoordinatesType.HOMOGENEOUS_COORDINATES);
367 
368                     @Override
369                     public double getThreshold() {
370                         return stopThreshold;
371                     }
372 
373                     @Override
374                     public int getTotalSamples() {
375                         return inputPoints.size();
376                     }
377 
378                     @Override
379                     public int getSubsetSize() {
380                         return AffineTransformation2DRobustEstimator.MINIMUM_SIZE;
381                     }
382 
383                     @Override
384                     public void estimatePreliminarSolutions(
385                             final int[] samplesIndices, final List<AffineTransformation2D> solutions) {
386                         final var inputPoint1 = inputPoints.get(samplesIndices[0]);
387                         final var inputPoint2 = inputPoints.get(samplesIndices[1]);
388                         final var inputPoint3 = inputPoints.get(samplesIndices[2]);
389 
390                         final var outputPoint1 = outputPoints.get(samplesIndices[0]);
391                         final var outputPoint2 = outputPoints.get(samplesIndices[1]);
392                         final var outputPoint3 = outputPoints.get(samplesIndices[2]);
393 
394                         try {
395                             final var transformation = new AffineTransformation2D(inputPoint1, inputPoint2, inputPoint3,
396                                     outputPoint1, outputPoint2, outputPoint3);
397                             solutions.add(transformation);
398                         } catch (final CoincidentPointsException e) {
399                             // if points are coincident, no solution is added
400                         }
401                     }
402 
403                     @Override
404                     public double computeResidual(final AffineTransformation2D currentEstimation, final int i) {
405                         final var inputPoint = inputPoints.get(i);
406                         final var outputPoint = outputPoints.get(i);
407 
408                         // transform input point and store result in mTestPoint
409                         currentEstimation.transform(inputPoint, testPoint);
410 
411                         return outputPoint.distanceTo(testPoint);
412                     }
413 
414                     @Override
415                     public boolean isReady() {
416                         return PROMedSPointCorrespondenceAffineTransformation2DRobustEstimator.this.isReady();
417                     }
418 
419                     @Override
420                     public void onEstimateStart(final RobustEstimator<AffineTransformation2D> estimator) {
421                         if (mListener != null) {
422                             mListener.onEstimateStart(
423                                     PROMedSPointCorrespondenceAffineTransformation2DRobustEstimator.this);
424                         }
425                     }
426 
427                     @Override
428                     public void onEstimateEnd(final RobustEstimator<AffineTransformation2D> estimator) {
429                         if (mListener != null) {
430                             mListener.onEstimateEnd(
431                                     PROMedSPointCorrespondenceAffineTransformation2DRobustEstimator.this);
432                         }
433                     }
434 
435                     @Override
436                     public void onEstimateNextIteration(
437                             final RobustEstimator<AffineTransformation2D> estimator, final int iteration) {
438                         if (mListener != null) {
439                             mListener.onEstimateNextIteration(
440                                     PROMedSPointCorrespondenceAffineTransformation2DRobustEstimator.this,
441                                     iteration);
442                         }
443                     }
444 
445                     @Override
446                     public void onEstimateProgressChange(
447                             final RobustEstimator<AffineTransformation2D> estimator, final float progress) {
448                         if (mListener != null) {
449                             mListener.onEstimateProgressChange(
450                                     PROMedSPointCorrespondenceAffineTransformation2DRobustEstimator.this,
451                                     progress);
452                         }
453                     }
454 
455                     @Override
456                     public double[] getQualityScores() {
457                         return qualityScores;
458                     }
459                 });
460 
461         try {
462             locked = true;
463             inliersData = null;
464             innerEstimator.setConfidence(confidence);
465             innerEstimator.setMaxIterations(maxIterations);
466             innerEstimator.setProgressDelta(progressDelta);
467             final var transformation = innerEstimator.estimate();
468             inliersData = innerEstimator.getInliersData();
469             return attemptRefine(transformation);
470         } catch (final com.irurueta.numerical.LockedException e) {
471             throw new LockedException(e);
472         } catch (final com.irurueta.numerical.NotReadyException e) {
473             throw new NotReadyException(e);
474         } finally {
475             locked = false;
476         }
477     }
478 
479     /**
480      * Returns method being used for robust estimation.
481      *
482      * @return method being used for robust estimation.
483      */
484     @Override
485     public RobustEstimatorMethod getMethod() {
486         return RobustEstimatorMethod.PROMEDS;
487     }
488 
489     /**
490      * Gets standard deviation used for Levenberg-Marquardt fitting during
491      * refinement.
492      * Returned value gives an indication of how much variance each residual
493      * has.
494      * Typically, this value is related to the threshold used on each robust
495      * estimation, since residuals of found inliers are within the range of such
496      * threshold.
497      *
498      * @return standard deviation used for refinement.
499      */
500     @Override
501     protected double getRefinementStandardDeviation() {
502         final var inliersData = (PROMedSRobustEstimator.PROMedSInliersData) getInliersData();
503 
504         // avoid setting a threshold too strict
505         final var threshold = inliersData.getEstimatedThreshold();
506         return Math.max(threshold, stopThreshold);
507     }
508 
509     /**
510      * Sets quality scores corresponding to each pair of matched points.
511      * This method is used internally and does not check whether instance is
512      * locked or not.
513      *
514      * @param qualityScores quality scores to be set.
515      * @throws IllegalArgumentException if provided quality scores length is
516      *                                  smaller than MINIMUM_SIZE.
517      */
518     private void internalSetQualityScores(final double[] qualityScores) {
519         if (qualityScores.length < MINIMUM_SIZE) {
520             throw new IllegalArgumentException();
521         }
522 
523         this.qualityScores = qualityScores;
524     }
525 }