PinholeCameraIntrinsicParameters.java

/*
 * Copyright (C) 2012 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.geometry;

import com.irurueta.algebra.Matrix;
import com.irurueta.algebra.WrongSizeException;

import java.io.Serializable;

/**
 * This class defines intrinsic parameters of a pinhole camera.
 * Extrinsic parameters on a pinhole camera are those external to the camera
 * such as rotation and translation, whereas intrinsic parameters are more
 * related to the inner workings of a camera.
 * Intrinsic parameters are those such as horizontal/vertical focal length,
 * skewness of axes or principal point of an image (which is usually related
 * to lens/sensor slanting).
 */
public class PinholeCameraIntrinsicParameters implements Serializable {

    /**
     * Constant defining the required number of rows of an intrinsic parameters
     * matrix.
     */
    public static final int INTRINSIC_MATRIX_ROWS = 3;

    /**
     * Constant defining the required number of columns of an intrinsic
     * parameters matrix.
     */
    public static final int INTRINSIC_MATRIX_COLS = 3;

    /**
     * Threshold to determine whether a given intrinsic parameters matrix is
     * valid (is upper triangular).
     */
    public static final double DEFAULT_VALID_THRESHOLD = 1e-12;

    /**
     * Internal matrix defining the intrinsic parameters of a camera.
     */
    private Matrix internalMatrix;

    /**
     * Constructor.
     * Creates canonical intrinsic parameters which has no effect on projected
     * 3D points into 2D points.
     */
    public PinholeCameraIntrinsicParameters() {
        try {
            internalMatrix = Matrix.identity(INTRINSIC_MATRIX_ROWS, INTRINSIC_MATRIX_COLS);
        } catch (final WrongSizeException ignore) {
            // never happens
        }
    }

    /**
     * Creates a copy of provided intrinsic parameters.
     *
     * @param params Intrinsic parameters to be copied.
     */
    public PinholeCameraIntrinsicParameters(final PinholeCameraIntrinsicParameters params) {
        internalMatrix = new Matrix(params.internalMatrix);
    }

    /**
     * Creates a new instance of camera intrinsic parameters using provided
     * matrix.
     * Provided matrix must be 3x3 and upper triangular.
     * Note: this constructor will attempt to normalize provided matrix, hence
     * its values might change after calling this constructor.
     *
     * @param internalMatrix Provided 3x3 and upper triangular matrix
     * @throws InvalidPinholeCameraIntrinsicParametersException thrown if
     *                                                          provided matrix is not 3x3 or upper triangular.
     */
    public PinholeCameraIntrinsicParameters(final Matrix internalMatrix)
            throws InvalidPinholeCameraIntrinsicParametersException {
        setInternalMatrix(internalMatrix);
    }

    /**
     * Creates a new instance of camera intrinsic parameters using provided
     * matrix and provided threshold to determine whether it is a valid matrix.
     * Provided matrix must be 3x3 and upper triangular up to provided threshold
     * Note: this constructor will attempt to normalize provided matrix, hence
     * its values might change after calling this constructor.
     *
     * @param internalMatrix Provided 3x3 and upper triangular matrix.
     * @param threshold      Threshold to determine whether provided matrix is upper
     *                       triangular. Matrix will be considered upper triangular if its lower
     *                       triangular elements are larger than this threshold in absolute terms.
     * @throws InvalidPinholeCameraIntrinsicParametersException thrown if
     *                                                          provided matrix is not 3x3 or upper triangular.
     * @throws IllegalArgumentException                         thrown if provided threshold is negative.
     */
    public PinholeCameraIntrinsicParameters(final Matrix internalMatrix, final double threshold)
            throws InvalidPinholeCameraIntrinsicParametersException {
        setInternalMatrix(internalMatrix, threshold);
    }

    /**
     * Creates a new instance of camera intrinsic parameters using provided
     * horizontal/vertical focal length, horizontal/vertical principal point and
     * skewness of axes.
     *
     * @param horizontalFocalLength    Horizontal focal length of camera.
     *                                 The larger the focal length the larger objects will appear, as focal
     *                                 length determines the amount of "zoom". The relation between horizontal
     *                                 and vertical focal length determines the aspect ratio of images.
     * @param verticalFocalLength      Vertical focal length of camera.
     *                                 The larger the focal length the larger objects will appear, as focal
     *                                 length determines the amount of "zoom". The relation between horizontal
     *                                 and vertical focal length determines the aspect ratio of images.
     * @param horizontalPrincipalPoint Horizontal principal point of camera.
     *                                 Determines where the origin of coordinates of 2D points will be located
     *                                 horizontally on the retinal plane. This is usually the center of an
     *                                 image. If not specified, then the origin of coordinates on the retinal
     *                                 plane is located at (0, 0).
     * @param verticalPrincipalPoint   Vertical principal point of camera.
     *                                 Determines where the origin of coordinates of 2D points will be located
     *                                 vertically on the retinal plane. This is usually the center of an image.
     *                                 If not specified, then the origin of coordinates on the retinal plane
     *                                 is located at (0, 0).
     * @param skewness                 Skewness of axes. This usually zero or a value close to
     *                                 zero. The larger the value in absolute terms the more skewed (i.e.
     *                                 slanted) x-y axes will be on projected images.
     */
    public PinholeCameraIntrinsicParameters(
            final double horizontalFocalLength, final double verticalFocalLength, final double horizontalPrincipalPoint,
            final double verticalPrincipalPoint, final double skewness) {

        // create instance initialized to the identity
        try {
            internalMatrix = Matrix.identity(INTRINSIC_MATRIX_ROWS, INTRINSIC_MATRIX_COLS);
        } catch (final WrongSizeException ignore) {
            // never happens
        }

        // set parameters
        setHorizontalFocalLength(horizontalFocalLength);
        setVerticalFocalLength(verticalFocalLength);
        setHorizontalPrincipalPoint(horizontalPrincipalPoint);
        setVerticalPrincipalPoint(verticalPrincipalPoint);
        setSkewness(skewness);
    }

    /**
     * Returns a copy of the internal matrix defining this instance parameters
     *
     * @return A copy of the internal matrix of this instance.
     */
    public Matrix getInternalMatrix() {
        return new Matrix(internalMatrix);
    }

    /**
     * Sets internal matrix of this instance.
     * Note: this method will attempt to normalize provided matrix, hence its
     * values might change after calling this method.
     *
     * @param internalMatrix Matrix to be set as the internal matrix of this
     *                       instance. This matrix needs to be 3x3 and upper triangular.
     * @throws InvalidPinholeCameraIntrinsicParametersException thrown if
     *                                                          provided matrix is not 3x3 or upper triangular.
     */
    public final void setInternalMatrix(final Matrix internalMatrix)
            throws InvalidPinholeCameraIntrinsicParametersException {
        setInternalMatrix(internalMatrix, DEFAULT_VALID_THRESHOLD);
    }

    /**
     * Sets the internal matrix of this instance using provided threshold to
     * determine whether it is upper triangular.
     * Note: this method will attempt to normalize provided matrix, hence its
     * values might change after calling this method.
     *
     * @param internalMatrix Matrix to be set as the internal matrix of this
     *                       instance. This matrix needs to be 3x3 and upper triangular.
     * @param threshold      Threshold to determine whether provided matrix is upper
     *                       triangular. Matrix will be considered upper triangular if its lower
     *                       triangular elements are larger than this threshold in absolute terms.
     * @throws InvalidPinholeCameraIntrinsicParametersException thrown if
     *                                                          provided matrix is not 3x3 or upper triangular.
     * @throws IllegalArgumentException                         thrown if provided threshold is negative.
     */
    public final void setInternalMatrix(final Matrix internalMatrix, final double threshold)
            throws InvalidPinholeCameraIntrinsicParametersException {

        // normalizes provided matrix to ensure that element 3,3 is 1
        normalize(internalMatrix);

        // check that provided matrix is upper-triangular and that lower right
        // element is 1.0 (using provided threshold as a measure of error
        // tolerance)
        final var valid = isValidMatrix(internalMatrix, threshold);
        if (!valid) {
            throw new InvalidPinholeCameraIntrinsicParametersException();
        }

        this.internalMatrix = internalMatrix;
    }

    /**
     * Computes the inverse of internal matrix and returns the result.
     * Calling this method is more efficient than calling.
     * com.irurueta.algebra.Utils.inverse.
     *
     * @return inverse of internal matrix.
     */
    public Matrix getInverseInternalMatrix() {
        Matrix result = null;
        try {
            result = new Matrix(INTRINSIC_MATRIX_ROWS,
                    INTRINSIC_MATRIX_COLS);
            getInverseInternalMatrix(result);
        } catch (final WrongSizeException ignore) {
            // never happens
        }

        return result;
    }

    /**
     * Computes the inverse of internal matrix and stores the result into
     * provided matrix.
     * Calling this method is more efficient than calling
     * com.irurueta.algebra.Utils.inverse.
     *
     * @param result instance where result will be stored.
     */
    public void getInverseInternalMatrix(Matrix result) {
        final var horizontalFocalLength = getHorizontalFocalLength();
        final var verticalFocalLength = getVerticalFocalLength();
        final var skewness = getSkewness();
        final var horizontalPrincipalPoint = getHorizontalPrincipalPoint();
        final var verticalPrincipalPoint = getVerticalPrincipalPoint();

        result.setElementAt(0, 0, 1.0 / horizontalFocalLength);
        result.setElementAt(0, 1, -skewness / (horizontalFocalLength * verticalFocalLength));
        result.setElementAt(1, 1, 1.0 / verticalFocalLength);
        result.setElementAt(0, 2,
                (skewness * verticalPrincipalPoint - verticalFocalLength * horizontalPrincipalPoint)
                        / (horizontalFocalLength * verticalFocalLength));
        result.setElementAt(1, 2, -verticalPrincipalPoint / verticalFocalLength);
        result.setElementAt(2, 2, 1.0);
    }

    /**
     * Returns the horizontal focal length of a camera.
     * The larger the focal length the larger objects will appear, as focal
     * length determines the amount of "zoom". The relation between horizontal
     * and vertical focal length determines the aspect ratio of images.
     * Note: Negative values will reverse projected points or geometric objects
     * horizontally.
     *
     * @return Horizontal focal length.
     */
    public double getHorizontalFocalLength() {
        return internalMatrix.getElementAt(0, 0);
    }

    /**
     * Sets the horizontal focal length of a camera.
     * The larger the focal length the larger objects will appear, as focal
     * length determines the amount of "zoom". The relation between horizontal
     * and vertical focal length determines the aspect ratio of images
     * Note: Negative values will reverse projected points or geometric objects
     * horizontally.
     *
     * @param horizontalFocalLength Horizontal focal length to be set.
     */
    public final void setHorizontalFocalLength(final double horizontalFocalLength) {
        internalMatrix.setElementAt(0, 0, horizontalFocalLength);
    }

    /**
     * Returns the vertical focal length of a camera.
     * The larger the focal length the larger objects will appear, as focal
     * length determines the amount of "zoom". The relation between horizontal
     * and vertical focal length determines the aspect ratio of images
     * Note: Negative values will reverse projected points or geometric objects
     * vertically.
     *
     * @return Vertical focal length.
     */
    public double getVerticalFocalLength() {
        return internalMatrix.getElementAt(1, 1);
    }

    /**
     * Sets the vertical focal length of a camera.
     * The larger the focal length the larger objects will appear, as focal
     * length determines the amount of "zoom". The relation between horizontal
     * and vertical focal length determines the aspect ratio of images
     * Note: Negative values will reverse projected points or geometric objects
     * vertically.
     *
     * @param verticalFocalLength Vertical focal length to be set.
     */
    public final void setVerticalFocalLength(final double verticalFocalLength) {
        internalMatrix.setElementAt(1, 1, verticalFocalLength);
    }

    /**
     * Returns aspect ratio.
     * Aspect ratio is the relation between vertical and horizontal focal
     * lengths.
     * If objects are meant to be displayed with no horizontal/vertical scaling
     * distortion, then aspect ratio must be set to 1.0, which means that both
     * horizontal and vertical focal lengths are equal.
     *
     * @return Aspect ratio.
     */
    public double getAspectRatio() {
        return internalMatrix.getElementAt(1, 1) / internalMatrix.getElementAt(0, 0);
    }

    /**
     * Sets provided aspect ratio but keeping current horizontal focal length
     * value.
     * If objects are meant to be displayed with no horizontal/vertical scaling
     * distortion, then aspect ratio must be set to 1.0, which means that both
     * horizontal and vertical focal lengths are equal.
     *
     * @param aspectRatio Aspect ratio to be set.
     */
    public void setAspectRatioKeepingHorizontalFocalLength(final double aspectRatio) {
        internalMatrix.setElementAt(1, 1,
                aspectRatio * internalMatrix.getElementAt(0, 0));
    }

    /**
     * Sets provided aspect ratio but keeping current vertical focal length
     * value.
     * If objects are meant to be displayed with no horizontal/vertical scaling
     * distortion, then aspect ratio must be set to 1.0, which means that both
     * horizontal and vertical focal lengths are equal.
     *
     * @param aspectRatio Aspect ratio to be set.
     */
    public void setAspectRatioKeepingVerticalFocalLength(final double aspectRatio) {
        internalMatrix.setElementAt(0, 0,
                internalMatrix.getElementAt(1, 1) / aspectRatio);
    }

    /**
     * Returns horizontal principal point.
     * Determines where the origin of coordinates of 2D points will be located
     * horizontally on the retinal plane. This is usually the center of an
     * image. If not specified, then the origin of coordinates on the retinal
     * plane is located at (0, 0) by default.
     * Principal point is usually related to the slanting between the camera
     * sensor and lens. If properly aligned then principal point is usually
     * located at image center.
     *
     * @return Horizontal principal point.
     */
    public double getHorizontalPrincipalPoint() {
        return internalMatrix.getElementAt(0, 2);
    }

    /**
     * Sets horizontal principal point.
     * Determines where the origin of coordinates of 2D points will be located
     * horizontally on the retinal plane. This is usually the center of an
     * image. If not specified, then the origin of coordinates on the retinal
     * plane is located at (0, 0) by default.
     * Principal point is usually related to the slanting between the camera
     * sensor and lens. If properly aligned then principal point is usually
     * located at image center.
     *
     * @param horizontalPrincipalPoint Horizontal principal point to be set.
     */
    public final void setHorizontalPrincipalPoint(final double horizontalPrincipalPoint) {
        internalMatrix.setElementAt(0, 2, horizontalPrincipalPoint);
    }

    /**
     * Returns vertical principal point.
     * Determines where the origin of coordinates of 2D points will be located
     * vertically on the retinal plane. This is usually the center of an
     * image. If not specified, then the origin of coordinates on the retinal
     * plane is located at (0, 0) by default.
     * Principal point is usually related to the slanting between the camera
     * sensor and lens. If properly aligned then principal point is usually
     * located at image center.
     *
     * @return Vertical principal point.
     */
    public double getVerticalPrincipalPoint() {
        return internalMatrix.getElementAt(1, 2);
    }

    /**
     * Sets vertical principal point.
     * Determines where the origin of coordinates of 2D points will be located
     * vertically on the retinal plane. This is usually the center of an
     * image. If not specified, then the origin of coordinates on the retinal
     * plane is located at (0, 0) by default.
     * Principal point is usually related to the slanting between the camera
     * sensor and lens. If properly aligned then principal point is usually
     * located at image center.
     *
     * @param verticalPrincipalPoint Vertical principal point to be set.
     */
    public final void setVerticalPrincipalPoint(final double verticalPrincipalPoint) {
        internalMatrix.setElementAt(1, 2, verticalPrincipalPoint);
    }

    /**
     * Returns skewness of axes on the retinal plane. This usually zero or a
     * value close to zero. The larger the value in absolute terms the more
     * skewed (i.e. slanted) x-y axes will be on projected images.
     *
     * @return Skewness of axes.
     */
    public double getSkewness() {
        return internalMatrix.getElementAt(0, 1);
    }

    /**
     * Sets skewness of axes on the retinal plane. This is usually zero or a
     * value close to zero. The larger the value in absolute terms the more
     * skewed (i.e. slanted) x-y axes will be on projected images.
     *
     * @param skewness Skewness of axes to be set.
     */
    public final void setSkewness(final double skewness) {
        internalMatrix.setElementAt(0, 1, skewness);
    }

    /**
     * Returns skewness angle in radians of retinal x-y axes associated to
     * current skewness value.
     * This is usually zero or a value close to zero. The larger the value in
     * absolute terms the more skewed (i.e. slanted) x-y axes will be on
     * projected images.
     *
     * @return Skewness angle in radians.
     */
    public double getSkewnessAngle() {
        return Math.atan2(internalMatrix.getElementAt(0, 1),
                internalMatrix.getElementAt(1, 1));
    }

    /**
     * Sets skewness angle in radians of retinal x-y axes associated to current
     * skewness value.
     * This is usually zero or a value close to zero. The larger the value in
     * absolute terms the more skewed (i.e. slanted) x-y axes will be on
     * projected images.
     *
     * @param skewnessAngle Skewness angle in radians to be set.
     */
    public void setSkewnessAngle(final double skewnessAngle) {
        internalMatrix.setElementAt(0, 1,
                internalMatrix.getElementAt(1, 1) * Math.tan(skewnessAngle));
    }

    /**
     * Creates a canonical intrinsic parameters instance which has no effect on
     * projected 3D points into 2D points.
     *
     * @return Canonical intrinsic parameters instance.
     */
    public static PinholeCameraIntrinsicParameters createCanonicalIntrinsicParameters() {
        return new PinholeCameraIntrinsicParameters();
    }

    /**
     * Creates typical intrinsic parameters for an image of provided size.
     * Typical intrinsic parameters have principal point located at image center
     * and a focal length which results in a frustum of approximately 45ยบ.
     *
     * @param imageWidth  Width of image in pixels.
     * @param imageHeight Height of image in pixels.
     * @return Typical intrinsic parameters.
     * @throws IllegalArgumentException raised if provided width or height is
     *                                  negative.
     */
    public static PinholeCameraIntrinsicParameters createTypicalIntrinsicParameters(
            final int imageWidth, final int imageHeight) {
        // throw exception if width or height is negative
        if (imageWidth < 0 || imageHeight < 0) {
            throw new IllegalArgumentException();
        }

        final var focalLength = (imageWidth + imageHeight) / 2.0;
        final var horizontalPrincipalPoint = imageWidth / 2.0;
        final var verticalPrincipalPoint = imageHeight / 2.0;
        final var skewness = 0.0;

        return new PinholeCameraIntrinsicParameters(focalLength, focalLength, horizontalPrincipalPoint,
                verticalPrincipalPoint, skewness);
    }

    /**
     * Determines whether provided matrix is considered a valid matrix for
     * pinhole camera intrinsic parameters.
     * Provided matrix must be 3x3, upper triangular and its last element (3,3)
     * must be 1.0.
     *
     * @param m 3x3 and upper triangular matrix to be checked.
     * @return True if provided matrix is valid, false otherwise.
     */
    public static boolean isValidMatrix(final Matrix m) {
        return isValidMatrix(m, DEFAULT_VALID_THRESHOLD);
    }

    /**
     * Determines whether provided matrix is considered a valid matrix for
     * pinhole camera intrinsic parameters.
     * Provided matrix must be 3x3, upper triangular (up to provided threshold)
     * and its last element (3,3) must be 1.0.
     *
     * @param m         3x3 and upper triangular matrix to be checked.
     * @param threshold Threshold to determine whether matrix is upper
     *                  triangular.
     * @return True if provided matrix is valid, false otherwise.
     * @throws IllegalArgumentException Raised if provided threshold is negative.
     */
    public static boolean isValidMatrix(final Matrix m, final double threshold) {

        if (threshold < 0) {
            throw new IllegalArgumentException();
        }

        if (m.getRows() != INTRINSIC_MATRIX_ROWS || m.getColumns() != INTRINSIC_MATRIX_COLS) {
            return false;
        }

        // TODO: create isUpperTriangular and isLowerTriangular methods in Utils class within Algebra library

        // check that provided matrix is upper triangular
        for (var u = 0; u < INTRINSIC_MATRIX_ROWS; u++) {
            for (var v = 0; v < u; v++) {
                if (Math.abs(m.getElementAt(u, v)) > threshold) {
                    return false;
                }
            }
        }

        // check that lower right element is 1.0
        return Math.abs(m.getElementAt(2, 2)) - 1.0 <= threshold;
    }

    /**
     * Normalizes provided matrix so that element (3,3) becomes one.
     *
     * @param m Matrix to be normalized.
     */
    private static void normalize(final Matrix m) {
        final var norm = m.getElementAt(2, 2);
        m.multiplyByScalar(1.0 / norm);
    }

    /**
     * Clones this instance of pinhole camera matrix.
     *
     * @return A copy of this instance.
     * @throws CloneNotSupportedException if clone fails.
     */
    @Override
    public PinholeCameraIntrinsicParameters clone() throws CloneNotSupportedException {
        final var result = (PinholeCameraIntrinsicParameters) super.clone();
        result.internalMatrix = new Matrix(this.internalMatrix);
        return result;
    }

    /**
     * Creates an instance of pinhole camera intrinsic parameters using
     * provided data. Created instance assumes that skewness is zero and that
     * principal point is located at origin of coordinates.
     * This method can be used if for instance actual camera sensor data such as
     * focal length expressed in millimeters and sensor size expressed in
     * millimeters is known along with the size of the obtained image data.
     *
     * @param focalLength  focal length of camera expressed in millimeters.
     * @param sensorWidth  camera sensor width expressed in millimeters.
     * @param sensorHeight camera sensor height expressed in millimeters.
     * @param imageWidth   captured image width expressed in pixels.
     * @param imageHeight  captured image height expressed in pixels.
     * @return an instance of pinhole camera intrinsic parameters.
     */
    public static PinholeCameraIntrinsicParameters create(
            final double focalLength, final double sensorWidth, final double sensorHeight, final int imageWidth,
            final int imageHeight) {

        // compute the size of a pixel taking into account sensor and image sizes
        final var pixelWidth = sensorWidth / imageWidth; // mm/px
        final var pixelHeight = sensorHeight / imageHeight; // mm/px

        // compute focal lengths expressed in pixels
        final var horizontalFocalLength = focalLength / pixelWidth;
        final var verticalFocalLength = focalLength / pixelHeight;

        return new PinholeCameraIntrinsicParameters(horizontalFocalLength, verticalFocalLength, 0.0,
                0.0, 0.0);
    }
}