View Javadoc
1   /*
2    * Copyright (C) 2012 Alberto Irurueta Carro (alberto@irurueta.com)
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *         http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package com.irurueta.geometry;
17  
18  import com.irurueta.algebra.Matrix;
19  import com.irurueta.algebra.WrongSizeException;
20  
21  import java.io.Serializable;
22  
23  /**
24   * This class defines intrinsic parameters of a pinhole camera.
25   * Extrinsic parameters on a pinhole camera are those external to the camera
26   * such as rotation and translation, whereas intrinsic parameters are more
27   * related to the inner workings of a camera.
28   * Intrinsic parameters are those such as horizontal/vertical focal length,
29   * skewness of axes or principal point of an image (which is usually related
30   * to lens/sensor slanting).
31   */
32  public class PinholeCameraIntrinsicParameters implements Serializable {
33  
34      /**
35       * Constant defining the required number of rows of an intrinsic parameters
36       * matrix.
37       */
38      public static final int INTRINSIC_MATRIX_ROWS = 3;
39  
40      /**
41       * Constant defining the required number of columns of an intrinsic
42       * parameters matrix.
43       */
44      public static final int INTRINSIC_MATRIX_COLS = 3;
45  
46      /**
47       * Threshold to determine whether a given intrinsic parameters matrix is
48       * valid (is upper triangular).
49       */
50      public static final double DEFAULT_VALID_THRESHOLD = 1e-12;
51  
52      /**
53       * Internal matrix defining the intrinsic parameters of a camera.
54       */
55      private Matrix internalMatrix;
56  
57      /**
58       * Constructor.
59       * Creates canonical intrinsic parameters which has no effect on projected
60       * 3D points into 2D points.
61       */
62      public PinholeCameraIntrinsicParameters() {
63          try {
64              internalMatrix = Matrix.identity(INTRINSIC_MATRIX_ROWS, INTRINSIC_MATRIX_COLS);
65          } catch (final WrongSizeException ignore) {
66              // never happens
67          }
68      }
69  
70      /**
71       * Creates a copy of provided intrinsic parameters.
72       *
73       * @param params Intrinsic parameters to be copied.
74       */
75      public PinholeCameraIntrinsicParameters(final PinholeCameraIntrinsicParameters params) {
76          internalMatrix = new Matrix(params.internalMatrix);
77      }
78  
79      /**
80       * Creates a new instance of camera intrinsic parameters using provided
81       * matrix.
82       * Provided matrix must be 3x3 and upper triangular.
83       * Note: this constructor will attempt to normalize provided matrix, hence
84       * its values might change after calling this constructor.
85       *
86       * @param internalMatrix Provided 3x3 and upper triangular matrix
87       * @throws InvalidPinholeCameraIntrinsicParametersException thrown if
88       *                                                          provided matrix is not 3x3 or upper triangular.
89       */
90      public PinholeCameraIntrinsicParameters(final Matrix internalMatrix)
91              throws InvalidPinholeCameraIntrinsicParametersException {
92          setInternalMatrix(internalMatrix);
93      }
94  
95      /**
96       * Creates a new instance of camera intrinsic parameters using provided
97       * matrix and provided threshold to determine whether it is a valid matrix.
98       * Provided matrix must be 3x3 and upper triangular up to provided threshold
99       * Note: this constructor will attempt to normalize provided matrix, hence
100      * its values might change after calling this constructor.
101      *
102      * @param internalMatrix Provided 3x3 and upper triangular matrix.
103      * @param threshold      Threshold to determine whether provided matrix is upper
104      *                       triangular. Matrix will be considered upper triangular if its lower
105      *                       triangular elements are larger than this threshold in absolute terms.
106      * @throws InvalidPinholeCameraIntrinsicParametersException thrown if
107      *                                                          provided matrix is not 3x3 or upper triangular.
108      * @throws IllegalArgumentException                         thrown if provided threshold is negative.
109      */
110     public PinholeCameraIntrinsicParameters(final Matrix internalMatrix, final double threshold)
111             throws InvalidPinholeCameraIntrinsicParametersException {
112         setInternalMatrix(internalMatrix, threshold);
113     }
114 
115     /**
116      * Creates a new instance of camera intrinsic parameters using provided
117      * horizontal/vertical focal length, horizontal/vertical principal point and
118      * skewness of axes.
119      *
120      * @param horizontalFocalLength    Horizontal focal length of camera.
121      *                                 The larger the focal length the larger objects will appear, as focal
122      *                                 length determines the amount of "zoom". The relation between horizontal
123      *                                 and vertical focal length determines the aspect ratio of images.
124      * @param verticalFocalLength      Vertical focal length of camera.
125      *                                 The larger the focal length the larger objects will appear, as focal
126      *                                 length determines the amount of "zoom". The relation between horizontal
127      *                                 and vertical focal length determines the aspect ratio of images.
128      * @param horizontalPrincipalPoint Horizontal principal point of camera.
129      *                                 Determines where the origin of coordinates of 2D points will be located
130      *                                 horizontally on the retinal plane. This is usually the center of an
131      *                                 image. If not specified, then the origin of coordinates on the retinal
132      *                                 plane is located at (0, 0).
133      * @param verticalPrincipalPoint   Vertical principal point of camera.
134      *                                 Determines where the origin of coordinates of 2D points will be located
135      *                                 vertically on the retinal plane. This is usually the center of an image.
136      *                                 If not specified, then the origin of coordinates on the retinal plane
137      *                                 is located at (0, 0).
138      * @param skewness                 Skewness of axes. This usually zero or a value close to
139      *                                 zero. The larger the value in absolute terms the more skewed (i.e.
140      *                                 slanted) x-y axes will be on projected images.
141      */
142     public PinholeCameraIntrinsicParameters(
143             final double horizontalFocalLength, final double verticalFocalLength, final double horizontalPrincipalPoint,
144             final double verticalPrincipalPoint, final double skewness) {
145 
146         // create instance initialized to the identity
147         try {
148             internalMatrix = Matrix.identity(INTRINSIC_MATRIX_ROWS, INTRINSIC_MATRIX_COLS);
149         } catch (final WrongSizeException ignore) {
150             // never happens
151         }
152 
153         // set parameters
154         setHorizontalFocalLength(horizontalFocalLength);
155         setVerticalFocalLength(verticalFocalLength);
156         setHorizontalPrincipalPoint(horizontalPrincipalPoint);
157         setVerticalPrincipalPoint(verticalPrincipalPoint);
158         setSkewness(skewness);
159     }
160 
161     /**
162      * Returns a copy of the internal matrix defining this instance parameters
163      *
164      * @return A copy of the internal matrix of this instance.
165      */
166     public Matrix getInternalMatrix() {
167         return new Matrix(internalMatrix);
168     }
169 
170     /**
171      * Sets internal matrix of this instance.
172      * Note: this method will attempt to normalize provided matrix, hence its
173      * values might change after calling this method.
174      *
175      * @param internalMatrix Matrix to be set as the internal matrix of this
176      *                       instance. This matrix needs to be 3x3 and upper triangular.
177      * @throws InvalidPinholeCameraIntrinsicParametersException thrown if
178      *                                                          provided matrix is not 3x3 or upper triangular.
179      */
180     public final void setInternalMatrix(final Matrix internalMatrix)
181             throws InvalidPinholeCameraIntrinsicParametersException {
182         setInternalMatrix(internalMatrix, DEFAULT_VALID_THRESHOLD);
183     }
184 
185     /**
186      * Sets the internal matrix of this instance using provided threshold to
187      * determine whether it is upper triangular.
188      * Note: this method will attempt to normalize provided matrix, hence its
189      * values might change after calling this method.
190      *
191      * @param internalMatrix Matrix to be set as the internal matrix of this
192      *                       instance. This matrix needs to be 3x3 and upper triangular.
193      * @param threshold      Threshold to determine whether provided matrix is upper
194      *                       triangular. Matrix will be considered upper triangular if its lower
195      *                       triangular elements are larger than this threshold in absolute terms.
196      * @throws InvalidPinholeCameraIntrinsicParametersException thrown if
197      *                                                          provided matrix is not 3x3 or upper triangular.
198      * @throws IllegalArgumentException                         thrown if provided threshold is negative.
199      */
200     public final void setInternalMatrix(final Matrix internalMatrix, final double threshold)
201             throws InvalidPinholeCameraIntrinsicParametersException {
202 
203         // normalizes provided matrix to ensure that element 3,3 is 1
204         normalize(internalMatrix);
205 
206         // check that provided matrix is upper-triangular and that lower right
207         // element is 1.0 (using provided threshold as a measure of error
208         // tolerance)
209         final var valid = isValidMatrix(internalMatrix, threshold);
210         if (!valid) {
211             throw new InvalidPinholeCameraIntrinsicParametersException();
212         }
213 
214         this.internalMatrix = internalMatrix;
215     }
216 
217     /**
218      * Computes the inverse of internal matrix and returns the result.
219      * Calling this method is more efficient than calling.
220      * com.irurueta.algebra.Utils.inverse.
221      *
222      * @return inverse of internal matrix.
223      */
224     public Matrix getInverseInternalMatrix() {
225         Matrix result = null;
226         try {
227             result = new Matrix(INTRINSIC_MATRIX_ROWS,
228                     INTRINSIC_MATRIX_COLS);
229             getInverseInternalMatrix(result);
230         } catch (final WrongSizeException ignore) {
231             // never happens
232         }
233 
234         return result;
235     }
236 
237     /**
238      * Computes the inverse of internal matrix and stores the result into
239      * provided matrix.
240      * Calling this method is more efficient than calling
241      * com.irurueta.algebra.Utils.inverse.
242      *
243      * @param result instance where result will be stored.
244      */
245     public void getInverseInternalMatrix(Matrix result) {
246         final var horizontalFocalLength = getHorizontalFocalLength();
247         final var verticalFocalLength = getVerticalFocalLength();
248         final var skewness = getSkewness();
249         final var horizontalPrincipalPoint = getHorizontalPrincipalPoint();
250         final var verticalPrincipalPoint = getVerticalPrincipalPoint();
251 
252         result.setElementAt(0, 0, 1.0 / horizontalFocalLength);
253         result.setElementAt(0, 1, -skewness / (horizontalFocalLength * verticalFocalLength));
254         result.setElementAt(1, 1, 1.0 / verticalFocalLength);
255         result.setElementAt(0, 2,
256                 (skewness * verticalPrincipalPoint - verticalFocalLength * horizontalPrincipalPoint)
257                         / (horizontalFocalLength * verticalFocalLength));
258         result.setElementAt(1, 2, -verticalPrincipalPoint / verticalFocalLength);
259         result.setElementAt(2, 2, 1.0);
260     }
261 
262     /**
263      * Returns the horizontal focal length of a camera.
264      * The larger the focal length the larger objects will appear, as focal
265      * length determines the amount of "zoom". The relation between horizontal
266      * and vertical focal length determines the aspect ratio of images.
267      * Note: Negative values will reverse projected points or geometric objects
268      * horizontally.
269      *
270      * @return Horizontal focal length.
271      */
272     public double getHorizontalFocalLength() {
273         return internalMatrix.getElementAt(0, 0);
274     }
275 
276     /**
277      * Sets the horizontal focal length of a camera.
278      * The larger the focal length the larger objects will appear, as focal
279      * length determines the amount of "zoom". The relation between horizontal
280      * and vertical focal length determines the aspect ratio of images
281      * Note: Negative values will reverse projected points or geometric objects
282      * horizontally.
283      *
284      * @param horizontalFocalLength Horizontal focal length to be set.
285      */
286     public final void setHorizontalFocalLength(final double horizontalFocalLength) {
287         internalMatrix.setElementAt(0, 0, horizontalFocalLength);
288     }
289 
290     /**
291      * Returns the vertical focal length of a camera.
292      * The larger the focal length the larger objects will appear, as focal
293      * length determines the amount of "zoom". The relation between horizontal
294      * and vertical focal length determines the aspect ratio of images
295      * Note: Negative values will reverse projected points or geometric objects
296      * vertically.
297      *
298      * @return Vertical focal length.
299      */
300     public double getVerticalFocalLength() {
301         return internalMatrix.getElementAt(1, 1);
302     }
303 
304     /**
305      * Sets the vertical focal length of a camera.
306      * The larger the focal length the larger objects will appear, as focal
307      * length determines the amount of "zoom". The relation between horizontal
308      * and vertical focal length determines the aspect ratio of images
309      * Note: Negative values will reverse projected points or geometric objects
310      * vertically.
311      *
312      * @param verticalFocalLength Vertical focal length to be set.
313      */
314     public final void setVerticalFocalLength(final double verticalFocalLength) {
315         internalMatrix.setElementAt(1, 1, verticalFocalLength);
316     }
317 
318     /**
319      * Returns aspect ratio.
320      * Aspect ratio is the relation between vertical and horizontal focal
321      * lengths.
322      * If objects are meant to be displayed with no horizontal/vertical scaling
323      * distortion, then aspect ratio must be set to 1.0, which means that both
324      * horizontal and vertical focal lengths are equal.
325      *
326      * @return Aspect ratio.
327      */
328     public double getAspectRatio() {
329         return internalMatrix.getElementAt(1, 1) / internalMatrix.getElementAt(0, 0);
330     }
331 
332     /**
333      * Sets provided aspect ratio but keeping current horizontal focal length
334      * value.
335      * If objects are meant to be displayed with no horizontal/vertical scaling
336      * distortion, then aspect ratio must be set to 1.0, which means that both
337      * horizontal and vertical focal lengths are equal.
338      *
339      * @param aspectRatio Aspect ratio to be set.
340      */
341     public void setAspectRatioKeepingHorizontalFocalLength(final double aspectRatio) {
342         internalMatrix.setElementAt(1, 1,
343                 aspectRatio * internalMatrix.getElementAt(0, 0));
344     }
345 
346     /**
347      * Sets provided aspect ratio but keeping current vertical focal length
348      * value.
349      * If objects are meant to be displayed with no horizontal/vertical scaling
350      * distortion, then aspect ratio must be set to 1.0, which means that both
351      * horizontal and vertical focal lengths are equal.
352      *
353      * @param aspectRatio Aspect ratio to be set.
354      */
355     public void setAspectRatioKeepingVerticalFocalLength(final double aspectRatio) {
356         internalMatrix.setElementAt(0, 0,
357                 internalMatrix.getElementAt(1, 1) / aspectRatio);
358     }
359 
360     /**
361      * Returns horizontal principal point.
362      * Determines where the origin of coordinates of 2D points will be located
363      * horizontally on the retinal plane. This is usually the center of an
364      * image. If not specified, then the origin of coordinates on the retinal
365      * plane is located at (0, 0) by default.
366      * Principal point is usually related to the slanting between the camera
367      * sensor and lens. If properly aligned then principal point is usually
368      * located at image center.
369      *
370      * @return Horizontal principal point.
371      */
372     public double getHorizontalPrincipalPoint() {
373         return internalMatrix.getElementAt(0, 2);
374     }
375 
376     /**
377      * Sets horizontal principal point.
378      * Determines where the origin of coordinates of 2D points will be located
379      * horizontally on the retinal plane. This is usually the center of an
380      * image. If not specified, then the origin of coordinates on the retinal
381      * plane is located at (0, 0) by default.
382      * Principal point is usually related to the slanting between the camera
383      * sensor and lens. If properly aligned then principal point is usually
384      * located at image center.
385      *
386      * @param horizontalPrincipalPoint Horizontal principal point to be set.
387      */
388     public final void setHorizontalPrincipalPoint(final double horizontalPrincipalPoint) {
389         internalMatrix.setElementAt(0, 2, horizontalPrincipalPoint);
390     }
391 
392     /**
393      * Returns vertical principal point.
394      * Determines where the origin of coordinates of 2D points will be located
395      * vertically on the retinal plane. This is usually the center of an
396      * image. If not specified, then the origin of coordinates on the retinal
397      * plane is located at (0, 0) by default.
398      * Principal point is usually related to the slanting between the camera
399      * sensor and lens. If properly aligned then principal point is usually
400      * located at image center.
401      *
402      * @return Vertical principal point.
403      */
404     public double getVerticalPrincipalPoint() {
405         return internalMatrix.getElementAt(1, 2);
406     }
407 
408     /**
409      * Sets vertical principal point.
410      * Determines where the origin of coordinates of 2D points will be located
411      * vertically on the retinal plane. This is usually the center of an
412      * image. If not specified, then the origin of coordinates on the retinal
413      * plane is located at (0, 0) by default.
414      * Principal point is usually related to the slanting between the camera
415      * sensor and lens. If properly aligned then principal point is usually
416      * located at image center.
417      *
418      * @param verticalPrincipalPoint Vertical principal point to be set.
419      */
420     public final void setVerticalPrincipalPoint(final double verticalPrincipalPoint) {
421         internalMatrix.setElementAt(1, 2, verticalPrincipalPoint);
422     }
423 
424     /**
425      * Returns skewness of axes on the retinal plane. This usually zero or a
426      * value close to zero. The larger the value in absolute terms the more
427      * skewed (i.e. slanted) x-y axes will be on projected images.
428      *
429      * @return Skewness of axes.
430      */
431     public double getSkewness() {
432         return internalMatrix.getElementAt(0, 1);
433     }
434 
435     /**
436      * Sets skewness of axes on the retinal plane. This is usually zero or a
437      * value close to zero. The larger the value in absolute terms the more
438      * skewed (i.e. slanted) x-y axes will be on projected images.
439      *
440      * @param skewness Skewness of axes to be set.
441      */
442     public final void setSkewness(final double skewness) {
443         internalMatrix.setElementAt(0, 1, skewness);
444     }
445 
446     /**
447      * Returns skewness angle in radians of retinal x-y axes associated to
448      * current skewness value.
449      * This is usually zero or a value close to zero. The larger the value in
450      * absolute terms the more skewed (i.e. slanted) x-y axes will be on
451      * projected images.
452      *
453      * @return Skewness angle in radians.
454      */
455     public double getSkewnessAngle() {
456         return Math.atan2(internalMatrix.getElementAt(0, 1),
457                 internalMatrix.getElementAt(1, 1));
458     }
459 
460     /**
461      * Sets skewness angle in radians of retinal x-y axes associated to current
462      * skewness value.
463      * This is usually zero or a value close to zero. The larger the value in
464      * absolute terms the more skewed (i.e. slanted) x-y axes will be on
465      * projected images.
466      *
467      * @param skewnessAngle Skewness angle in radians to be set.
468      */
469     public void setSkewnessAngle(final double skewnessAngle) {
470         internalMatrix.setElementAt(0, 1,
471                 internalMatrix.getElementAt(1, 1) * Math.tan(skewnessAngle));
472     }
473 
474     /**
475      * Creates a canonical intrinsic parameters instance which has no effect on
476      * projected 3D points into 2D points.
477      *
478      * @return Canonical intrinsic parameters instance.
479      */
480     public static PinholeCameraIntrinsicParameters createCanonicalIntrinsicParameters() {
481         return new PinholeCameraIntrinsicParameters();
482     }
483 
484     /**
485      * Creates typical intrinsic parameters for an image of provided size.
486      * Typical intrinsic parameters have principal point located at image center
487      * and a focal length which results in a frustum of approximately 45º.
488      *
489      * @param imageWidth  Width of image in pixels.
490      * @param imageHeight Height of image in pixels.
491      * @return Typical intrinsic parameters.
492      * @throws IllegalArgumentException raised if provided width or height is
493      *                                  negative.
494      */
495     public static PinholeCameraIntrinsicParameters createTypicalIntrinsicParameters(
496             final int imageWidth, final int imageHeight) {
497         // throw exception if width or height is negative
498         if (imageWidth < 0 || imageHeight < 0) {
499             throw new IllegalArgumentException();
500         }
501 
502         final var focalLength = (imageWidth + imageHeight) / 2.0;
503         final var horizontalPrincipalPoint = imageWidth / 2.0;
504         final var verticalPrincipalPoint = imageHeight / 2.0;
505         final var skewness = 0.0;
506 
507         return new PinholeCameraIntrinsicParameters(focalLength, focalLength, horizontalPrincipalPoint,
508                 verticalPrincipalPoint, skewness);
509     }
510 
511     /**
512      * Determines whether provided matrix is considered a valid matrix for
513      * pinhole camera intrinsic parameters.
514      * Provided matrix must be 3x3, upper triangular and its last element (3,3)
515      * must be 1.0.
516      *
517      * @param m 3x3 and upper triangular matrix to be checked.
518      * @return True if provided matrix is valid, false otherwise.
519      */
520     public static boolean isValidMatrix(final Matrix m) {
521         return isValidMatrix(m, DEFAULT_VALID_THRESHOLD);
522     }
523 
524     /**
525      * Determines whether provided matrix is considered a valid matrix for
526      * pinhole camera intrinsic parameters.
527      * Provided matrix must be 3x3, upper triangular (up to provided threshold)
528      * and its last element (3,3) must be 1.0.
529      *
530      * @param m         3x3 and upper triangular matrix to be checked.
531      * @param threshold Threshold to determine whether matrix is upper
532      *                  triangular.
533      * @return True if provided matrix is valid, false otherwise.
534      * @throws IllegalArgumentException Raised if provided threshold is negative.
535      */
536     public static boolean isValidMatrix(final Matrix m, final double threshold) {
537 
538         if (threshold < 0) {
539             throw new IllegalArgumentException();
540         }
541 
542         if (m.getRows() != INTRINSIC_MATRIX_ROWS || m.getColumns() != INTRINSIC_MATRIX_COLS) {
543             return false;
544         }
545 
546         // TODO: create isUpperTriangular and isLowerTriangular methods in Utils class within Algebra library
547 
548         // check that provided matrix is upper triangular
549         for (var u = 0; u < INTRINSIC_MATRIX_ROWS; u++) {
550             for (var v = 0; v < u; v++) {
551                 if (Math.abs(m.getElementAt(u, v)) > threshold) {
552                     return false;
553                 }
554             }
555         }
556 
557         // check that lower right element is 1.0
558         return Math.abs(m.getElementAt(2, 2)) - 1.0 <= threshold;
559     }
560 
561     /**
562      * Normalizes provided matrix so that element (3,3) becomes one.
563      *
564      * @param m Matrix to be normalized.
565      */
566     private static void normalize(final Matrix m) {
567         final var norm = m.getElementAt(2, 2);
568         m.multiplyByScalar(1.0 / norm);
569     }
570 
571     /**
572      * Clones this instance of pinhole camera matrix.
573      *
574      * @return A copy of this instance.
575      * @throws CloneNotSupportedException if clone fails.
576      */
577     @Override
578     public PinholeCameraIntrinsicParameters clone() throws CloneNotSupportedException {
579         final var result = (PinholeCameraIntrinsicParameters) super.clone();
580         result.internalMatrix = new Matrix(this.internalMatrix);
581         return result;
582     }
583 
584     /**
585      * Creates an instance of pinhole camera intrinsic parameters using
586      * provided data. Created instance assumes that skewness is zero and that
587      * principal point is located at origin of coordinates.
588      * This method can be used if for instance actual camera sensor data such as
589      * focal length expressed in millimeters and sensor size expressed in
590      * millimeters is known along with the size of the obtained image data.
591      *
592      * @param focalLength  focal length of camera expressed in millimeters.
593      * @param sensorWidth  camera sensor width expressed in millimeters.
594      * @param sensorHeight camera sensor height expressed in millimeters.
595      * @param imageWidth   captured image width expressed in pixels.
596      * @param imageHeight  captured image height expressed in pixels.
597      * @return an instance of pinhole camera intrinsic parameters.
598      */
599     public static PinholeCameraIntrinsicParameters create(
600             final double focalLength, final double sensorWidth, final double sensorHeight, final int imageWidth,
601             final int imageHeight) {
602 
603         // compute the size of a pixel taking into account sensor and image sizes
604         final var pixelWidth = sensorWidth / imageWidth; // mm/px
605         final var pixelHeight = sensorHeight / imageHeight; // mm/px
606 
607         // compute focal lengths expressed in pixels
608         final var horizontalFocalLength = focalLength / pixelWidth;
609         final var verticalFocalLength = focalLength / pixelHeight;
610 
611         return new PinholeCameraIntrinsicParameters(horizontalFocalLength, verticalFocalLength, 0.0,
612                 0.0, 0.0);
613     }
614 }