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