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.algebra.Matrix;
19  import com.irurueta.algebra.WrongSizeException;
20  import com.irurueta.ar.calibration.estimators.LMedSRadialDistortionRobustEstimator;
21  import com.irurueta.ar.calibration.estimators.MSACRadialDistortionRobustEstimator;
22  import com.irurueta.ar.calibration.estimators.PROMedSRadialDistortionRobustEstimator;
23  import com.irurueta.ar.calibration.estimators.PROSACRadialDistortionRobustEstimator;
24  import com.irurueta.ar.calibration.estimators.RANSACRadialDistortionRobustEstimator;
25  import com.irurueta.ar.calibration.estimators.RadialDistortionRobustEstimator;
26  import com.irurueta.ar.calibration.estimators.RadialDistortionRobustEstimatorListener;
27  import com.irurueta.geometry.AxisRotation3D;
28  import com.irurueta.geometry.HomogeneousPoint2D;
29  import com.irurueta.geometry.HomogeneousPoint3D;
30  import com.irurueta.geometry.PinholeCamera;
31  import com.irurueta.geometry.Point2D;
32  import com.irurueta.geometry.Point3D;
33  import com.irurueta.geometry.Rotation3DType;
34  import com.irurueta.geometry.estimators.LockedException;
35  import com.irurueta.geometry.estimators.NotReadyException;
36  import com.irurueta.numerical.EvaluationException;
37  import com.irurueta.numerical.JacobianEstimator;
38  import com.irurueta.numerical.MultiVariateFunctionEvaluatorListener;
39  import com.irurueta.numerical.fitting.LevenbergMarquardtMultiVariateFitter;
40  import com.irurueta.numerical.fitting.LevenbergMarquardtMultiVariateFunctionEvaluator;
41  import com.irurueta.numerical.robust.RobustEstimatorMethod;
42  
43  import java.util.ArrayList;
44  import java.util.Arrays;
45  import java.util.List;
46  import java.util.logging.Level;
47  import java.util.logging.Logger;
48  
49  /**
50   * Calibrates a camera in order to find its intrinsic parameters and radial
51   * distortion by first estimating the intrinsic parameters without accounting
52   * for radial distortion and then use an optimization algorithm to minimize
53   * error and adjust estimated camera pose, intrinsic parameters and radial
54   * distortion parameters.
55   * <p>
56   * This class is based on technique described at:
57   * Zhengyou Zhang. A Flexible New Technique for Camera Calibration. Technical
58   * Report. MSR-TR-98-71. December 2, 1998.
59   */
60  @SuppressWarnings("DuplicatedCode")
61  public class ErrorOptimizationCameraCalibrator extends CameraCalibrator {
62  
63      /**
64       * Default robust estimator method to be used for radial distortion
65       * estimation.
66       */
67      public static final RobustEstimatorMethod DEFAULT_RADIAL_DISTORTION_METHOD = RobustEstimatorMethod.PROSAC;
68  
69      /**
70       * Indicates whether an initial radial distortion guess is estimated based
71       * on sampled data and estimated camera poses before starting the actual
72       * radial distortion optimization process.
73       */
74      public static final boolean DEFAULT_ESTIMATE_INITIAL_RADIAL_DISTORTION = true;
75  
76      /**
77       * Default maximum number of iterations to be used when adjusting parameters
78       * using Levenberg-Marquardt algorithm.
79       */
80      public static final int DEFAULT_LEVENBERG_MARQUARDT_MAX_ITERS = 1000;
81  
82      /**
83       * Default tolerance to assume that Levenberg-Marquardt algorithm has
84       * reached convergence when adjusting parameters.
85       */
86      public static final double DEFAULT_LEVENBERG_MARQUARDT_TOLERANCE = 1e-12;
87  
88      /**
89       * Maximum number of iterations to be used when adjusting parameters using
90       * Levenberg-Marquardt algorithm.
91       */
92      private int levenbergMarquardtMaxIters;
93  
94      /**
95       * Tolerance to assume that Levenberg-Marquardt algorithm has reached
96       * convergence when adjusting parameters.
97       */
98      private double levenbergMarquardtTolerance;
99  
100     /**
101      * Robust estimator method to be used for radial distortion estimation.
102      */
103     private RobustEstimatorMethod distortionMethod;
104 
105     /**
106      * Indicates whether an initial radial distortion guess is estimated based
107      * on sampled data and estimated camera poses before starting the actual
108      * radial distortion optimization process.
109      */
110     private final boolean estimateInitialRadialDistortion;
111 
112     /**
113      * Robust estimator of radial distortion.
114      */
115     private RadialDistortionRobustEstimator distortionEstimator;
116 
117     /**
118      * Listener for robust estimator of radial distortion.
119      */
120     private RadialDistortionRobustEstimatorListener distortionEstimatorListener;
121 
122     /**
123      * Indicates progress of radial distortion estimation.
124      */
125     private float radialDistortionProgress;
126 
127     /**
128      * Indicates progress of Levenberg-Marquardt fitting.
129      */
130     private float fittingProgress;
131 
132     /**
133      * Previously notified progress.
134      */
135     private float previousNotifiedProgress;
136 
137     /**
138      * Array to keep a relation between each point at index i-th and the sample
139      * (i.e. view) where it belongs to.
140      */
141     private int[] indexToView;
142 
143 
144     /**
145      * Constructor.
146      */
147     public ErrorOptimizationCameraCalibrator() {
148         super();
149         levenbergMarquardtMaxIters = DEFAULT_LEVENBERG_MARQUARDT_MAX_ITERS;
150         levenbergMarquardtTolerance = DEFAULT_LEVENBERG_MARQUARDT_TOLERANCE;
151         internalSetDistortionMethod(DEFAULT_RADIAL_DISTORTION_METHOD);
152         estimateInitialRadialDistortion = DEFAULT_ESTIMATE_INITIAL_RADIAL_DISTORTION;
153     }
154 
155     /**
156      * Constructor.
157      *
158      * @param pattern 2D pattern to use for calibration.
159      * @param samples samples of the pattern taken with the camera to calibrate.
160      * @throws IllegalArgumentException if not enough samples are provided.
161      */
162     public ErrorOptimizationCameraCalibrator(final Pattern2D pattern, final List<CameraCalibratorSample> samples) {
163         super(pattern, samples);
164         levenbergMarquardtMaxIters = DEFAULT_LEVENBERG_MARQUARDT_MAX_ITERS;
165         levenbergMarquardtTolerance = DEFAULT_LEVENBERG_MARQUARDT_TOLERANCE;
166         internalSetDistortionMethod(DEFAULT_RADIAL_DISTORTION_METHOD);
167         estimateInitialRadialDistortion = DEFAULT_ESTIMATE_INITIAL_RADIAL_DISTORTION;
168     }
169 
170     /**
171      * Constructor.
172      *
173      * @param pattern              2D pattern to use for calibration.
174      * @param samples              samples of the pattern taken with the camera to calibrate.
175      * @param samplesQualityScores quality scores for each sample.
176      * @throws IllegalArgumentException if not enough samples are provided or
177      *                                  both samples and quality scores do not have the same size.
178      */
179     public ErrorOptimizationCameraCalibrator(
180             final Pattern2D pattern, final List<CameraCalibratorSample> samples, final double[] samplesQualityScores) {
181         super(pattern, samples, samplesQualityScores);
182         levenbergMarquardtMaxIters = DEFAULT_LEVENBERG_MARQUARDT_MAX_ITERS;
183         levenbergMarquardtTolerance = DEFAULT_LEVENBERG_MARQUARDT_TOLERANCE;
184         internalSetDistortionMethod(DEFAULT_RADIAL_DISTORTION_METHOD);
185         estimateInitialRadialDistortion = DEFAULT_ESTIMATE_INITIAL_RADIAL_DISTORTION;
186     }
187 
188     /**
189      * Returns maximum number of iterations to be used when adjusting parameters
190      * using Levenberg-Marquardt algorithm.
191      *
192      * @return maximum number of iterations to be used when adjusting parameters
193      * using Levenberg-Marquardt algorithm.
194      */
195     public int getLevenbergMarquardtMaxIters() {
196         return levenbergMarquardtMaxIters;
197     }
198 
199     /**
200      * Sets maximum number of iterations to be used when adjusting parameters
201      * using Levenberg-Marquardt algorithm.
202      *
203      * @param levenbergMarquardtMaxIters maximum number of iterations to be used
204      *                                   when adjusting parameters using Levenberg-Marquardt algorithm.
205      * @throws IllegalArgumentException if provided value is zero or negative.
206      * @throws LockedException          if this instance is locked.
207      */
208     public void setLevenbergMarquardtMaxIters(final int levenbergMarquardtMaxIters) throws LockedException {
209         if (isLocked()) {
210             throw new LockedException();
211         }
212         if (levenbergMarquardtMaxIters <= 0) {
213             throw new IllegalArgumentException();
214         }
215         this.levenbergMarquardtMaxIters = levenbergMarquardtMaxIters;
216     }
217 
218     /**
219      * Returns tolerance to assume that Levenberg-Marquardt algorithm has
220      * reached convergence when adjusting parameters.
221      *
222      * @return tolerance to assume that Levenberg-Marquardt algorithm has
223      * reached convergence when adjusting parameters.
224      */
225     public double getLevenbergMarquardtTolerance() {
226         return levenbergMarquardtTolerance;
227     }
228 
229     /**
230      * Sets tolerance to assume that Levenberg-Marquardt algorithm has reached
231      * convergence when adjusting parameters.
232      *
233      * @param levenbergMarquardtTolerance tolerance to assume that
234      *                                    Levenberg-Marquardt algorithm has reached convergence when
235      *                                    adjusting parameter.
236      * @throws IllegalArgumentException if provided value is zero or negative.
237      * @throws LockedException          if this instance is locked.
238      */
239     public void setLevenbergMarquardtTolerance(final double levenbergMarquardtTolerance) throws LockedException {
240         if (isLocked()) {
241             throw new LockedException();
242         }
243         if (levenbergMarquardtTolerance <= 0.0) {
244             throw new IllegalArgumentException();
245         }
246         this.levenbergMarquardtTolerance = levenbergMarquardtTolerance;
247     }
248 
249     /**
250      * Returns robust estimator method to be used for radial distortion
251      * estimation.
252      *
253      * @return robust estimator method to be used for radial distortion
254      * estimation.
255      */
256     public RobustEstimatorMethod getDistortionMethod() {
257         return distortionMethod;
258     }
259 
260     /**
261      * Sets robust estimator method to be used for radial distortion
262      * estimation.
263      *
264      * @param distortionMethod robust estimator method to be used for
265      *                         radial distortion estimation.
266      * @throws LockedException if this instance is locked.
267      */
268     public void setDistortionMethod(final RobustEstimatorMethod distortionMethod) throws LockedException {
269         if (isLocked()) {
270             throw new LockedException();
271         }
272         internalSetDistortionMethod(distortionMethod);
273     }
274 
275     /**
276      * Returns radial distortion estimator, which can be retrieved in case
277      * that some additional parameter needed to be adjusted.
278      * It is discouraged to directly access the distortion estimator during
279      * camera calibration, as it might interfere with the results.
280      *
281      * @return radial distortion estimator.
282      */
283     public RadialDistortionRobustEstimator getDistortionEstimator() {
284         return distortionEstimator;
285     }
286 
287     /**
288      * Returns threshold to robustly estimate radial distortion.
289      * Usually the default value is good enough for most situations, but this
290      * setting can be changed for finer adjustments.
291      *
292      * @return threshold to robustly estimate radial distortion.
293      */
294     public double getDistortionEstimatorThreshold() {
295         return switch (distortionEstimator.getMethod()) {
296             case LMEDS -> ((LMedSRadialDistortionRobustEstimator) distortionEstimator).getStopThreshold();
297             case MSAC -> ((MSACRadialDistortionRobustEstimator) distortionEstimator).getThreshold();
298             case PROSAC -> ((PROSACRadialDistortionRobustEstimator) distortionEstimator).getThreshold();
299             case PROMEDS -> ((PROMedSRadialDistortionRobustEstimator) distortionEstimator).getStopThreshold();
300             default -> ((RANSACRadialDistortionRobustEstimator) distortionEstimator).getThreshold();
301         };
302     }
303 
304     /**
305      * Sets threshold to robustly estimate radial distortion.
306      * Usually the default value is good enough for most situations, but this
307      * setting can be changed for finder adjustments.
308      *
309      * @param distortionEstimatorThreshold threshold to robustly estimate
310      *                                     radial distortion .
311      * @throws LockedException          if this instance is locked.
312      * @throws IllegalArgumentException if provided value is zero or negative.
313      */
314     public void setDistortionEstimatorThreshold(final double distortionEstimatorThreshold) throws LockedException {
315         if (isLocked()) {
316             throw new LockedException();
317         }
318 
319         switch (distortionEstimator.getMethod()) {
320             case LMEDS:
321                 ((LMedSRadialDistortionRobustEstimator) distortionEstimator).setStopThreshold(
322                         distortionEstimatorThreshold);
323                 break;
324             case MSAC:
325                 ((MSACRadialDistortionRobustEstimator) distortionEstimator).setThreshold(distortionEstimatorThreshold);
326                 break;
327             case PROSAC:
328                 ((PROSACRadialDistortionRobustEstimator) distortionEstimator).setThreshold(
329                         distortionEstimatorThreshold);
330                 break;
331             case PROMEDS:
332                 ((PROMedSRadialDistortionRobustEstimator) distortionEstimator).setStopThreshold(
333                         distortionEstimatorThreshold);
334                 break;
335             case RANSAC:
336             default:
337                 ((RANSACRadialDistortionRobustEstimator) distortionEstimator).setThreshold(
338                         distortionEstimatorThreshold);
339                 break;
340         }
341     }
342 
343     /**
344      * Returns 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      * @return confidence to robustly estimate homographies.
357      */
358     public double getDistortionEstimatorConfidence() {
359         return distortionEstimator.getConfidence();
360     }
361 
362     /**
363      * Sets confidence to robustly estimate radial distortion.
364      * Usually the default value is good enough for most situations, but this
365      * setting can be changed for finer adjustments.
366      * Confidence is expressed as a value between 0.0 (0%) and 1.0 (100%). The
367      * amount of confidence indicates the probability that the estimated
368      * homography is correct (i.e. no outliers were used for the estimation,
369      * because they were successfully discarded).
370      * Typically, this value will be close to 1.0, but not exactly 1.0, because
371      * a 100% confidence would require an infinite number of iterations.
372      * Usually the default value is good enough for most situations, but this
373      * setting can be changed for finer adjustments.
374      *
375      * @param distortionEstimatorConfidence confidence to robustly estimate
376      *                                      radial distortion.
377      * @throws LockedException          if this instance is locked.
378      * @throws IllegalArgumentException if provided value is not between 0.0 and
379      *                                  1.0.
380      */
381     public void setDistortionEstimatorConfidence(final double distortionEstimatorConfidence) throws LockedException {
382         if (isLocked()) {
383             throw new LockedException();
384         }
385 
386         distortionEstimator.setConfidence(distortionEstimatorConfidence);
387     }
388 
389     /**
390      * Returns the maximum number of iterations to be done when estimating
391      * the radial distortion.
392      * If the maximum allowed number of iterations is reached, resulting
393      * estimation might not have desired confidence.
394      * Usually the default value is good enough for most situations, but this
395      * setting can be changed for finer adjustments.
396      *
397      * @return maximum number of iterations to be done when estimating the
398      * homographies.
399      */
400     public int getDistortionEstimatorMaxIterations() {
401         return distortionEstimator.getMaxIterations();
402     }
403 
404     /**
405      * Sets the maximum number of iterations to be done when estimating the
406      * radial distortion.
407      * If the maximum allowed number of iterations is reached, resulting
408      * estimation might not have desired confidence.
409      * Usually the default value is good enough for most situations, but this
410      * setting can be changed for finer adjustments.
411      *
412      * @param distortionEstimatorMaxIterations maximum number of iterations to
413      *                                         be done when estimating radial distortion.
414      * @throws LockedException          if this instance is locked.
415      * @throws IllegalArgumentException if provided value is negative or zero.
416      */
417     public void setDistortionEstimatorMaxIterations(final int distortionEstimatorMaxIterations) throws LockedException {
418         if (isLocked()) {
419             throw new LockedException();
420         }
421 
422         distortionEstimator.setMaxIterations(distortionEstimatorMaxIterations);
423     }
424 
425     /**
426      * Starts the calibration process.
427      * Depending on the settings the following will be estimated:
428      * intrinsic pinhole camera parameters, radial distortion of lens,
429      * camera pose (rotation and translation) for each sample, and the
430      * associated homobraphy of sampled points respect to the ideal pattern
431      * samples.
432      *
433      * @throws CalibrationException if calibration fails for some reason.
434      * @throws LockedException      if this instance is locked because calibration is
435      *                              already in progress.
436      * @throws NotReadyException    if this instance does not have enough data to
437      *                              start camera calibration.
438      */
439     @Override
440     public void calibrate() throws CalibrationException, LockedException, NotReadyException {
441 
442         if (isLocked()) {
443             throw new LockedException();
444         }
445         if (!isReady()) {
446             throw new NotReadyException();
447         }
448 
449         locked = true;
450 
451         homographyQualityScoresRequired = (distortionEstimator.getMethod() == RobustEstimatorMethod.PROSAC
452                 || distortionEstimator.getMethod() == RobustEstimatorMethod.PROMEDS);
453 
454         if (listener != null) {
455             listener.onCalibrateStart(this);
456         }
457 
458         reset();
459         radialDistortionProgress = fittingProgress = previousNotifiedProgress = 0.0f;
460 
461         final var idealFallbackPatternMarkers = pattern.getIdealPoints();
462 
463         try {
464             // estimate intrinsic parameters
465             estimateIntrinsicParameters(idealFallbackPatternMarkers);
466 
467             if (estimateRadialDistortion) {
468                 // estimate radial distortion
469                 estimateRadialDistortion(idealFallbackPatternMarkers);
470             }
471 
472             if (listener != null) {
473                 listener.onCalibrateEnd(this);
474             }
475         } finally {
476             locked = false;
477         }
478     }
479 
480     /**
481      * Returns the camera calibrator method used by this instance.
482      *
483      * @return the camera calibrator method.
484      */
485     @Override
486     public CameraCalibratorMethod getMethod() {
487         return CameraCalibratorMethod.ERROR_OPTIMIZATION;
488     }
489 
490     /**
491      * Notifies progress to current listener, if needed.
492      */
493     @Override
494     protected void notifyProgress() {
495         final float progress;
496         if (estimateInitialRadialDistortion) {
497             progress = (radialDistortionProgress + intrinsicProgress + fittingProgress) / 3.0f;
498         } else {
499             progress = 0.5f * intrinsicProgress + 0.5f * fittingProgress;
500         }
501 
502         if (listener != null && (progress - previousNotifiedProgress) > progressDelta) {
503             listener.onCalibrateProgressChange(this, progress);
504             previousNotifiedProgress = progress;
505         }
506     }
507 
508     /**
509      * Estimates radial distortion by minimizing the re-projection error by
510      * adjusting the camera pose and radial distortion parameters using an
511      * optimization algorithm.
512      * The initial solution for the optimization algorithm is the estimated
513      * camera pose and intrinsic parameters without accounting for radial
514      * distortion and radial distortion parameters equal to 0.0.
515      *
516      * @param idealFallbackPatternMarkers ideal pattern markers coordinates.
517      *                                    These coordinates are used as fallback when a given sample
518      *                                    does not have an associated pattern.
519      * @return average re-projection error, obtained after projecting ideal
520      * pattern markers using estimated camera poses and then doing a comparison
521      * with sampled points taking into account estimated distortion to undo
522      * their corresponding distortion.
523      * @throws CalibrationException if anything fails.
524      */
525     protected double estimateRadialDistortion(final List<Point2D> idealFallbackPatternMarkers)
526             throws CalibrationException {
527 
528         radialDistortionProgress = 0.0f;
529 
530         if (listener != null) {
531             listener.onRadialDistortionEstimationStarts(this);
532         }
533 
534         // compute total points for samples where homography could be estimated
535         var totalPoints = 0;
536         var totalHomographies = 0;
537         for (final var sample : samples) {
538             if (sample.getHomography() != null) {
539                 totalPoints += sample.getSampledMarkers().size();
540                 totalHomographies++;
541             }
542         }
543 
544         indexToView = new int[totalPoints];
545 
546         if (estimateInitialRadialDistortion) {
547             final var distortedPoints = new ArrayList<Point2D>();
548             final var undistortedPoints = new ArrayList<Point2D>();
549 
550             double[] qualityScores = null;
551             if (distortionMethod == RobustEstimatorMethod.PROSAC || distortionMethod == RobustEstimatorMethod.PROMEDS) {
552                 qualityScores = new double[totalPoints];
553             }
554 
555             // estimate camera pose for each sample
556             var pointCounter = 0;
557             var sampleCounter = 0;
558             for (final var sample : samples) {
559                 if (sample.getHomography() == null) {
560                     // homography computation failed, so we cannot compute camera
561                     // pose for this sample
562                     continue;
563                 }
564                 sample.computeCameraPose(intrinsic);
565 
566                 // transform ideal pattern markers using estimated homography
567                 final List<Point2D> idealPatternMarkers;
568                 if (sample.getPattern() != null) {
569                     // use points generated by pattern in sample
570                     idealPatternMarkers = sample.getPattern().getIdealPoints();
571                 } else {
572                     // use fallback pattern points
573                     idealPatternMarkers = idealFallbackPatternMarkers;
574                 }
575 
576                 final var transformedIdealPatternMarkers = sample.getHomography().transformPointsAndReturnNew(
577                         idealPatternMarkers);
578 
579                 distortedPoints.addAll(sample.getSampledMarkers());
580                 undistortedPoints.addAll(transformedIdealPatternMarkers);
581 
582                 final var markersSize = transformedIdealPatternMarkers.size();
583 
584                 // fills array indicating to which sample (i.e. view) each point
585                 // belongs to
586                 Arrays.fill(indexToView, pointCounter, pointCounter + markersSize, sampleCounter);
587 
588                 // if distortion estimator requires quality scores, set them
589                 if (qualityScores != null && (distortionMethod == RobustEstimatorMethod.PROSAC
590                         || distortionMethod == RobustEstimatorMethod.PROMEDS)) {
591 
592                     final var sampleQuality = homographyQualityScores[sampleCounter];
593 
594                     // assign to all points (markers) in the sample the same sample
595                     // quality
596                     for (var i = pointCounter; i < pointCounter + markersSize; i++) {
597                         qualityScores[i] = sampleQuality;
598                     }
599 
600                     pointCounter += markersSize;
601                     sampleCounter++;
602                 }
603             }
604 
605             // estimate radial distortion
606             try {
607                 distortionEstimator.setIntrinsic(intrinsic);
608                 distortionEstimator.setPoints(distortedPoints, undistortedPoints);
609                 distortionEstimator.setQualityScores(qualityScores);
610 
611                 distortion = distortionEstimator.estimate();
612             } catch (final Exception e) {
613                 throw new CalibrationException(e);
614             }
615         } else {
616 
617             // estimate camera pose for each sample
618             for (final var sample : samples) {
619                 if (sample.getHomography() == null) {
620                     // homography computation failed, so we cannot compute camera
621                     // pose for this sample
622                     continue;
623                 }
624                 sample.computeCameraPose(intrinsic);
625             }
626 
627             // set initial radial distortion as if there was no distortion
628             distortion = new RadialDistortion(0.0, 0.0);
629         }
630 
631         // optimize cost function to refine camera poses and radial distortion
632 
633         try {
634             // compute initial parameters to fit a function using
635             // Levenberg-Marquardt
636             final var initParams = new double[numParameters(totalHomographies)];
637             paramsFromData(initParams);
638             // compute x data (input points)
639             final var x = dataXToMatrix(idealFallbackPatternMarkers);
640             // compute y data (output points)
641             final var y = dataYToMatrix();
642 
643             // Evaluator for Levenberg-Marquardt fitter. This is in charge of
644             // evaluating function to be fitted using current parameters and
645             // also in charge of estimating function Jacobian for each point
646             // where the function is evaluated
647             final var evaluator = new LevenbergMarquardtMultiVariateFunctionEvaluator() {
648 
649                 // position of current point being evaluated
650                 private int i;
651 
652                 // current point being evaluated
653                 private double[] point;
654 
655                 // Instance in charge of estimating Jacobian of function being
656                 // fitted at current point and for provided parameters. Jacobian
657                 // is computed by keeping point constant and computing the
658                 // partial derivatives for each parameter
659                 private final JacobianEstimator jacobianEstimator = new JacobianEstimator(
660                         new MultiVariateFunctionEvaluatorListener() {
661 
662                             // We provide params so that jacobian is computed as the
663                             // partial derivatives respect parameters
664                             @Override
665                             public void evaluate(final double[] params, final double[] result) {
666                                 evaluateFunction(i, point, params, result);
667                             }
668 
669                             // Function being fitted is multi variate returning 2D points
670                             // (having horizontal and vertical inhomogeneous coordinates)
671                             @Override
672                             public int getNumberOfVariables() {
673                                 return Point2D.POINT2D_INHOMOGENEOUS_COORDINATES_LENGTH;
674                             }
675                         });
676 
677                 // Function being fitted has as input data 2D points (having
678                 // horizontal and vertical inhomogeneous coordinates)
679                 @Override
680                 public int getNumberOfDimensions() {
681                     return Point2D.POINT2D_INHOMOGENEOUS_COORDINATES_LENGTH;
682                 }
683 
684                 // Function being fitted is multi variate returning 2D points
685                 // (having horizontal and vertical inhomogeneous coordinates)
686                 @Override
687                 public int getNumberOfVariables() {
688                     return Point2D.POINT2D_INHOMOGENEOUS_COORDINATES_LENGTH;
689                 }
690 
691                 // Creates array where parameters are stored. This array is
692                 // initialized with the parameter values initially used by the
693                 // Levenberg-Marquardt algorithm
694                 @Override
695                 public double[] createInitialParametersArray() {
696                     return initParams;
697                 }
698 
699                 // Evaluates function to be fitted and computes Jacobian
700                 @Override
701                 public void evaluate(final int i, final double[] point, final double[] result, final double[] params,
702                                      final Matrix jacobian) throws EvaluationException {
703                     this.i = i;
704                     this.point = point;
705                     evaluateFunction(this.i, this.point, params, result);
706                     jacobianEstimator.jacobian(params, jacobian);
707                 }
708             };
709 
710             // fits function
711             final var sigma = 1.0;
712             final var fitter = new LevenbergMarquardtMultiVariateFitter(evaluator, x, y, sigma);
713             fitter.setItmax(levenbergMarquardtMaxIters);
714             fitter.setTol(levenbergMarquardtTolerance);
715 
716             final var estimatedParams = fitter.getA();
717             // updates camera poses from estimated parameters
718             dataFromParams(estimatedParams);
719 
720             // computes re-projection errors between sampled and ideal data using
721             // fitted parameters
722             final var error = computeReprojectionError(idealFallbackPatternMarkers);
723 
724             if (listener != null) {
725                 listener.onRadialDistortionEstimationEnds(this, distortion);
726             }
727 
728             return error;
729 
730         } catch (Exception e) {
731             throw new CalibrationException(e);
732         }
733     }
734 
735     /**
736      * Refreshes listener of distortion estimator when robust estimator method
737      * is changed for the distortion estimator.
738      */
739     protected void refreshDistortionEstimatorListener() {
740         if (distortionEstimatorListener == null) {
741             distortionEstimatorListener = new RadialDistortionRobustEstimatorListener() {
742 
743                 @Override
744                 public void onEstimateStart(final RadialDistortionRobustEstimator estimator) {
745                     radialDistortionProgress = 0.0f;
746                     notifyProgress();
747                 }
748 
749                 @Override
750                 public void onEstimateEnd(final RadialDistortionRobustEstimator estimator) {
751                     radialDistortionProgress = 1.0f;
752                     notifyProgress();
753                 }
754 
755                 @Override
756                 public void onEstimateNextIteration(
757                         final RadialDistortionRobustEstimator estimator, final int iteration) {
758                     // not used
759                 }
760 
761                 @Override
762                 public void onEstimateProgressChange(
763                         final RadialDistortionRobustEstimator estimator, final float progress) {
764                     radialDistortionProgress = progress;
765                     notifyProgress();
766                 }
767             };
768         }
769 
770         try {
771             distortionEstimator.setListener(distortionEstimatorListener);
772         } catch (final LockedException e) {
773             Logger.getLogger(AlternatingCameraCalibrator.class.getName()).log(Level.WARNING,
774                     "Could not set radial distortion estimator listener", e);
775         }
776     }
777 
778     /**
779      * Evaluates provided point and parameters to obtain the distorted points
780      * that would be obtained. Levenberg-Marquardt algorithm will iteratively
781      * change parameters until the obtained result approximates sampled Y data
782      *
783      * @param i      index of point among all provided data.
784      * @param point  input point to be evaluated.
785      * @param params parameters to evaluate function.
786      * @param result result of evaluation.
787      */
788     private void evaluateFunction(final int i, final double[] point, final double[] params, final double[] result) {
789         // set data from current params (updates camera poses for each sample -
790         // i.e. view)
791         dataFromParams(params);
792 
793         final var numView = indexToView[i];
794 
795         // obtain camera pose for numView
796         final var sample = samples.get(numView);
797         final var camera = sample.getCamera();
798 
799         final var idealPoint3D = new HomogeneousPoint3D();
800         // ideal 3D point is the marker point assumed to be at plane z = 0
801         idealPoint3D.setInhomogeneousCoordinates(point[0], point[1], 0.0);
802 
803         // project ideal point using estimated camera
804         final var undistortedPoint = camera.project(idealPoint3D);
805 
806         // add distortion
807         final var distortedPoint = new HomogeneousPoint2D();
808         distortion.distort(undistortedPoint, distortedPoint);
809 
810         result[0] = distortedPoint.getInhomX();
811         result[1] = distortedPoint.getInhomY();
812     }
813 
814     /**
815      * Converts undistorted points corresponding to ideal pattern markers into
816      * a matrix to be used by Levenberg-Marquardt as the input data to be used
817      * by the function being fitted.
818      *
819      * @param idealFallbackPatternMarkers ideal pattern markers coordinates.
820      *                                    These coordinates are used as fallback when a given sample
821      *                                    does not have an associated pattern.
822      * @return a matrix.
823      * @throws WrongSizeException if no undistorted points are available.
824      */
825     private Matrix dataXToMatrix(final List<Point2D> idealFallbackPatternMarkers) throws WrongSizeException {
826         final var idealPoints = new ArrayList<Point2D>();
827         for (final var sample : samples) {
828             final List<Point2D> idealPatternMarkers;
829             if (sample.getPattern() != null) {
830                 // use points generated by pattern in sample
831                 idealPatternMarkers = sample.getPattern().getIdealPoints();
832             } else {
833                 // use fallback pattern points
834                 idealPatternMarkers = idealFallbackPatternMarkers;
835             }
836 
837             idealPoints.addAll(idealPatternMarkers);
838         }
839 
840         final var nPoints = idealPoints.size();
841 
842         final var m = new Matrix(nPoints, Point2D.POINT2D_INHOMOGENEOUS_COORDINATES_LENGTH);
843         var i = 0;
844         for (final var sample : samples) {
845             final List<Point2D> idealPatternMarkers;
846             if (sample.getPattern() != null) {
847                 // use points generated by pattern in sample
848                 idealPatternMarkers = sample.getPattern().getIdealPoints();
849             } else {
850                 // use fallback pattern points
851                 idealPatternMarkers = idealFallbackPatternMarkers;
852             }
853 
854             for (final var point : idealPatternMarkers) {
855                 m.setElementAt(i, 0, point.getInhomX());
856                 m.setElementAt(i, 1, point.getInhomY());
857                 i++;
858             }
859         }
860 
861         return m;
862     }
863 
864     /**
865      * Converts sampled distorted points into a matrix to be used by
866      * Levenberg-Marquardt algorithm as the sampled function evaluations.
867      *
868      * @return a matrix.
869      * @throws WrongSizeException if no sampled points are available.
870      */
871     private Matrix dataYToMatrix() throws WrongSizeException {
872         var nPoints = 0;
873         for (final var sample : samples) {
874             nPoints += sample.getSampledMarkers().size();
875         }
876 
877         final var m = new Matrix(nPoints, Point2D.POINT2D_INHOMOGENEOUS_COORDINATES_LENGTH);
878         var i = 0;
879         for (final var sample : samples) {
880             for (final var point : sample.getSampledMarkers()) {
881                 m.setElementAt(i, 0, point.getInhomX());
882                 m.setElementAt(i, 1, point.getInhomY());
883                 i++;
884             }
885         }
886 
887         return m;
888     }
889 
890     /**
891      * Sets parameters of function to be fitted using Levenberg-Marquardt
892      * algorithm.
893      * These parameters will be used as an initial solution and on each
894      * iteration of the Levenberg-Marquardt algorithm.
895      *
896      * @param params arrays where parameters will be set using current data.
897      */
898     private void paramsFromData(final double[] params) {
899         var pos = 0;
900 
901         // common parameters (intrinsic camera parameters and radial distortion
902         // parameters)
903 
904         // intrinsic parameters
905         if (!isZeroSkewness()) {
906             // aspect ratio is not known (2 different focal distances) and
907             // skewness is not zero
908             params[pos] = intrinsic.getHorizontalFocalLength();
909             pos++;
910             params[pos] = intrinsic.getVerticalFocalLength();
911             pos++;
912             params[pos] = intrinsic.getSkewness();
913             pos++;
914         } else {
915             // skewness is always zero (so it is not stored in vector)
916             params[pos] = intrinsic.getHorizontalFocalLength();
917             pos++;
918 
919             if (!isFocalDistanceAspectRatioKnown()) {
920                 // focal distances are different, so we also store vertical
921                 // one
922                 params[pos] = intrinsic.getVerticalFocalLength();
923                 pos++;
924             }
925         }
926 
927         if (!isPrincipalPointAtOrigin()) {
928             // principal point is not zero
929             params[pos] = intrinsic.getHorizontalPrincipalPoint();
930             pos++;
931             params[pos] = intrinsic.getVerticalPrincipalPoint();
932             pos++;
933         }
934 
935         // radial distortion parameters
936         final var kParams = distortion.getKParams();
937         for (final var kParam : kParams) {
938             params[pos] = kParam;
939             pos++;
940         }
941 
942 
943         // parameters for each sample (camera rotation and translation)
944         for (final var sample : samples) {
945             if (sample.getHomography() == null) {
946                 continue;
947             }
948 
949             // 4 rotation parameters
950             AxisRotation3D rot;
951             if (sample.getRotation().getType() == Rotation3DType.AXIS_ROTATION3D) {
952                 rot = (AxisRotation3D) sample.getRotation();
953             } else {
954                 rot = new AxisRotation3D(sample.getRotation());
955             }
956 
957             params[pos] = rot.getRotationAngle();
958             pos++;
959             params[pos] = rot.getAxisX();
960             pos++;
961             params[pos] = rot.getAxisY();
962             pos++;
963             params[pos] = rot.getAxisZ();
964             pos++;
965 
966             // 3 translation parameters (camera center)
967             final var center = sample.getCameraCenter();
968             params[pos] = center.getInhomX();
969             pos++;
970             params[pos] = center.getInhomY();
971             pos++;
972             params[pos] = center.getInhomZ();
973             pos++;
974         }
975     }
976 
977     /**
978      * Sets data in samples from parameters values fitted by the
979      * Levenberg-Marquardt algorithm.
980      *
981      * @param params vector containing estimated parameters .
982      */
983     private void dataFromParams(final double[] params) {
984         var pos = 0;
985 
986         // intrinsic parameters
987         double horizontalFocalLength;
988         double verticalFocalLength;
989         double skewness;
990         double horizontalPrincipalPoint;
991         double verticalPrincipalPoint;
992         if (!isZeroSkewness()) {
993             // aspect ratio is not known (2 different focal distances) and
994             // skewness is not zero
995             horizontalFocalLength = params[pos];
996             pos++;
997             verticalFocalLength = params[pos];
998             pos++;
999             skewness = params[pos];
1000             pos++;
1001         } else {
1002             // skewness is always zero (so it is not stored in vector)
1003             skewness = 0.0;
1004             horizontalFocalLength = params[pos];
1005             pos++;
1006 
1007             if (!isFocalDistanceAspectRatioKnown()) {
1008                 // focal distances are different
1009                 verticalFocalLength = params[pos];
1010                 pos++;
1011             } else {
1012                 // vertical focal distance is related to horizontal one
1013                 // through aspect ratio
1014                 verticalFocalLength = horizontalFocalLength * getFocalDistanceAspectRatio();
1015             }
1016         }
1017 
1018         if (!isPrincipalPointAtOrigin()) {
1019             // principal point is not zero
1020             horizontalPrincipalPoint = params[pos];
1021             pos++;
1022             verticalPrincipalPoint = params[pos];
1023             pos++;
1024         } else {
1025             // principal point is zero
1026             horizontalPrincipalPoint = verticalPrincipalPoint = 0.0;
1027         }
1028 
1029         // update intrinsic parameters
1030         intrinsic.setHorizontalFocalLength(horizontalFocalLength);
1031         intrinsic.setVerticalFocalLength(verticalFocalLength);
1032         intrinsic.setSkewness(skewness);
1033         intrinsic.setHorizontalPrincipalPoint(horizontalPrincipalPoint);
1034         intrinsic.setVerticalPrincipalPoint(verticalPrincipalPoint);
1035 
1036         // radial distortion parameters
1037         final var kParams = distortion.getKParams();
1038         for (var i = 0; i < kParams.length; i++) {
1039             kParams[i] = params[pos];
1040             pos++;
1041         }
1042 
1043         // sample parameters
1044         for (final var sample : samples) {
1045             if (sample.getHomography() == null) {
1046                 continue;
1047             }
1048 
1049             // 4 rotation parameters
1050             final AxisRotation3D rot;
1051             if (sample.getRotation().getType() == Rotation3DType.AXIS_ROTATION3D) {
1052                 rot = (AxisRotation3D) sample.getRotation();
1053             } else {
1054                 rot = new AxisRotation3D();
1055                 // update sample
1056                 sample.setRotation(rot);
1057             }
1058 
1059             final var rotAngle = params[pos];
1060             pos++;
1061             final var axisX = params[pos];
1062             pos++;
1063             final var axisY = params[pos];
1064             pos++;
1065             final var axisZ = params[pos];
1066             pos++;
1067 
1068             rot.setAxisAndRotation(axisX, axisY, axisZ, rotAngle);
1069 
1070             // 3 translation parameters (camera center)
1071             final var inhomX = params[pos];
1072             pos++;
1073             final var inhomY = params[pos];
1074             pos++;
1075             final var inhomZ = params[pos];
1076             pos++;
1077             final var center = sample.getCameraCenter();
1078             center.setInhomogeneousCoordinates(inhomX, inhomY, inhomZ);
1079 
1080             // update camera
1081             final var camera = sample.getCamera();
1082             camera.setIntrinsicAndExtrinsicParameters(intrinsic, rot, center);
1083         }
1084     }
1085 
1086     /**
1087      * Computes re-projection error taking into account ideal pattern marker
1088      * points, transforming them using estimated homography, adding to them
1089      * distortion and comparing them with sampled points.
1090      *
1091      * @param idealFallbackPatternMarkers ideal 2D pattern marker points used
1092      *                                    as fallback in case that a given sample does not have an
1093      *                                    associated pattern.
1094      * @return average re-projection error.
1095      */
1096     private double computeReprojectionError(final List<Point2D> idealFallbackPatternMarkers) {
1097         // distorted points are the sampled points
1098         // undistorted points are the ideal pattern marker points projected
1099         // using current camera pose
1100         PinholeCamera camera;
1101         Point2D marker2D;
1102         final var marker3D = Point3D.create();
1103         final var undistortedPoint = Point2D.create();
1104         var totalPoints = 0;
1105         final var distortedPoint = Point2D.create();
1106         Point2D sampledPoint;
1107         var avgError = 0.0;
1108         for (final var sample : samples) {
1109             camera = sample.getCamera();
1110             if (camera == null) {
1111                 continue;
1112             }
1113 
1114             final List<Point2D> idealPatternMarkers;
1115             if (sample.getPattern() != null) {
1116                 idealPatternMarkers = sample.getPattern().getIdealPoints();
1117             } else {
1118                 idealPatternMarkers = idealFallbackPatternMarkers;
1119             }
1120 
1121             final var pointsPerSample = idealPatternMarkers.size();
1122             for (var i = 0; i < pointsPerSample; i++) {
1123                 marker2D = idealPatternMarkers.get(i);
1124                 sampledPoint = sample.getSampledMarkers().get(i);
1125 
1126                 // convert ideal marker point into a 3D point placed in plane
1127                 // z = 0
1128                 marker3D.setInhomogeneousCoordinates(marker2D.getInhomX(), marker2D.getInhomY(), 0.0);
1129 
1130                 // project 3D marker point using estimated camera on current
1131                 // sample (i.e. view)
1132                 camera.project(marker3D, undistortedPoint);
1133 
1134                 // add distortion to ideal projected point
1135                 distortion.distort(undistortedPoint, distortedPoint);
1136 
1137                 // obtain distance between sampled point and ideal projected
1138                 // point with added distortion
1139                 avgError += sampledPoint.distanceTo(distortedPoint);
1140                 totalPoints++;
1141             }
1142         }
1143 
1144         if (totalPoints == 0) {
1145             avgError = Double.MAX_VALUE;
1146         } else {
1147             avgError /= totalPoints;
1148         }
1149 
1150         return avgError;
1151     }
1152 
1153     /**
1154      * Sets robust estimator method to be used for radial distortion estimation.
1155      * If method changes, then a new radial distortion estimator is created and
1156      * configured.
1157      *
1158      * @param distortionMethod robust estimator method to be used for
1159      *                         radial distortion estimation.
1160      */
1161     private void internalSetDistortionMethod(RobustEstimatorMethod distortionMethod) {
1162         // if method changes, recreate estimator
1163         if (distortionMethod != this.distortionMethod) {
1164             final var previousAvailable = this.distortionMethod != null;
1165             var threshold = 0.0;
1166             var confidence = 0.0;
1167             var maxIterations = 0;
1168             if (previousAvailable) {
1169                 threshold = getDistortionEstimatorThreshold();
1170                 confidence = getDistortionEstimatorConfidence();
1171                 maxIterations = getDistortionEstimatorMaxIterations();
1172             }
1173 
1174             distortionEstimator = RadialDistortionRobustEstimator.create(distortionMethod);
1175 
1176             // configure new estimator
1177             refreshDistortionEstimatorListener();
1178             if (previousAvailable) {
1179                 try {
1180                     setDistortionEstimatorThreshold(threshold);
1181                     setDistortionEstimatorConfidence(confidence);
1182                     setDistortionEstimatorMaxIterations(maxIterations);
1183                 } catch (final LockedException e) {
1184                     Logger.getLogger(AlternatingCameraCalibrator.class.getName()).log(Level.WARNING,
1185                             "Could not reconfigure distortion estimator", e);
1186                 }
1187             }
1188         }
1189 
1190         this.distortionMethod = distortionMethod;
1191     }
1192 
1193     /**
1194      * Returns number of parameters of cost function.
1195      *
1196      * @param numHomographies number of valid estimated homographies.
1197      * @return number of parameters of cost function.
1198      */
1199     private int numParameters(final int numHomographies) {
1200         // For each homography there are:
1201         // - 4 rotation parameters (angle and axis coordinates x, y, z)
1202         // - 3 translation parameters
1203         // - x intrinsic parameters (depending on settings)
1204         // - x radial distortion parameters (K params length)
1205 
1206         return 7 * numHomographies + numIntrinsicParameters() + distortion.getKParams().length;
1207     }
1208 
1209     /**
1210      * Returns number of intrinsic parameters to be taken into account in
1211      * cost function.
1212      *
1213      * @return number of intrinsic parameters to be taken into account in
1214      * cost function.
1215      */
1216     private int numIntrinsicParameters() {
1217         // if no constraints, there are 5 intrinsic parameters (horizontal
1218         // focal length, vertical focal length, skewness, horizontal principal
1219         // point and vertical principal point
1220         var num = 5;
1221         if (isZeroSkewness()) {
1222             if (isFocalDistanceAspectRatioKnown()) {
1223                 num--;
1224             }
1225             num--;
1226         }
1227         if (isPrincipalPointAtOrigin()) {
1228             num -= 2;
1229         }
1230 
1231         return num;
1232     }
1233 }