CameraCalibrator.java

/*
 * Copyright (C) 2015 Alberto Irurueta Carro (alberto@irurueta.com)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.irurueta.ar.calibration;

import com.irurueta.ar.calibration.estimators.ImageOfAbsoluteConicRobustEstimator;
import com.irurueta.ar.calibration.estimators.ImageOfAbsoluteConicRobustEstimatorListener;
import com.irurueta.ar.calibration.estimators.LMedSImageOfAbsoluteConicRobustEstimator;
import com.irurueta.ar.calibration.estimators.MSACImageOfAbsoluteConicRobustEstimator;
import com.irurueta.ar.calibration.estimators.PROMedSImageOfAbsoluteConicRobustEstimator;
import com.irurueta.ar.calibration.estimators.PROSACImageOfAbsoluteConicRobustEstimator;
import com.irurueta.ar.calibration.estimators.RANSACImageOfAbsoluteConicRobustEstimator;
import com.irurueta.geometry.GeometryException;
import com.irurueta.geometry.HomogeneousPoint2D;
import com.irurueta.geometry.PinholeCameraIntrinsicParameters;
import com.irurueta.geometry.Point2D;
import com.irurueta.geometry.Transformation2D;
import com.irurueta.geometry.estimators.*;
import com.irurueta.numerical.NumericalException;
import com.irurueta.numerical.robust.RobustEstimatorMethod;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Calibrates a camera in order to find its intrinsic parameters and other
 * parameters such as radial distortion.
 */
public abstract class CameraCalibrator {

    /**
     * Default robust estimator method to be used for homography estimations.
     */
    public static final RobustEstimatorMethod DEFAULT_HOMOGRAPHY_METHOD = RobustEstimatorMethod.PROSAC;

    /**
     * Default robust estimator method to be used for IAC estimation.
     */
    public static final RobustEstimatorMethod DEFAULT_IAC_METHOD = RobustEstimatorMethod.PROSAC;

    /**
     * Indicates whether radial distortion must be estimated or not by default.
     */
    public static final boolean DEFAULT_ESTIMATE_RADIAL_DISTORTION = true;

    /**
     * Default amount of progress variation before notifying a change in
     * estimation progress. By default, this is set to 5%.
     */
    public static final float DEFAULT_PROGRESS_DELTA = 0.05f;

    /**
     * Minimum allowed value for progress delta.
     */
    public static final float MIN_PROGRESS_DELTA = 0.0f;

    /**
     * Maximum allowed value for progress delta.
     */
    public static final float MAX_PROGRESS_DELTA = 1.0f;

    /**
     * Default method used for camera calibration.
     * The default method uses an alternating technique where first intrinsic
     * parameters and camera pose are estimated without accounting for
     * distortion and then the results are used to obtain an initial guess
     * for distortion, which is then used to correct initially sampled points
     * and repeat the whole process until convergence is achieved.
     */
    public static final CameraCalibratorMethod DEFAULT_METHOD = CameraCalibratorMethod.ERROR_OPTIMIZATION;

    /**
     * Pattern used for camera calibration. Each pattern contains a unique
     * combination of 2D points that must be sampled using the camera to be
     * calibrated.
     */
    protected Pattern2D pattern;

    /**
     * List of samples obtained from different pictures using the same camera
     * device (or same camera model). Several samples can be used to calibrate
     * the camera. The more samples are used, typically the better the results.
     */
    protected List<CameraCalibratorSample> samples;

    /**
     * Quality scores for samples. This can be used on certain
     * robust estimation methods of the IAC such as PROSAC and PROMedS.
     * If not provided, homography quality scores will be estimated based on
     * re-projection error and this value will be ignored.
     * Typically, this will not be provided, but it can be used in case that it
     * can be assured by some means that one sample is better than another.
     */
    protected double[] samplesQualityScores;

    /**
     * Estimated homographies from provided list of samples respect to provided
     * pattern.
     */
    protected List<Transformation2D> homographies;

    /**
     * Quality scores for estimated homographies to be used during IAC
     * estimation when PROSAC or PROMedS robust method is used. This value
     * is only computed when no samples quality scores are provided.
     * Homography quality scores are obtained based on re-projection error of
     * marker coordinates.
     */
    protected double[] homographyQualityScores;

    /**
     * Indicates whether homography quality scores need to be estimated if
     * samples quality scores are not provided.
     */
    protected boolean homographyQualityScoresRequired;

    /**
     * Estimated image of absolute conic. This can be used to obtain intrinsic
     * pinhole camera intrinsic parameters.
     */
    protected ImageOfAbsoluteConic iac;

    /**
     * Estimated intrinsic pinhole camera parameters. Intrinsic parameters
     * contain data related to the camera sensor such as focal length,
     * skewness or principal point. Except focal length, typically intrinsic
     * parameters are fixed, and even in some situations such as when camera
     * lens is fixed, focal length also remains constant. Because the latter is
     * true in most phone cameras, it can be considered that intrinsic
     * parameters remain constant for all phones of the same maker and model,
     * and for that reason a calibrator can be used with pictures taken from
     * different phones as long as they are the same phone model.
     */
    protected PinholeCameraIntrinsicParameters intrinsic;

    /**
     * Estimated radial distortion. Radial distortion is inherent to the camera
     * lens, and remains constant as long as the lens doesn't change.
     * Because in most phone cameras the lens remains constant, lens distortion
     * can be modeled once for each phone model, and for that reason a single
     * calibrator can be used with pictures taken from different phones as long
     * as they are the same phone model.
     */
    protected RadialDistortion distortion;

    /**
     * Indicates whether radial distortion must be estimated or not.
     */
    protected boolean estimateRadialDistortion;

    /**
     * Robust estimator method to be used during homography estimation.
     * This will only be taken into account if more than 4 markers are detected
     * on a single sample, otherwise no robust method is used and a single LMSE
     * solution for the homography is found.
     */
    protected RobustEstimatorMethod homographyMethod;

    /**
     * Robust estimator method to be used during IAC estimation.
     * This will only be taken into account if more than 1 sample is provided,
     * otherwise no robust method is used and a single LMSE solution for the
     * IAC is found.
     */
    protected RobustEstimatorMethod imageOfAbsoluteConicMethod;

    /**
     * Robust estimator of homographies between sampled markers and ideal
     * pattern markers.
     */
    protected PointCorrespondenceProjectiveTransformation2DRobustEstimator homographyEstimator;

    /**
     * Robust estimator of the Image of Absolute Conic (IAC).
     */
    protected ImageOfAbsoluteConicRobustEstimator iacEstimator;

    /**
     * Listener for homography estimator.
     */
    protected ProjectiveTransformation2DRobustEstimatorListener homographyEstimatorListener;

    /**
     * Listener for image of absolute conic estimator.
     */
    protected ImageOfAbsoluteConicRobustEstimatorListener iacEstimatorListener;

    /**
     * Indicates whether this instance is locked because calibration is in
     * progress.
     */
    protected volatile boolean locked;

    /**
     * Amount of progress variation before notifying a progress change during
     * estimation.
     */
    protected float progressDelta;

    /**
     * Listener to notify when calibration starts, finishes or its progress
     * significantly changes.
     */
    protected CameraCalibratorListener listener;

    /**
     * Indicates progress of homography estimation.
     */
    protected float homographyProgress;

    /**
     * Indicates progress of homography estimation for all samples.
     */
    protected float sampleProgress;

    /**
     * Indicates progress of IAC estimation.
     */
    protected float iacProgress;

    /**
     * Indicates progress of estimation of intrinsic parameters.
     */
    protected float intrinsicProgress;

    /**
     * Constructor.
     */
    protected CameraCalibrator() {
        pattern = null;
        homographies = null;
        homographyQualityScores = null;
        iac = null;
        intrinsic = null;
        distortion = null;
        estimateRadialDistortion = DEFAULT_ESTIMATE_RADIAL_DISTORTION;

        internalSetHomographyMethod(DEFAULT_HOMOGRAPHY_METHOD);
        internalSetImageOfAbsoluteConicMethod(DEFAULT_IAC_METHOD);

        samples = null;
        samplesQualityScores = null;

        progressDelta = DEFAULT_PROGRESS_DELTA;
    }

    /**
     * Constructor.
     *
     * @param pattern 2D pattern to use for calibration.
     * @param samples samples of the pattern taken with the camera to calibrate.
     * @throws IllegalArgumentException if not enough samples are provided.
     */
    protected CameraCalibrator(final Pattern2D pattern, final List<CameraCalibratorSample> samples) {
        this.pattern = pattern;
        homographies = null;
        homographyQualityScores = null;
        iac = null;
        intrinsic = null;
        distortion = null;
        estimateRadialDistortion = DEFAULT_ESTIMATE_RADIAL_DISTORTION;

        internalSetHomographyMethod(DEFAULT_HOMOGRAPHY_METHOD);
        internalSetImageOfAbsoluteConicMethod(DEFAULT_IAC_METHOD);

        internalSetSamples(samples);
        samplesQualityScores = null;

        progressDelta = DEFAULT_PROGRESS_DELTA;
    }

    /**
     * Constructor.
     *
     * @param pattern              2D pattern to use for calibration.
     * @param samples              samples of the pattern taken with the camera to calibrate.
     * @param samplesQualityScores quality scores for each sample.
     * @throws IllegalArgumentException if not enough samples are provided or if
     *                                  both samples and quality scores do not have the same size.
     */
    protected CameraCalibrator(final Pattern2D pattern, final List<CameraCalibratorSample> samples,
                               final double[] samplesQualityScores) {
        if (samples.size() != samplesQualityScores.length) {
            throw new IllegalArgumentException();
        }

        this.pattern = pattern;
        homographies = null;
        homographyQualityScores = null;
        iac = null;
        intrinsic = null;
        distortion = null;
        estimateRadialDistortion = DEFAULT_ESTIMATE_RADIAL_DISTORTION;

        internalSetHomographyMethod(DEFAULT_HOMOGRAPHY_METHOD);
        internalSetImageOfAbsoluteConicMethod(DEFAULT_IAC_METHOD);

        internalSetSamples(samples);
        internalSetSamplesQualityScores(samplesQualityScores);

        progressDelta = DEFAULT_PROGRESS_DELTA;
    }

    /**
     * Returns pattern used for camera calibration. Each pattern contain a
     * unique combination of 2D points that must be sampled using the camera to
     * be calibrated.
     *
     * @return pattern used for camera calibration.
     */
    public Pattern2D getPattern() {
        return pattern;
    }

    /**
     * Sets pattern used for camera calibration. Each pattern contains a unique
     * combination of 2D points that must be sampled using the camera to be
     * calibrated.
     *
     * @param pattern pattern used for camera calibration.
     * @throws LockedException if this instance is locked.
     */
    public void setPattern(final Pattern2D pattern) throws LockedException {
        if (isLocked()) {
            throw new LockedException();
        }

        this.pattern = pattern;
    }

    /**
     * Returns list of samples obtained from different pictures using the same
     * camera device (or same camera model). Several samples can be used to
     * calibrate the camera (a pinhole camera can be estimated for each sample).
     * The more samples are used, typically the better the results.
     *
     * @return list of samples.
     */
    public List<CameraCalibratorSample> getSamples() {
        return samples;
    }

    /**
     * Sets list of samples obtained from different pictures using the same
     * camera device (or same camera model). Several samples can be used to
     * calibrate the camera (a pinhole camera can be estimated for each sample).
     * The more samples are used, typically the better the results.
     *
     * @param samples list of samples.
     * @throws LockedException          if this instance is locked.
     * @throws IllegalArgumentException if not enough samples are provided to
     *                                  estimate the intrinsic parameters. By default, the minimum is 1,
     *                                  but depending on the settings at least 3 samples might be required.
     */
    public void setSamples(final List<CameraCalibratorSample> samples) throws LockedException {
        if (isLocked()) {
            throw new LockedException();
        }
        internalSetSamples(samples);
    }

    /**
     * Returns quality scores assigned to each provided sample. This can be used
     * on certain robust estimation methods of the IAC such as PROSAC and
     * PROMedS. If not provided, homography quality scores will be estimated
     * based on re-projection error and this value will be ignored.
     * Typically, this will not be provided, but it can be used in case that it
     * can be assured by some means that one sample is better than another
     *
     * @return quality scores assigned to each provided sample.
     */
    public double[] getSamplesQualityScores() {
        return samplesQualityScores;
    }

    /**
     * Sets quality scores assigned to each provided sample. This can be used on
     * certain robust estimation methods of the IAC such as PROSAC and PROMedS.
     * If not provided, homography quality scores will be estimated based on
     * re-projection error and this value will be ignored.
     *
     * @param samplesQualityScores quality scores assigned to each provided
     *                             sample.
     * @throws LockedException          if this instance is locked.
     * @throws IllegalArgumentException if not enough quality scores are
     *                                  provided for the corresponding samples to estimate the intrinsic
     *                                  parameters. By default, the minimum is 1, but depending on the
     *                                  settings at least 3 samples might be required.
     */
    public void setSamplesQualityScores(final double[] samplesQualityScores) throws LockedException {
        if (isLocked()) {
            throw new LockedException();
        }
        internalSetSamplesQualityScores(samplesQualityScores);
    }

    /**
     * Returns quality scores for estimated homographies.
     * If samples quality scores were provided, these will be equal
     * to those provided. If no samples quality scores were provided,
     * these scores will be related to the estimated homography
     * estimation error based on sampled data and ideal data.
     * This should rarely be used. It can be used for debugging purposes
     * teo determine whether homographies used for calibration are
     * reliable or not.
     *
     * @return estimated quality scores for homographies.
     */
    public double[] getHomographyQualityScores() {
        return homographyQualityScores;
    }

    /**
     * Returns estimated image of absolute conic. This can be used to obtain
     * intrinsic pinhole camera intrinsic parameters.
     *
     * @return estimated image of absolute conic or null if estimation has not
     * been completed.
     */
    public ImageOfAbsoluteConic getEstimatedImageOfAbsoluteConic() {
        return iac;
    }

    /**
     * Returns estimated pinhole camera intrinsic parameters.
     *
     * @return estimated pinhole camera intrinsic parameters or null if
     * estimation has not been completed.
     */
    public PinholeCameraIntrinsicParameters getEstimatedIntrinsicParameters() {
        return intrinsic;
    }

    /**
     * Returns estimated radial distortion due to camera lens.
     *
     * @return estimated radial distortion or null if estimation has not
     * completed or radial distortion was not requested.
     */
    public RadialDistortion getDistortion() {
        return distortion;
    }

    /**
     * Returns boolean indicating whether radial distortion must be estimated or
     * not during calibration.
     *
     * @return true if radial distortion must be estimated, false otherwise.
     */
    public boolean getEstimateRadialDistortion() {
        return estimateRadialDistortion;
    }

    /**
     * Sets boolean indicating whether radial distortion must be estimated or
     * not during calibration.
     *
     * @param estimateRadialDistortion true if radial distortion must be
     *                                 estimated, false otherwise.
     * @throws LockedException if this instance is locked.
     */
    public void setEstimateRadialDistortion(final boolean estimateRadialDistortion) throws LockedException {
        if (isLocked()) {
            throw new LockedException();
        }

        this.estimateRadialDistortion = estimateRadialDistortion;
    }

    /**
     * Returns robust estimator method to be used during homography estimation.
     * This will only be taken into account if more than 4 markers are detected
     * on a single sample, otherwise no robust method is used and a single LMSE
     * solution for the homography is found.
     *
     * @return robust estimator method to be used during homography estimation.
     */
    public RobustEstimatorMethod getHomographyMethod() {
        return homographyMethod;
    }

    /**
     * Sets robust estimator method to be used during homography estimation.
     * This will only be taken into account if more than 4 markers are detected
     * on a single sample, otherwise no robust method is used and a single LMSE
     * solution for the homography is found.
     *
     * @param homographyMethod robust estimator method to be used during
     *                         homography estimation.
     * @throws LockedException if this instance is locked.
     */
    public void setHomographyMethod(final RobustEstimatorMethod homographyMethod) throws LockedException {
        if (isLocked()) {
            throw new LockedException();
        }
        internalSetHomographyMethod(homographyMethod);
    }

    /**
     * Returns robust estimator method to be used during IAC estimation.
     * This will only be taken into account if more than 1 sample is provided,
     * otherwise no robust method is used and a single LMSE solution for the
     * IAC is found
     *
     * @return robust estimator method to be used during IAC estimation
     */
    public RobustEstimatorMethod getImageOfAbsoluteConicMethod() {
        return imageOfAbsoluteConicMethod;
    }

    /**
     * Sets robust estimator method to be used during IAC estimation.
     * This will only be taken into account if more than 1 sample is provided,
     * otherwise no robust method is used and a single LMSE solution for the
     * IAC is found.
     *
     * @param imageOfAbsoluteConicMethod robust estimator method to be used
     *                                   during IAC estimation.
     * @throws LockedException if this instance is locked.
     */
    public void setImageOfAbsoluteConicMethod(final RobustEstimatorMethod imageOfAbsoluteConicMethod)
            throws LockedException {
        if (isLocked()) {
            throw new LockedException();
        }
        internalSetImageOfAbsoluteConicMethod(imageOfAbsoluteConicMethod);
    }

    /**
     * Returns homography estimator, which can be retrieved in case that some
     * additional parameter needed to be adjusted.
     * It is discouraged to directly access the homography estimator during
     * camera calibration, as it might interfere with the results.
     *
     * @return homography estimator.
     */
    public PointCorrespondenceProjectiveTransformation2DRobustEstimator getHomographyEstimator() {
        return homographyEstimator;
    }

    /**
     * Returns IAC estimator, which can be retrieved in case that some
     * additional parameter needed to be adjusted.
     * It is discouraged to directly access the homography estimator during
     * camera calibration, as it might interfere with the results.
     *
     * @return IAC estimator.
     */
    public ImageOfAbsoluteConicRobustEstimator getIACEstimator() {
        return iacEstimator;
    }

    /**
     * Returns boolean indicating whether camera skewness is assumed to be zero
     * or not.
     * Skewness determines whether LCD sensor cells are properly aligned or not,
     * where zero indicates perfect alignment.
     * Typically, skewness is a value equal or very close to zero.
     *
     * @return true if camera skewness is assumed to be zero, otherwise camera
     * skewness is estimated.
     */
    public boolean isZeroSkewness() {
        return iacEstimator.isZeroSkewness();
    }

    /**
     * Sets boolean indicating whether camera skewness is assumed to be zero or
     * not.
     * Skewness determines whether LCD sensor cells are properly aligned or not,
     * where zero indicates perfect alignment.
     * Typically, skewness is a value equal or very close to zero.
     *
     * @param zeroSkewness true if camera skewness is assumed to be zero,
     *                     otherwise camera skewness is estimated.
     * @throws LockedException if this instance is locked.
     */
    public void setZeroSkewness(final boolean zeroSkewness) throws LockedException {
        if (isLocked()) {
            throw new LockedException();
        }

        iacEstimator.setZeroSkewness(zeroSkewness);
    }

    /**
     * Returns boolean indicating whether principal point is assumed to be at
     * origin of coordinates or not.
     * Typically principal point is located at image center (origin of
     * coordinates), and usually matches the center of radial distortion if it
     * is taken into account.
     *
     * @return true if principal point is assumed to be at origin of
     * coordinates, false if principal point must be estimated.
     */
    public boolean isPrincipalPointAtOrigin() {
        return iacEstimator.isPrincipalPointAtOrigin();
    }

    /**
     * Sets boolean indicating whether principal point is assumed to be at
     * origin of coordinates or not.
     * Typically principal point is located at image center (origin of
     * coordinates), and usually matches the center of radial distortion if it
     * is taken into account.
     *
     * @param principalPointAtOrigin true if principal point is assumed to bet
     *                               at origin of coordinates, false if principal point must be estimated.
     * @throws LockedException if estimator is locked.
     */
    public void setPrincipalPointAtOrigin(final boolean principalPointAtOrigin) throws LockedException {
        if (isLocked()) {
            throw new LockedException();
        }

        iacEstimator.setPrincipalPointAtOrigin(principalPointAtOrigin);
    }

    /**
     * Returns boolean indicating whether aspect ratio of focal distances (i.e.
     * vertical focal distance divided by horizontal focal distance) is known or
     * not.
     * Notice that focal distance aspect ratio is not related to image size
     * aspect ratio. Typically, LCD sensor cells are square and hence aspect
     * ratio of focal distances is known and equal to 1.
     * This value is only taken into account if skewness is assumed to be zero,
     * otherwise it is ignored.
     *
     * @return true if focal distance aspect ratio is known, false otherwise.
     */
    public boolean isFocalDistanceAspectRatioKnown() {
        return iacEstimator.isFocalDistanceAspectRatioKnown();
    }

    /**
     * Sets boolean indicating whether aspect ratio of focal distances (i.e.
     * vertical focal distance divided by horizontal focal distance) is known or
     * not.
     * Notice that focal distance aspect ratio is not related to image size
     * aspect ratio. Typically, LCD sensor cells are square and hence aspect
     * ratio of focal distances is known and equal to 1.
     * This value is only taken into account if skewness is assumed to be zero,
     * otherwise it is ignored.
     *
     * @param focalDistanceAspectRatioKnown true if focal distance aspect ratio
     *                                      is known, false otherwise.
     * @throws LockedException if estimator is locked.
     */
    public void setFocalDistanceAspectRatioKnown(final boolean focalDistanceAspectRatioKnown) throws LockedException {
        if (isLocked()) {
            throw new LockedException();
        }

        iacEstimator.setFocalDistanceAspectRatioKnown(focalDistanceAspectRatioKnown);
    }

    /**
     * Returns aspect ratio of focal distances (i.e. vertical focal distance
     * divided by horizontal focal distance).
     * This value is only taken into account if skewness is assumed to be zero
     * and focal distance aspect ratio is marked as known, otherwise it is
     * ignored.
     * By default, this is 1.0, since it is taken into account that typically
     * LCD sensor cells are square and hence aspect ratio focal distances is
     * known and equal to 1.
     * Notice that focal distance aspect ratio is not related to image size
     * aspect ratio.
     * Notice that a negative aspect ratio indicates that vertical axis is
     * reversed. This can be useful in some situations where image vertical
     * coordinates are reversed respect to the physical world (i.e. in computer
     * graphics typically image vertical coordinates go downwards, while in
     * physical world they go upwards).
     *
     * @return aspect ratio of focal distances.
     */
    public double getFocalDistanceAspectRatio() {
        return iacEstimator.getFocalDistanceAspectRatio();
    }

    /**
     * Sets aspect ratio of focal distances (i.e. vertical focal distance
     * divided by horizontal focal distance).
     * This value is only taken into account if skewness is assumed to be zero
     * and focal distance aspect ratio is marked as known, otherwise it is
     * ignored.
     * By default, this is 1.0, since it is taken into account that typically
     * LCD sensor cells are square and hence aspect ratio focal distances is
     * known and equal to 1.
     * Notice that focal distance aspect ratio is not related to image size
     * aspect ratio.
     * Notice that a negative aspect ratio indicates that vertical axis is
     * reversed. This can be useful in some situations where image vertical
     * coordinates are reversed respect to the physical world (i.e. in computer
     * graphics typically image vertical coordinates go downwards, while in
     * physical world they go upwards).
     *
     * @param focalDistanceAspectRatio aspect ratio of focal distances to be set.
     * @throws LockedException          if estimator is locked.
     * @throws IllegalArgumentException if focal distance aspect ratio is too
     *                                  close to zero, as it might produce numerical instabilities.
     */
    public void setFocalDistanceAspectRatio(final double focalDistanceAspectRatio) throws LockedException {
        if (isLocked()) {
            throw new LockedException();
        }

        iacEstimator.setFocalDistanceAspectRatio(focalDistanceAspectRatio);
    }

    /**
     * Indicates whether this instance is locked because calibration is in
     * progress.
     *
     * @return true if this instance, false otherwise.
     */
    public boolean isLocked() {
        return locked;
    }

    /**
     * Returns amount of progress variation before notifying a progress change
     * during estimation.
     *
     * @return amount of progress variation before notifying a progress change
     * during estimation.
     */
    public float getProgressDelta() {
        return progressDelta;
    }

    /**
     * Sets amount of progress variation before notifying a progress change
     * during estimation.
     *
     * @param progressDelta amount of progress variation before notifying a
     *                      progress change during estimation.
     * @throws IllegalArgumentException if progress delta is less than zero or
     *                                  greater than 1.
     * @throws LockedException          if this estimator is locked because an estimation
     *                                  is being computed.
     */
    public void setProgressDelta(final float progressDelta) throws LockedException {
        if (isLocked()) {
            throw new LockedException();
        }
        if (progressDelta < MIN_PROGRESS_DELTA || progressDelta > MAX_PROGRESS_DELTA) {
            throw new IllegalArgumentException();
        }
        this.progressDelta = progressDelta;
    }

    /**
     * Indicates whether this instance is ready to start camera calibration
     *
     * @return true if this instance has enough data to start camera
     * calibration, false otherwise.
     */
    public boolean isReady() {
        return pattern != null && samples != null
                && samples.size() >= iacEstimator.getMinNumberOfRequiredHomographies();
    }

    /**
     * Returns threshold to robustly estimate homographies between ideal pattern
     * markers and sampled pattern markers.
     * Usually the default value is good enough for most situations, but this
     * setting can be changed for finer adjustments
     *
     * @return threshold to robustly estimate homographies between ideal pattern
     * markers and sampled pattern markers.
     */
    public double getHomographyEstimatorThreshold() {
        return switch (homographyEstimator.getMethod()) {
            case LMEDS -> ((LMedSPointCorrespondenceProjectiveTransformation2DRobustEstimator) homographyEstimator)
                    .getStopThreshold();
            case MSAC -> ((MSACPointCorrespondenceProjectiveTransformation2DRobustEstimator) homographyEstimator)
                    .getThreshold();
            case PROSAC -> ((PROSACPointCorrespondenceProjectiveTransformation2DRobustEstimator) homographyEstimator)
                    .getThreshold();
            case PROMEDS -> ((PROMedSPointCorrespondenceProjectiveTransformation2DRobustEstimator) homographyEstimator)
                    .getStopThreshold();
            default -> ((RANSACPointCorrespondenceProjectiveTransformation2DRobustEstimator) homographyEstimator)
                    .getThreshold();
        };
    }

    /**
     * Sets threshold to robustly estimate homographies between ideal pattern
     * markers and sampled pattern markers.
     * Usually the default value is good enough for most situations, but this
     * setting can be changed for finer adjustments.
     *
     * @param homographyEstimatorThreshold threshold to robustly estimate
     *                                     homographies between ideal pattern markers and sampled pattern markers.
     * @throws LockedException          if this instance is locked.
     * @throws IllegalArgumentException if provided value is zero or negative.
     */
    public void setHomographyEstimatorThreshold(final double homographyEstimatorThreshold) throws LockedException {
        if (isLocked()) {
            throw new LockedException();
        }

        switch (homographyEstimator.getMethod()) {
            case LMEDS:
                ((LMedSPointCorrespondenceProjectiveTransformation2DRobustEstimator) homographyEstimator).
                        setStopThreshold(homographyEstimatorThreshold);
                break;
            case MSAC:
                ((MSACPointCorrespondenceProjectiveTransformation2DRobustEstimator) homographyEstimator).
                        setThreshold(homographyEstimatorThreshold);
                break;
            case PROSAC:
                ((PROSACPointCorrespondenceProjectiveTransformation2DRobustEstimator) homographyEstimator).
                        setThreshold(homographyEstimatorThreshold);
                break;
            case PROMEDS:
                ((PROMedSPointCorrespondenceProjectiveTransformation2DRobustEstimator) homographyEstimator).
                        setStopThreshold(homographyEstimatorThreshold);
                break;
            case RANSAC:
            default:
                ((RANSACPointCorrespondenceProjectiveTransformation2DRobustEstimator) homographyEstimator).
                        setThreshold(homographyEstimatorThreshold);
                break;
        }
    }

    /**
     * Returns confidence to robustly estimate homographies between ideal
     * pattern markers and sampled pattern markers.
     * Usually the default value is good enough for most situations, but this
     * setting can be changed for finer adjustments.
     * Confidence is expressed as a value between 0.0 (0%) and 1.0 (100%). The
     * amount of confidence indicates the probability that the estimated
     * homography is correct (i.e. no outliers were used for the estimation,
     * because they were successfully discarded).
     * Typically, this value will be close to 1.0, but not exactly 1.0, because
     * a 100% confidence would require an infinite number of iterations.
     * Usually the default value is good enough for most situations, but this
     * setting can be changed for finer adjustments.
     *
     * @return confidence to robustly estimate homographies.
     */
    public double getHomographyEstimatorConfidence() {
        return homographyEstimator.getConfidence();
    }

    /**
     * Sets confidence to robustly estimate homographies between ideal pattern
     * markers and sampled pattern markers.
     * Usually the default value is good enough for most situations, but this
     * setting can be changed for finer adjustments.
     * Confidence is expressed as a value between 0.0 (0%) and 1.0 (100%). The
     * amount of confidence indicates the probability that the estimated
     * homography is correct (i.e. no outliers were used for the estimation,
     * because they were successfully discarded).
     * Typically, this value will be close to 1.0, but not exactly 1.0, because
     * a 100% confidence would require an infinite number of iterations.
     * Usually the default value is good enough for most situations, but this
     * setting can be changed for finer adjustments.
     *
     * @param homographyEstimatorConfidence confidence to robustly estimate
     *                                      homographies.
     * @throws LockedException          if this instance is locked.
     * @throws IllegalArgumentException if provided value is not between 0.0 and
     *                                  1.0.
     */
    public void setHomographyEstimatorConfidence(final double homographyEstimatorConfidence) throws LockedException {
        if (isLocked()) {
            throw new LockedException();
        }

        homographyEstimator.setConfidence(homographyEstimatorConfidence);
    }

    /**
     * Returns the maximum number of iterations to be done when estimating
     * the homographies between ideal pattern markers and sampled pattern
     * markers.
     * If the maximum allowed number of iterations is reached, resulting
     * estimation might not have desired confidence.
     * Usually the default value is good enough for most situations, but this
     * setting can be changed for finer adjustments.
     *
     * @return maximum number of iterations to be done when estimating the
     * homographies.
     */
    public int getHomographyEstimatorMaxIterations() {
        return homographyEstimator.getMaxIterations();
    }

    /**
     * Sets the maximum number of iterations to be done when estimating the
     * homographies between ideal pattern markers and sampled pattern markers.
     * If the maximum allowed number of iterations is reached, resulting
     * estimation might not have desired confidence.
     * Usually the default value is good enough for most situations, but this
     * setting can be changed for finer adjustments.
     *
     * @param homographyEstimatorMaxIterations maximum number of iterations to
     *                                         be done when estimating the homographies between ideal
     *                                         pattern markers and sampled pattern markers.
     * @throws LockedException          if this instance is locked.
     * @throws IllegalArgumentException if provided value is negative or zero.
     */
    public void setHomographyEstimatorMaxIterations(final int homographyEstimatorMaxIterations) throws LockedException {
        if (isLocked()) {
            throw new LockedException();
        }

        homographyEstimator.setMaxIterations(homographyEstimatorMaxIterations);
    }

    /**
     * Returns threshold to robustly estimate the image of absolute conic.
     * Usually the default value is good enough for most situations, but this
     * setting can be changed for finer adjustments.
     *
     * @return threshold to robustly estimate the image of absolute conic.
     */
    public double getIACEstimatorThreshold() {
        return switch (iacEstimator.getMethod()) {
            case LMEDS -> ((LMedSImageOfAbsoluteConicRobustEstimator) iacEstimator).getStopThreshold();
            case MSAC -> ((MSACImageOfAbsoluteConicRobustEstimator) iacEstimator).getThreshold();
            case PROSAC -> ((PROSACImageOfAbsoluteConicRobustEstimator) iacEstimator).getThreshold();
            case PROMEDS -> ((PROMedSImageOfAbsoluteConicRobustEstimator) iacEstimator).getStopThreshold();
            default -> ((RANSACImageOfAbsoluteConicRobustEstimator) iacEstimator).getThreshold();
        };
    }

    /**
     * Sets threshold to robustly estimate the image of absolute conic.
     * Usually the default value is good enough for most situations, but this
     * setting can be changed for finer adjustments.
     *
     * @param iacEstimatorThreshold threshold to robustly estimate the image of
     *                              absolute conic.
     * @throws LockedException          if this instance is locked.
     * @throws IllegalArgumentException if provided value is zero or negative.
     */
    public void setIACEstimatorThreshold(final double iacEstimatorThreshold) throws LockedException {
        if (isLocked()) {
            throw new LockedException();
        }

        switch (iacEstimator.getMethod()) {
            case LMEDS:
                ((LMedSImageOfAbsoluteConicRobustEstimator) iacEstimator).setStopThreshold(iacEstimatorThreshold);
                break;
            case MSAC:
                ((MSACImageOfAbsoluteConicRobustEstimator) iacEstimator).setThreshold(iacEstimatorThreshold);
                break;
            case PROSAC:
                ((PROSACImageOfAbsoluteConicRobustEstimator) iacEstimator).setThreshold(iacEstimatorThreshold);
                break;
            case PROMEDS:
                ((PROMedSImageOfAbsoluteConicRobustEstimator) iacEstimator).setStopThreshold(iacEstimatorThreshold);
                break;
            case RANSAC:
            default:
                ((RANSACImageOfAbsoluteConicRobustEstimator) iacEstimator).setThreshold(iacEstimatorThreshold);
                break;
        }
    }

    /**
     * Returns confidence to robustly estimate image of absolute conic
     * Usually the default value is good enough for most situations, but this
     * setting can be changed for finer adjustments.
     * Confidence is expressed as a value between 0.0 (0%) and 1.0 (100%). The
     * amount of confidence indicates the probability that the estimated
     * homography is correct (i.e. no outliers were used for the estimation,
     * because they were successfully discarded).
     * Typically, this value will be close to 1.0, but not exactly 1.0, because
     * a 100% confidence would require an infinite number of iterations.
     * Usually the default value is good enough for most situations, but this
     * setting can be changed for finer adjustments.
     *
     * @return confidence to robustly estimate the IAC.
     */
    public double getIACEstimatorConfidence() {
        return iacEstimator.getConfidence();
    }

    /**
     * Sets confidence to robustly estimate image of absolute conic.
     * Usually the default value is good enough for most situations, but this
     * setting can be changed for finer adjustments.
     * Confidence is expressed as a value between 0.0 (0%) and 1.0 (100%). The
     * amount of confidence indicates the probability that the estimated
     * homography is correct (i.e. no outliers were used for the estimation,
     * because they were successfully discarded).
     * Typically, this value will be close to 1.0, but not exactly 1.0, because
     * a 100% confidence would require an infinite number of iterations.
     * Usually the default value is good enough for most situations, but this
     * setting can be changed for finer adjustments.
     *
     * @param iacEstimatorConfidence confidence to robustly estimate the IAC.
     * @throws LockedException          if this instance is locked.
     * @throws IllegalArgumentException if provided value is not between 0.0 and
     *                                  1.0.
     */
    public void setIACEstimatorConfidence(final double iacEstimatorConfidence) throws LockedException {
        if (isLocked()) {
            throw new LockedException();
        }

        iacEstimator.setConfidence(iacEstimatorConfidence);
    }

    /**
     * Returns the maximum number of iterations to be done when estimating
     * the image of absolute conic.
     * If the maximum allowed number of iterations is reached, resulting
     * estimation might not have desired confidence.
     * Usually the default value is good enough for most situations, but this
     * setting can be changed for finer adjustments.
     *
     * @return maximum number of iterations to be done when estimating the
     * image of absolute conic.
     */
    public int getIACEstimatorMaxIterations() {
        return iacEstimator.getMaxIterations();
    }

    /**
     * Sets the maximum number of iterations to be done when estimating the
     * image of absolute conic.
     * If the maximum allowed number of iterations is reached, resulting
     * estimation might not have desired confidence.
     * Usually the default value is good enough for most situations, but this
     * setting can be changed for finer adjustments.
     *
     * @param iacEstimatorMaxIterations maximum number of iterations to be done
     *                                  when estimating the image of absolute conic.
     * @throws LockedException          if this instance is locked.
     * @throws IllegalArgumentException if provided value is negative or zero.
     */
    public void setIACEstimatorMaxIterations(final int iacEstimatorMaxIterations) throws LockedException {
        if (isLocked()) {
            throw new LockedException();
        }

        iacEstimator.setMaxIterations(iacEstimatorMaxIterations);
    }

    /**
     * Returns listener to notify when calibration starts, finishes or its
     * progress significantly changes.
     *
     * @return listener to notify when calibration starts, finishes or its
     * progress significantly changes.
     */
    public CameraCalibratorListener getListener() {
        return listener;
    }

    /**
     * Sets listener to notify when calibration starts, finishes or its progress
     * significantly changes.
     *
     * @param listener listener to notify when calibration starts, finishes or
     *                 its progress significantly changes.
     * @throws LockedException if this instance is locked.
     */
    public void setListener(final CameraCalibratorListener listener) throws LockedException {
        if (isLocked()) {
            throw new LockedException();
        }

        this.listener = listener;
    }

    /**
     * Creates a camera calibrator using provided method.
     *
     * @param method a camera calibrator method.
     * @return a camera calibrator.
     */
    public static CameraCalibrator create(final CameraCalibratorMethod method) {
        return method == CameraCalibratorMethod.ERROR_OPTIMIZATION
                ? new ErrorOptimizationCameraCalibrator() : new AlternatingCameraCalibrator();
    }

    /**
     * Creates a camera calibrator using provided pattern, samples and
     * method.
     *
     * @param pattern a 2D pattern to use for calibration.
     * @param samples samples of the 2D pattern taken with the camera to be
     *                calibrated.
     * @param method  a camera calibrator method.
     * @return a camera calibrator.
     * @throws IllegalArgumentException if not enough samples are provided.
     */
    public static CameraCalibrator create(final Pattern2D pattern, final List<CameraCalibratorSample> samples,
                                          final CameraCalibratorMethod method) {
        return method == CameraCalibratorMethod.ERROR_OPTIMIZATION
                ? new ErrorOptimizationCameraCalibrator(pattern, samples)
                : new AlternatingCameraCalibrator(pattern, samples);
    }

    /**
     * Creates a camera calibrator using provided pattern, samples and method.
     *
     * @param pattern              a 2D pattern to use for calibration.
     * @param samples              samples of the 2D pattern taken with the camera to be
     *                             calibrated.
     * @param samplesQualityScores quality scores for each sample.
     * @param method               a camera calibrator method.
     * @return a camera calibrator.
     * @throws IllegalArgumentException if not enough samples are provided.
     */
    public static CameraCalibrator create(
            final Pattern2D pattern, final List<CameraCalibratorSample> samples, final double[] samplesQualityScores,
            final CameraCalibratorMethod method) {
        return method == CameraCalibratorMethod.ERROR_OPTIMIZATION
                ? new ErrorOptimizationCameraCalibrator(pattern, samples, samplesQualityScores)
                : new AlternatingCameraCalibrator(pattern, samples, samplesQualityScores);
    }

    /**
     * Creates a camera calibrator using default method.
     *
     * @return a camera calibrator.
     */
    public static CameraCalibrator create() {
        return create(DEFAULT_METHOD);
    }

    /**
     * Creates a camera calibrator using provided pattern, samples and
     * default method.
     *
     * @param pattern a 2D pattern to use for calibration.
     * @param samples samples of the 2D pattern taken with the camera to be
     *                calibrated.
     * @return a camera calibrator.
     * @throws IllegalArgumentException if not enough samples are provided.
     */
    public static CameraCalibrator create(final Pattern2D pattern, final List<CameraCalibratorSample> samples) {
        return create(pattern, samples, DEFAULT_METHOD);
    }

    /**
     * Creates a camera calibrator using provided pattern, samples and default
     * method.
     *
     * @param pattern              a 2D pattern to use for calibration.
     * @param samples              samples of the 2D pattern taken with the camera to be
     *                             calibrated.
     * @param samplesQualityScores quality scores for each sample.
     * @return a camera calibrator.
     * @throws IllegalArgumentException if not enough samples are provided.
     */
    public static CameraCalibrator create(
            final Pattern2D pattern, final List<CameraCalibratorSample> samples, final double[] samplesQualityScores) {
        return create(pattern, samples, samplesQualityScores, DEFAULT_METHOD);
    }

    /**
     * Starts the calibration process.
     * Depending on the settings the following will be estimated:
     * intrinsic pinhole camera parameters, radial distortion of lens,
     * camera pose (rotation and translation) for each sample, and the
     * associated homobraphy of sampled points respect to the ideal pattern
     * samples.
     *
     * @throws CalibrationException if calibration fails for some reason.
     * @throws LockedException      if this instance is locked because calibration is
     *                              already in progress.
     * @throws NotReadyException    if this instance does not have enough data to
     *                              start camera calibration.
     */
    public abstract void calibrate() throws CalibrationException, LockedException, NotReadyException;

    /**
     * Returns the camera calibrator method used by this instance.
     *
     * @return the camera calibrator method.
     */
    public abstract CameraCalibratorMethod getMethod();

    /**
     * Notifies progress to current listener, if needed.
     */
    protected abstract void notifyProgress();

    /**
     * Computes intrinsic estimation progress.
     */
    protected void computeIntrinsicProgress() {
        final var lambda = 1.0f / samples.size();
        intrinsicProgress = 0.5f * (sampleProgress + lambda * homographyProgress) + 0.5f * iacProgress;
    }

    /**
     * Resets estimated value to their initial values.
     */
    protected void reset() {
        homographies = null;
        for (final var sample : samples) {
            sample.setUndistortedMarkers(null);
            sample.setHomography(null);
            sample.setRotation(null);
            sample.setCameraCenter(null);
            sample.setCamera(null);
        }
        homographyQualityScores = null;
        iac = null;
        intrinsic = null;
        distortion = null;
    }

    /**
     * Estimates pinhole camera intrinsic parameters without accounting for
     * lens radial distortion.
     *
     * @param idealFallbackPatternMarkers ideal pattern markers coordinates used
     *                                    as fallback if pattern is not provided for a given sample.
     * @throws CalibrationException if calibration fails for some reason.
     */
    protected void estimateIntrinsicParameters(final List<Point2D> idealFallbackPatternMarkers)
            throws CalibrationException {

        homographyProgress = sampleProgress = iacProgress = intrinsicProgress = 0.0f;

        if (listener != null) {
            listener.onIntrinsicParametersEstimationStarts(this);
        }

        // for each sample estimate the homography between the ideal pattern
        // markers and the sampled ones
        List<Point2D> sampledPatternMarkers;
        final var sampleSize = samples.size();
        homographies = new ArrayList<>(sampleSize);
        final var tmpHomographyQualityScores = new double[sampleSize];
        var index = 0;
        var counter = 0;
        double error;
        for (final var sample : samples) {
            try {
                // reset homography before estimation in case that estimation
                // fails on current iteration
                sample.setHomography(null);
                final List<Point2D> idealPatternMarkers;
                if (sample.getPattern() != null) {
                    // use sample pattern for homography estimation
                    idealPatternMarkers = sample.getPattern().getIdealPoints();
                } else {
                    // use fallback pattern common to all samples
                    idealPatternMarkers = idealFallbackPatternMarkers;
                }
                final var homography = sample.estimateHomography(homographyEstimator, idealPatternMarkers);
                sampledPatternMarkers = sample.getUndistortedMarkers() != null
                        ? sample.getUndistortedMarkers() : sample.getSampledMarkers();
                sample.setHomography(homography);
                homographies.add(homography);

                if (iacEstimator.getMethod() == RobustEstimatorMethod.PROSAC
                        || iacEstimator.getMethod() == RobustEstimatorMethod.PROMEDS
                        || homographyQualityScoresRequired) {
                    if (samplesQualityScores != null) {
                        // pick corresponding quality score
                        tmpHomographyQualityScores[counter] = samplesQualityScores[index];
                    } else {
                        // compute re-projection error
                        error = homographyTransformationError(homography, idealPatternMarkers, sampledPatternMarkers);
                        tmpHomographyQualityScores[counter] = 1.0 / (1.0 + error);
                    }
                }
                // counter of homography estimation successes
                counter++;
            } catch (final NumericalException | GeometryException ignore) {
                // homographies are attempted to be estimated. It's ok if some fail
            }

            // position index (regardless of homography estimation success)
            index++;

            homographyProgress = 0.0f;
            sampleProgress = (float) index / (float) sampleSize;
            computeIntrinsicProgress();
            notifyProgress();
        }

        if (iacEstimator.getMethod() == RobustEstimatorMethod.PROSAC
                || iacEstimator.getMethod() == RobustEstimatorMethod.PROMEDS || homographyQualityScoresRequired) {

            // truncate tmpHomographyQualityScores to contain only actual number of
            // successfully estimated homographies
            homographyQualityScores = Arrays.copyOf(tmpHomographyQualityScores, counter);
        }

        // estimate IAC using estimated homographies
        try {
            iacEstimator.setHomographies(homographies);
            iacEstimator.setQualityScores(homographyQualityScores);
            iac = iacEstimator.estimate();

            intrinsic = iac.getIntrinsicParameters();
        } catch (final GeometryException | NumericalException e) {
            throw new CalibrationException(e);
        }

        if (listener != null) {
            listener.onIntrinsicParametersEstimationEnds(this, intrinsic);
        }
    }

    /**
     * Computes average transformation error as a result of comparing sampled
     * pattern markers against the transformation of ideal pattern markers using
     * the estimated homography.
     *
     * @param homography            estimated homography.
     * @param idealPatternMarkers   ideal pattern markers.
     * @param sampledPatternMarkers sampled pattern markers.
     * @return average re-projection error.
     */
    protected static double homographyTransformationError(
            final Transformation2D homography, final List<Point2D> idealPatternMarkers,
            final List<Point2D> sampledPatternMarkers) {

        final var transformedPoint = new HomogeneousPoint2D();
        Point2D idealPatternMarker;
        Point2D sampledPatternMarker;
        var avgError = 0.0;
        double distance;
        final var size = sampledPatternMarkers.size();
        for (var i = 0; i < size; i++) {
            idealPatternMarker = idealPatternMarkers.get(i);
            sampledPatternMarker = sampledPatternMarkers.get(i);

            homography.transform(idealPatternMarker, transformedPoint);
            distance = transformedPoint.distanceTo(sampledPatternMarker);
            avgError += distance;
        }

        avgError /= size;

        return avgError;
    }

    /**
     * Refreshes listener of homography estimator.
     */
    protected void refreshHomographyEstimatorListener() {
        if (homographyEstimatorListener == null) {
            homographyEstimatorListener = new ProjectiveTransformation2DRobustEstimatorListener() {

                @Override
                public void onEstimateStart(final ProjectiveTransformation2DRobustEstimator estimator) {
                    homographyProgress = 0.0f;
                    computeIntrinsicProgress();
                    notifyProgress();
                }

                @Override
                public void onEstimateEnd(final ProjectiveTransformation2DRobustEstimator estimator) {
                    homographyProgress = 1.0f;
                    computeIntrinsicProgress();
                    notifyProgress();
                }

                @Override
                public void onEstimateNextIteration(
                        final ProjectiveTransformation2DRobustEstimator estimator, int iteration) {
                    // not used
                }

                @Override
                public void onEstimateProgressChange(
                        final ProjectiveTransformation2DRobustEstimator estimator, final float progress) {
                    homographyProgress = progress;
                    computeIntrinsicProgress();
                    notifyProgress();
                }
            };
        }

        try {
            homographyEstimator.setListener(homographyEstimatorListener);
        } catch (final LockedException e) {
            Logger.getLogger(CameraCalibrator.class.getName()).log(Level.WARNING,
                    "Could not set homography estimator listener", e);
        }
    }

    /**
     * Refreshes listener of IAC estimator.
     */
    protected void refreshIACEstimatorListener() {
        if (iacEstimatorListener == null) {
            iacEstimatorListener = new ImageOfAbsoluteConicRobustEstimatorListener() {

                @Override
                public void onEstimateStart(final ImageOfAbsoluteConicRobustEstimator estimator) {
                    iacProgress = 0.0f;
                    computeIntrinsicProgress();
                    notifyProgress();
                }

                @Override
                public void onEstimateEnd(final ImageOfAbsoluteConicRobustEstimator estimator) {
                    iacProgress = 1.0f;
                    computeIntrinsicProgress();
                    notifyProgress();
                }

                @Override
                public void onEstimateNextIteration(
                        final ImageOfAbsoluteConicRobustEstimator estimator, final int iteration) {
                    // not used
                }

                @Override
                public void onEstimateProgressChange(
                        final ImageOfAbsoluteConicRobustEstimator estimator, final float progress) {
                    iacProgress = progress;
                    computeIntrinsicProgress();
                    notifyProgress();
                }
            };
        }

        try {
            iacEstimator.setListener(iacEstimatorListener);
        } catch (final LockedException e) {
            Logger.getLogger(CameraCalibrator.class.getName()).log(Level.WARNING,
                    "Could not set IAC estimator listener", e);
        }
    }

    /**
     * Internal method to set list of samples obtained from different pictures
     * using the same camera device (or same camera model). Several samples can
     * be used to calibrate the camera (a pinhole camera can be estimated for
     * each sample). The more samples are used, typically the better the
     * results.
     * This method is for internal use only and does not check whether this
     * instance is locked or not.
     *
     * @param samples list of samples.
     * @throws IllegalArgumentException if not enough samples are provided to
     *                                  estimate the intrinsic parameters. By default, the minimum is 1,
     *                                  but depending on the settings at least 3 samples might be required.
     */
    private void internalSetSamples(final List<CameraCalibratorSample> samples) {
        if (samples.size() < iacEstimator.getMinNumberOfRequiredHomographies()) {
            throw new IllegalArgumentException();
        }

        this.samples = samples;
    }

    /**
     * Sets quality scores assigned to each provided sample. This can be used on
     * certain robust estimation methods of the IAC such as PROSAC and PROMedS.
     * If not provided, homography quality scores will be estimated based on
     * re-projection error and this value will be ignored.
     * This method is for internal use only and does not check whether this
     * instance is locked or not.
     *
     * @param samplesQualityScores quality scores assigned to each provided
     *                             sample.
     * @throws IllegalArgumentException if not enough quality scores are
     *                                  provided for the corresponding samples to estimate the intrinsic
     *                                  parameters. By default, the minimum is 1, but depending on the
     *                                  settings at least 3 samples might be required.
     */
    private void internalSetSamplesQualityScores(final double[] samplesQualityScores) {
        if (samplesQualityScores.length < iacEstimator.getMinNumberOfRequiredHomographies()) {
            throw new IllegalArgumentException();
        }

        this.samplesQualityScores = samplesQualityScores;
    }

    /**
     * Sets robust homography estimation method.
     * If method changes, then a new homography robust estimator is created and
     * configured.
     *
     * @param homographyMethod robust homography estimation method to be set.
     */
    private void internalSetHomographyMethod(final RobustEstimatorMethod homographyMethod) {
        // if method changes, homography estimator must be recreated
        if (homographyMethod != this.homographyMethod) {
            final var previousAvailable = homographyEstimator != null;
            var threshold = 0.0;
            var confidence = 0.0;
            var maxIterations = 0;
            if (previousAvailable) {
                threshold = getHomographyEstimatorThreshold();
                confidence = getHomographyEstimatorConfidence();
                maxIterations = getHomographyEstimatorMaxIterations();
            }

            homographyEstimator = PointCorrespondenceProjectiveTransformation2DRobustEstimator.create(homographyMethod);

            // configure new estimator
            refreshHomographyEstimatorListener();
            if (previousAvailable) {
                try {
                    setHomographyEstimatorThreshold(threshold);
                    setHomographyEstimatorConfidence(confidence);
                    setHomographyEstimatorMaxIterations(maxIterations);
                } catch (final LockedException e) {
                    Logger.getLogger(CameraCalibrator.class.getName()).log(Level.WARNING,
                            "Could not reconfigure homography estimator", e);
                }
            }
        }
        this.homographyMethod = homographyMethod;
    }

    /**
     * Sets robust IAC estimation method.
     * IF method changes, then a new IAC robust estimator is created and
     * configured
     *
     * @param imageOfAbsoluteConicMethod robust IAC estimation method to be set
     */
    private void internalSetImageOfAbsoluteConicMethod(final RobustEstimatorMethod imageOfAbsoluteConicMethod) {
        // if method changes, iac estimator must be recreated
        if (imageOfAbsoluteConicMethod != this.imageOfAbsoluteConicMethod) {
            final var previousAvailable = iacEstimator != null;
            var threshold = 0.0;
            var confidence = 0.0;
            var maxIterations = 0;
            var zeroSkewness = false;
            var principalPointAtOrigin = false;
            var focalDistanceAspectRatioKnown = false;
            var focalDistanceAspectRatio = 0.0;
            if (previousAvailable) {
                threshold = getIACEstimatorThreshold();
                confidence = getIACEstimatorConfidence();
                maxIterations = getIACEstimatorMaxIterations();
                zeroSkewness = isZeroSkewness();
                principalPointAtOrigin = isPrincipalPointAtOrigin();
                focalDistanceAspectRatioKnown = isFocalDistanceAspectRatioKnown();
                focalDistanceAspectRatio = getFocalDistanceAspectRatio();
            }

            iacEstimator = ImageOfAbsoluteConicRobustEstimator.create(imageOfAbsoluteConicMethod);

            // configure new estimator
            refreshIACEstimatorListener();
            if (previousAvailable) {
                try {
                    setIACEstimatorThreshold(threshold);
                    setIACEstimatorConfidence(confidence);
                    setIACEstimatorMaxIterations(maxIterations);
                    setZeroSkewness(zeroSkewness);
                    setPrincipalPointAtOrigin(principalPointAtOrigin);
                    setFocalDistanceAspectRatioKnown(focalDistanceAspectRatioKnown);
                    setFocalDistanceAspectRatio(focalDistanceAspectRatio);
                } catch (final LockedException e) {
                    Logger.getLogger(CameraCalibrator.class.getName()).log(Level.WARNING,
                            "Could not reconfigure IAC estimator", e);
                }
            }
        }
        this.imageOfAbsoluteConicMethod = imageOfAbsoluteConicMethod;
    }
}