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 }