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.CoincidentPointsException;
19 import com.irurueta.geometry.CoordinatesType;
20 import com.irurueta.geometry.Point2D;
21 import com.irurueta.geometry.ProjectiveTransformation2D;
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 projective 2D transformation for provided collections of
32 * matched 2D points using PROMedS algorithm.
33 */
34 @SuppressWarnings("DuplicatedCode")
35 public class PROMedSPointCorrespondenceProjectiveTransformation2DRobustEstimator
36 extends PointCorrespondenceProjectiveTransformation2DRobustEstimator {
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 PROMedSPointCorrespondenceProjectiveTransformation2DRobustEstimator() {
89 super();
90 stopThreshold = DEFAULT_STOP_THRESHOLD;
91 }
92
93 /**
94 * Constructor with lists of points to be used to estimate a projective 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 a
101 * projective 2D transformation.
102 * @param outputPoints list of output points to be used to estimate a
103 * projective 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 PROMedSPointCorrespondenceProjectiveTransformation2DRobustEstimator(
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 PROMedSPointCorrespondenceProjectiveTransformation2DRobustEstimator(
120 final ProjectiveTransformation2DRobustEstimatorListener 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 a
127 * projective 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 a
135 * projective 2D transformation.
136 * @param outputPoints list of output points to be used to estimate a
137 * projective 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 PROMedSPointCorrespondenceProjectiveTransformation2DRobustEstimator(
142 final ProjectiveTransformation2DRobustEstimatorListener 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 PROMedSPointCorrespondenceProjectiveTransformation2DRobustEstimator(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 a projective 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 a
170 * projective 2D transformation.
171 * @param outputPoints list of output points to be used to estimate a
172 * projective 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 PROMedSPointCorrespondenceProjectiveTransformation2DRobustEstimator(
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 PROMedSPointCorrespondenceProjectiveTransformation2DRobustEstimator(
202 final ProjectiveTransformation2DRobustEstimatorListener 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 a
210 * projective 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 * starts, ends or its progress significantly changes.
217 * @param inputPoints list of input points to be used to estimate a
218 * projective 2D transformation.
219 * @param outputPoints list of output points to be used to estimate a
220 * projective 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 PROMedSPointCorrespondenceProjectiveTransformation2DRobustEstimator(
227 final ProjectiveTransformation2DRobustEstimatorListener 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 a projective 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 projective 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 ProjectiveTransformation2D 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<ProjectiveTransformation2D>() {
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 ProjectiveTransformation2DRobustEstimator.MINIMUM_SIZE;
381 }
382
383 @Override
384 public void estimatePreliminarSolutions(
385 final int[] samplesIndices, final List<ProjectiveTransformation2D> 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 final var inputPoint4 = inputPoints.get(samplesIndices[3]);
390
391 final var outputPoint1 = outputPoints.get(samplesIndices[0]);
392 final var outputPoint2 = outputPoints.get(samplesIndices[1]);
393 final var outputPoint3 = outputPoints.get(samplesIndices[2]);
394 final var outputPoint4 = outputPoints.get(samplesIndices[3]);
395
396 try {
397 final var transformation = new ProjectiveTransformation2D(inputPoint1, inputPoint2,
398 inputPoint3, inputPoint4, outputPoint1, outputPoint2, outputPoint3, outputPoint4);
399 solutions.add(transformation);
400 } catch (final CoincidentPointsException e) {
401 // if points are coincident, no solution is added
402 }
403 }
404
405 @Override
406 public double computeResidual(final ProjectiveTransformation2D currentEstimation, final int i) {
407 final var inputPoint = inputPoints.get(i);
408 final var outputPoint = outputPoints.get(i);
409
410 // transform input point and store result in mTestPoint
411 currentEstimation.transform(inputPoint, testPoint);
412
413 return outputPoint.distanceTo(testPoint);
414 }
415
416 @Override
417 public boolean isReady() {
418 return PROMedSPointCorrespondenceProjectiveTransformation2DRobustEstimator.this.isReady();
419 }
420
421 @Override
422 public void onEstimateStart(final RobustEstimator<ProjectiveTransformation2D> estimator) {
423 if (listener != null) {
424 listener.onEstimateStart(
425 PROMedSPointCorrespondenceProjectiveTransformation2DRobustEstimator.this);
426 }
427 }
428
429 @Override
430 public void onEstimateEnd(final RobustEstimator<ProjectiveTransformation2D> estimator) {
431 if (listener != null) {
432 listener.onEstimateEnd(
433 PROMedSPointCorrespondenceProjectiveTransformation2DRobustEstimator.this);
434 }
435 }
436
437 @Override
438 public void onEstimateNextIteration(
439 final RobustEstimator<ProjectiveTransformation2D> estimator, final int iteration) {
440 if (listener != null) {
441 listener.onEstimateNextIteration(
442 PROMedSPointCorrespondenceProjectiveTransformation2DRobustEstimator.this,
443 iteration);
444 }
445 }
446
447 @Override
448 public void onEstimateProgressChange(
449 final RobustEstimator<ProjectiveTransformation2D> estimator, final float progress) {
450 if (listener != null) {
451 listener.onEstimateProgressChange(
452 PROMedSPointCorrespondenceProjectiveTransformation2DRobustEstimator.this,
453 progress);
454 }
455 }
456
457 @Override
458 public double[] getQualityScores() {
459 return qualityScores;
460 }
461 });
462
463 try {
464 locked = true;
465 inliersData = null;
466 innerEstimator.setConfidence(confidence);
467 innerEstimator.setMaxIterations(maxIterations);
468 innerEstimator.setProgressDelta(progressDelta);
469 final var transformation = innerEstimator.estimate();
470 inliersData = innerEstimator.getInliersData();
471 return attemptRefine(transformation);
472 } catch (final com.irurueta.numerical.LockedException e) {
473 throw new LockedException(e);
474 } catch (final com.irurueta.numerical.NotReadyException e) {
475 throw new NotReadyException(e);
476 } finally {
477 locked = false;
478 }
479 }
480
481 /**
482 * Returns method being used for robust estimation.
483 *
484 * @return method being used for robust estimation.
485 */
486 @Override
487 public RobustEstimatorMethod getMethod() {
488 return RobustEstimatorMethod.PROMEDS;
489 }
490
491 /**
492 * Gets standard deviation used for Levenberg-Marquardt fitting during
493 * refinement.
494 * Returned value gives an indication of how much variance each residual
495 * has.
496 * Typically, this value is related to the threshold used on each robust
497 * estimation, since residuals of found inliers are within the range of such
498 * threshold.
499 *
500 * @return standard deviation used for refinement.
501 */
502 @Override
503 protected double getRefinementStandardDeviation() {
504 final var inliersData = (PROMedSRobustEstimator.PROMedSInliersData) getInliersData();
505
506 // avoid setting a threshold too strict
507 final var threshold = inliersData.getEstimatedThreshold();
508 return Math.max(threshold, stopThreshold);
509 }
510
511 /**
512 * Sets quality scores corresponding to each pair of matched points.
513 * This method is used internally and does not check whether instance is
514 * locked or not.
515 *
516 * @param qualityScores quality scores to be set.
517 * @throws IllegalArgumentException if provided quality scores length is
518 * smaller than MINIMUM_SIZE.
519 */
520 private void internalSetQualityScores(final double[] qualityScores) {
521 if (qualityScores.length < MINIMUM_SIZE) {
522 throw new IllegalArgumentException();
523 }
524
525 this.qualityScores = qualityScores;
526 }
527 }