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.PROMedSRobustEstimator;
23 import com.irurueta.numerical.robust.PROMedSRobustEstimatorListener;
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 PROMedS algorithm.
33 */
34 @SuppressWarnings("DuplicatedCode")
35 public class PROMedSPointCorrespondenceAffineTransformation2DRobustEstimator
36 extends PointCorrespondenceAffineTransformation2DRobustEstimator {
37
38 /**
39 * Default value to be used for stop threshold. Stop threshold can be used
40 * to keep the algorithm iterating in case that best estimated threshold
41 * using median of residuals is not small enough. Once a solution is found
42 * that generates a threshold below this value, the algorithm will stop.
43 * The stop threshold can be used to prevent the LMedS algorithm iterating
44 * too many times in cases where samples have a very similar accuracy.
45 * For instance, in cases where proportion of outliers is very small (close
46 * to 0%), and samples are very accurate (i.e. 1e-6), the algorithm would
47 * iterate for a long time trying to find the best solution when indeed
48 * there is no need to do that if a reasonable threshold has already been
49 * reached.
50 * Because of this behaviour the stop threshold can be set to a value much
51 * lower than the one typically used in RANSAC, and yet the algorithm could
52 * still produce even smaller thresholds in estimated results.
53 */
54 public static final double DEFAULT_STOP_THRESHOLD = 1.0;
55
56 /**
57 * Minimum allowed stop threshold value.
58 */
59 public static final double MIN_STOP_THRESHOLD = 0.0;
60
61 /**
62 * Threshold to be used to keep the algorithm iterating in case that best
63 * estimated threshold using median of residuals is not small enough. Once
64 * a solution is found that generates a threshold below this value, the
65 * algorithm will stop.
66 * The stop threshold can be used to prevent the LMedS algorithm iterating
67 * too many times in cases where samples have a very similar accuracy.
68 * For instance, in cases where proportion of outliers is very small (close
69 * to 0%), and samples are very accurate (i.e. 1e-6), the algorithm would
70 * iterate for a long time trying to find the best solution when indeed
71 * there is no need to do that if a reasonable threshold has already been
72 * reached.
73 * Because of this behaviour the stop threshold can be set to a value much
74 * lower than the one typically used in RANSAC, and yet the algorithm could
75 * still produce even smaller thresholds in estimated results.
76 */
77 private double stopThreshold;
78
79 /**
80 * Quality scores corresponding to each pair of matched points.
81 * The larger the score value the better the quality of the matching.
82 */
83 private double[] qualityScores;
84
85 /**
86 * Constructor.
87 */
88 public PROMedSPointCorrespondenceAffineTransformation2DRobustEstimator() {
89 super();
90 stopThreshold = DEFAULT_STOP_THRESHOLD;
91 }
92
93 /**
94 * Constructor with lists of points to be used to estimate an affine 2D
95 * transformation.
96 * Points in the list located at the same position are considered to be
97 * matched. Hence, both lists must have the same size, and their size must
98 * be greater or equal than MINIMUM_SIZE.
99 *
100 * @param inputPoints list of input points to be used to estimate an
101 * affine 2D transformation.
102 * @param outputPoints list of output points to be used to estimate an
103 * affine 2D transformation.
104 * @throws IllegalArgumentException if provided lists of points don't have
105 * the same size or their size is smaller than MINIMUM_SIZE.
106 */
107 public PROMedSPointCorrespondenceAffineTransformation2DRobustEstimator(
108 final List<Point2D> inputPoints, final List<Point2D> outputPoints) {
109 super(inputPoints, outputPoints);
110 stopThreshold = DEFAULT_STOP_THRESHOLD;
111 }
112
113 /**
114 * Constructor.
115 *
116 * @param listener listener to be notified of events such as when estimation
117 * starts, ends or its progress significantly changes.
118 */
119 public PROMedSPointCorrespondenceAffineTransformation2DRobustEstimator(
120 final AffineTransformation2DRobustEstimatorListener listener) {
121 super(listener);
122 stopThreshold = DEFAULT_STOP_THRESHOLD;
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 * stars, 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 PROMedSPointCorrespondenceAffineTransformation2DRobustEstimator(
142 final AffineTransformation2DRobustEstimatorListener listener,
143 final List<Point2D> inputPoints, final List<Point2D> outputPoints) {
144 super(listener, inputPoints, outputPoints);
145 stopThreshold = DEFAULT_STOP_THRESHOLD;
146 }
147
148 /**
149 * Constructor.
150 *
151 * @param qualityScores quality scores corresponding to each pair of matched
152 * points.
153 * @throws IllegalArgumentException if provided quality scores length is
154 * smaller than MINIMUM_SIZE (i.e. 3 samples).
155 */
156 public PROMedSPointCorrespondenceAffineTransformation2DRobustEstimator(final double[] qualityScores) {
157 super();
158 stopThreshold = DEFAULT_STOP_THRESHOLD;
159 internalSetQualityScores(qualityScores);
160 }
161
162 /**
163 * Constructor with lists of points to be used to estimate an affine 2D
164 * transformation.
165 * Points in the list located at the same position are considered to be
166 * matched. Hence, both lists must have the same size, and their size must
167 * be greater or equal than MINIMUM_SIZE.
168 *
169 * @param inputPoints list of input points to be used to estimate an
170 * affine 2D transformation.
171 * @param outputPoints list of output points to be used to estimate an
172 * affine 2D transformation.
173 * @param qualityScores quality scores corresponding to each pair of matched
174 * points.
175 * @throws IllegalArgumentException if provided lists of points and array
176 * of quality scores don't have the same size or their size is smaller than
177 * MINIMUM_SIZE.
178 */
179 public PROMedSPointCorrespondenceAffineTransformation2DRobustEstimator(
180 final List<Point2D> inputPoints, final List<Point2D> outputPoints, final double[] qualityScores) {
181 super(inputPoints, outputPoints);
182
183 if (qualityScores.length != inputPoints.size()) {
184 throw new IllegalArgumentException();
185 }
186
187 stopThreshold = DEFAULT_STOP_THRESHOLD;
188 internalSetQualityScores(qualityScores);
189 }
190
191 /**
192 * Constructor.
193 *
194 * @param listener listener to be notified of events such as when estimation
195 * starts, ends or its progress significantly changes.
196 * @param qualityScores quality scores corresponding to each pair of matched
197 * points.
198 * @throws IllegalArgumentException if provided quality scores length is
199 * smaller than MINIMUM_SIZE (i.e. 3 samples).
200 */
201 public PROMedSPointCorrespondenceAffineTransformation2DRobustEstimator(
202 final AffineTransformation2DRobustEstimatorListener listener, final double[] qualityScores) {
203 super(listener);
204 stopThreshold = DEFAULT_STOP_THRESHOLD;
205 internalSetQualityScores(qualityScores);
206 }
207
208 /**
209 * Constructor with listener and lists of points to be used to estimate an
210 * affine 2D transformation.
211 * Points in the list located at the same position are considered to be
212 * matched. Hence, both lists must have the same size, and their size must
213 * be greater or equal than MINIMUM_SIZE.
214 *
215 * @param listener listener to be notified of events such as when estimation
216 * stars, ends or its progress significantly changes.
217 * @param inputPoints list of input points to be used to estimate an
218 * affine 2D transformation.
219 * @param outputPoints list of output points to be used to estimate an
220 * affine 2D transformation.
221 * @param qualityScores quality scores corresponding to each pair of matched
222 * points.
223 * @throws IllegalArgumentException if provided lists of points don't have
224 * the same size or their size is smaller than MINIMUM_SIZE.
225 */
226 public PROMedSPointCorrespondenceAffineTransformation2DRobustEstimator(
227 final AffineTransformation2DRobustEstimatorListener listener,
228 final List<Point2D> inputPoints, final List<Point2D> outputPoints, final double[] qualityScores) {
229 super(listener, inputPoints, outputPoints);
230
231 if (qualityScores.length != inputPoints.size()) {
232 throw new IllegalArgumentException();
233 }
234
235 stopThreshold = DEFAULT_STOP_THRESHOLD;
236 internalSetQualityScores(qualityScores);
237 }
238
239 /**
240 * Returns threshold to be used to keep the algorithm iterating in case that
241 * best estimated threshold using median of residuals is not small enough.
242 * Once a solution is found that generates a threshold below this value, the
243 * algorithm will stop.
244 * As in LMedS, the stop threshold can be used to prevent the PROMedS
245 * algorithm iterating too many times in cases where samples have a very
246 * similar accuracy.
247 * For instance, in cases where proportion of outliers is very small (close
248 * to 0%), and samples are very accurate (i.e. 1e-6), the algorithm would
249 * iterate for a long time trying to find the best solution when indeed
250 * there is no need to do that if a reasonable threshold has already been
251 * reached.
252 * Because of this behaviour the stop threshold can be set to a value much
253 * lower than the one typically used in RANSAC, and yet the algorithm could
254 * still produce even smaller thresholds in estimated results.
255 *
256 * @return stop threshold to stop the algorithm prematurely when a certain
257 * accuracy has been reached.
258 */
259 public double getStopThreshold() {
260 return stopThreshold;
261 }
262
263 /**
264 * Sets threshold to be used to keep the algorithm iterating in case that
265 * best estimated threshold using median of residuals is not small enough.
266 * Once a solution is found that generates a threshold below this value, the
267 * algorithm will stop.
268 * As in LMedS, the stop threshold can be used to prevent the PROMedS
269 * algorithm iterating too many times in cases where samples have a very
270 * similar accuracy.
271 * For instance, in cases where proportion of outliers is very small (close
272 * to 0%), and samples are very accurate (i.e. 1e-6), the algorithm would
273 * iterate for a long time trying to find the best solution when indeed
274 * there is no need to do that if a reasonable threshold has already been
275 * reached.
276 * Because of this behaviour the stop threshold can be set to a value much
277 * lower than the one typically used in RANSAC, and yet the algorithm could
278 * still produce even smaller thresholds in estimated results.
279 *
280 * @param stopThreshold stop threshold to stop the algorithm prematurely
281 * when a certain accuracy has been reached.
282 * @throws IllegalArgumentException if provided value is zero or negative.
283 * @throws LockedException if robust estimator is locked because an
284 * estimation is already in progress.
285 */
286 public void setStopThreshold(final double stopThreshold) throws LockedException {
287 if (isLocked()) {
288 throw new LockedException();
289 }
290 if (stopThreshold <= MIN_STOP_THRESHOLD) {
291 throw new IllegalArgumentException();
292 }
293
294 this.stopThreshold = stopThreshold;
295 }
296
297 /**
298 * Returns quality scores corresponding to each pair of matched points.
299 * The larger the score value the better the quality of the matching.
300 *
301 * @return quality scores corresponding to each pair of matched points.
302 */
303 @Override
304 public double[] getQualityScores() {
305 return qualityScores;
306 }
307
308 /**
309 * Sets quality scores corresponding to each pair of matched points.
310 * The larger the score value the better the quality of the matching.
311 *
312 * @param qualityScores quality scores corresponding to each pair of matched
313 * points.
314 * @throws LockedException if robust estimator is locked because an
315 * estimation is already in progress.
316 * @throws IllegalArgumentException if provided quality scores length is
317 * smaller than MINIMUM_SIZE (i.e. 3 samples).
318 */
319 @Override
320 public void setQualityScores(final double[] qualityScores) throws LockedException {
321 if (isLocked()) {
322 throw new LockedException();
323 }
324 internalSetQualityScores(qualityScores);
325 }
326
327 /**
328 * Indicates if estimator is ready to start the affine 2D transformation
329 * estimation.
330 * This is true when input data (i.e. lists of matched points and quality
331 * scores) are provided and a minimum of MINIMUM_SIZE points are available.
332 *
333 * @return true if estimator is ready, false otherwise.
334 */
335 @Override
336 public boolean isReady() {
337 return super.isReady() && qualityScores != null && qualityScores.length == inputPoints.size();
338 }
339
340 /**
341 * Estimates an affine 2D transformation using a robust estimator and
342 * the best set of matched 2D point correspondences found using the robust
343 * estimator.
344 *
345 * @return an affine 2D transformation.
346 * @throws LockedException if robust estimator is locked because an
347 * estimation is already in progress.
348 * @throws NotReadyException if provided input data is not enough to start
349 * the estimation.
350 * @throws RobustEstimatorException if estimation fails for any reason
351 * (i.e. numerical instability, no solution available, etc).
352 */
353 @Override
354 public AffineTransformation2D estimate() throws LockedException, NotReadyException, RobustEstimatorException {
355 if (isLocked()) {
356 throw new LockedException();
357 }
358 if (!isReady()) {
359 throw new NotReadyException();
360 }
361
362 final var innerEstimator = new PROMedSRobustEstimator<>(
363 new PROMedSRobustEstimatorListener<AffineTransformation2D>() {
364
365 // point to be reused when computing residuals
366 private final Point2D testPoint = Point2D.create(CoordinatesType.HOMOGENEOUS_COORDINATES);
367
368 @Override
369 public double getThreshold() {
370 return stopThreshold;
371 }
372
373 @Override
374 public int getTotalSamples() {
375 return inputPoints.size();
376 }
377
378 @Override
379 public int getSubsetSize() {
380 return AffineTransformation2DRobustEstimator.MINIMUM_SIZE;
381 }
382
383 @Override
384 public void estimatePreliminarSolutions(
385 final int[] samplesIndices, final List<AffineTransformation2D> solutions) {
386 final var inputPoint1 = inputPoints.get(samplesIndices[0]);
387 final var inputPoint2 = inputPoints.get(samplesIndices[1]);
388 final var inputPoint3 = inputPoints.get(samplesIndices[2]);
389
390 final var outputPoint1 = outputPoints.get(samplesIndices[0]);
391 final var outputPoint2 = outputPoints.get(samplesIndices[1]);
392 final var outputPoint3 = outputPoints.get(samplesIndices[2]);
393
394 try {
395 final var transformation = new AffineTransformation2D(inputPoint1, inputPoint2, inputPoint3,
396 outputPoint1, outputPoint2, outputPoint3);
397 solutions.add(transformation);
398 } catch (final CoincidentPointsException e) {
399 // if points are coincident, no solution is added
400 }
401 }
402
403 @Override
404 public double computeResidual(final AffineTransformation2D currentEstimation, final int i) {
405 final var inputPoint = inputPoints.get(i);
406 final var outputPoint = outputPoints.get(i);
407
408 // transform input point and store result in mTestPoint
409 currentEstimation.transform(inputPoint, testPoint);
410
411 return outputPoint.distanceTo(testPoint);
412 }
413
414 @Override
415 public boolean isReady() {
416 return PROMedSPointCorrespondenceAffineTransformation2DRobustEstimator.this.isReady();
417 }
418
419 @Override
420 public void onEstimateStart(final RobustEstimator<AffineTransformation2D> estimator) {
421 if (mListener != null) {
422 mListener.onEstimateStart(
423 PROMedSPointCorrespondenceAffineTransformation2DRobustEstimator.this);
424 }
425 }
426
427 @Override
428 public void onEstimateEnd(final RobustEstimator<AffineTransformation2D> estimator) {
429 if (mListener != null) {
430 mListener.onEstimateEnd(
431 PROMedSPointCorrespondenceAffineTransformation2DRobustEstimator.this);
432 }
433 }
434
435 @Override
436 public void onEstimateNextIteration(
437 final RobustEstimator<AffineTransformation2D> estimator, final int iteration) {
438 if (mListener != null) {
439 mListener.onEstimateNextIteration(
440 PROMedSPointCorrespondenceAffineTransformation2DRobustEstimator.this,
441 iteration);
442 }
443 }
444
445 @Override
446 public void onEstimateProgressChange(
447 final RobustEstimator<AffineTransformation2D> estimator, final float progress) {
448 if (mListener != null) {
449 mListener.onEstimateProgressChange(
450 PROMedSPointCorrespondenceAffineTransformation2DRobustEstimator.this,
451 progress);
452 }
453 }
454
455 @Override
456 public double[] getQualityScores() {
457 return qualityScores;
458 }
459 });
460
461 try {
462 locked = true;
463 inliersData = null;
464 innerEstimator.setConfidence(confidence);
465 innerEstimator.setMaxIterations(maxIterations);
466 innerEstimator.setProgressDelta(progressDelta);
467 final var transformation = innerEstimator.estimate();
468 inliersData = innerEstimator.getInliersData();
469 return attemptRefine(transformation);
470 } catch (final com.irurueta.numerical.LockedException e) {
471 throw new LockedException(e);
472 } catch (final com.irurueta.numerical.NotReadyException e) {
473 throw new NotReadyException(e);
474 } finally {
475 locked = false;
476 }
477 }
478
479 /**
480 * Returns method being used for robust estimation.
481 *
482 * @return method being used for robust estimation.
483 */
484 @Override
485 public RobustEstimatorMethod getMethod() {
486 return RobustEstimatorMethod.PROMEDS;
487 }
488
489 /**
490 * Gets standard deviation used for Levenberg-Marquardt fitting during
491 * refinement.
492 * Returned value gives an indication of how much variance each residual
493 * has.
494 * Typically, this value is related to the threshold used on each robust
495 * estimation, since residuals of found inliers are within the range of such
496 * threshold.
497 *
498 * @return standard deviation used for refinement.
499 */
500 @Override
501 protected double getRefinementStandardDeviation() {
502 final var inliersData = (PROMedSRobustEstimator.PROMedSInliersData) getInliersData();
503
504 // avoid setting a threshold too strict
505 final var threshold = inliersData.getEstimatedThreshold();
506 return Math.max(threshold, stopThreshold);
507 }
508
509 /**
510 * Sets quality scores corresponding to each pair of matched points.
511 * This method is used internally and does not check whether instance is
512 * locked or not.
513 *
514 * @param qualityScores quality scores to be set.
515 * @throws IllegalArgumentException if provided quality scores length is
516 * smaller than MINIMUM_SIZE.
517 */
518 private void internalSetQualityScores(final double[] qualityScores) {
519 if (qualityScores.length < MINIMUM_SIZE) {
520 throw new IllegalArgumentException();
521 }
522
523 this.qualityScores = qualityScores;
524 }
525 }