1 /*
2 * Copyright (C) 2017 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.CoordinatesType;
19 import com.irurueta.geometry.MetricTransformation2D;
20 import com.irurueta.geometry.Point2D;
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 metric 2D transformation for provided collections of
32 * matched 2D points using RANSAC algorithm.
33 */
34 public class RANSACMetricTransformation2DRobustEstimator extends MetricTransformation2DRobustEstimator {
35
36 /**
37 * Constant defining default threshold to determine whether points are
38 * inliers or not.
39 * By default, 1.0 is considered a good value for cases where measures are
40 * done on pixels, since typically the minimum resolution is 1 pixel.
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 points 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 matched pair of points.
65 */
66 private double threshold;
67
68 /**
69 * Indicates whether inliers must be computed and kept.
70 */
71 private boolean computeAndKeepInliers;
72
73 /**
74 * Indicates whether residuals must be computed and kept.
75 */
76 private boolean computeAndKeepResiduals;
77
78 /**
79 * Constructor.
80 */
81 public RANSACMetricTransformation2DRobustEstimator() {
82 super();
83 threshold = DEFAULT_THRESHOLD;
84 computeAndKeepInliers = DEFAULT_COMPUTE_AND_KEEP_INLIERS;
85 computeAndKeepResiduals = DEFAULT_COMPUTE_AND_KEEP_RESIDUALS;
86 }
87
88 /**
89 * Constructor with lists of points to be used to estimate a metric 2D
90 * transformation.
91 * Points in the list located at the same position are considered to be
92 * matched. Hence, both lists must have the same size, and their size must
93 * be greater or equal than MINIMUM_SIZE.
94 *
95 * @param inputPoints list of input points to be used to estimate a
96 * metric 2D transformation.
97 * @param outputPoints list of output points to be used to estimate a
98 * metric 2D transformation.
99 * @throws IllegalArgumentException if provided lists of points don't have
100 * the same size or their size is smaller than MINIMUM_SIZE.
101 */
102 public RANSACMetricTransformation2DRobustEstimator(
103 final List<Point2D> inputPoints, final List<Point2D> outputPoints) {
104 super(inputPoints, outputPoints);
105 threshold = DEFAULT_THRESHOLD;
106 computeAndKeepInliers = DEFAULT_COMPUTE_AND_KEEP_INLIERS;
107 computeAndKeepResiduals = DEFAULT_COMPUTE_AND_KEEP_RESIDUALS;
108 }
109
110 /**
111 * Constructor.
112 *
113 * @param listener listener to be notified of events such as when estimation
114 * starts, ends or its progress significantly changes.
115 */
116 public RANSACMetricTransformation2DRobustEstimator(final MetricTransformation2DRobustEstimatorListener listener) {
117 super(listener);
118 threshold = DEFAULT_THRESHOLD;
119 computeAndKeepInliers = DEFAULT_COMPUTE_AND_KEEP_INLIERS;
120 computeAndKeepResiduals = DEFAULT_COMPUTE_AND_KEEP_RESIDUALS;
121 }
122
123 /**
124 * Constructor with listener and lists of points to be used to estimate a
125 * metric 2D transformation.
126 * Points in the list located at the same position are considered to be
127 * matched. Hence, both lists must have the same size, and their size must
128 * be greater or equal than MINIMUM_SIZE.
129 *
130 * @param listener listener to be notified of events such as when estimation
131 * starts, ends or its progress significantly changes.
132 * @param inputPoints list of input points to be used to estimate a
133 * metric 2D transformation.
134 * @param outputPoints list of output points to be used to estimate a
135 * metric 2D transformation.
136 * @throws IllegalArgumentException if provided lists of points don't have
137 * the same size or their size is smaller than MINIMUM_SIZE.
138 */
139 public RANSACMetricTransformation2DRobustEstimator(
140 final MetricTransformation2DRobustEstimatorListener listener,
141 final List<Point2D> inputPoints, final List<Point2D> outputPoints) {
142 super(listener, inputPoints, outputPoints);
143 threshold = DEFAULT_THRESHOLD;
144 computeAndKeepInliers = DEFAULT_COMPUTE_AND_KEEP_INLIERS;
145 computeAndKeepResiduals = DEFAULT_COMPUTE_AND_KEEP_RESIDUALS;
146 }
147
148 /**
149 * Constructor.
150 *
151 * @param weakMinimumSizeAllowed true allows 2 points, false requires 3.
152 */
153 public RANSACMetricTransformation2DRobustEstimator(final boolean weakMinimumSizeAllowed) {
154 super(weakMinimumSizeAllowed);
155 threshold = DEFAULT_THRESHOLD;
156 computeAndKeepInliers = DEFAULT_COMPUTE_AND_KEEP_INLIERS;
157 computeAndKeepResiduals = DEFAULT_COMPUTE_AND_KEEP_RESIDUALS;
158 }
159
160 /**
161 * Constructor with lists of points to be used to estimate a metric 2D
162 * transformation.
163 * Points in the list located at the same position are considered to be
164 * matched. Hence, both lists must have the same size, and their size must
165 * be greater or equal than MINIMUM_SIZE.
166 *
167 * @param inputPoints list of input points to be used to estimate a
168 * metric 2D transformation.
169 * @param outputPoints list of output points to be used to estimate a
170 * metric 2D transformation.
171 * @param weakMinimumSizeAllowed true allows 2 points, false requires 3.
172 * @throws IllegalArgumentException if provided lists of points don't have
173 * the same size or their size is smaller than MINIMUM_SIZE.
174 */
175 public RANSACMetricTransformation2DRobustEstimator(
176 final List<Point2D> inputPoints, final List<Point2D> outputPoints, final boolean weakMinimumSizeAllowed) {
177 super(inputPoints, outputPoints, weakMinimumSizeAllowed);
178 threshold = DEFAULT_THRESHOLD;
179 computeAndKeepInliers = DEFAULT_COMPUTE_AND_KEEP_INLIERS;
180 computeAndKeepResiduals = DEFAULT_COMPUTE_AND_KEEP_RESIDUALS;
181 }
182
183 /**
184 * Constructor.
185 *
186 * @param listener listener to be notified of events such as when estimation
187 * starts, ends or its progress significantly changes.
188 * @param weakMinimumSizeAllowed true allows 2 points, false requires 3.
189 */
190 public RANSACMetricTransformation2DRobustEstimator(
191 final MetricTransformation2DRobustEstimatorListener listener, final boolean weakMinimumSizeAllowed) {
192 super(listener, weakMinimumSizeAllowed);
193 threshold = DEFAULT_THRESHOLD;
194 computeAndKeepInliers = DEFAULT_COMPUTE_AND_KEEP_INLIERS;
195 computeAndKeepResiduals = DEFAULT_COMPUTE_AND_KEEP_RESIDUALS;
196 }
197
198 /**
199 * Constructor with listener and lists of points to be used to estimate a
200 * metric 2D transformation.
201 * Points in the list located at the same position are considered to be
202 * matched. Hence, both lists must have the same size, and their size must
203 * be greater or equal than MINIMUM_SIZE.
204 *
205 * @param listener listener to be notified of events such as when estimation
206 * starts, ends or its progress significantly changes.
207 * @param inputPoints list of input points to be used to estimate a
208 * metric 2D transformation.
209 * @param outputPoints list of output points to be used to estimate a
210 * metric 2D transformation.
211 * @param weakMinimumSizeAllowed true allows 2 points, false requires 3.
212 * @throws IllegalArgumentException if provided lists of points don't have
213 * the same size or their size is smaller than MINIMUM_SIZE.
214 */
215 public RANSACMetricTransformation2DRobustEstimator(
216 final MetricTransformation2DRobustEstimatorListener listener,
217 final List<Point2D> inputPoints, final List<Point2D> outputPoints, final boolean weakMinimumSizeAllowed) {
218 super(listener, inputPoints, outputPoints, weakMinimumSizeAllowed);
219 threshold = DEFAULT_THRESHOLD;
220 computeAndKeepInliers = DEFAULT_COMPUTE_AND_KEEP_INLIERS;
221 computeAndKeepResiduals = DEFAULT_COMPUTE_AND_KEEP_RESIDUALS;
222 }
223
224 /**
225 * Returns threshold to determine whether points are inliers or not when
226 * testing possible estimation solutions.
227 * The threshold refers to the amount of error (i.e. Euclidean distance) a
228 * possible solution has on a matched pair of points.
229 *
230 * @return threshold to determine whether points are inliers or not when
231 * testing possible estimation solutions.
232 */
233 public double getThreshold() {
234 return threshold;
235 }
236
237 /**
238 * Sets threshold to determine whether points are inliers or not when
239 * testing possible estimation solutions.
240 * The threshold refers to the amount of error (i.e. Euclidean distance) a
241 * possible solution has on a matched pair of points.
242 *
243 * @param threshold threshold to be set.
244 * @throws IllegalArgumentException if provided value is equal or less than
245 * zero.
246 * @throws LockedException if robust estimator is locked because an
247 * estimation is already in progress.
248 */
249 public void setThreshold(final double threshold) throws LockedException {
250 if (isLocked()) {
251 throw new LockedException();
252 }
253 if (threshold <= MIN_THRESHOLD) {
254 throw new IllegalArgumentException();
255 }
256 this.threshold = threshold;
257 }
258
259 /**
260 * Indicates whether inliers must be computed and kept.
261 *
262 * @return true if inliers must be computed and kept, false if inliers only
263 * need to be computed but not kept.
264 */
265 public boolean isComputeAndKeepInliersEnabled() {
266 return computeAndKeepInliers;
267 }
268
269 /**
270 * Specifies whether inliers must be computed and kept.
271 *
272 * @param computeAndKeepInliers true if inliers must be computed and kept,
273 * false if inliers only need to be computed but not kept.
274 * @throws LockedException if estimator is locked.
275 */
276 public void setComputeAndKeepInliersEnabled(final boolean computeAndKeepInliers) throws LockedException {
277 if (isLocked()) {
278 throw new LockedException();
279 }
280 this.computeAndKeepInliers = computeAndKeepInliers;
281 }
282
283 /**
284 * Indicates whether residuals must be computed and kept.
285 *
286 * @return true if residuals must be computed and kept, false if residuals
287 * only need to be computed but not kept.
288 */
289 public boolean isComputeAndKeepResidualsEnabled() {
290 return computeAndKeepResiduals;
291 }
292
293 /**
294 * Specifies whether residuals must be computed and kept.
295 *
296 * @param computeAndKeepResiduals true if residuals must be computed and
297 * kept, false if residuals only need to be computed but not kept.
298 * @throws LockedException if estimator is locked.
299 */
300 public void setComputeAndKeepResidualsEnabled(final boolean computeAndKeepResiduals) throws LockedException {
301 if (isLocked()) {
302 throw new LockedException();
303 }
304 this.computeAndKeepResiduals = computeAndKeepResiduals;
305 }
306
307 /**
308 * Estimates a metric 2D transformation using a robust estimator and
309 * the best set of matched 2D point correspondences found using the robust
310 * estimator.
311 *
312 * @return a metric 2D transformation.
313 * @throws LockedException if robust estimator is locked because an
314 * estimation is already in progress.
315 * @throws NotReadyException if provided input data is not enough to start
316 * the estimation.
317 * @throws RobustEstimatorException if estimation fails for any reason
318 * (i.e. numerical instability, no solution available, etc).
319 */
320 @SuppressWarnings("DuplicatedCode")
321 @Override
322 public MetricTransformation2D estimate() throws LockedException, NotReadyException, RobustEstimatorException {
323 if (isLocked()) {
324 throw new LockedException();
325 }
326 if (!isReady()) {
327 throw new NotReadyException();
328 }
329
330 final var innerEstimator = new RANSACRobustEstimator<>(
331 new RANSACRobustEstimatorListener<MetricTransformation2D>() {
332
333 // point to be reused when computing residuals
334 private final Point2D testPoint = Point2D.create(CoordinatesType.HOMOGENEOUS_COORDINATES);
335
336 private final MetricTransformation2DEstimator nonRobustEstimator =
337 new MetricTransformation2DEstimator(isWeakMinimumSizeAllowed());
338
339 private final List<Point2D> subsetInputPoints = new ArrayList<>();
340 private final List<Point2D> subsetOutputPoints = new ArrayList<>();
341
342 @Override
343 public double getThreshold() {
344 return threshold;
345 }
346
347 @Override
348 public int getTotalSamples() {
349 return inputPoints.size();
350 }
351
352 @Override
353 public int getSubsetSize() {
354 return nonRobustEstimator.getMinimumPoints();
355 }
356
357 @Override
358 public void estimatePreliminarSolutions(
359 final int[] samplesIndices, final List<MetricTransformation2D> solutions) {
360 subsetInputPoints.clear();
361 subsetOutputPoints.clear();
362 for (final var samplesIndex : samplesIndices) {
363 subsetInputPoints.add(inputPoints.get(samplesIndex));
364 subsetOutputPoints.add(outputPoints.get(samplesIndex));
365 }
366
367 try {
368 nonRobustEstimator.setPoints(subsetInputPoints, subsetOutputPoints);
369 solutions.add(nonRobustEstimator.estimate());
370 } catch (final Exception e) {
371 // if points are coincident, no solution is added
372 }
373 }
374
375 @Override
376 public double computeResidual(final MetricTransformation2D currentEstimation, final int i) {
377 final var inputPoint = inputPoints.get(i);
378 final var outputPoint = outputPoints.get(i);
379
380 // transform input point and store result in mTestPoint
381 currentEstimation.transform(inputPoint, testPoint);
382
383 return outputPoint.distanceTo(testPoint);
384 }
385
386 @Override
387 public boolean isReady() {
388 return RANSACMetricTransformation2DRobustEstimator.this.isReady();
389 }
390
391 @Override
392 public void onEstimateStart(final RobustEstimator<MetricTransformation2D> estimator) {
393 if (listener != null) {
394 listener.onEstimateStart(RANSACMetricTransformation2DRobustEstimator.this);
395 }
396 }
397
398 @Override
399 public void onEstimateEnd(final RobustEstimator<MetricTransformation2D> estimator) {
400 if (listener != null) {
401 listener.onEstimateEnd(RANSACMetricTransformation2DRobustEstimator.this);
402 }
403 }
404
405 @Override
406 public void onEstimateNextIteration(
407 final RobustEstimator<MetricTransformation2D> estimator, final int iteration) {
408 if (listener != null) {
409 listener.onEstimateNextIteration(
410 RANSACMetricTransformation2DRobustEstimator.this, iteration);
411 }
412 }
413
414 @Override
415 public void onEstimateProgressChange(
416 final RobustEstimator<MetricTransformation2D> estimator, final float progress) {
417 if (listener != null) {
418 listener.onEstimateProgressChange(
419 RANSACMetricTransformation2DRobustEstimator.this, progress);
420 }
421 }
422 });
423
424 try {
425 locked = true;
426 inliersData = null;
427 innerEstimator.setComputeAndKeepInliersEnabled(computeAndKeepInliers || refineResult);
428 innerEstimator.setComputeAndKeepResidualsEnabled(computeAndKeepResiduals || refineResult);
429 innerEstimator.setConfidence(confidence);
430 innerEstimator.setMaxIterations(maxIterations);
431 innerEstimator.setProgressDelta(progressDelta);
432 final var transformation = innerEstimator.estimate();
433 inliersData = innerEstimator.getInliersData();
434 return attemptRefine(transformation);
435 } catch (final com.irurueta.numerical.LockedException e) {
436 throw new LockedException(e);
437 } catch (final com.irurueta.numerical.NotReadyException e) {
438 throw new NotReadyException(e);
439 } finally {
440 locked = false;
441 }
442 }
443
444 /**
445 * Returns method being used for robust estimation.
446 *
447 * @return method being used for robust estimation.
448 */
449 @Override
450 public RobustEstimatorMethod getMethod() {
451 return RobustEstimatorMethod.RANSAC;
452 }
453
454 /**
455 * Gets standard deviation used for Levenberg-Marquardt fitting during
456 * refinement.
457 * Returned value gives an indication of how much variance each residual
458 * has.
459 * Typically, this value is related to the threshold used on each robust
460 * estimation, since residuals of found inliers are within the range of
461 * such threshold.
462 *
463 * @return standard deviation used for refinement.
464 */
465 @Override
466 protected double getRefinementStandardDeviation() {
467 return threshold;
468 }
469 }