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.Line2D;
19  import com.irurueta.geometry.NoIntersectionException;
20  import com.irurueta.geometry.Point2D;
21  import com.irurueta.numerical.robust.PROMedSRobustEstimator;
22  import com.irurueta.numerical.robust.PROMedSRobustEstimatorListener;
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.List;
28  
29  /**
30   * Finds the best 2D point for provided collection of 2D lines using PROMedS
31   * algorithm.
32   */
33  @SuppressWarnings("DuplicatedCode")
34  public class PROMedSPoint2DRobustEstimator extends Point2DRobustEstimator {
35  
36      /**
37       * Default value to be used for stop threshold. Stop threshold can be used
38       * to keep the algorithm iterating in case that best estimated threshold
39       * using median of residuals is not small enough. Once a solution is found
40       * that generates a threshold below this value, the algorithm will stop.
41       * The stop threshold can be used to prevent the LMedS algorithm iterating
42       * too many times in cases where samples have a very similar accuracy.
43       * For instance, in cases where proportion of outliers is very small (close
44       * to 0%), and samples are very accurate (i.e. 1e-6), the algorithm would
45       * iterate for a long time trying to find the best solution when indeed
46       * there is no need to do that if a reasonable threshold has already been
47       * reached.
48       * Because of this behaviour the stop threshold can be set to a value much
49       * lower than the one typically used in RANSAC, and yet the algorithm could
50       * still produce even smaller thresholds in estimated results.
51       */
52      public static final double DEFAULT_STOP_THRESHOLD = 1e-3;
53  
54      /**
55       * Minimum allowed stop threshold value.
56       */
57      public static final double MIN_STOP_THRESHOLD = 0.0;
58  
59      /**
60       * Threshold to be used to keep the algorithm iterating in case that best
61       * estimated threshold using median of residuals is not small enough. Once
62       * a solution is found that generates a threshold below this value, the
63       * algorithm will stop.
64       * The stop threshold can be used to prevent the LMedS algorithm iterating
65       * too many times in cases where samples have a very similar accuracy.
66       * For instance, in cases where proportion of outliers is very small (close
67       * to 0%), and samples are very accurate (i.e. 1e-6), the algorithm would
68       * iterate for a long time trying to find the best solution when indeed
69       * there is no need to do that if a reasonable threshold has already been
70       * reached.
71       * Because of this behaviour the stop threshold can be set to a value much
72       * lower than the one typically used in RANSAC, and yet the algorithm could
73       * still produce even smaller thresholds in estimated results.
74       */
75      private double stopThreshold;
76  
77      /**
78       * Quality scores corresponding to each provided point.
79       * The larger the score value the better the quality of the sample.
80       */
81      private double[] qualityScores;
82  
83      /**
84       * Constructor.
85       */
86      public PROMedSPoint2DRobustEstimator() {
87          super();
88          stopThreshold = DEFAULT_STOP_THRESHOLD;
89      }
90  
91      /**
92       * Constructor with lines.
93       *
94       * @param lines 2D lines to estimate a 2D point.
95       * @throws IllegalArgumentException if provided list of lines don't have
96       *                                  a size greater or equal than MINIMUM_SIZE.
97       */
98      public PROMedSPoint2DRobustEstimator(final List<Line2D> lines) {
99          super(lines);
100         stopThreshold = DEFAULT_STOP_THRESHOLD;
101     }
102 
103     /**
104      * Constructor.
105      *
106      * @param listener listener to be notified of events such as when estimation
107      *                 starts, ends or its progress significantly changes.
108      */
109     public PROMedSPoint2DRobustEstimator(final Point2DRobustEstimatorListener listener) {
110         super(listener);
111         stopThreshold = DEFAULT_STOP_THRESHOLD;
112     }
113 
114 
115     /**
116      * Constructor.
117      *
118      * @param listener listener to be notified of events such as when estimation
119      *                 starts, ends or its progress significantly changes.
120      * @param lines    2D lines to estimate a 2D point.
121      * @throws IllegalArgumentException if provided list of lines don't have
122      *                                  a size greater or equal than MINIMUM_SIZE.
123      */
124     public PROMedSPoint2DRobustEstimator(final Point2DRobustEstimatorListener listener, final List<Line2D> lines) {
125         super(listener, lines);
126         stopThreshold = DEFAULT_STOP_THRESHOLD;
127     }
128 
129     /**
130      * Constructor.
131      *
132      * @param qualityScores quality scores corresponding to each provided line.
133      * @throws IllegalArgumentException if provided quality scores length is
134      *                                  smaller than MINIMUM_SIZE (i.e. 2 lines).
135      */
136     public PROMedSPoint2DRobustEstimator(final double[] qualityScores) {
137         super();
138         stopThreshold = DEFAULT_STOP_THRESHOLD;
139         internalSetQualityScores(qualityScores);
140     }
141 
142     /**
143      * Constructor with lines.
144      *
145      * @param lines         2D lines to estimate a 2D point.
146      * @param qualityScores quality scores corresponding to each provided line.
147      * @throws IllegalArgumentException if provided list of lines don't have
148      *                                  the same size as the list of provided quality scores, or it their size
149      *                                  is not greater or equal than MINIMUM_SIZE.
150      */
151     public PROMedSPoint2DRobustEstimator(final List<Line2D> lines, final double[] qualityScores) {
152         super(lines);
153 
154         if (qualityScores.length != lines.size()) {
155             throw new IllegalArgumentException();
156         }
157 
158         stopThreshold = DEFAULT_STOP_THRESHOLD;
159         internalSetQualityScores(qualityScores);
160     }
161 
162     /**
163      * Constructor.
164      *
165      * @param listener      listener to be notified of events such as when estimation
166      *                      starts, ends or its progress significantly changes.
167      * @param qualityScores quality scores corresponding to each provided line.
168      * @throws IllegalArgumentException if provided quality scores length is
169      *                                  smaller than MINIMUM_SIZE (i.e. 2 lines).
170      */
171     public PROMedSPoint2DRobustEstimator(
172             final Point2DRobustEstimatorListener listener, final double[] qualityScores) {
173         super(listener);
174         stopThreshold = DEFAULT_STOP_THRESHOLD;
175         internalSetQualityScores(qualityScores);
176     }
177 
178 
179     /**
180      * Constructor.
181      *
182      * @param listener      listener to be notified of events such as when estimation
183      *                      starts, ends or its progress significantly changes.
184      * @param lines         2D lines to estimate a 2D point.
185      * @param qualityScores quality scores corresponding to each provided line.
186      * @throws IllegalArgumentException if provided list of lines don't have
187      *                                  the same size as the list of provided quality scores, or it their size
188      *                                  is not greater or equal than MINIMUM_SIZE.
189      */
190     public PROMedSPoint2DRobustEstimator(
191             final Point2DRobustEstimatorListener listener, final List<Line2D> lines, final double[] qualityScores) {
192         super(listener, lines);
193 
194         if (qualityScores.length != lines.size()) {
195             throw new IllegalArgumentException();
196         }
197 
198         stopThreshold = DEFAULT_STOP_THRESHOLD;
199         internalSetQualityScores(qualityScores);
200     }
201 
202     /**
203      * Returns threshold to be used to keep the algorithm iterating in case that
204      * best estimated threshold using median of residuals is not small enough.
205      * Once a solution is found that generates a threshold below this value, the
206      * algorithm will stop.
207      * The stop threshold can be used to prevent the LMedS algorithm iterating
208      * too many times in cases where samples have a very similar accuracy.
209      * For instance, in cases where proportion of outliers is very small (close
210      * to 0%), and samples are very accurate (i.e. 1e-6), the algorithm would
211      * iterate for a long time trying to find the best solution when indeed
212      * there is no need to do that if a reasonable threshold has already been
213      * reached.
214      * Because of this behaviour the stop threshold can be set to a value much
215      * lower than the one typically used in RANSAC, and yet the algorithm could
216      * still produce even smaller thresholds in estimated results.
217      *
218      * @return stop threshold to stop the algorithm prematurely when a certain
219      * accuracy has been reached.
220      */
221     public double getStopThreshold() {
222         return stopThreshold;
223     }
224 
225     /**
226      * Sets threshold to be used to keep the algorithm iterating in case that
227      * best estimated threshold using median of residuals is not small enough.
228      * Once a solution is found that generates a threshold below this value, the
229      * algorithm will stop.
230      * The stop threshold can be used to prevent the LMedS algorithm iterating
231      * too many times in cases where samples have a very similar accuracy.
232      * For instance, in cases where proportion of outliers is very small (close
233      * to 0%), and samples are very accurate (i.e. 1e-6), the algorithm would
234      * iterate for a long time trying to find the best solution when indeed
235      * there is no need to do that if a reasonable threshold has already been
236      * reached.
237      * Because of this behaviour the stop threshold can be set to a value much
238      * lower than the one typically used in RANSAC, and yet the algorithm could
239      * still produce even smaller thresholds in estimated results.
240      *
241      * @param stopThreshold stop threshold to stop the algorithm prematurely
242      *                      when a certain accuracy has been reached.
243      * @throws IllegalArgumentException if provided value is zero or negative.
244      * @throws LockedException          if robust estimator is locked because an
245      *                                  estimation is already in progress.
246      */
247     public void setStopThreshold(final double stopThreshold) throws LockedException {
248         if (isLocked()) {
249             throw new LockedException();
250         }
251         if (stopThreshold <= MIN_STOP_THRESHOLD) {
252             throw new IllegalArgumentException();
253         }
254 
255         this.stopThreshold = stopThreshold;
256     }
257 
258     /**
259      * Returns quality scores corresponding to each provided point.
260      * The larger the score value the better the quality of the sampled point
261      *
262      * @return quality scores corresponding to each point.
263      */
264     @Override
265     public double[] getQualityScores() {
266         return qualityScores;
267     }
268 
269     /**
270      * Sets quality scores corresponding to each provided point.
271      * The larger the score value the better the quality of the sampled point.
272      *
273      * @param qualityScores quality scores corresponding to each point.
274      * @throws LockedException          if robust estimator is locked because an
275      *                                  estimation is already in progress.
276      * @throws IllegalArgumentException if provided quality scores length is
277      *                                  smaller than MINIMUM_SIZE (i.e. 3 samples).
278      */
279     @Override
280     public void setQualityScores(final double[] qualityScores) throws LockedException {
281         if (isLocked()) {
282             throw new LockedException();
283         }
284         internalSetQualityScores(qualityScores);
285     }
286 
287     /**
288      * Indicates if estimator is ready to start the 2D point estimation.
289      * This is true when input data (i.e. 2D points and quality scores) are
290      * provided and a minimum of MINIMUM_SIZE points are available.
291      *
292      * @return true if estimator is ready, false otherwise.
293      */
294     @Override
295     public boolean isReady() {
296         return super.isReady() && qualityScores != null && qualityScores.length == lines.size();
297     }
298 
299     /**
300      * Estimates a 2D point using a robust estimator and the best set of 2D
301      * lines that intersect into the estimated 2D point.
302      *
303      * @return a 2D point.
304      * @throws LockedException          if robust estimator is locked because an
305      *                                  estimation is already in progress.
306      * @throws NotReadyException        if provided input data is not enough to start
307      *                                  the estimation.
308      * @throws RobustEstimatorException if estimation fails for any reason
309      *                                  (i.e. numerical instability, no solution available, etc).
310      */
311     @Override
312     public Point2D estimate() throws LockedException, NotReadyException, RobustEstimatorException {
313         if (isLocked()) {
314             throw new LockedException();
315         }
316         if (!isReady()) {
317             throw new NotReadyException();
318         }
319 
320         final var innerEstimator = new PROMedSRobustEstimator<>(new PROMedSRobustEstimatorListener<Point2D>() {
321 
322             @Override
323             public double getThreshold() {
324                 return stopThreshold;
325             }
326 
327             @Override
328             public int getTotalSamples() {
329                 return lines.size();
330             }
331 
332             @Override
333             public int getSubsetSize() {
334                 return Point2DRobustEstimator.MINIMUM_SIZE;
335             }
336 
337             @Override
338             public void estimatePreliminarSolutions(final int[] samplesIndices, final List<Point2D> solutions) {
339                 final var line1 = lines.get(samplesIndices[0]);
340                 final var line2 = lines.get(samplesIndices[1]);
341 
342                 try {
343                     final var point = line1.getIntersection(line2);
344                     solutions.add(point);
345                 } catch (final NoIntersectionException e) {
346                     // if points are coincident, no solution is added
347                 }
348             }
349 
350             @Override
351             public double computeResidual(final Point2D currentEstimation, final int i) {
352                 return residual(currentEstimation, lines.get(i));
353             }
354 
355             @Override
356             public boolean isReady() {
357                 return PROMedSPoint2DRobustEstimator.this.isReady();
358             }
359 
360             @Override
361             public void onEstimateStart(final RobustEstimator<Point2D> estimator) {
362                 if (listener != null) {
363                     listener.onEstimateStart(PROMedSPoint2DRobustEstimator.this);
364                 }
365             }
366 
367             @Override
368             public void onEstimateEnd(final RobustEstimator<Point2D> estimator) {
369                 if (listener != null) {
370                     listener.onEstimateEnd(PROMedSPoint2DRobustEstimator.this);
371                 }
372             }
373 
374             @Override
375             public void onEstimateNextIteration(final RobustEstimator<Point2D> estimator, final int iteration) {
376                 if (listener != null) {
377                     listener.onEstimateNextIteration(PROMedSPoint2DRobustEstimator.this, iteration);
378                 }
379             }
380 
381             @Override
382             public void onEstimateProgressChange(final RobustEstimator<Point2D> estimator, final float progress) {
383                 if (listener != null) {
384                     listener.onEstimateProgressChange(PROMedSPoint2DRobustEstimator.this, progress);
385                 }
386             }
387 
388             @Override
389             public double[] getQualityScores() {
390                 return qualityScores;
391             }
392         });
393 
394         try {
395             locked = true;
396             inliersData = null;
397             innerEstimator.setConfidence(confidence);
398             innerEstimator.setMaxIterations(maxIterations);
399             innerEstimator.setProgressDelta(progressDelta);
400             final var result = innerEstimator.estimate();
401             inliersData = innerEstimator.getInliersData();
402             return attemptRefine(result);
403         } catch (final com.irurueta.numerical.LockedException e) {
404             throw new LockedException(e);
405         } catch (final com.irurueta.numerical.NotReadyException e) {
406             throw new NotReadyException(e);
407         } finally {
408             locked = false;
409         }
410     }
411 
412     /**
413      * Returns method being used for robust estimation.
414      *
415      * @return method being used for robust estimation.
416      */
417     @Override
418     public RobustEstimatorMethod getMethod() {
419         return RobustEstimatorMethod.PROMEDS;
420     }
421 
422     /**
423      * Gets standard deviation used for Levenberg-Marquardt fitting during
424      * refinement.
425      * Returned value gives an indication of how much variance each residual
426      * has.
427      * Typically, this value is related to the threshold used on each robust
428      * estimation, since residuals of found inliers are within the range of
429      * such threshold.
430      *
431      * @return standard deviation used for refinement.
432      */
433     @Override
434     protected double getRefinementStandardDeviation() {
435         final var inliersData = (PROMedSRobustEstimator.PROMedSInliersData) getInliersData();
436         return inliersData.getEstimatedThreshold();
437     }
438 
439     /**
440      * Sets quality scores corresponding to each provided point.
441      * This method is used internally and does not check whether instance is
442      * locked or not.
443      *
444      * @param qualityScores quality scores to be set.
445      * @throws IllegalArgumentException if provided quality scores length is
446      *                                  smaller than MINIMUM_SIZE.
447      */
448     private void internalSetQualityScores(final double[] qualityScores) {
449         if (qualityScores.length < MINIMUM_SIZE) {
450             throw new IllegalArgumentException();
451         }
452 
453         this.qualityScores = qualityScores;
454     }
455 }