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.PinholeCamera;
20  import com.irurueta.geometry.Plane;
21  import com.irurueta.numerical.robust.RANSACRobustEstimator;
22  import com.irurueta.numerical.robust.RANSACRobustEstimatorListener;
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.ArrayList;
28  import java.util.List;
29  
30  /**
31   * Finds the best pinhole camera for provided collections of matched lines and
32   * planes using RANSAC algorithm.
33   */
34  @SuppressWarnings("DuplicatedCode")
35  public class RANSACDLTLinePlaneCorrespondencePinholeCameraRobustEstimator
36          extends DLTLinePlaneCorrespondencePinholeCameraRobustEstimator {
37  
38      /**
39       * Constant defining default threshold to determine whether planes are
40       * inliers or not.
41       * Residuals to determine whether planes are inliers or not are computed by
42       * comparing two planes algebraically (e.g. doing the dot product of their
43       * parameters).
44       * A residual of 0 indicates that dot product was 1 or -1 and lines were
45       * equal.
46       * A residual of 1 indicates that dot product was 0 and lines were
47       * orthogonal.
48       * If dot product between lines is -1, then although their director vectors
49       * are opposed, lines are considered equal, since sign changes are not taken
50       * into account and their residuals will be 0.
51       */
52      public static final double DEFAULT_THRESHOLD = 1e-6;
53  
54      /**
55       * Minimum value that can be set as threshold.
56       * Threshold must be strictly greater than 0.0.
57       */
58      public static final double MIN_THRESHOLD = 0.0;
59  
60      /**
61       * Indicates that by default inliers will only be computed but not kept.
62       */
63      public static final boolean DEFAULT_COMPUTE_AND_KEEP_INLIERS = false;
64  
65      /**
66       * Indicates that by default residuals will only be computed but not kept.
67       */
68      public static final boolean DEFAULT_COMPUTE_AND_KEEP_RESIDUALS = false;
69  
70      /**
71       * Threshold to determine whether planes are inliers or not when testing
72       * possible estimation solutions.
73       * The threshold refers to the amount of error a possible solution has on a
74       * plane respect the back-projected plane of a line using estimated camera.
75       */
76      private double threshold;
77  
78      /**
79       * Indicates whether inliers must be computed and kept.
80       */
81      private boolean computeAndKeepInliers;
82  
83      /**
84       * Indicates whether residuals must be computed and kept.
85       */
86      private boolean computeAndKeepResiduals;
87  
88      /**
89       * Constructor.
90       */
91      public RANSACDLTLinePlaneCorrespondencePinholeCameraRobustEstimator() {
92          super();
93          threshold = DEFAULT_THRESHOLD;
94          computeAndKeepInliers = DEFAULT_COMPUTE_AND_KEEP_INLIERS;
95          computeAndKeepResiduals = DEFAULT_COMPUTE_AND_KEEP_RESIDUALS;
96      }
97  
98      /**
99       * Constructor with lists of matched planes and 2D lines to estimate a
100      * pinhole camera.
101      * Points and lines in the lists located at the same position are considered
102      * to be matched. Hence, both lists must have the same size, and their size
103      * must be greater or equal than MIN_NUMBER_OF_LINE_PLANE_CORRESPONDENCES
104      * (4 matches).
105      *
106      * @param planes list of planes used to estimate a pinhole camera.
107      * @param lines  list of corresponding projected 2D lines used to estimate
108      *               a pinhole camera.
109      * @throws IllegalArgumentException if provided lists don't have the same
110      *                                  size or their size is smaller than required minimum size (4 matches).
111      */
112     public RANSACDLTLinePlaneCorrespondencePinholeCameraRobustEstimator(
113             final List<Plane> planes, final List<Line2D> lines) {
114         super(planes, lines);
115         threshold = DEFAULT_THRESHOLD;
116         computeAndKeepInliers = DEFAULT_COMPUTE_AND_KEEP_INLIERS;
117         computeAndKeepResiduals = DEFAULT_COMPUTE_AND_KEEP_RESIDUALS;
118     }
119 
120     /**
121      * Constructor with listener.
122      *
123      * @param listener listener to be notified of events such as when estimation
124      *                 starts, ends or its progress significantly changes.
125      */
126     public RANSACDLTLinePlaneCorrespondencePinholeCameraRobustEstimator(
127             final PinholeCameraRobustEstimatorListener listener) {
128         super(listener);
129         threshold = DEFAULT_THRESHOLD;
130         computeAndKeepInliers = DEFAULT_COMPUTE_AND_KEEP_INLIERS;
131         computeAndKeepResiduals = DEFAULT_COMPUTE_AND_KEEP_RESIDUALS;
132     }
133 
134     /**
135      * Constructor with listener and lists of matched planes and 2D lines to
136      * estimate a pinhole camera.
137      * Points and lines in the lists located at the same position are considered
138      * to be matched. Hence, both lists must have the same size, and their size
139      * must be greater or equal than MIN_NUMBER_OF_LINE_PLANE_CORRESPONDENCES
140      * (4 matches).
141      *
142      * @param listener listener to be notified of events such as when estimation
143      *                 starts, ends or its progress significantly changes.
144      * @param planes   list of planes used to estimate a pinhole camera.
145      * @param lines    list of corresponding projected 2D lines used to estimate
146      *                 a pinhole camera.
147      * @throws IllegalArgumentException if provided lists don't have the same
148      *                                  size or their size is smaller than required minimum size (4 matches).
149      */
150     public RANSACDLTLinePlaneCorrespondencePinholeCameraRobustEstimator(
151             final PinholeCameraRobustEstimatorListener listener, final List<Plane> planes, final List<Line2D> lines) {
152         super(listener, planes, lines);
153         threshold = DEFAULT_THRESHOLD;
154         computeAndKeepInliers = DEFAULT_COMPUTE_AND_KEEP_INLIERS;
155         computeAndKeepResiduals = DEFAULT_COMPUTE_AND_KEEP_RESIDUALS;
156     }
157 
158     /**
159      * Returns threshold to determine whether planes are inliers or not when
160      * testing possible estimation solutions.
161      * The threshold refers to the amount of error a possible solution has on a
162      * plane respect the back-projected plane of a line using estimated camera
163      * Residuals to determine whether planes are inliers or not are computed by
164      * comparing two planes algebraically (e.g. doing the dot product of their
165      * parameters).
166      * A residual of 0 indicates that dot product was 1 or -1 and planes were
167      * equal.
168      * A residual of 1 indicates that dot product was 0 and planes were
169      * orthogonal.
170      * If dot product between planes is -1, then although their director vectors
171      * are opposed, planes are considered equal, since sign changes are not
172      * taken into account and their residuals will be 0.
173      *
174      * @return threshold to determine whether matched planes are inliers or not.
175      */
176     public double getThreshold() {
177         return threshold;
178     }
179 
180     /**
181      * Sets threshold to determine whether planes are inliers or not when
182      * testing possible estimation solutions.
183      * The threshold refers to the amount of error a possible solution has on a
184      * plane respect the back-projected plane of a line using estimated camera
185      * Residuals to determine whether planes are inliers or not are computed by
186      * comparing two planes algebraically (e.g. doing the dot product of their
187      * parameters).
188      * A residual of 0 indicates that dot product was 1 or -1 and planes were
189      * equal.
190      * A residual of 1 indicates that dot product was 0 and planes were
191      * orthogonal.
192      * If dot product between planes is -1, then although their director vectors
193      * are opposed, planes are considered equal, since sign changes are not
194      * taken into account and their residuals will be 0.
195      *
196      * @param threshold threshold to determine whether matched planes are
197      *                  inliers or not.
198      * @throws IllegalArgumentException if provided value is equal or less than
199      *                                  zero.
200      * @throws LockedException          if robust estimator is locked because an
201      *                                  estimation is already in progress.
202      */
203     public void setThreshold(final double threshold) throws LockedException {
204         if (isLocked()) {
205             throw new LockedException();
206         }
207         if (threshold <= MIN_THRESHOLD) {
208             throw new IllegalArgumentException();
209         }
210         this.threshold = threshold;
211     }
212 
213     /**
214      * Indicates whether inliers must be computed and kept.
215      *
216      * @return true if inliers must be computed and kept, false if inliers
217      * only need to be computed but not kept.
218      */
219     public boolean isComputeAndKeepInliersEnabled() {
220         return computeAndKeepInliers;
221     }
222 
223     /**
224      * Specifies whether inliers must be computed and kept.
225      *
226      * @param computeAndKeepInliers true if inliers must be computed and kept,
227      *                              false if inliers only need to be computed but not kept.
228      * @throws LockedException if estimator is locked.
229      */
230     public void setComputeAndKeepInliersEnabled(final boolean computeAndKeepInliers) throws LockedException {
231         if (isLocked()) {
232             throw new LockedException();
233         }
234         this.computeAndKeepInliers = computeAndKeepInliers;
235     }
236 
237     /**
238      * Indicates whether residuals must be computed and kept.
239      *
240      * @return true if residuals must be computed and kept, false if residuals
241      * only need to be computed but not kept.
242      */
243     public boolean isComputeAndKeepResidualsEnabled() {
244         return computeAndKeepResiduals;
245     }
246 
247     /**
248      * Specifies whether residuals must be computed and kept.
249      *
250      * @param computeAndKeepResiduals true if residuals must be computed and
251      *                                kept, false if residuals only need to be computed but not kept.
252      * @throws LockedException if estimator is locked.
253      */
254     public void setComputeAndKeepResidualsEnabled(final boolean computeAndKeepResiduals) throws LockedException {
255         if (isLocked()) {
256             throw new LockedException();
257         }
258         this.computeAndKeepResiduals = computeAndKeepResiduals;
259     }
260 
261     /**
262      * Estimates a pinhole camera using a robust estimator and
263      * the best set of matched 2D line/3D plane correspondences found using the
264      * robust estimator.
265      *
266      * @return a pinhole camera.
267      * @throws LockedException          if robust estimator is locked because an
268      *                                  estimation is already in progress.
269      * @throws NotReadyException        if provided input data is not enough to start
270      *                                  the estimation.
271      * @throws RobustEstimatorException if estimation fails for any reason
272      *                                  (i.e. numerical instability, no solution available, etc).
273      */
274     @Override
275     public PinholeCamera estimate() throws LockedException, NotReadyException, RobustEstimatorException {
276         if (isLocked()) {
277             throw new LockedException();
278         }
279         if (!isReady()) {
280             throw new NotReadyException();
281         }
282 
283         // pinhole camera estimator using DLT (Direct Linear Transform) algorithm
284         final var nonRobustEstimator = new DLTLinePlaneCorrespondencePinholeCameraEstimator();
285 
286         nonRobustEstimator.setLMSESolutionAllowed(false);
287 
288         // suggestions
289         nonRobustEstimator.setSuggestSkewnessValueEnabled(isSuggestSkewnessValueEnabled());
290         nonRobustEstimator.setSuggestedSkewnessValue(getSuggestedSkewnessValue());
291         nonRobustEstimator.setSuggestHorizontalFocalLengthEnabled(isSuggestHorizontalFocalLengthEnabled());
292         nonRobustEstimator.setSuggestedHorizontalFocalLengthValue(getSuggestedHorizontalFocalLengthValue());
293         nonRobustEstimator.setSuggestVerticalFocalLengthEnabled(isSuggestVerticalFocalLengthEnabled());
294         nonRobustEstimator.setSuggestedVerticalFocalLengthValue(getSuggestedVerticalFocalLengthValue());
295         nonRobustEstimator.setSuggestAspectRatioEnabled(isSuggestAspectRatioEnabled());
296         nonRobustEstimator.setSuggestedAspectRatioValue(getSuggestedAspectRatioValue());
297         nonRobustEstimator.setSuggestPrincipalPointEnabled(isSuggestPrincipalPointEnabled());
298         nonRobustEstimator.setSuggestedPrincipalPointValue(getSuggestedPrincipalPointValue());
299         nonRobustEstimator.setSuggestRotationEnabled(isSuggestRotationEnabled());
300         nonRobustEstimator.setSuggestedRotationValue(getSuggestedRotationValue());
301         nonRobustEstimator.setSuggestCenterEnabled(isSuggestCenterEnabled());
302         nonRobustEstimator.setSuggestedCenterValue(getSuggestedCenterValue());
303 
304         final var innerEstimator = new RANSACRobustEstimator<>(new RANSACRobustEstimatorListener<PinholeCamera>() {
305 
306             // 3D planes for a subset of samples
307             private final List<Plane> subsetPlanes = new ArrayList<>();
308 
309             // 2D lines for a subset of samples
310             private final List<Line2D> subsetLines = new ArrayList<>();
311 
312             @Override
313             public double getThreshold() {
314                 return threshold;
315             }
316 
317             @Override
318             public int getTotalSamples() {
319                 return planes.size();
320             }
321 
322             @Override
323             public int getSubsetSize() {
324                 return LinePlaneCorrespondencePinholeCameraEstimator.MIN_NUMBER_OF_LINE_PLANE_CORRESPONDENCES;
325             }
326 
327             @Override
328             public void estimatePreliminarSolutions(final int[] samplesIndices, final List<PinholeCamera> solutions) {
329                 subsetPlanes.clear();
330                 subsetPlanes.add(planes.get(samplesIndices[0]));
331                 subsetPlanes.add(planes.get(samplesIndices[1]));
332                 subsetPlanes.add(planes.get(samplesIndices[2]));
333                 subsetPlanes.add(planes.get(samplesIndices[3]));
334 
335                 subsetLines.clear();
336                 subsetLines.add(lines.get(samplesIndices[0]));
337                 subsetLines.add(lines.get(samplesIndices[1]));
338                 subsetLines.add(lines.get(samplesIndices[2]));
339                 subsetLines.add(lines.get(samplesIndices[3]));
340 
341                 try {
342                     nonRobustEstimator.setLists(subsetPlanes, subsetLines);
343 
344                     final var cam = nonRobustEstimator.estimate();
345                     solutions.add(cam);
346                 } catch (final Exception e) {
347                     // if lines/planes configuration is degenerate, no solution is added
348                 }
349             }
350 
351             @Override
352             public double computeResidual(final PinholeCamera currentEstimation, final int i) {
353                 final var inputLine = lines.get(i);
354                 final var inputPlane = planes.get(i);
355 
356                 return singleBackprojectionResidual(currentEstimation, inputLine, inputPlane);
357             }
358 
359             @Override
360             public boolean isReady() {
361                 return RANSACDLTLinePlaneCorrespondencePinholeCameraRobustEstimator.this.isReady();
362             }
363 
364             @Override
365             public void onEstimateStart(final RobustEstimator<PinholeCamera> estimator) {
366                 if (listener != null) {
367                     listener.onEstimateStart(
368                             RANSACDLTLinePlaneCorrespondencePinholeCameraRobustEstimator.this);
369                 }
370             }
371 
372             @Override
373             public void onEstimateEnd(final RobustEstimator<PinholeCamera> estimator) {
374                 if (listener != null) {
375                     listener.onEstimateEnd(RANSACDLTLinePlaneCorrespondencePinholeCameraRobustEstimator.this);
376                 }
377             }
378 
379             @Override
380             public void onEstimateNextIteration(final RobustEstimator<PinholeCamera> estimator, final int iteration) {
381                 if (listener != null) {
382                     listener.onEstimateNextIteration(
383                             RANSACDLTLinePlaneCorrespondencePinholeCameraRobustEstimator.this, iteration);
384                 }
385             }
386 
387             @Override
388             public void onEstimateProgressChange(final RobustEstimator<PinholeCamera> estimator, final float progress) {
389                 if (listener != null) {
390                     listener.onEstimateProgressChange(
391                             RANSACDLTLinePlaneCorrespondencePinholeCameraRobustEstimator.this, progress);
392                 }
393             }
394         });
395 
396         try {
397             locked = true;
398             inliersData = null;
399             innerEstimator.setComputeAndKeepInliersEnabled(computeAndKeepInliers || refineResult);
400             innerEstimator.setComputeAndKeepResidualsEnabled(computeAndKeepResiduals || refineResult);
401             innerEstimator.setConfidence(confidence);
402             innerEstimator.setMaxIterations(maxIterations);
403             innerEstimator.setProgressDelta(progressDelta);
404             final var result = innerEstimator.estimate();
405             inliersData = innerEstimator.getInliersData();
406             return attemptRefine(result, nonRobustEstimator.getMaxSuggestionWeight());
407 
408         } catch (final com.irurueta.numerical.LockedException e) {
409             throw new LockedException(e);
410         } catch (final com.irurueta.numerical.NotReadyException e) {
411             throw new NotReadyException(e);
412         } finally {
413             locked = false;
414         }
415     }
416 
417     /**
418      * Returns method being used for robust estimation.
419      *
420      * @return method being used for robust estimation.
421      */
422     @Override
423     public RobustEstimatorMethod getMethod() {
424         return RobustEstimatorMethod.RANSAC;
425     }
426 
427     /**
428      * Gets standard deviation used for Levenberg-Marquardt fitting during
429      * refinement.
430      * Returned value gives an indication of how much variance each residual
431      * has.
432      * Typically, this value is related to the threshold used on each robust
433      * estimation, since residuals of found inliers are within the range of
434      * such threshold.
435      *
436      * @return standard deviation used for refinement.
437      */
438     @Override
439     protected double getRefinementStandardDeviation() {
440         return threshold;
441     }
442 }