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.AffineTransformation2D;
19 import com.irurueta.geometry.CoincidentPointsException;
20 import com.irurueta.geometry.CoordinatesType;
21 import com.irurueta.geometry.Point2D;
22 import com.irurueta.numerical.robust.RANSACRobustEstimator;
23 import com.irurueta.numerical.robust.RANSACRobustEstimatorListener;
24 import com.irurueta.numerical.robust.RobustEstimator;
25 import com.irurueta.numerical.robust.RobustEstimatorException;
26 import com.irurueta.numerical.robust.RobustEstimatorMethod;
27
28 import java.util.List;
29
30 /**
31 * Finds the best affine 2D transformation for provided collections of matched
32 * 2D points using RANSAC algorithm.
33 */
34 public class RANSACPointCorrespondenceAffineTransformation2DRobustEstimator
35 extends PointCorrespondenceAffineTransformation2DRobustEstimator {
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 RANSACPointCorrespondenceAffineTransformation2DRobustEstimator() {
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 affine 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 * affine 2D transformation.
98 * @param outputPoints list of output points to be used to estimate an
99 * affine 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 RANSACPointCorrespondenceAffineTransformation2DRobustEstimator(
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 RANSACPointCorrespondenceAffineTransformation2DRobustEstimator(
118 final AffineTransformation2DRobustEstimatorListener 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 * affine 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 * affine 2D transformation.
136 * @param outputPoints list of output points to be used to estimate an
137 * affine 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 RANSACPointCorrespondenceAffineTransformation2DRobustEstimator(
142 final AffineTransformation2DRobustEstimatorListener 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 * Returns threshold to determine whether points are inliers or not when
152 * testing possible estimation solutions.
153 * The threshold refers to the amount of error (i.e. Euclidean distance) a
154 * possible solution has on a matched pair of points.
155 *
156 * @return threshold to determine whether points are inliers or not when
157 * testing possible estimation solutions.
158 */
159 public double getThreshold() {
160 return threshold;
161 }
162
163 /**
164 * Sets threshold to determine whether points are inliers or not when
165 * testing possible estimation solutions.
166 * The threshold refers to the amount of error (i.e. Euclidean distance) a
167 * possible solution has on a matched pair of points.
168 *
169 * @param threshold threshold to be set.
170 * @throws IllegalArgumentException if provided value is equal or less than
171 * zero.
172 * @throws LockedException if robust estimator is locked because an
173 * estimation is already in progress.
174 */
175 public void setThreshold(final double threshold) throws LockedException {
176 if (isLocked()) {
177 throw new LockedException();
178 }
179 if (threshold <= MIN_THRESHOLD) {
180 throw new IllegalArgumentException();
181 }
182 this.threshold = threshold;
183 }
184
185 /**
186 * Indicates whether inliers must be computed and kept.
187 *
188 * @return true if inliers must be computed and kept, false if inliers only
189 * need to be computed but not kept.
190 */
191 public boolean isComputeAndKeepInliersEnabled() {
192 return computeAndKeepInliers;
193 }
194
195 /**
196 * Specifies whether inliers must be computed and kept.
197 *
198 * @param computeAndKeepInliers true if inliers must be computed and kept,
199 * false if inliers only need to be computed but not kept.
200 * @throws LockedException if estimator is locked.
201 */
202 public void setComputeAndKeepInliersEnabled(final boolean computeAndKeepInliers) throws LockedException {
203 if (isLocked()) {
204 throw new LockedException();
205 }
206 this.computeAndKeepInliers = computeAndKeepInliers;
207 }
208
209 /**
210 * Indicates whether residuals must be computed and kept.
211 *
212 * @return true if residuals must be computed and kept, false if residuals
213 * only need to be computed but not kept.
214 */
215 public boolean isComputeAndKeepResidualsEnabled() {
216 return computeAndKeepResiduals;
217 }
218
219 /**
220 * Specifies whether residuals must be computed and kept.
221 *
222 * @param computeAndKeepResiduals true if residuals must be computed and
223 * kept, false if residuals only need to be computed but not kept.
224 * @throws LockedException if estimator is locked.
225 */
226 public void setComputeAndKeepResidualsEnabled(final boolean computeAndKeepResiduals) throws LockedException {
227 if (isLocked()) {
228 throw new LockedException();
229 }
230 this.computeAndKeepResiduals = computeAndKeepResiduals;
231 }
232
233 /**
234 * Estimates an affine 2D transformation using a robust estimator and
235 * the best set of matched 2D point correspondences found using the robust
236 * estimator.
237 *
238 * @return an affine 2D transformation.
239 * @throws LockedException if robust estimator is locked because an
240 * estimation is already in progress.
241 * @throws NotReadyException if provided input data is not enough to start
242 * the estimation.
243 * @throws RobustEstimatorException if estimation fails for any reason
244 * (i.e. numerical instability, no solution available, etc).
245 */
246 @SuppressWarnings("DuplicatedCode")
247 @Override
248 public AffineTransformation2D estimate() throws LockedException, NotReadyException, RobustEstimatorException {
249 if (isLocked()) {
250 throw new LockedException();
251 }
252 if (!isReady()) {
253 throw new NotReadyException();
254 }
255
256 final var innerEstimator = new RANSACRobustEstimator<>(
257 new RANSACRobustEstimatorListener<AffineTransformation2D>() {
258
259 // point to be reused when computing residuals
260 private final Point2D testPoint = Point2D.create(CoordinatesType.HOMOGENEOUS_COORDINATES);
261
262 @Override
263 public double getThreshold() {
264 return threshold;
265 }
266
267 @Override
268 public int getTotalSamples() {
269 return inputPoints.size();
270 }
271
272 @Override
273 public int getSubsetSize() {
274 return AffineTransformation2DRobustEstimator.MINIMUM_SIZE;
275 }
276
277 @Override
278 public void estimatePreliminarSolutions(
279 final int[] samplesIndices, final List<AffineTransformation2D> solutions) {
280 final var inputPoint1 = inputPoints.get(samplesIndices[0]);
281 final var inputPoint2 = inputPoints.get(samplesIndices[1]);
282 final var inputPoint3 = inputPoints.get(samplesIndices[2]);
283
284 final var outputPoint1 = outputPoints.get(samplesIndices[0]);
285 final var outputPoint2 = outputPoints.get(samplesIndices[1]);
286 final var outputPoint3 = outputPoints.get(samplesIndices[2]);
287
288 try {
289 final var transformation = new AffineTransformation2D(inputPoint1, inputPoint2, inputPoint3,
290 outputPoint1, outputPoint2, outputPoint3);
291 solutions.add(transformation);
292 } catch (final CoincidentPointsException e) {
293 // if points are coincident, no solution is added
294 }
295 }
296
297 @Override
298 public double computeResidual(final AffineTransformation2D currentEstimation, final int i) {
299 final var inputPoint = inputPoints.get(i);
300 final var outputPoint = outputPoints.get(i);
301
302 // transform input point and store result in mTestPoint
303 currentEstimation.transform(inputPoint, testPoint);
304
305 return outputPoint.distanceTo(testPoint);
306 }
307
308 @Override
309 public boolean isReady() {
310 return RANSACPointCorrespondenceAffineTransformation2DRobustEstimator.this.isReady();
311 }
312
313 @Override
314 public void onEstimateStart(final RobustEstimator<AffineTransformation2D> estimator) {
315 if (mListener != null) {
316 mListener.onEstimateStart(
317 RANSACPointCorrespondenceAffineTransformation2DRobustEstimator.this);
318 }
319 }
320
321 @Override
322 public void onEstimateEnd(final RobustEstimator<AffineTransformation2D> estimator) {
323 if (mListener != null) {
324 mListener.onEstimateEnd(
325 RANSACPointCorrespondenceAffineTransformation2DRobustEstimator.this);
326 }
327 }
328
329 @Override
330 public void onEstimateNextIteration(
331 final RobustEstimator<AffineTransformation2D> estimator, final int iteration) {
332 if (mListener != null) {
333 mListener.onEstimateNextIteration(
334 RANSACPointCorrespondenceAffineTransformation2DRobustEstimator.this,
335 iteration);
336 }
337 }
338
339 @Override
340 public void onEstimateProgressChange(
341 final RobustEstimator<AffineTransformation2D> estimator, final float progress) {
342 if (mListener != null) {
343 mListener.onEstimateProgressChange(
344 RANSACPointCorrespondenceAffineTransformation2DRobustEstimator.this,
345 progress);
346 }
347 }
348 });
349
350 try {
351 locked = true;
352 inliersData = null;
353 innerEstimator.setComputeAndKeepInliersEnabled(computeAndKeepInliers || refineResult);
354 innerEstimator.setComputeAndKeepResidualsEnabled(computeAndKeepResiduals || refineResult);
355 innerEstimator.setConfidence(confidence);
356 innerEstimator.setMaxIterations(maxIterations);
357 innerEstimator.setProgressDelta(progressDelta);
358 final var transformation = innerEstimator.estimate();
359 inliersData = innerEstimator.getInliersData();
360 return attemptRefine(transformation);
361 } catch (final com.irurueta.numerical.LockedException e) {
362 throw new LockedException(e);
363 } catch (final com.irurueta.numerical.NotReadyException e) {
364 throw new NotReadyException(e);
365 } finally {
366 locked = false;
367 }
368 }
369
370 /**
371 * Returns method being used for robust estimation.
372 *
373 * @return method being used for robust estimation.
374 */
375 @Override
376 public RobustEstimatorMethod getMethod() {
377 return RobustEstimatorMethod.RANSAC;
378 }
379
380 /**
381 * Gets standard deviation used for Levenberg-Marquardt fitting during
382 * refinement.
383 * Returned value gives an indication of how much variance each residual
384 * has.
385 * Typically, this value is related to the threshold used on each robust
386 * estimation, since residuals of found inliers are within the range of
387 * such threshold.
388 *
389 * @return standard deviation used for refinement.
390 */
391 @Override
392 protected double getRefinementStandardDeviation() {
393 return threshold;
394 }
395 }