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.NoIntersectionException;
19  import com.irurueta.geometry.Plane;
20  import com.irurueta.geometry.Point3D;
21  import com.irurueta.numerical.robust.PROSACRobustEstimator;
22  import com.irurueta.numerical.robust.PROSACRobustEstimatorListener;
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 3D point for provided collection of 3D planes using PROSAC
31   * algorithm.
32   */
33  @SuppressWarnings("DuplicatedCode")
34  public class PROSACPoint3DRobustEstimator extends Point3DRobustEstimator {
35  
36      /**
37       * Constant defining default threshold to determine whether points are
38       * inliers or not.
39       * Because typical resolution for points is 1 voxel, then default threshold
40       * is defined as 1.
41       */
42      public static final double DEFAULT_THRESHOLD = 1.0;
43  
44      /**
45       * Minimum value that can be set as threshold.
46       * Threshold must be strictly greater than 0.0.
47       */
48      public static final double MIN_THRESHOLD = 0.0;
49  
50      /**
51       * Indicates that by default inliers will only be computed but not kept.
52       */
53      public static final boolean DEFAULT_COMPUTE_AND_KEEP_INLIERS = false;
54  
55      /**
56       * Indicates that by default residuals will only be computed but not kept.
57       */
58      public static final boolean DEFAULT_COMPUTE_AND_KEEP_RESIDUALS = false;
59  
60      /**
61       * Threshold to determine whether planes are inliers or not when testing
62       * possible estimation solutions.
63       * The threshold refers to the amount of error (i.e. distance) a possible
64       * solution has on a sampled plane.
65       */
66      private double threshold;
67  
68      /**
69       * Quality scores corresponding to each provided point.
70       * The larger the score value the better the quality of the sample.
71       */
72      private double[] qualityScores;
73  
74      /**
75       * Indicates whether inliers must be computed and kept.
76       */
77      private boolean computeAndKeepInliers;
78  
79      /**
80       * Indicates whether residuals must be computed and kept.
81       */
82      private boolean computeAndKeepResiduals;
83  
84      /**
85       * Constructor.
86       */
87      public PROSACPoint3DRobustEstimator() {
88          super();
89          threshold = DEFAULT_THRESHOLD;
90          computeAndKeepInliers = DEFAULT_COMPUTE_AND_KEEP_INLIERS;
91          computeAndKeepResiduals = DEFAULT_COMPUTE_AND_KEEP_RESIDUALS;
92      }
93  
94      /**
95       * Constructor with planes.
96       *
97       * @param planes 3D planes to estimate a 3D point.
98       * @throws IllegalArgumentException if provided list of planes doesn't have
99       *                                  a size greater or equal than MINIMUM_SIZE.
100      */
101     public PROSACPoint3DRobustEstimator(final List<Plane> planes) {
102         super(planes);
103         threshold = DEFAULT_THRESHOLD;
104         computeAndKeepInliers = DEFAULT_COMPUTE_AND_KEEP_INLIERS;
105         computeAndKeepResiduals = DEFAULT_COMPUTE_AND_KEEP_RESIDUALS;
106     }
107 
108     /**
109      * Constructor.
110      *
111      * @param listener listener to be notified of events such as when estimation
112      *                 starts, ends or its progress significantly changes.
113      */
114     public PROSACPoint3DRobustEstimator(final Point3DRobustEstimatorListener listener) {
115         super(listener);
116         threshold = DEFAULT_THRESHOLD;
117         computeAndKeepInliers = DEFAULT_COMPUTE_AND_KEEP_INLIERS;
118         computeAndKeepResiduals = DEFAULT_COMPUTE_AND_KEEP_RESIDUALS;
119     }
120 
121 
122     /**
123      * Constructor.
124      *
125      * @param listener listener to be notified of events such as when estimation
126      *                 starts, ends or its progress significantly changes.
127      * @param planes   3D planes to estimate a 3D point.
128      * @throws IllegalArgumentException if provided list of planes doesn't have
129      *                                  a size greater or equal than MINIMUM_SIZE.
130      */
131     public PROSACPoint3DRobustEstimator(final Point3DRobustEstimatorListener listener, final List<Plane> planes) {
132         super(listener, planes);
133         threshold = DEFAULT_THRESHOLD;
134         computeAndKeepInliers = DEFAULT_COMPUTE_AND_KEEP_INLIERS;
135         computeAndKeepResiduals = DEFAULT_COMPUTE_AND_KEEP_RESIDUALS;
136     }
137 
138     /**
139      * Constructor.
140      *
141      * @param qualityScores quality scores corresponding to each provided plane.
142      * @throws IllegalArgumentException if provided quality scores length is
143      *                                  smaller than MINIMUM_SIZE (i.e. 2 planes).
144      */
145     public PROSACPoint3DRobustEstimator(final double[] qualityScores) {
146         super();
147         threshold = DEFAULT_THRESHOLD;
148         internalSetQualityScores(qualityScores);
149         computeAndKeepInliers = DEFAULT_COMPUTE_AND_KEEP_INLIERS;
150         computeAndKeepResiduals = DEFAULT_COMPUTE_AND_KEEP_RESIDUALS;
151     }
152 
153     /**
154      * Constructor with planes.
155      *
156      * @param planes        3D planes to estimate a 3D point.
157      * @param qualityScores quality scores corresponding to each provided plane.
158      * @throws IllegalArgumentException if provided list of planes doesn't have
159      *                                  the same size as the list of provided quality scores, or it their size
160      *                                  is not greater or equal than MINIMUM_SIZE.
161      */
162     public PROSACPoint3DRobustEstimator(final List<Plane> planes, final double[] qualityScores) {
163         super(planes);
164 
165         if (qualityScores.length != planes.size()) {
166             throw new IllegalArgumentException();
167         }
168 
169         threshold = DEFAULT_THRESHOLD;
170         internalSetQualityScores(qualityScores);
171         computeAndKeepInliers = DEFAULT_COMPUTE_AND_KEEP_INLIERS;
172         computeAndKeepResiduals = DEFAULT_COMPUTE_AND_KEEP_RESIDUALS;
173     }
174 
175     /**
176      * Constructor.
177      *
178      * @param listener      listener to be notified of events such as when estimation
179      *                      starts, ends or its progress significantly changes.
180      * @param qualityScores quality scores corresponding to each provided plane.
181      * @throws IllegalArgumentException if provided quality scores length is
182      *                                  smaller than MINIMUM_SIZE (i.e. 3 planes).
183      */
184     public PROSACPoint3DRobustEstimator(final Point3DRobustEstimatorListener listener, final double[] qualityScores) {
185         super(listener);
186         threshold = DEFAULT_THRESHOLD;
187         internalSetQualityScores(qualityScores);
188         computeAndKeepInliers = DEFAULT_COMPUTE_AND_KEEP_INLIERS;
189         computeAndKeepResiduals = DEFAULT_COMPUTE_AND_KEEP_RESIDUALS;
190     }
191 
192 
193     /**
194      * Constructor.
195      *
196      * @param listener      listener to be notified of events such as when estimation
197      *                      starts, ends or its progress significantly changes.
198      * @param planes        3D planes to estimate a 3D point.
199      * @param qualityScores quality scores corresponding to each provided plane.
200      * @throws IllegalArgumentException if provided list of planes doesn't have
201      *                                  the same size as the list of provided quality scores, or it their size
202      *                                  is not greater or equal than MINIMUM_SIZE.
203      */
204     public PROSACPoint3DRobustEstimator(
205             final Point3DRobustEstimatorListener listener, final List<Plane> planes, final double[] qualityScores) {
206         super(listener, planes);
207 
208         if (qualityScores.length != planes.size()) {
209             throw new IllegalArgumentException();
210         }
211 
212         threshold = DEFAULT_THRESHOLD;
213         internalSetQualityScores(qualityScores);
214         computeAndKeepInliers = DEFAULT_COMPUTE_AND_KEEP_INLIERS;
215         computeAndKeepResiduals = DEFAULT_COMPUTE_AND_KEEP_RESIDUALS;
216     }
217 
218     /**
219      * Returns threshold to determine whether planes are inliers or not when
220      * testing possible estimation solutions.
221      * The threshold refers to the amount of error a possible solution has on a
222      * given plane.
223      *
224      * @return threshold to determine whether planes are inliers or not when
225      * testing possible estimation solutions.
226      */
227     public double getThreshold() {
228         return threshold;
229     }
230 
231     /**
232      * Sets threshold to determine whether planes are inliers or not when
233      * testing possible estimation solutions.
234      * The threshold refers to the amount of error a possible solution has on
235      * a given plane.
236      *
237      * @param threshold threshold to be set.
238      * @throws IllegalArgumentException if provided value is equal or less than
239      *                                  zero.
240      * @throws LockedException          if robust estimator is locked because an
241      *                                  estimation is already in progress.
242      */
243     public void setThreshold(final double threshold) throws LockedException {
244         if (isLocked()) {
245             throw new LockedException();
246         }
247         if (threshold <= MIN_THRESHOLD) {
248             throw new IllegalArgumentException();
249         }
250         this.threshold = threshold;
251     }
252 
253     /**
254      * Returns quality scores corresponding to each provided plane.
255      * The larger the score value the better the quality of the sampled plane.
256      *
257      * @return quality scores corresponding to each plane.
258      */
259     @Override
260     public double[] getQualityScores() {
261         return qualityScores;
262     }
263 
264     /**
265      * Sets quality scores corresponding to each provided plane.
266      * The larger the score value the better the quality of the sampled plane.
267      *
268      * @param qualityScores quality scores corresponding to each plane.
269      * @throws LockedException          if robust estimator is locked because an
270      *                                  estimation is already in progress.
271      * @throws IllegalArgumentException if provided quality scores length is
272      *                                  smaller than MINIMUM_SIZE (i.e. 23 samples).
273      */
274     @Override
275     public void setQualityScores(final double[] qualityScores) throws LockedException {
276         if (isLocked()) {
277             throw new LockedException();
278         }
279         internalSetQualityScores(qualityScores);
280     }
281 
282     /**
283      * Indicates if estimator is ready to start the 3D point estimation.
284      * This is true when input data (i.e. 3D planes and quality scores) are
285      * provided and a minimum of MINIMUM_SIZE points are available.
286      *
287      * @return true if estimator is ready, false otherwise.
288      */
289     @Override
290     public boolean isReady() {
291         return super.isReady() && qualityScores != null && qualityScores.length == planes.size();
292     }
293 
294     /**
295      * Indicates whether inliers must be computed and kept.
296      *
297      * @return true if inliers must be computed and kept, false if inliers
298      * only need to be computed but not kept.
299      */
300     public boolean isComputeAndKeepInliersEnabled() {
301         return computeAndKeepInliers;
302     }
303 
304     /**
305      * Specifies whether inliers must be computed and kept.
306      *
307      * @param computeAndKeepInliers true if inliers must be computed and kept,
308      *                              false if inliers only need to be computed but not kept.
309      * @throws LockedException if estimator is locked.
310      */
311     public void setComputeAndKeepInliersEnabled(final boolean computeAndKeepInliers) throws LockedException {
312         if (isLocked()) {
313             throw new LockedException();
314         }
315         this.computeAndKeepInliers = computeAndKeepInliers;
316     }
317 
318     /**
319      * Indicates whether residuals must be computed and kept.
320      *
321      * @return true if residuals must be computed and kept, false if residuals
322      * only need to be computed but not kept.
323      */
324     public boolean isComputeAndKeepResidualsEnabled() {
325         return computeAndKeepResiduals;
326     }
327 
328     /**
329      * Specifies whether residuals must be computed and kept.
330      *
331      * @param computeAndKeepResiduals true if residuals must be computed and
332      *                                kept, false if residuals only need to be computed but not kept.
333      * @throws LockedException if estimator is locked.
334      */
335     public void setComputeAndKeepResidualsEnabled(final boolean computeAndKeepResiduals) throws LockedException {
336         if (isLocked()) {
337             throw new LockedException();
338         }
339         this.computeAndKeepResiduals = computeAndKeepResiduals;
340     }
341 
342     /**
343      * Estimates a 3D point using a robust estimator and the best set of 3D
344      * planes that intersect into the estimated 3D point.
345      *
346      * @return a 3D point.
347      * @throws LockedException          if robust estimator is locked because an
348      *                                  estimation is already in progress.
349      * @throws NotReadyException        if provided input data is not enough to start
350      *                                  the estimation.
351      * @throws RobustEstimatorException if estimation fails for any reason
352      *                                  (i.e. numerical instability, no solution available, etc).
353      */
354     @Override
355     public Point3D estimate() throws LockedException, NotReadyException, RobustEstimatorException {
356         if (isLocked()) {
357             throw new LockedException();
358         }
359         if (!isReady()) {
360             throw new NotReadyException();
361         }
362 
363         final var innerEstimator = new PROSACRobustEstimator<>(new PROSACRobustEstimatorListener<Point3D>() {
364 
365             @Override
366             public double getThreshold() {
367                 return threshold;
368             }
369 
370             @Override
371             public int getTotalSamples() {
372                 return planes.size();
373             }
374 
375             @Override
376             public int getSubsetSize() {
377                 return Point3DRobustEstimator.MINIMUM_SIZE;
378             }
379 
380             @Override
381             public void estimatePreliminarSolutions(final int[] samplesIndices, final List<Point3D> solutions) {
382                 final var plane1 = planes.get(samplesIndices[0]);
383                 final var plane2 = planes.get(samplesIndices[1]);
384                 final var plane3 = planes.get(samplesIndices[2]);
385 
386                 try {
387                     final var point = plane1.getIntersection(plane2, plane3);
388                     solutions.add(point);
389                 } catch (final NoIntersectionException e) {
390                     // if points are coincident, no solution is added
391                 }
392             }
393 
394             @Override
395             public double computeResidual(final Point3D currentEstimation, final int i) {
396                 return residual(currentEstimation, planes.get(i));
397             }
398 
399             @Override
400             public boolean isReady() {
401                 return PROSACPoint3DRobustEstimator.this.isReady();
402             }
403 
404             @Override
405             public void onEstimateStart(final RobustEstimator<Point3D> estimator) {
406                 if (listener != null) {
407                     listener.onEstimateStart(PROSACPoint3DRobustEstimator.this);
408                 }
409             }
410 
411             @Override
412             public void onEstimateEnd(final RobustEstimator<Point3D> estimator) {
413                 if (listener != null) {
414                     listener.onEstimateEnd(PROSACPoint3DRobustEstimator.this);
415                 }
416             }
417 
418             @Override
419             public void onEstimateNextIteration(final RobustEstimator<Point3D> estimator, final int iteration) {
420                 if (listener != null) {
421                     listener.onEstimateNextIteration(PROSACPoint3DRobustEstimator.this, iteration);
422                 }
423             }
424 
425             @Override
426             public void onEstimateProgressChange(final RobustEstimator<Point3D> estimator, final float progress) {
427                 if (listener != null) {
428                     listener.onEstimateProgressChange(PROSACPoint3DRobustEstimator.this, progress);
429                 }
430             }
431 
432             @Override
433             public double[] getQualityScores() {
434                 return qualityScores;
435             }
436         });
437 
438         try {
439             locked = true;
440             inliersData = null;
441             innerEstimator.setComputeAndKeepInliersEnabled(computeAndKeepInliers || refineResult);
442             innerEstimator.setComputeAndKeepResidualsEnabled(computeAndKeepResiduals || refineResult);
443             innerEstimator.setConfidence(confidence);
444             innerEstimator.setMaxIterations(maxIterations);
445             innerEstimator.setProgressDelta(progressDelta);
446             final var result = innerEstimator.estimate();
447             inliersData = innerEstimator.getInliersData();
448             return attemptRefine(result);
449         } catch (final com.irurueta.numerical.LockedException e) {
450             throw new LockedException(e);
451         } catch (final com.irurueta.numerical.NotReadyException e) {
452             throw new NotReadyException(e);
453         } finally {
454             locked = false;
455         }
456     }
457 
458     /**
459      * Returns method being used for robust estimation.
460      *
461      * @return method being used for robust estimation.
462      */
463     @Override
464     public RobustEstimatorMethod getMethod() {
465         return RobustEstimatorMethod.PROSAC;
466     }
467 
468     /**
469      * Gets standard deviation used for Levenberg-Marquardt fitting during
470      * refinement.
471      * Returned value gives an indication of how much variance each residual
472      * has.
473      * Typically, this value is related to the threshold used on each robust
474      * estimation, since residuals of found inliers are within the range of
475      * such threshold.
476      *
477      * @return standard deviation used for refinement.
478      */
479     @Override
480     protected double getRefinementStandardDeviation() {
481         return threshold;
482     }
483 
484     /**
485      * Sets quality scores corresponding to each provided line.
486      * This method is used internally and does not check whether instance is
487      * locked or not.
488      *
489      * @param qualityScores quality scores to be set.
490      * @throws IllegalArgumentException if provided quality scores length is
491      *                                  smaller than MINIMUM_SIZE.
492      */
493     private void internalSetQualityScores(final double[] qualityScores) {
494         if (qualityScores.length < MINIMUM_SIZE) {
495             throw new IllegalArgumentException();
496         }
497 
498         this.qualityScores = qualityScores;
499     }
500 }