View Javadoc
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;
17  
18  import com.irurueta.ar.calibration.estimators.LMedSRadialDistortionRobustEstimator;
19  import com.irurueta.ar.calibration.estimators.MSACRadialDistortionRobustEstimator;
20  import com.irurueta.ar.calibration.estimators.PROMedSRadialDistortionRobustEstimator;
21  import com.irurueta.ar.calibration.estimators.PROSACRadialDistortionRobustEstimator;
22  import com.irurueta.ar.calibration.estimators.RANSACRadialDistortionRobustEstimator;
23  import com.irurueta.ar.calibration.estimators.RadialDistortionRobustEstimator;
24  import com.irurueta.ar.calibration.estimators.RadialDistortionRobustEstimatorListener;
25  import com.irurueta.geometry.GeometryException;
26  import com.irurueta.geometry.PinholeCameraIntrinsicParameters;
27  import com.irurueta.geometry.Point2D;
28  import com.irurueta.geometry.estimators.LockedException;
29  import com.irurueta.geometry.estimators.NotReadyException;
30  import com.irurueta.numerical.NumericalException;
31  import com.irurueta.numerical.robust.RobustEstimatorMethod;
32  
33  import java.util.ArrayList;
34  import java.util.List;
35  import java.util.logging.Level;
36  import java.util.logging.Logger;
37  
38  /**
39   * Calibrates a camera in order to find its intrinsic parameters and radial
40   * distortion by using an alternating technique where first an initial guess
41   * of the intrinsic parameters, rotation and translation is obtained to model
42   * the camera used to sample the calibration pattern, and then the result is
43   * used to find the best possible radial distortion to account for all remaining
44   * errors. The result is then used to undo the distortion effect and calibrate
45   * again to estimate the intrinsic parameters and camera pose. This alternating
46   * process is repeated until convergence is reached.
47   * <p>
48   * This class is based on technique described at:
49   * Zhengyou Zhang. A Flexible New Technique for Camera Calibration. Technical
50   * Report. MSR-TR-98-71. December 2, 1998
51   */
52  @SuppressWarnings("DuplicatedCode")
53  public class AlternatingCameraCalibrator extends CameraCalibrator {
54  
55      /**
56       * Default maximum number of times to do an alternating iteration to refine
57       * the results.
58       */
59      public static final int DEFAULT_MAX_ITERATIONS = 20;
60  
61      /**
62       * Minimum allowed value to be set as max iterations.
63       */
64      public static final int MIN_MAX_ITERATIONS = 1;
65  
66      /**
67       * Default threshold to determine that convergence of the result has been
68       * reached.
69       */
70      public static final double DEFAULT_CONVERGENCE_THRESHOLD = 1e-8;
71  
72      /**
73       * Minimum allowed value to be set as convergence threshold.
74       */
75      public static final double MIN_CONVERGENCE_THRESHOLD = 0.0;
76  
77      /**
78       * Default robust estimator method to be used for radial distortion
79       * estimation.
80       */
81      public static final RobustEstimatorMethod DEFAULT_RADIAL_DISTORTION_METHOD = RobustEstimatorMethod.PROSAC;
82  
83      /**
84       * Maximum number of times to do an alternating iteration to refine the
85       * results.
86       */
87      private int maxIterations;
88  
89      /**
90       * Default threshold to determine that convergence of the result has been
91       * reached.
92       */
93      private double convergenceThreshold;
94  
95      /**
96       * Robust estimator method to be used for radial distortion estimation.
97       */
98      private RobustEstimatorMethod distortionMethod;
99  
100     /**
101      * Robust estimator of radial distortion.
102      */
103     private RadialDistortionRobustEstimator distortionEstimator;
104 
105     /**
106      * Listener for robust estimator of radial distortion.
107      */
108     private RadialDistortionRobustEstimatorListener distortionEstimatorListener;
109 
110     /**
111      * Indicates progress of radial distortion estimation.
112      */
113     private float radialDistortionProgress;
114 
115     /**
116      * Overall progress taking into account current number of iteration.
117      */
118     private float iterProgress;
119 
120     /**
121      * Previously notified progress.
122      */
123     private float previousNotifiedProgress;
124 
125     /**
126      * Constructor.
127      */
128     public AlternatingCameraCalibrator() {
129         super();
130         maxIterations = DEFAULT_MAX_ITERATIONS;
131         convergenceThreshold = DEFAULT_CONVERGENCE_THRESHOLD;
132 
133         internalSetDistortionMethod(DEFAULT_RADIAL_DISTORTION_METHOD);
134     }
135 
136     /**
137      * Constructor.
138      *
139      * @param pattern 2D pattern to use for calibration.
140      * @param samples samples of the pattern taken with the camera to calibrate.
141      * @throws IllegalArgumentException if not enough samples are provided.
142      */
143     public AlternatingCameraCalibrator(final Pattern2D pattern, final List<CameraCalibratorSample> samples) {
144         super(pattern, samples);
145         maxIterations = DEFAULT_MAX_ITERATIONS;
146         convergenceThreshold = DEFAULT_CONVERGENCE_THRESHOLD;
147 
148         internalSetDistortionMethod(DEFAULT_RADIAL_DISTORTION_METHOD);
149     }
150 
151     /**
152      * Constructor.
153      *
154      * @param pattern              2D pattern to use for calibration.
155      * @param samples              samples of the pattern taken with the camera to calibrate.
156      * @param samplesQualityScores quality scores for each sample.
157      * @throws IllegalArgumentException if not enough samples are provided or if
158      *                                  both samples and quality scores do not have the same size.
159      */
160     public AlternatingCameraCalibrator(
161             final Pattern2D pattern, final List<CameraCalibratorSample> samples, final double[] samplesQualityScores) {
162         super(pattern, samples, samplesQualityScores);
163         maxIterations = DEFAULT_MAX_ITERATIONS;
164         convergenceThreshold = DEFAULT_CONVERGENCE_THRESHOLD;
165 
166         internalSetDistortionMethod(DEFAULT_RADIAL_DISTORTION_METHOD);
167     }
168 
169     /**
170      * Returns maximum number of times to do an alternating iteration to refine
171      * the results.
172      *
173      * @return maximum number of times to do an alternating iteration.
174      */
175     public int getMaxIterations() {
176         return maxIterations;
177     }
178 
179     /**
180      * Sets maximum number of times to do an alternating iteration to refine the
181      * results.
182      *
183      * @param maxIterations maximum number of times to do an alternating
184      *                      iteration.
185      * @throws LockedException          if this instance is locked.
186      * @throws IllegalArgumentException if provided value is zero or negative.
187      */
188     public void setMaxIterations(final int maxIterations) throws LockedException {
189         if (isLocked()) {
190             throw new LockedException();
191         }
192         if (maxIterations < MIN_MAX_ITERATIONS) {
193             throw new IllegalArgumentException();
194         }
195 
196         this.maxIterations = maxIterations;
197     }
198 
199     /**
200      * Returns threshold to determine that convergence of the result has been
201      * reached.
202      *
203      * @return threshold to determine that convergence of the result has been
204      * reached.
205      */
206     public double getConvergenceThreshold() {
207         return convergenceThreshold;
208     }
209 
210     /**
211      * Sets threshold to determine that convergence of the result has been
212      * reached.
213      *
214      * @param convergenceThreshold threshold to determine that convergence of
215      *                             the result has been reached.
216      * @throws LockedException          if this instance is locked.
217      * @throws IllegalArgumentException if provided value is negative.
218      */
219     public void setConvergenceThreshold(final double convergenceThreshold) throws LockedException {
220         if (isLocked()) {
221             throw new LockedException();
222         }
223         if (convergenceThreshold < MIN_CONVERGENCE_THRESHOLD) {
224             throw new IllegalArgumentException();
225         }
226 
227         this.convergenceThreshold = convergenceThreshold;
228     }
229 
230     /**
231      * Returns robust estimator method to be used for radial distortion
232      * estimation.
233      *
234      * @return robust estimator method to be used for radial distortion
235      * estimation.
236      */
237     public RobustEstimatorMethod getDistortionMethod() {
238         return distortionMethod;
239     }
240 
241     /**
242      * Sets robust estimator method to be used for radial distortion
243      * estimation.
244      *
245      * @param distortionMethod robust estimator method to be used for
246      *                         radial distortion estimation.
247      * @throws LockedException if this instance is locked.
248      */
249     public void setDistortionMethod(final RobustEstimatorMethod distortionMethod) throws LockedException {
250         if (isLocked()) {
251             throw new LockedException();
252         }
253         internalSetDistortionMethod(distortionMethod);
254     }
255 
256     /**
257      * Returns radial distortion estimator, which can be retrieved in case
258      * that some additional parameter needed to be adjusted.
259      * It is discouraged to directly access the distortion estimator during
260      * camera calibration, as it might interfere with the results.
261      *
262      * @return radial distortion estimator.
263      */
264     public RadialDistortionRobustEstimator getDistortionEstimator() {
265         return distortionEstimator;
266     }
267 
268     /**
269      * Returns threshold to robustly estimate radial distortion.
270      * Usually the default value is good enough for most situations, but this
271      * setting can be changed for finer adjustments.
272      *
273      * @return threshold to robustly estimate radial distortion.
274      */
275     public double getDistortionEstimatorThreshold() {
276         return switch (distortionEstimator.getMethod()) {
277             case LMEDS -> ((LMedSRadialDistortionRobustEstimator) distortionEstimator).getStopThreshold();
278             case MSAC -> ((MSACRadialDistortionRobustEstimator) distortionEstimator).getThreshold();
279             case PROSAC -> ((PROSACRadialDistortionRobustEstimator) distortionEstimator).getThreshold();
280             case PROMEDS -> ((PROMedSRadialDistortionRobustEstimator) distortionEstimator).getStopThreshold();
281             default -> ((RANSACRadialDistortionRobustEstimator) distortionEstimator).getThreshold();
282         };
283     }
284 
285     /**
286      * Sets threshold to robustly estimate radial distortion.
287      * Usually the default value is good enough for most situations, but this
288      * setting can be changed for finder adjustments.
289      *
290      * @param distortionEstimatorThreshold threshold to robustly estimate
291      *                                     radial distortion .
292      * @throws LockedException          if this instance is locked.
293      * @throws IllegalArgumentException if provided value is zero or negative.
294      */
295     public void setDistortionEstimatorThreshold(final double distortionEstimatorThreshold) throws LockedException {
296         if (isLocked()) {
297             throw new LockedException();
298         }
299 
300         switch (distortionEstimator.getMethod()) {
301             case LMEDS:
302                 ((LMedSRadialDistortionRobustEstimator) distortionEstimator)
303                         .setStopThreshold(distortionEstimatorThreshold);
304                 break;
305             case MSAC:
306                 ((MSACRadialDistortionRobustEstimator) distortionEstimator).setThreshold(distortionEstimatorThreshold);
307                 break;
308             case PROSAC:
309                 ((PROSACRadialDistortionRobustEstimator) distortionEstimator)
310                         .setThreshold(distortionEstimatorThreshold);
311                 break;
312             case PROMEDS:
313                 ((PROMedSRadialDistortionRobustEstimator) distortionEstimator)
314                         .setStopThreshold(distortionEstimatorThreshold);
315                 break;
316             case RANSAC:
317             default:
318                 ((RANSACRadialDistortionRobustEstimator) distortionEstimator)
319                         .setThreshold(distortionEstimatorThreshold);
320                 break;
321         }
322     }
323 
324     /**
325      * Returns confidence to robustly estimate radial distortion.
326      * Usually the default value is good enough for most situations, but this
327      * setting can be changed for finer adjustments.
328      * Confidence is expressed as a value between 0.0 (0%) and 1.0 (100%). The
329      * amount of confidence indicates the probability that the estimated
330      * homography is correct (i.e. no outliers were used for the estimation,
331      * because they were successfully discarded).
332      * Typically, this value will be close to 1.0, but not exactly 1.0, because
333      * a 100% confidence would require an infinite number of iterations.
334      * Usually the default value is good enough for most situations, but this
335      * setting can be changed for finer adjustments.
336      *
337      * @return confidence to robustly estimate homographies.
338      */
339     public double getDistortionEstimatorConfidence() {
340         return distortionEstimator.getConfidence();
341     }
342 
343     /**
344      * Sets confidence to robustly estimate radial distortion.
345      * Usually the default value is good enough for most situations, but this
346      * setting can be changed for finer adjustments.
347      * Confidence is expressed as a value between 0.0 (0%) and 1.0 (100%). The
348      * amount of confidence indicates the probability that the estimated
349      * homography is correct (i.e. no outliers were used for the estimation,
350      * because they were successfully discarded).
351      * Typically, this value will be close to 1.0, but not exactly 1.0, because
352      * a 100% confidence would require an infinite number of iterations.
353      * Usually the default value is good enough for most situations, but this
354      * setting can be changed for finer adjustments.
355      *
356      * @param distortionEstimatorConfidence confidence to robustly estimate
357      *                                      radial distortion.
358      * @throws LockedException          if this instance is locked.
359      * @throws IllegalArgumentException if provided value is not between 0.0 and
360      *                                  1.0.
361      */
362     public void setDistortionEstimatorConfidence(final double distortionEstimatorConfidence) throws LockedException {
363         if (isLocked()) {
364             throw new LockedException();
365         }
366 
367         distortionEstimator.setConfidence(distortionEstimatorConfidence);
368     }
369 
370     /**
371      * Returns the maximum number of iterations to be done when estimating
372      * the radial distortion.
373      * If the maximum allowed number of iterations is reached, resulting
374      * estimation might not have desired confidence.
375      * Usually the default value is good enough for most situations, but this
376      * setting can be changed for finer adjustments.
377      *
378      * @return maximum number of iterations to be done when estimating the
379      * homographies.
380      */
381     public int getDistortionEstimatorMaxIterations() {
382         return distortionEstimator.getMaxIterations();
383     }
384 
385     /**
386      * Sets the maximum number of iterations to be done when estimating the
387      * radial distortion.
388      * If the maximum allowed number of iterations is reached, resulting
389      * estimation might not have desired confidence.
390      * Usually the default value is good enough for most situations, but this
391      * setting can be changed for finer adjustments.
392      *
393      * @param distortionEstimatorMaxIterations maximum number of iterations to
394      *                                         be done when estimating radial distortion.
395      * @throws LockedException          if this instance is locked.
396      * @throws IllegalArgumentException if provided value is negative or zero.
397      */
398     public void setDistortionEstimatorMaxIterations(final int distortionEstimatorMaxIterations) throws LockedException {
399         if (isLocked()) {
400             throw new LockedException();
401         }
402 
403         distortionEstimator.setMaxIterations(distortionEstimatorMaxIterations);
404     }
405 
406     /**
407      * Starts the calibration process.
408      * Depending on the settings the following will be estimated:
409      * intrinsic pinhole camera parameters, radial distortion of lens,
410      * camera pose (rotation and translation) for each sample, and the
411      * associated homobraphy of sampled points respect to the ideal pattern
412      * samples.
413      *
414      * @throws CalibrationException if calibration fails for some reason.
415      * @throws LockedException      if this instance is locked because calibration is
416      *                              already in progress.
417      * @throws NotReadyException    if this instance does not have enough data to
418      *                              start camera calibration.
419      */
420     @Override
421     public void calibrate() throws CalibrationException, LockedException, NotReadyException {
422 
423         if (isLocked()) {
424             throw new LockedException();
425         }
426         if (!isReady()) {
427             throw new NotReadyException();
428         }
429 
430         locked = true;
431 
432         homographyQualityScoresRequired = (distortionEstimator.getMethod() == RobustEstimatorMethod.PROSAC
433                 || distortionEstimator.getMethod() == RobustEstimatorMethod.PROMEDS);
434 
435         if (listener != null) {
436             listener.onCalibrateStart(this);
437         }
438 
439         reset();
440         radialDistortionProgress = iterProgress = previousNotifiedProgress = 0.0f;
441 
442         final var idealFallbackPatternMarkers = pattern.getIdealPoints();
443 
444         try {
445             double errorDiff;
446             double previousError;
447             var currentError = Double.MAX_VALUE;
448             var bestError = Double.MAX_VALUE;
449             PinholeCameraIntrinsicParameters bestIntrinsic = null;
450             RadialDistortion bestDistortion = null;
451 
452             // iterate until error converges
453             var iter = 0;
454             do {
455                 previousError = currentError;
456 
457                 // estimate intrinsic parameters
458                 estimateIntrinsicParameters(idealFallbackPatternMarkers);
459 
460                 if (!estimateRadialDistortion) {
461                     break;
462                 }
463 
464                 // estimate radial distortion using estimated intrinsic
465                 // parameters and camera poses and obtain average re-projection
466                 // error
467                 currentError = estimateRadialDistortion(idealFallbackPatternMarkers);
468 
469                 if (currentError < bestError) {
470                     bestError = currentError;
471                     bestIntrinsic = intrinsic;
472                     bestDistortion = distortion;
473                 }
474 
475                 errorDiff = Math.abs(previousError - currentError);
476                 iter++;
477                 iterProgress = (float) iter / (float) maxIterations;
478                 notifyProgress();
479 
480             } while (errorDiff > convergenceThreshold && iter < maxIterations);
481 
482             if (bestIntrinsic != null) {
483                 intrinsic = bestIntrinsic;
484             }
485             if (bestDistortion != null) {
486                 distortion = bestDistortion;
487             }
488 
489             if (listener != null) {
490                 listener.onCalibrateEnd(this);
491             }
492         } finally {
493             locked = false;
494         }
495     }
496 
497     /**
498      * Estimates radial distortion using estimated intrinsic parameters among
499      * all samples to estimate their camera poses to find non-distorted points
500      * and compare them with the sampled ones.
501      *
502      * @param idealFallbackPatternMarkers ideal pattern markers coordinates
503      *                                    These coordinates are used as fallback when a given sample does
504      *                                    not have an associated pattern.
505      * @return average re-projection error, obtained after projecting ideal
506      * pattern markers using estimated camera poses and then doing a comparison
507      * with sampled points taking into account estimated distortion to undo
508      * their corresponding distortion.
509      * @throws CalibrationException if anything fails.
510      */
511     protected double estimateRadialDistortion(final List<Point2D> idealFallbackPatternMarkers)
512             throws CalibrationException {
513 
514         radialDistortionProgress = 0.0f;
515 
516         if (listener != null) {
517             listener.onRadialDistortionEstimationStarts(this);
518         }
519 
520         final var distortedPoints = new ArrayList<Point2D>();
521         final var undistortedPoints = new ArrayList<Point2D>();
522         // compute total points for samples where homography could be estimated
523         var totalPoints = 0;
524         for (final var sample : samples) {
525             if (sample.getHomography() != null) {
526                 totalPoints += sample.getSampledMarkers().size();
527             }
528         }
529 
530         double[] qualityScores = null;
531         if (distortionMethod == RobustEstimatorMethod.PROSAC || distortionMethod == RobustEstimatorMethod.PROMEDS) {
532             qualityScores = new double[totalPoints];
533         }
534 
535         // estimate camera pose for each sample
536         var pointCounter = 0;
537         var sampleCounter = 0;
538         for (final var sample : samples) {
539             if (sample.getHomography() == null) {
540                 // homography computation failed, so we cannot compute camera
541                 // pose for this sample, or use this sample for radial distortion
542                 // estimation
543                 continue;
544             }
545             sample.computeCameraPose(intrinsic);
546 
547             // transform ideal pattern markers using estimated homography
548             final List<Point2D> idealPatternMarkers;
549             if (sample.getPattern() != null) {
550                 // use points generated by pattern in sample
551                 idealPatternMarkers = sample.getPattern().getIdealPoints();
552             } else {
553                 // use fallback pattern points
554                 idealPatternMarkers = idealFallbackPatternMarkers;
555             }
556 
557             final var transformedIdealPatternMarkers = sample.getHomography()
558                     .transformPointsAndReturnNew(idealPatternMarkers);
559 
560             // transformedIdealPatternMarkers are considered the undistorted
561             // points, because camera follows a pure pinhole model without
562             // distortion, and we have transformed the ideal points using a
563             // pure projective homography without distortion.
564             // sample.getSampledMarkers() contains the sampled coordinates using
565             // the actual camera, which will be distorted
566 
567             // the sampled markers are the ones considered to be distorted for
568             // radial distortion estimation purposes, because they are obtained
569             // directly from the camera
570 
571             // stack together all distorted and undistorted points from all
572             // samples
573 
574             distortedPoints.addAll(sample.getSampledMarkers());
575             undistortedPoints.addAll(transformedIdealPatternMarkers);
576 
577             final var markersSize = transformedIdealPatternMarkers.size();
578 
579             // if distortion estimator requires quality scores, set them
580             if (qualityScores != null && (distortionMethod == RobustEstimatorMethod.PROSAC
581                     || distortionMethod == RobustEstimatorMethod.PROMEDS)) {
582 
583                 final var sampleQuality = homographyQualityScores[sampleCounter];
584 
585                 // assign to all points (markers) in the sample the same sample
586                 // quality
587                 for (var i = pointCounter; i < pointCounter + markersSize; i++) {
588                     qualityScores[i] = sampleQuality;
589                 }
590 
591                 pointCounter += markersSize;
592                 sampleCounter++;
593             }
594         }
595 
596         // estimate radial distortion
597         var avgError = 0.0;
598         try {
599             distortionEstimator.setIntrinsic(intrinsic);
600             distortionEstimator.setPoints(distortedPoints, undistortedPoints);
601             distortionEstimator.setQualityScores(qualityScores);
602 
603             final var distortion = distortionEstimator.estimate();
604 
605             // add distortion to undistorted points (which are ideal pattern
606             // markers with homography applied)
607             final var distortedPoints2 = distortion.distort(undistortedPoints);
608 
609             // set undistorted points obtained after un-distorting sampled points
610             // to refine homography on next iteration
611             for (final var sample : samples) {
612                 if (sample.getHomography() == null) {
613                     continue;
614                 }
615 
616                 // undo distortion of distorted (sampled) points using estimated
617                 // distortion
618 
619                 final var undistortedPoints2 = distortion.undistort(sample.getSampledMarkers());
620 
621                 sample.setUndistortedMarkers(undistortedPoints2);
622             }
623 
624             // compare distortedPoints (obtained by using sampled data)
625             // with distortedPoints2 (obtained after applying homography to
626             // ideal marker points and applying distortion with estimated
627             // distortion)
628             Point2D distortedPoint1;
629             Point2D distortedPoint2;
630             totalPoints = distortedPoints.size();
631             var inlierCount = 0;
632             for (var i = 0; i < totalPoints; i++) {
633                 distortedPoint1 = distortedPoints.get(i);
634                 distortedPoint2 = distortedPoints2.get(i);
635 
636                 final var distance = distortedPoint1.distanceTo(distortedPoint2);
637                 if (distance < getDistortionEstimatorThreshold()) {
638                     avgError += distance;
639                     inlierCount++;
640                 }
641             }
642 
643             if (inlierCount == 0) {
644                 throw new CalibrationException();
645             }
646 
647             avgError /= inlierCount;
648 
649             this.distortion = distortion;
650 
651         } catch (final GeometryException | NumericalException | DistortionException e) {
652             throw new CalibrationException(e);
653         }
654 
655         if (listener != null) {
656             listener.onRadialDistortionEstimationEnds(this, distortion);
657         }
658 
659         return avgError;
660     }
661 
662     /**
663      * Returns the camera calibrator method used by this instance.
664      *
665      * @return the camera calibrator method.
666      */
667     @Override
668     public CameraCalibratorMethod getMethod() {
669         return CameraCalibratorMethod.ALTERNATING_CALIBRATOR;
670     }
671 
672     /**
673      * Notifies progress to current listener, if needed.
674      */
675     @Override
676     protected void notifyProgress() {
677         final var lambda = 1.0f / maxIterations;
678         final var partial = 0.5f * intrinsicProgress + 0.5f * radialDistortionProgress;
679 
680         final float progress;
681         if (!estimateRadialDistortion) {
682             // we do not iterate if there is no need to
683             // estimate radial distortion
684             progress = partial;
685         } else {
686             progress = iterProgress + lambda * partial;
687         }
688 
689         if (listener != null && (progress - previousNotifiedProgress) > progressDelta) {
690             listener.onCalibrateProgressChange(this, progress);
691             previousNotifiedProgress = progress;
692         }
693     }
694 
695     /**
696      * Refreshes listener of distortion estimator
697      */
698     protected void refreshDistortionEstimatorListener() {
699         if (distortionEstimatorListener == null) {
700             distortionEstimatorListener = new RadialDistortionRobustEstimatorListener() {
701 
702                 @Override
703                 public void onEstimateStart(final RadialDistortionRobustEstimator estimator) {
704                     radialDistortionProgress = 0.0f;
705                     notifyProgress();
706                 }
707 
708                 @Override
709                 public void onEstimateEnd(final RadialDistortionRobustEstimator estimator) {
710                     radialDistortionProgress = 1.0f;
711                     notifyProgress();
712                 }
713 
714                 @Override
715                 public void onEstimateNextIteration(
716                         final RadialDistortionRobustEstimator estimator, final int iteration) {
717                     // not needed
718                 }
719 
720                 @Override
721                 public void onEstimateProgressChange(
722                         final RadialDistortionRobustEstimator estimator, final float progress) {
723                     radialDistortionProgress = progress;
724                     notifyProgress();
725                 }
726             };
727         }
728 
729         try {
730             distortionEstimator.setListener(distortionEstimatorListener);
731         } catch (final LockedException e) {
732             Logger.getLogger(AlternatingCameraCalibrator.class.getName()).log(Level.WARNING,
733                     "Could not set radial distortion estimator listener", e);
734         }
735     }
736 
737     /**
738      * Sets robust estimator method to be used for radial distortion estimation.
739      * If method changes, then a new radial distortion estimator is created and
740      * configured.
741      *
742      * @param distortionMethod robust estimator method to be used for
743      *                         radial distortion estimation.
744      */
745     private void internalSetDistortionMethod(final RobustEstimatorMethod distortionMethod) {
746         // if method changes, recreate estimator
747         if (distortionMethod != this.distortionMethod) {
748             var previousAvailable = this.distortionMethod != null;
749             var threshold = 0.0;
750             var confidence = 0.0;
751             var maxIters = 0;
752             if (previousAvailable) {
753                 threshold = getDistortionEstimatorThreshold();
754                 confidence = getDistortionEstimatorConfidence();
755                 maxIters = getDistortionEstimatorMaxIterations();
756             }
757 
758             distortionEstimator = RadialDistortionRobustEstimator.create(distortionMethod);
759 
760             // configure new estimator
761             refreshDistortionEstimatorListener();
762             if (previousAvailable) {
763                 try {
764                     setDistortionEstimatorThreshold(threshold);
765                     setDistortionEstimatorConfidence(confidence);
766                     setDistortionEstimatorMaxIterations(maxIters);
767                 } catch (final LockedException e) {
768                     Logger.getLogger(AlternatingCameraCalibrator.class.getName()).log(Level.WARNING,
769                             "Could not reconfigure distortion estimator", e);
770                 }
771             }
772         }
773 
774         this.distortionMethod = distortionMethod;
775     }
776 }