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.ar.calibration.estimators; 17 18 import com.irurueta.ar.calibration.RadialDistortion; 19 import com.irurueta.geometry.CoordinatesType; 20 import com.irurueta.geometry.Point2D; 21 import com.irurueta.geometry.estimators.LockedException; 22 import com.irurueta.geometry.estimators.NotReadyException; 23 import com.irurueta.numerical.robust.PROMedSRobustEstimator; 24 import com.irurueta.numerical.robust.PROMedSRobustEstimatorListener; 25 import com.irurueta.numerical.robust.RobustEstimator; 26 import com.irurueta.numerical.robust.RobustEstimatorException; 27 import com.irurueta.numerical.robust.RobustEstimatorMethod; 28 29 import java.util.ArrayList; 30 import java.util.List; 31 import java.util.logging.Level; 32 import java.util.logging.Logger; 33 34 /** 35 * Finds the best radial distortion for provided collections of 2D points using 36 * PROMedS algorithm. 37 */ 38 public class PROMedSRadialDistortionRobustEstimator extends RadialDistortionRobustEstimator { 39 40 /** 41 * Default value to be used for stop threshold. Stop threshold can be used 42 * to keep the algorithm iterating in case that best estimated threshold 43 * using median of residuals is not small enough. Once a solution is found 44 * that generates a threshold below this value, the algorithm will stop. 45 * The stop threshold can be used to prevent the LMedS algorithm iterating 46 * too many times in cases where samples have a very similar accuracy. 47 * For instance, in cases where proportion of outliers is very small (close 48 * to 0%), and samples are very accurate (i.e. 1e-6), the algorithm would 49 * iterate for a long time trying to find the best solution when indeed 50 * there is no need to do that if a reasonable threshold has already been 51 * reached. 52 * Because of this behaviour the stop threshold can be set to a value much 53 * lower than the one typically used in RANSAC, and yet the algorithm could 54 * still produce even smaller thresholds in estimated results. 55 */ 56 public static final double DEFAULT_STOP_THRESHOLD = 1e-3; 57 58 /** 59 * Minimum allowed stop threshold value. 60 */ 61 public static final double MIN_STOP_THRESHOLD = 0.0; 62 63 /** 64 * Threshold to be used to keep the algorithm iterating in case that best 65 * estimated threshold using median of residuals is not small enough. Once 66 * a solution is found that generates a threshold below this value, the 67 * algorithm will stop. 68 * The stop threshold can be used to prevent the LMedS algorithm iterating 69 * too many times in cases where samples have a very similar accuracy. 70 * For instance, in cases where proportion of outliers is very small (close 71 * to 0%), and samples are very accurate (i.e. 1e-6), the algorithm would 72 * iterate for a long time trying to find the best solution when indeed 73 * there is no need to do that if a reasonable threshold has already been 74 * reached. 75 * Because of this behaviour the stop threshold can be set to a value much 76 * lower than the one typically used in RANSAC, and yet the algorithm could 77 * still produce even smaller thresholds in estimated results. 78 */ 79 private double stopThreshold; 80 81 /** 82 * Quality scores corresponding to each provided point. 83 * The larger the score value the better the quality of the sample. 84 */ 85 private double[] qualityScores; 86 87 /** 88 * Constructor. 89 */ 90 public PROMedSRadialDistortionRobustEstimator() { 91 super(); 92 stopThreshold = DEFAULT_STOP_THRESHOLD; 93 } 94 95 /** 96 * Constructor. 97 * 98 * @param listener listener to be notified of events such as when 99 * estimation starts, ends or its progress significantly changes. 100 */ 101 public PROMedSRadialDistortionRobustEstimator(final RadialDistortionRobustEstimatorListener listener) { 102 super(listener); 103 stopThreshold = DEFAULT_STOP_THRESHOLD; 104 } 105 106 /** 107 * Constructor. 108 * 109 * @param distortedPoints list of distorted points. Distorted points are 110 * obtained after radial distortion is applied to an undistorted point. 111 * @param undistortedPoints list of undistorted points. 112 * @throws IllegalArgumentException if provided lists of points don't have 113 * the same size or their size is smaller than MIN_NUMBER_OF_POINTS. 114 */ 115 public PROMedSRadialDistortionRobustEstimator( 116 final List<Point2D> distortedPoints, final List<Point2D> undistortedPoints) { 117 super(distortedPoints, undistortedPoints); 118 stopThreshold = DEFAULT_STOP_THRESHOLD; 119 } 120 121 /** 122 * Constructor. 123 * 124 * @param distortedPoints list of distorted points. Distorted points are 125 * obtained after radial distortion is applied to an undistorted point. 126 * @param undistortedPoints list of undistorted points. 127 * @param listener listener to be notified of events such as when 128 * estimation starts, ends or its progress significantly changes. 129 * @throws IllegalArgumentException if provided lists of points don't have 130 * the same size or their size is smaller than MIN_NUMBER_OF_POINTS. 131 */ 132 public PROMedSRadialDistortionRobustEstimator( 133 final List<Point2D> distortedPoints, final List<Point2D> undistortedPoints, 134 final RadialDistortionRobustEstimatorListener listener) { 135 super(distortedPoints, undistortedPoints, listener); 136 stopThreshold = DEFAULT_STOP_THRESHOLD; 137 } 138 139 /** 140 * Constructor. 141 * 142 * @param distortedPoints list of distorted points. Distorted points are 143 * obtained after radial distortion is applied to an undistorted point. 144 * @param undistortedPoints list of undistorted points. 145 * @param distortionCenter radial distortion center. If null it is assumed 146 * to be the origin of coordinates, otherwise this is typically equal to 147 * the camera principal point. 148 * @throws IllegalArgumentException if provided lists of points don't have 149 * the same size or their size is smaller than MIN_NUMBER_OF_POINTS. 150 */ 151 public PROMedSRadialDistortionRobustEstimator( 152 final List<Point2D> distortedPoints, final List<Point2D> undistortedPoints, 153 final Point2D distortionCenter) { 154 super(distortedPoints, undistortedPoints, distortionCenter); 155 stopThreshold = DEFAULT_STOP_THRESHOLD; 156 } 157 158 /** 159 * Constructor. 160 * 161 * @param distortedPoints list of distorted points. Distorted points are 162 * obtained after radial distortion is applied to an undistorted point. 163 * @param undistortedPoints list of undistorted points. 164 * @param distortionCenter radial distortion center. If null it is assumed 165 * to be the origin of coordinates, otherwise this is typically equal to 166 * the camera principal point. 167 * @param listener listener to be notified of events such as when 168 * estimation starts, ends or its progress significantly changes. 169 * @throws IllegalArgumentException if provided lists of points don't have 170 * the same size or their size is smaller than MIN_NUMBER_OF_POINTS. 171 */ 172 public PROMedSRadialDistortionRobustEstimator(final List<Point2D> distortedPoints, 173 final List<Point2D> undistortedPoints, 174 final Point2D distortionCenter, 175 final RadialDistortionRobustEstimatorListener listener) { 176 super(distortedPoints, undistortedPoints, distortionCenter, listener); 177 stopThreshold = DEFAULT_STOP_THRESHOLD; 178 } 179 180 /** 181 * Constructor. 182 * 183 * @param qualityScores quality scores corresponding to each provided point. 184 * @throws IllegalArgumentException if provided quality scores length is 185 * smaller than required size (i.e. 2 points). 186 */ 187 public PROMedSRadialDistortionRobustEstimator(final double[] qualityScores) { 188 this(); 189 internalSetQualityScores(qualityScores); 190 } 191 192 /** 193 * Constructor. 194 * 195 * @param qualityScores quality scores corresponding to each provided point. 196 * @param listener listener to be notified of events such as when 197 * estimation starts, ends or its progress significantly changes. 198 * @throws IllegalArgumentException if provided quality scores length is 199 * smaller than required size (i.e. 2 points). 200 */ 201 public PROMedSRadialDistortionRobustEstimator( 202 final double[] qualityScores, final RadialDistortionRobustEstimatorListener listener) { 203 this(listener); 204 internalSetQualityScores(qualityScores); 205 } 206 207 /** 208 * Constructor. 209 * 210 * @param distortedPoints list of distorted points. Distorted points are 211 * obtained after radial distortion is applied to an undistorted point. 212 * @param undistortedPoints list of undistorted points. 213 * @param qualityScores quality scores corresponding to each provided point. 214 * @throws IllegalArgumentException if provided lists of points and quality 215 * scores don't have the same size or their size is smaller than 216 * MIN_NUMBER_OF_POINTS (i.e. 2 points). 217 */ 218 public PROMedSRadialDistortionRobustEstimator( 219 final List<Point2D> distortedPoints, final List<Point2D> undistortedPoints, final double[] qualityScores) { 220 this(distortedPoints, undistortedPoints); 221 internalSetQualityScores(qualityScores); 222 } 223 224 /** 225 * Constructor. 226 * 227 * @param distortedPoints list of distorted points. Distorted points are 228 * obtained after radial distortion is applied to an undistorted point. 229 * @param undistortedPoints list of undistorted points. 230 * @param qualityScores quality scores corresponding to each provided point. 231 * @param listener listener to be notified of events such as when 232 * estimation starts, ends or its progress significantly changes. 233 * @throws IllegalArgumentException if provided lists of points or quality 234 * scores don't have the same size or their size is smaller than 235 * MIN_NUMBER_OF_POINTS (i.e. 2 points). 236 */ 237 public PROMedSRadialDistortionRobustEstimator(final List<Point2D> distortedPoints, 238 final List<Point2D> undistortedPoints, 239 final double[] qualityScores, 240 final RadialDistortionRobustEstimatorListener listener) { 241 this(distortedPoints, undistortedPoints, listener); 242 internalSetQualityScores(qualityScores); 243 } 244 245 /** 246 * Constructor. 247 * 248 * @param distortedPoints list of distorted points. Distorted points are 249 * obtained after radial distortion is applied to an undistorted point. 250 * @param undistortedPoints list of undistorted points. 251 * @param qualityScores quality scores corresponding to each provided point. 252 * @param distortionCenter radial distortion center. If null it is assumed 253 * to be the origin of coordinates, otherwise this is typically equal to 254 * the camera principal point. 255 * @throws IllegalArgumentException if provided lists of points or quality 256 * scores don't have the same size or their size is smaller than 257 * MIN_NUMBER_OF_POINTS (i.e. 2 points). 258 */ 259 public PROMedSRadialDistortionRobustEstimator(final List<Point2D> distortedPoints, 260 final List<Point2D> undistortedPoints, 261 final double[] qualityScores, 262 final Point2D distortionCenter) { 263 this(distortedPoints, undistortedPoints, distortionCenter); 264 internalSetQualityScores(qualityScores); 265 } 266 267 /** 268 * Constructor. 269 * 270 * @param distortedPoints list of distorted points. Distorted points are 271 * obtained after radial distortion is applied to an undistorted point. 272 * @param undistortedPoints list of undistorted points. 273 * @param qualityScores quality scores corresponding to each provided point. 274 * @param distortionCenter radial distortion center. If null it is assumed 275 * to be the origin of coordinates, otherwise this is typically equal to 276 * the camera principal point. 277 * @param listener listener to be notified of events such as when 278 * estimation starts, ends or its progress significantly changes. 279 * @throws IllegalArgumentException if provided lists of points or quality 280 * scores don't have the same size or their size is smaller than 281 * MIN_NUMBER_OF_POINTS (i.e. 2 points). 282 */ 283 public PROMedSRadialDistortionRobustEstimator(final List<Point2D> distortedPoints, 284 final List<Point2D> undistortedPoints, 285 final double[] qualityScores, 286 final Point2D distortionCenter, 287 final RadialDistortionRobustEstimatorListener listener) { 288 this(distortedPoints, undistortedPoints, distortionCenter, listener); 289 internalSetQualityScores(qualityScores); 290 } 291 292 /** 293 * Returns threshold to be used to keep the algorithm iterating in case that 294 * best estimated threshold using median of residuals is not small enough. 295 * Once a solution is found that generates a threshold below this value, the 296 * algorithm will stop. 297 * The stop threshold can be used to prevent the LMedS algorithm iterating 298 * too many times in cases where samples have a very similar accuracy. 299 * For instance, in cases where proportion of outliers is very small (close 300 * to 0%), and samples are very accurate (i.e. 1e-6), the algorithm would 301 * iterate for a long time trying to find the best solution when indeed 302 * there is no need to do that if a reasonable threshold has already been 303 * reached. 304 * Because of this behaviour the stop threshold can be set to a value much 305 * lower than the one typically used in RANSAC, and yet the algorithm could 306 * still produce even smaller thresholds in estimated results. 307 * 308 * @return stop threshold to stop the algorithm prematurely when a certain 309 * accuracy has been reached. 310 */ 311 public double getStopThreshold() { 312 return stopThreshold; 313 } 314 315 /** 316 * Sets threshold to be used to keep the algorithm iterating in case that 317 * best estimated threshold using median of residuals is not small enough. 318 * Once a solution is found that generates a threshold below this value, the 319 * algorithm will stop. 320 * The stop threshold can be used to prevent the LMedS algorithm iterating 321 * too many times in cases where samples have a very similar accuracy. 322 * For instance, in cases where proportion of outliers is very small (close 323 * to 0%), and samples are very accurate (i.e. 1e-6), the algorithm would 324 * iterate for a long time trying to find the best solution when indeed 325 * there is no need to do that if a reasonable threshold has already been 326 * reached. 327 * Because of this behaviour the stop threshold can be set to a value much 328 * lower than the one typically used in RANSAC, and yet the algorithm could 329 * still produce even smaller thresholds in estimated results. 330 * 331 * @param stopThreshold stop threshold to stop the algorithm prematurely 332 * when a certain accuracy has been reached. 333 * @throws IllegalArgumentException if provided value is zero or negative. 334 * @throws LockedException if robust estimator is locked because an 335 * estimation is already in progress. 336 */ 337 public void setStopThreshold(final double stopThreshold) throws LockedException { 338 if (isLocked()) { 339 throw new LockedException(); 340 } 341 if (stopThreshold <= MIN_STOP_THRESHOLD) { 342 throw new IllegalArgumentException(); 343 } 344 345 this.stopThreshold = stopThreshold; 346 } 347 348 /** 349 * Returns quality scores corresponding to each provided point. 350 * The larger the score value the better the quality of the sampled point. 351 * 352 * @return quality scores corresponding to each point. 353 */ 354 @Override 355 public double[] getQualityScores() { 356 return qualityScores; 357 } 358 359 /** 360 * Sets quality scores corresponding to each provided point. 361 * The larger the score value the better the quality of the sampled point. 362 * 363 * @param qualityScores quality scores corresponding to each point. 364 * @throws LockedException if robust estimator is locked because an 365 * estimation is already in progress. 366 * @throws IllegalArgumentException if provided quality scores length is 367 * smaller than MINIMUM_SIZE (i.e. 3 samples). 368 */ 369 @Override 370 public void setQualityScores(final double[] qualityScores) throws LockedException { 371 if (isLocked()) { 372 throw new LockedException(); 373 } 374 internalSetQualityScores(qualityScores); 375 } 376 377 /** 378 * Indicates if estimator is ready to start the radial distortion 379 * estimation. 380 * This is true when input data (i.e. 2D points and quality scores) are 381 * provided and a minimum of 2 points are available. 382 * 383 * @return true if estimator is ready, false otherwise. 384 */ 385 @Override 386 public boolean isReady() { 387 return super.isReady() && qualityScores != null && qualityScores.length == distortedPoints.size(); 388 } 389 390 /** 391 * Estimates a radial distortion using a robust estimator and 392 * the best set of matched 2D points found using the robust estimator. 393 * 394 * @return a radial distortion. 395 * @throws LockedException if robust estimator is locked because an 396 * estimation is already in progress. 397 * @throws NotReadyException if provided input data is not enough to start 398 * the estimation. 399 * @throws RobustEstimatorException if estimation fails for any reason 400 * (i.e. numerical instability, no solution available, etc). 401 */ 402 @SuppressWarnings("DuplicatedCode") 403 @Override 404 public RadialDistortion estimate() throws LockedException, NotReadyException, RobustEstimatorException { 405 if (isLocked()) { 406 throw new LockedException(); 407 } 408 if (!isReady()) { 409 throw new NotReadyException(); 410 } 411 412 final var innerEstimator = new PROMedSRobustEstimator<RadialDistortion>(new PROMedSRobustEstimatorListener<>() { 413 414 // point to be reused when computing residuals 415 private final Point2D testPoint = Point2D.create(CoordinatesType.INHOMOGENEOUS_COORDINATES); 416 417 // non-robust radial distortion estimator 418 private final LMSERadialDistortionEstimator radialDistortionEstimator = new LMSERadialDistortionEstimator(); 419 420 // subset of distorted (i.e. measured) points 421 private final List<Point2D> subsetDistorted = new ArrayList<>(); 422 423 // subset of undistorted (i.e. ideal) points 424 private final List<Point2D> subsetUndistorted = new ArrayList<>(); 425 426 @Override 427 public double getThreshold() { 428 return stopThreshold; 429 } 430 431 @Override 432 public int getTotalSamples() { 433 return distortedPoints.size(); 434 } 435 436 @Override 437 public int getSubsetSize() { 438 return RadialDistortionRobustEstimator.MIN_NUMBER_OF_POINTS; 439 } 440 441 @Override 442 public void estimatePreliminarSolutions( 443 final int[] samplesIndices, final List<RadialDistortion> solutions) { 444 subsetDistorted.clear(); 445 subsetDistorted.add(distortedPoints.get(samplesIndices[0])); 446 subsetDistorted.add(distortedPoints.get(samplesIndices[1])); 447 448 subsetUndistorted.clear(); 449 subsetUndistorted.add(undistortedPoints.get(samplesIndices[0])); 450 subsetUndistorted.add(undistortedPoints.get(samplesIndices[1])); 451 452 try { 453 radialDistortionEstimator.setPoints(distortedPoints, undistortedPoints); 454 radialDistortionEstimator.setPoints(subsetDistorted, subsetUndistorted); 455 456 final var distortion = radialDistortionEstimator.estimate(); 457 solutions.add(distortion); 458 } catch (final Exception e) { 459 // if anything fails, no solution is added 460 } 461 } 462 463 @Override 464 public double computeResidual(final RadialDistortion currentEstimation, final int i) { 465 final var distortedPoint = distortedPoints.get(i); 466 final var undistortedPoint = undistortedPoints.get(i); 467 468 currentEstimation.distort(undistortedPoint, testPoint); 469 470 return testPoint.distanceTo(distortedPoint); 471 } 472 473 @Override 474 public boolean isReady() { 475 return PROMedSRadialDistortionRobustEstimator.this.isReady(); 476 } 477 478 @Override 479 public void onEstimateStart(final RobustEstimator<RadialDistortion> estimator) { 480 try { 481 radialDistortionEstimator.setLMSESolutionAllowed(false); 482 radialDistortionEstimator.setIntrinsic(getIntrinsic()); 483 } catch (final Exception e) { 484 Logger.getLogger(PROMedSRadialDistortionRobustEstimator.class.getName()).log(Level.WARNING, 485 "Could not set intrinsic parameters on radial distortion estimator", e); 486 } 487 488 if (listener != null) { 489 listener.onEstimateStart(PROMedSRadialDistortionRobustEstimator.this); 490 } 491 } 492 493 @Override 494 public void onEstimateEnd(final RobustEstimator<RadialDistortion> estimator) { 495 if (listener != null) { 496 listener.onEstimateEnd(PROMedSRadialDistortionRobustEstimator.this); 497 } 498 } 499 500 @Override 501 public void onEstimateNextIteration( 502 final RobustEstimator<RadialDistortion> estimator, final int iteration) { 503 if (listener != null) { 504 listener.onEstimateNextIteration(PROMedSRadialDistortionRobustEstimator.this, iteration); 505 } 506 } 507 508 @Override 509 public void onEstimateProgressChange( 510 final RobustEstimator<RadialDistortion> estimator, final float progress) { 511 if (listener != null) { 512 listener.onEstimateProgressChange(PROMedSRadialDistortionRobustEstimator.this, progress); 513 } 514 } 515 516 @Override 517 public double[] getQualityScores() { 518 return qualityScores; 519 } 520 }); 521 522 try { 523 locked = true; 524 innerEstimator.setConfidence(confidence); 525 innerEstimator.setMaxIterations(maxIterations); 526 innerEstimator.setProgressDelta(progressDelta); 527 return innerEstimator.estimate(); 528 } catch (final com.irurueta.numerical.LockedException e) { 529 throw new LockedException(e); 530 } catch (final com.irurueta.numerical.NotReadyException e) { 531 throw new NotReadyException(e); 532 } finally { 533 locked = false; 534 } 535 } 536 537 /** 538 * Returns method being used for robust estimation 539 * 540 * @return method being used for robust estimation 541 */ 542 @Override 543 public RobustEstimatorMethod getMethod() { 544 return RobustEstimatorMethod.PROMEDS; 545 } 546 547 /** 548 * Sets quality scores corresponding to each provided point. 549 * This method is used internally and does not check whether instance is 550 * locked or not. 551 * 552 * @param qualityScores quality scores to be set. 553 * @throws IllegalArgumentException if provided quality scores length is 554 * smaller than MINIMUM_SIZE. 555 */ 556 private void internalSetQualityScores(final double[] qualityScores) { 557 if (qualityScores.length < MIN_NUMBER_OF_POINTS) { 558 throw new IllegalArgumentException(); 559 } 560 561 this.qualityScores = qualityScores; 562 } 563 }