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.algebra.AlgebraException;
19 import com.irurueta.geometry.CoincidentLinesException;
20 import com.irurueta.geometry.Line2D;
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 lines using PROMedS algorithm.
33 */
34 @SuppressWarnings("DuplicatedCode")
35 public class PROMedSLineCorrespondenceProjectiveTransformation2DRobustEstimator
36 extends LineCorrespondenceProjectiveTransformation2DRobustEstimator {
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 = 1e-6;
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 lines.
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 PROMedSLineCorrespondenceProjectiveTransformation2DRobustEstimator() {
89 super();
90 stopThreshold = DEFAULT_STOP_THRESHOLD;
91 }
92
93 /**
94 * Constructor with lists of lines to be used to estimate a projective 2D
95 * transformation.
96 * Lines 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 inputLines list of input lines to be used to estimate a projective
101 * 2D transformation.
102 * @param outputLines list of output lines to be used to estimate a
103 * projective 2D transformation.
104 * @throws IllegalArgumentException if provided lists of lines don't have
105 * the same size or their size is smaller than MINIMUM_SIZE.
106 */
107 public PROMedSLineCorrespondenceProjectiveTransformation2DRobustEstimator(
108 final List<Line2D> inputLines, final List<Line2D> outputLines) {
109 super(inputLines, outputLines);
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 PROMedSLineCorrespondenceProjectiveTransformation2DRobustEstimator(
120 final ProjectiveTransformation2DRobustEstimatorListener listener) {
121 super(listener);
122 stopThreshold = DEFAULT_STOP_THRESHOLD;
123 }
124
125 /**
126 * Constructor with listener and lists of lines to be used to estimate a
127 * projective 2D transformation.
128 * Lines 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 inputLines list of input lines to be used to estimate a projective
135 * 2D transformation.
136 * @param outputLines list of output lines to be used to estimate a
137 * projective 2D transformation.
138 * @throws IllegalArgumentException if provided lists of lines don't have
139 * the same size or their size is smaller than MINIMUM_SIZE.
140 */
141 public PROMedSLineCorrespondenceProjectiveTransformation2DRobustEstimator(
142 final ProjectiveTransformation2DRobustEstimatorListener listener,
143 final List<Line2D> inputLines, final List<Line2D> outputLines) {
144 super(listener, inputLines, outputLines);
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 PROMedSLineCorrespondenceProjectiveTransformation2DRobustEstimator(final double[] qualityScores) {
157 super();
158 stopThreshold = DEFAULT_STOP_THRESHOLD;
159 internalSetQualityScores(qualityScores);
160 }
161
162 /**
163 * Constructor with lists of lines to be used to estimate a projective 2D
164 * transformation.
165 * Lines 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 inputLines list of input lines to be used to estimate a projective
170 * 2D transformation.
171 * @param outputLines list of output lines to be used to estimate a
172 * projective 2D transformation.
173 * @param qualityScores quality scores corresponding to each pair of matched
174 * lines.
175 * @throws IllegalArgumentException if provided lists of lines and array
176 * of quality scores don't have the same size or their size is smaller than
177 * MINIMUM_SIZE.
178 */
179 public PROMedSLineCorrespondenceProjectiveTransformation2DRobustEstimator(
180 final List<Line2D> inputLines, final List<Line2D> outputLines, final double[] qualityScores) {
181 super(inputLines, outputLines);
182
183 if (qualityScores.length != inputLines.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 * lines.
198 * @throws IllegalArgumentException if provided quality scores length is
199 * smaller than MINIMUM_SIZE (i.e. 3 samples).
200 */
201 public PROMedSLineCorrespondenceProjectiveTransformation2DRobustEstimator(
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 lines to be used to estimate a
210 * projective 2D transformation.
211 * Lines 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 inputLines list of input lines to be used to estimate a projective
218 * 2D transformation.
219 * @param outputLines list of output lines to be used to estimate a
220 * projective 2D transformation.
221 * @param qualityScores quality scores corresponding to each pair of matched
222 * lines.
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 PROMedSLineCorrespondenceProjectiveTransformation2DRobustEstimator(
227 final ProjectiveTransformation2DRobustEstimatorListener listener,
228 final List<Line2D> inputLines, final List<Line2D> outputLines, final double[] qualityScores) {
229 super(listener, inputLines, outputLines);
230
231 if (qualityScores.length != inputLines.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 * The stop threshold can be used to prevent the LMedS algorithm iterating
245 * too many times in cases where samples have a very similar accuracy.
246 * For instance, in cases where proportion of outliers is very small (close
247 * to 0%), and samples are very accurate (i.e. 1e-6), the algorithm would
248 * iterate for a long time trying to find the best solution when indeed
249 * there is no need to do that if a reasonable threshold has already been
250 * reached.
251 * Because of this behaviour the stop threshold can be set to a value much
252 * lower than the one typically used in RANSAC, and yet the algorithm could
253 * still produce even smaller thresholds in estimated results.
254 *
255 * @return stop threshold to stop the algorithm prematurely when a certain
256 * accuracy has been reached.
257 */
258 public double getStopThreshold() {
259 return stopThreshold;
260 }
261
262 /**
263 * Sets threshold to be used to keep the algorithm iterating in case that
264 * best estimated threshold using median of residuals is not small enough.
265 * Once a solution is found that generates a threshold below this value, the
266 * algorithm will stop.
267 * The stop threshold can be used to prevent the LMedS algorithm iterating
268 * too many times in cases where samples have a very similar accuracy.
269 * For instance, in cases where proportion of outliers is very small (close
270 * to 0%), and samples are very accurate (i.e. 1e-6), the algorithm would
271 * iterate for a long time trying to find the best solution when indeed
272 * there is no need to do that if a reasonable threshold has already been
273 * reached.
274 * Because of this behaviour the stop threshold can be set to a value much
275 * lower than the one typically used in RANSAC, and yet the algorithm could
276 * still produce even smaller thresholds in estimated results.
277 *
278 * @param stopThreshold stop threshold to stop the algorithm prematurely
279 * when a certain accuracy has been reached.
280 * @throws IllegalArgumentException if provided value is zero or negative.
281 * @throws LockedException if robust estimator is locked because an
282 * estimation is already in progress.
283 */
284 public void setStopThreshold(final double stopThreshold) throws LockedException {
285 if (isLocked()) {
286 throw new LockedException();
287 }
288 if (stopThreshold <= MIN_STOP_THRESHOLD) {
289 throw new IllegalArgumentException();
290 }
291
292 this.stopThreshold = stopThreshold;
293 }
294
295 /**
296 * Returns quality scores corresponding to each pair of matched lines.
297 * The larger the score value the better the quality of the matching.
298 *
299 * @return quality scores corresponding to each pair of matched lines.
300 */
301 @Override
302 public double[] getQualityScores() {
303 return qualityScores;
304 }
305
306 /**
307 * Sets quality scores corresponding to each pair of matched lines.
308 * The larger the score value the better the quality of the matching.
309 *
310 * @param qualityScores quality scores corresponding to each pair of matched
311 * lines.
312 * @throws LockedException if robust estimator is locked because an
313 * estimation is already in progress.
314 * @throws IllegalArgumentException if provided quality scores length is
315 * smaller than MINIMUM_SIZE (i.e. 3 samples).
316 */
317 @Override
318 public void setQualityScores(final double[] qualityScores) throws LockedException {
319 if (isLocked()) {
320 throw new LockedException();
321 }
322 internalSetQualityScores(qualityScores);
323 }
324
325 /**
326 * Indicates if estimator is ready to start the affine 2D transformation
327 * estimation.
328 * This is true when input data (i.e. lists of matched lines and quality
329 * scores) are provided and a minimum of MINIMUM_SIZE lines are available.
330 *
331 * @return true if estimator is ready, false otherwise.
332 */
333 @Override
334 public boolean isReady() {
335 return super.isReady() && qualityScores != null && qualityScores.length == inputLines.size();
336 }
337
338 /**
339 * Estimates a projective 2D transformation using a robust estimator and
340 * the best set of matched 2D lines correspondences found using the robust
341 * estimator.
342 *
343 * @return a projective 2D transformation.
344 * @throws LockedException if robust estimator is locked because an
345 * estimation is already in progress.
346 * @throws NotReadyException if provided input data is not enough to start
347 * the estimation.
348 * @throws RobustEstimatorException if estimation fails for any reason
349 * (i.e. numerical instability, no solution available, etc).
350 */
351 @Override
352 public ProjectiveTransformation2D estimate() throws LockedException, NotReadyException, RobustEstimatorException {
353 if (isLocked()) {
354 throw new LockedException();
355 }
356 if (!isReady()) {
357 throw new NotReadyException();
358 }
359
360 final var innerEstimator = new PROMedSRobustEstimator<>(
361 new PROMedSRobustEstimatorListener<ProjectiveTransformation2D>() {
362
363 // line to be reused when computing residuals
364 private final Line2D testLine = new Line2D();
365
366 @Override
367 public double getThreshold() {
368 return stopThreshold;
369 }
370
371 @Override
372 public int getTotalSamples() {
373 return inputLines.size();
374 }
375
376 @Override
377 public int getSubsetSize() {
378 return ProjectiveTransformation2DRobustEstimator.MINIMUM_SIZE;
379 }
380
381 @Override
382 public void estimatePreliminarSolutions(
383 final int[] samplesIndices, final List<ProjectiveTransformation2D> solutions) {
384 final var inputLine1 = inputLines.get(samplesIndices[0]);
385 final var inputLine2 = inputLines.get(samplesIndices[1]);
386 final var inputLine3 = inputLines.get(samplesIndices[2]);
387 final var inputLine4 = inputLines.get(samplesIndices[3]);
388
389 final var outputLine1 = outputLines.get(samplesIndices[0]);
390 final var outputLine2 = outputLines.get(samplesIndices[1]);
391 final var outputLine3 = outputLines.get(samplesIndices[2]);
392 final var outputLine4 = outputLines.get(samplesIndices[3]);
393
394 try {
395 final var transformation = new ProjectiveTransformation2D(inputLine1, inputLine2,
396 inputLine3, inputLine4, outputLine1, outputLine2, outputLine3, outputLine4);
397 solutions.add(transformation);
398 } catch (final CoincidentLinesException e) {
399 // if lines are coincident, no solution is added
400 }
401 }
402
403 @Override
404 public double computeResidual(final ProjectiveTransformation2D currentEstimation, final int i) {
405 final var inputLine = inputLines.get(i);
406 final var outputLine = outputLines.get(i);
407
408 // transform input line and store result in mTestLine
409 try {
410 currentEstimation.transform(inputLine, testLine);
411
412 return getResidual(outputLine, testLine);
413 } catch (final AlgebraException e) {
414 // this happens when internal matrix of affine transformation
415 // cannot be reverse (i.e. transformation is not well-defined,
416 // numerical instabilities, etc.)
417 return Double.MAX_VALUE;
418 }
419 }
420
421 @Override
422 public boolean isReady() {
423 return PROMedSLineCorrespondenceProjectiveTransformation2DRobustEstimator.this.isReady();
424 }
425
426 @Override
427 public void onEstimateStart(final RobustEstimator<ProjectiveTransformation2D> estimator) {
428 if (listener != null) {
429 listener.onEstimateStart(
430 PROMedSLineCorrespondenceProjectiveTransformation2DRobustEstimator.this);
431 }
432 }
433
434 @Override
435 public void onEstimateEnd(final RobustEstimator<ProjectiveTransformation2D> estimator) {
436 if (listener != null) {
437 listener.onEstimateEnd(
438 PROMedSLineCorrespondenceProjectiveTransformation2DRobustEstimator.this);
439 }
440 }
441
442 @Override
443 public void onEstimateNextIteration(
444 final RobustEstimator<ProjectiveTransformation2D> estimator, final int iteration) {
445 if (listener != null) {
446 listener.onEstimateNextIteration(
447 PROMedSLineCorrespondenceProjectiveTransformation2DRobustEstimator.this,
448 iteration);
449 }
450 }
451
452 @Override
453 public void onEstimateProgressChange(
454 final RobustEstimator<ProjectiveTransformation2D> estimator, final float progress) {
455 if (listener != null) {
456 listener.onEstimateProgressChange(
457 PROMedSLineCorrespondenceProjectiveTransformation2DRobustEstimator.this,
458 progress);
459 }
460 }
461
462 @Override
463 public double[] getQualityScores() {
464 return qualityScores;
465 }
466 });
467
468 try {
469 locked = true;
470 inliersData = null;
471 innerEstimator.setConfidence(confidence);
472 innerEstimator.setMaxIterations(maxIterations);
473 innerEstimator.setProgressDelta(progressDelta);
474 final var transformation = innerEstimator.estimate();
475 inliersData = innerEstimator.getInliersData();
476 return attemptRefine(transformation);
477 } catch (final com.irurueta.numerical.LockedException e) {
478 throw new LockedException(e);
479 } catch (final com.irurueta.numerical.NotReadyException e) {
480 throw new NotReadyException(e);
481 } finally {
482 locked = false;
483 }
484 }
485
486 /**
487 * Returns method being used for robust estimation.
488 *
489 * @return method being used for robust estimation.
490 */
491 @Override
492 public RobustEstimatorMethod getMethod() {
493 return RobustEstimatorMethod.PROMEDS;
494 }
495
496 /**
497 * Gets standard deviation used for Levenberg-Marquardt fitting during
498 * refinement.
499 * Returned value gives an indication of how much variance each residual
500 * has.
501 * Typically, this value is related to the threshold used on each robust
502 * estimation, since residuals of found inliers are within the range of such
503 * threshold.
504 *
505 * @return standard deviation used for refinement.
506 */
507 @Override
508 protected double getRefinementStandardDeviation() {
509 final var inliersData = (PROMedSRobustEstimator.PROMedSInliersData) getInliersData();
510
511 // avoid setting a threshold too strict
512 final var threshold = inliersData.getEstimatedThreshold();
513 return Math.max(threshold, stopThreshold);
514 }
515
516 /**
517 * Sets quality scores corresponding to each pair of matched lines.
518 * This method is used internally and does not check whether instance is
519 * locked or not.
520 *
521 * @param qualityScores quality scores to be set.
522 * @throws IllegalArgumentException if provided quality scores length is
523 * smaller than MINIMUM_SIZE.
524 */
525 private void internalSetQualityScores(final double[] qualityScores) {
526 if (qualityScores.length < MINIMUM_SIZE) {
527 throw new IllegalArgumentException();
528 }
529
530 this.qualityScores = qualityScores;
531 }
532 }