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 }