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