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