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.AlgebraException;
19  import com.irurueta.algebra.Matrix;
20  import com.irurueta.algebra.SingularValueDecomposer;
21  import com.irurueta.algebra.Utils;
22  import com.irurueta.algebra.WrongSizeException;
23  
24  import java.io.Serializable;
25  
26  /**
27   * This class defines the amount of rotation for 3D points or planes.
28   * Rotation is defined internally as a matrix.
29   */
30  @SuppressWarnings("DuplicatedCode")
31  public class MatrixRotation3D extends Rotation3D implements Serializable {
32  
33      /**
34       * Constant defining the number of rows on a 3D rotation matrix expressed
35       * in inhomogeneous coordinates.
36       */
37      public static final int ROTATION3D_INHOM_MATRIX_ROWS = 3;
38  
39      /**
40       * Constant defining the number of columns on a 3D rotation matrix expressed
41       * in inhomogeneous coordinates.
42       */
43      public static final int ROTATION3D_INHOM_MATRIX_COLS = 3;
44  
45      /**
46       * Constant defining the number of rows on a 3D rotation matrix expressed
47       * in homogeneous coordinates.
48       */
49      public static final int ROTATION3D_HOM_MATRIX_ROWS = 4;
50  
51      /**
52       * Constant defining the number of columns on a 3D rotation matrix expressed
53       * in homogeneous coordinates.
54       */
55      public static final int ROTATION3D_HOM_MATRIX_COLS = 4;
56  
57      /**
58       * Threshold to determine that a gimbal locked might have been achieved
59       * when trying to find roll, pitch and yaw angles.
60       */
61      public static final double GIMBAL_THRESHOLD = 1e-6;
62  
63      /**
64       * Internal matrix containing rotation using inhomogeneous coordinates.
65       * This matrix will be square, 3x3, orthogonal and will have determinant
66       * equal to one.
67       */
68      protected Matrix internalMatrix;
69  
70  
71      /**
72       * Empty Constructor.
73       * Initializes rotation so that no rotation exists (i.e. internal matrix is
74       * the identity).
75       */
76      public MatrixRotation3D() {
77          try {
78              internalMatrix = Matrix.identity(ROTATION3D_INHOM_MATRIX_ROWS, ROTATION3D_INHOM_MATRIX_ROWS);
79          } catch (final WrongSizeException ignore) {
80              // never happens
81          }
82      }
83  
84      /**
85       * Copy constructor.
86       * Copies provided rotation into this instance.
87       *
88       * @param rotation Instance to be copied.
89       */
90      public MatrixRotation3D(final MatrixRotation3D rotation) {
91          internalMatrix = new Matrix(rotation.internalMatrix);
92      }
93  
94      /**
95       * Copy constructor.
96       * Copies and converts provided rotation into this instance.
97       *
98       * @param rotation Instance to be copied.
99       */
100     public MatrixRotation3D(final Rotation3D rotation) {
101         internalMatrix = rotation.asInhomogeneousMatrix();
102     }
103 
104     /**
105      * Constructor.
106      * Creates a 3D rotation using provided matrix.
107      * Provided matrix can be expressed in either homogeneous or inhomogeneous
108      * coordinates, and it must also be orthogonal and having determinant equal
109      * to 1.
110      * The threshold to determine whether provided matrix is orthonormal will
111      * be DEFAULT_VALID_THRESHOLD.
112      *
113      * @param m Matrix to create rotation from.
114      * @throws InvalidRotationMatrixException Raised if provided matrix is not
115      *                                        valid (its size is wrong, or it is not orthonormal).
116      *                                        {@link #isValidRotationMatrix(Matrix)}.
117      */
118     public MatrixRotation3D(final Matrix m) throws InvalidRotationMatrixException {
119         fromMatrix(m);
120     }
121 
122     /**
123      * Constructor.
124      * Creates a 3D rotation using provided matrix.
125      * Provided matrix can be expressed in either homogeneous or inhomogeneous
126      * coordinates, and it must also be orthogonal up to provided threshold, and
127      * must have determinant equal to 1.
128      *
129      * @param m         Matrix to create rotation from.
130      * @param threshold Threshold to determine whether matrix is orthonormal.
131      * @throws InvalidRotationMatrixException Raised if provided matrix is not
132      *                                        valid (its size is wrong, or it is not orthonormal).
133      * @throws IllegalArgumentException       Raised if provided threshold is
134      *                                        negative.
135      *                                        {@link #isValidRotationMatrix(Matrix)}.
136      */
137     public MatrixRotation3D(final Matrix m, final double threshold) throws InvalidRotationMatrixException {
138         fromMatrix(m, threshold);
139     }
140 
141     /**
142      * Constructor.
143      * Creates a 3D rotation using provided Euler angles expressed in radians.
144      *
145      * @param alphaEuler Alpha Euler angle expressed in radians.
146      * @param betaEuler  Beta Euler angle expressed in radians.
147      * @param gammaEuler Gamma Euler angle expressed in radians.
148      */
149     public MatrixRotation3D(final double alphaEuler, final double betaEuler, final double gammaEuler) {
150         setEulerAngles(alphaEuler, betaEuler, gammaEuler);
151     }
152 
153     /**
154      * Constructor.
155      * Creates a 3D reconstruction using provided rotation axis and rotation
156      * angle expressed in radians.
157      *
158      * @param axis  Axis of rotation. Axis must be a length-3 array containing
159      *              the axis vector. For better accuracy axis coordinates should be
160      *              normalized (norm equal to 1).
161      * @param theta Angle of rotation respect the axis expressed in radians.
162      * @throws IllegalArgumentException Raised if provided axis does not have
163      *                                  length 3.
164      */
165     public MatrixRotation3D(final double[] axis, final double theta) {
166         setAxisAndRotation(axis, theta);
167     }
168 
169     /**
170      * Constructor.
171      * Creates a 3D reconstruction using provided rotation axis coordinates and
172      * rotation angle expressed in radians.
173      * Note: for better accuracy axis coordinates should be normalized (norm
174      * equal to 1).
175      *
176      * @param axisX X coordinate of axis.
177      * @param axisY Y coordinate of axis.
178      * @param axisZ Z coordinate of axis.
179      * @param theta Angle of rotation respect the axis expressed in radians.
180      */
181     public MatrixRotation3D(final double axisX, final double axisY, final double axisZ, final double theta) {
182         setAxisAndRotation(axisX, axisY, axisZ, theta);
183     }
184 
185     /**
186      * Returns type of this rotation.
187      *
188      * @return Type of this rotation.
189      */
190     @Override
191     public Rotation3DType getType() {
192         return Rotation3DType.MATRIX_ROTATION3D;
193     }
194 
195     /**
196      * Returns a copy of the internal matrix so that the internal matrix cannot
197      * be modified accidentally.
198      * Returned matrix will be 3x3, orthogonal and will have determinant equal
199      * to one.
200      *
201      * @return Internal matrix containing rotation of this instance.
202      */
203     public Matrix getInternalMatrix() {
204         return new Matrix(internalMatrix);
205     }
206 
207     /**
208      * Sets the internal matrix of this rotation. Provided matrix must be 3x3
209      * and orthonormal (orthogonal with determinant equal to 1).
210      *
211      * @param internalMatrix Internal matrix to be set.
212      * @throws InvalidRotationMatrixException Raised if provided matrix is not
213      *                                        3x3, or it is not orthonormal.
214      */
215     public final void setInternalMatrix(final Matrix internalMatrix) throws InvalidRotationMatrixException {
216         setInternalMatrix(internalMatrix, DEFAULT_VALID_THRESHOLD);
217     }
218 
219     /**
220      * Sets the internal matrix of this rotation. Provided matrix must be 3x3
221      * and orthonormal (orthogonal with determinant equal to 1) up to an error
222      * equal to provided threshold.
223      *
224      * @param m         Internal matrix to be set.
225      * @param threshold Threshold to determine whether matrix is orthonormal or
226      *                  not.
227      * @throws InvalidRotationMatrixException Raised if provided matrix is not
228      *                                        3x3, or it is not orthonormal.
229      * @throws IllegalArgumentException       Raised if provided threshold is
230      *                                        negative.
231      */
232     public final void setInternalMatrix(final Matrix m, final double threshold) throws InvalidRotationMatrixException {
233         if (m.getRows() != ROTATION3D_INHOM_MATRIX_ROWS || m.getColumns() != ROTATION3D_INHOM_MATRIX_COLS) {
234             throw new InvalidRotationMatrixException();
235         }
236         if (!isValidRotationMatrix(m, threshold)) {
237             throw new InvalidRotationMatrixException();
238         }
239 
240         internalMatrix = m;
241     }
242 
243     /**
244      * Returns alpha euler angle within the range -pi and pi.
245      *
246      * @return Alpha euler angle expressed in radians.
247      */
248     public double getAlphaEulerAngle() {
249         return Math.atan2(-internalMatrix.getElementAt(2, 0),
250                 internalMatrix.getElementAt(2, 2));
251     }
252 
253     /**
254      * Returns beta euler angle within the range -pi/2 and pi/2.
255      *
256      * @return Beta euler angle expressed in radians.
257      */
258     public double getBetaEulerAngle() {
259         return Math.asin(internalMatrix.getElementAt(2, 1));
260     }
261 
262     /**
263      * Returns gamma euler angle within the range -pi and pi.
264      *
265      * @return Gamma euler angle expressed in radians.
266      */
267     public double getGammaEulerAngle() {
268         return Math.atan2(-internalMatrix.getElementAt(0, 1),
269                 internalMatrix.getElementAt(1, 1));
270     }
271 
272     /**
273      * Sets euler angles of this rotation, expressed in radians.
274      *
275      * @param alphaEuler Alpha euler angle in radians.
276      * @param betaEuler  Beta euler angle in radians.
277      * @param gammaEuler Gamma euler angle in radians.
278      */
279     public final void setEulerAngles(final double alphaEuler, final double betaEuler, final double gammaEuler) {
280         final var sinAlpha = Math.sin(alphaEuler);
281         final var cosAlpha = Math.cos(alphaEuler);
282 
283         final var sinBeta = Math.sin(betaEuler);
284         final var cosBeta = Math.cos(betaEuler);
285 
286         final var sinGamma = Math.sin(gammaEuler);
287         final var cosGamma = Math.cos(gammaEuler);
288 
289         // reuse internal matrix if possible
290         try {
291             if (internalMatrix == null) {
292                 internalMatrix = new Matrix(ROTATION3D_INHOM_MATRIX_ROWS, ROTATION3D_INHOM_MATRIX_COLS);
293             }
294 
295             internalMatrix.setElementAt(0, 0, cosAlpha * cosGamma - sinAlpha * sinBeta * sinGamma);
296             internalMatrix.setElementAt(1, 0, cosAlpha * sinGamma + sinAlpha * sinBeta * cosGamma);
297             internalMatrix.setElementAt(2, 0, -sinAlpha * cosBeta);
298 
299             internalMatrix.setElementAt(0, 1, -cosBeta * sinGamma);
300             internalMatrix.setElementAt(1, 1, cosBeta * cosGamma);
301             internalMatrix.setElementAt(2, 1, sinBeta);
302 
303             internalMatrix.setElementAt(0, 2, sinAlpha * cosGamma + cosAlpha * sinBeta * sinGamma);
304             internalMatrix.setElementAt(1, 2, sinAlpha * sinGamma - cosAlpha * sinBeta * cosGamma);
305             internalMatrix.setElementAt(2, 2, cosAlpha * cosBeta);
306         } catch (final WrongSizeException ignore) {
307             // never happens
308         }
309     }
310 
311     /**
312      * Returns roll angle around x-axis expressed in radians for the 1st
313      * possible set of solutions.
314      * When obtaining roll, pitch and yaw angles from a rotation matrix, there
315      * might be two possible sets of solutions (#getRollAngle(),
316      * #getPitchAngle(), #getYawAngle()) or (#getRollAngle2(),
317      * #getPitchAngle2(), #getYawAngle2()).
318      *
319      * @return roll angle around x-axis.
320      * @see <a href="http://www.staff.city.ac.uk/~sbbh653/publications/euler.pdf">http://www.staff.city.ac.uk/~sbbh653/publications/euler.pdf</a>
321      * @see <a href="https://github.com/joansola/slamtb">R2e.m at https://github.com/joansola/slamtb</a>
322      */
323     public double getRollAngle() {
324         return getRollAngle(getPitchAngle());
325     }
326 
327     /**
328      * Returns roll angle around x-axis expressed in radians for the 2nd
329      * possible set of solutions.
330      * When obtaining roll, pitch and yaw angles from a rotation matrix, there
331      * might be two possible sets of solutions (#getRollAngle(),
332      * #getPitchAngle(), #getYawAngle()) or (#getRollAngle2(),
333      * #getPitchAngle2(), #getYawAngle2()).
334      *
335      * @return roll angle around x-axis.
336      * @see <a href="http://www.staff.city.ac.uk/~sbbh653/publications/euler.pdf">http://www.staff.city.ac.uk/~sbbh653/publications/euler.pdf</a>
337      * @see <a href="https://github.com/joansola/slamtb">R2e.m at https://github.com/joansola/slamtb</a>
338      */
339     public double getRollAngle2() {
340         return getRollAngle(getPitchAngle2());
341     }
342 
343     /**
344      * Returns roll angle around x-axis expressed in radians corresponding to
345      * provided pitch value.
346      * This method is used internally.
347      *
348      * @param pitch pitch angle expressed in radians.
349      * @return roll angle around x-axis.
350      */
351     private double getRollAngle(final double pitch) {
352         if (!hasGimbalLock()) {
353             final var cosPitch = Math.cos(pitch);
354             return Math.atan2(internalMatrix.getElementAt(2, 1) / cosPitch,
355                     internalMatrix.getElementAt(2, 2) / cosPitch);
356         } else {
357             // gimbal lock (pitch is close to +-90 degrees)
358             if (internalMatrix.getElementAt(2, 0) < 0.0) {
359                 // pitch is +90 degrees
360                 return Math.atan2(internalMatrix.getElementAt(0, 1),
361                         internalMatrix.getElementAt(0, 2));
362             } else {
363                 // pitch is -90 degrees
364                 return Math.atan2(-internalMatrix.getElementAt(0, 1),
365                         -internalMatrix.getElementAt(0, 2));
366             }
367         }
368     }
369 
370     /**
371      * Returns pitch angle around y-axis expressed in radians for the 1st
372      * possible set of solutions.
373      * When obtaining roll, pitch and yaw angles from a rotation matrix, there
374      * might be two possible sets of solutions (#getRollAngle(),
375      * #getPitchAngle(), #getYawAngle()) or (#getRollAngle2(),
376      * #getPitchAngle2(), #getYawAngle2()).
377      *
378      * @return pitch angle around y-axis.
379      * @see <a href="http://www.staff.city.ac.uk/~sbbh653/publications/euler.pdf">http://www.staff.city.ac.uk/~sbbh653/publications/euler.pdf</a>
380      * @see <a href="https://github.com/joansola/slamtb">R2e.m at https://github.com/joansola/slamtb</a>
381      */
382     public double getPitchAngle() {
383         return -Math.asin(internalMatrix.getElementAt(2, 0));
384     }
385 
386     /**
387      * Returns pitch angle around y-axis expressed in radians for the 2nd
388      * possible set of solutions.
389      * When obtaining roll, pitch and yaw angles from a rotation matrix, there
390      * might be two possible sets of solutions (#getRollAngle(),
391      * #getPitchAngle(), #getYawAngle()) or (#getRollAngle2(),
392      * #getPitchAngle2(), #getYawAngle2()).
393      * When a gimbal lock occurs, both pitch angles are equal because only
394      * yaw is undefined, but pitch and roll are unique.
395      *
396      * @return pitch angle around y-axis.
397      * @see <a href="http://www.staff.city.ac.uk/~sbbh653/publications/euler.pdf">http://www.staff.city.ac.uk/~sbbh653/publications/euler.pdf</a>
398      * @see <a href="https://github.com/joansola/slamtb">R2e.m at https://github.com/joansola/slamtb</a>
399      */
400     public double getPitchAngle2() {
401         if (!hasGimbalLock()) {
402             return Math.PI - getPitchAngle();
403         } else {
404             return getPitchAngle();
405         }
406     }
407 
408     /**
409      * Returns yaw angle around z axis expressed in radians for the 1st possible
410      * set of solutions.
411      * When a gimbal lock occurs (pitch angle is close to +- 90 degrees), then
412      * yaw angle is undefined, and can be any value, although this method will
413      * return 0.0.
414      * When obtaining roll, pitch and yaw angles from a rotation matrix, there
415      * might be two possible sets of solutions (#getRollAngle(),
416      * #getPitchAngle(), #getYawAngle()) or (#getRollAngle2(),
417      * #getPitchAngle2(), #getYawAngle2()).
418      *
419      * @return yaw angle around z axis.
420      * @see <a href="http://www.staff.city.ac.uk/~sbbh653/publications/euler.pdf">http://www.staff.city.ac.uk/~sbbh653/publications/euler.pdf</a>
421      * @see <a href="https://github.com/joansola/slamtb">R2e.m at https://github.com/joansola/slamtb</a>
422      */
423     public double getYawAngle() {
424         return getYawAngle(getPitchAngle());
425     }
426 
427     /**
428      * Returns yaw angle around z axis expressed in radians for the 2nd possible
429      * set of solutions.
430      * When a gimbal lock occurs (pitch angle is close to +- 90 degrees), then
431      * yaw angle is undefined, and can be any value, although this method will
432      * return 0.0.
433      * When obtaining roll, pitch and yaw angles from a rotation matrix, there
434      * might be two possible sets of solutions (#getRollAngle(),
435      * #getPitchAngle(), #getYawAngle()) or (#getRollAngle2(),
436      * #getPitchAngle2(), #getYawAngle2()).
437      *
438      * @return yaw angle around z axis.
439      * @see <a href="http://www.staff.city.ac.uk/~sbbh653/publications/euler.pdf">http://www.staff.city.ac.uk/~sbbh653/publications/euler.pdf</a>
440      * @see <a href="https://github.com/joansola/slamtb">R2e.m at https://github.com/joansola/slamtb</a>
441      */
442     public double getYawAngle2() {
443         return getYawAngle(getPitchAngle2());
444     }
445 
446     /**
447      * Returns yaw angle around x-axis expressed in radians corresponding to
448      * provided pitch value.
449      * This method is used internally.
450      *
451      * @param pitch pitch angle expressed in radians.
452      * @return yaw angle around x-axis.
453      */
454     private double getYawAngle(final double pitch) {
455         if (!hasGimbalLock()) {
456             final var cosPitch = Math.cos(pitch);
457             return Math.atan2(internalMatrix.getElementAt(1, 0) / cosPitch,
458                     internalMatrix.getElementAt(0, 0) / cosPitch);
459         } else {
460             // gimbal lock (pitch is close to +-90 degrees)
461             // can be anything.
462             return 0.0;
463         }
464     }
465 
466     /**
467      * Indicates whether current rotation contains ambiguities (a.k.a. gimbal
468      * lock). This situation happens when pitch angle is close to +-90 degrees.
469      *
470      * @return true if current rotation contains a gimbal lock, false otherwise.
471      * @see <a href="http://www.staff.city.ac.uk/~sbbh653/publications/euler.pdf">http://www.staff.city.ac.uk/~sbbh653/publications/euler.pdf</a>
472      * @see <a href="https://github.com/joansola/slamtb">R2e.m at https://github.com/joansola/slamtb</a>
473      */
474     public boolean hasGimbalLock() {
475         return Math.abs(Math.abs(internalMatrix.getElementAt(2, 0)) - 1.0) <
476                 GIMBAL_THRESHOLD;
477     }
478 
479     /**
480      * Sets rotation angles, expressed in radians.
481      *
482      * @param roll  roll angle in radians around x-axis.
483      * @param pitch pitch angle in radians around y-axis.
484      * @param yaw   yaw angle in radians around z-axis.
485      * @see <a href="http://www.staff.city.ac.uk/~sbbh653/publications/euler.pdf">http://www.staff.city.ac.uk/~sbbh653/publications/euler.pdf</a>
486      * @see <a href="https://github.com/joansola/slamtb">e2R.m at https://github.com/joansola/slamtb</a>
487      */
488     public void setRollPitchYaw(final double roll, final double pitch, final double yaw) {
489         final var sr = Math.sin(roll);
490         final var cr = Math.cos(roll);
491 
492         final var sp = Math.sin(pitch);
493         final var cp = Math.cos(pitch);
494 
495         final var sy = Math.sin(yaw);
496         final var cy = Math.cos(yaw);
497 
498         try {
499             // reuse internal matrix if possible
500             if (internalMatrix == null) {
501                 internalMatrix = new Matrix(ROTATION3D_INHOM_MATRIX_ROWS, ROTATION3D_INHOM_MATRIX_COLS);
502             }
503 
504             internalMatrix.setElementAt(0, 0, cp * cy);
505             internalMatrix.setElementAt(1, 0, cp * sy);
506             internalMatrix.setElementAt(2, 0, -sp);
507 
508             internalMatrix.setElementAt(0, 1, -cr * sy + sr * sp * cy);
509             internalMatrix.setElementAt(1, 1, cr * cy + sr * sp * sy);
510             internalMatrix.setElementAt(2, 1, sr * cp);
511 
512             internalMatrix.setElementAt(0, 2, sr * sy + cr * sp * cy);
513             internalMatrix.setElementAt(1, 2, -sr * cy + cr * sp * sy);
514             internalMatrix.setElementAt(2, 2, cr * cp);
515         } catch (final WrongSizeException ignore) {
516             // never happens
517         }
518     }
519 
520     /**
521      * Sets the axis and rotation of this instance.
522      * Once set, points will rotate around provided axis an amount equal to
523      * provided rotation angle in radians.
524      * Note: to avoid numerical instabilities and improve accuracy, axis
525      * coordinates should be normalized (e.g. norm equal to 1).
526      *
527      * @param axisX X coordinate of rotation axis.
528      * @param axisY Y coordinate of rotation axis.
529      * @param axisZ Z coordinate of rotation axis.
530      * @param theta Amount of rotation in radians.
531      */
532     @Override
533     public final void setAxisAndRotation(
534             final double axisX, final double axisY, final double axisZ, final double theta) {
535         final var axisX2 = axisX * axisX;
536         final var axisY2 = axisY * axisY;
537         final var axisZ2 = axisZ * axisZ;
538 
539         final var axisXY = axisX * axisY;
540         final var axisXZ = axisX * axisZ;
541         final var axisYZ = axisY * axisZ;
542 
543         final var sinTheta = Math.sin(theta);
544         final var cosTheta = Math.cos(theta);
545 
546         try {
547             if (internalMatrix == null) {
548                 internalMatrix = new Matrix(ROTATION3D_INHOM_MATRIX_ROWS, ROTATION3D_INHOM_MATRIX_COLS);
549             }
550 
551             internalMatrix.setElementAt(0, 0, axisX2 + (1.0 - axisX2) * cosTheta);
552             internalMatrix.setElementAt(1, 0, axisXY * (1.0 - cosTheta) + axisZ * sinTheta);
553             internalMatrix.setElementAt(2, 0, axisXZ * (1.0 - cosTheta) - axisY * sinTheta);
554 
555             internalMatrix.setElementAt(0, 1, axisXY * (1.0 - cosTheta) - axisZ * sinTheta);
556             internalMatrix.setElementAt(1, 1, axisY2 + (1.0 - axisY2) * cosTheta);
557             internalMatrix.setElementAt(2, 1, axisYZ * (1.0 - cosTheta) + axisX * sinTheta);
558 
559             internalMatrix.setElementAt(0, 2, axisXZ * (1.0 - cosTheta) + axisY * sinTheta);
560             internalMatrix.setElementAt(1, 2, axisYZ * (1.0 - cosTheta) - axisX * sinTheta);
561             internalMatrix.setElementAt(2, 2, axisZ2 + (1.0 - axisZ2) * cosTheta);
562         } catch (final WrongSizeException ignore) {
563             // never happens
564         }
565     }
566 
567     /**
568      * Returns rotation axis corresponding to this instance.
569      * Result is stored in provided axis array, which must have length 3.
570      *
571      * @param axis Array where axis coordinates will be stored.
572      * @throws IllegalArgumentException Raised if provided array does not have
573      *                                  length 3.
574      * @throws RotationException        Raised if numerical instabilities happen.
575      *                                  Because internal matrix will always be well-defined (orthogonal and
576      *                                  determinant equal to 1), this exception will rarely happen.
577      */
578     @Override
579     public void rotationAxis(final double[] axis) throws RotationException {
580         if (axis.length != ROTATION3D_INHOM_MATRIX_ROWS) {
581             throw new IllegalArgumentException();
582         }
583 
584         try {
585             // Rotation axis follows: R*v = v, which means that rotation axis is
586             // left unchanged after rotation. Hence:
587             // R*v = I*v --> (R - I)*v = 0, consequently rotation axis is the
588             // null-space of R-I
589             final Matrix identity = Matrix.identity(ROTATION3D_INHOM_MATRIX_ROWS, ROTATION3D_INHOM_MATRIX_COLS);
590             // line below: identity = internalMatrix - identity
591             internalMatrix.subtract(identity, identity);
592             // internalMatrix - identity
593 
594             final SingularValueDecomposer decomposer = new SingularValueDecomposer(identity);
595 
596             decomposer.decompose();
597 
598             final var v = decomposer.getV();
599 
600             // last column of V contains axis values
601             axis[0] = v.getElementAt(0, 2);
602             axis[1] = v.getElementAt(1, 2);
603             axis[2] = v.getElementAt(2, 2);
604         } catch (final AlgebraException e) {
605             throw new RotationException(e);
606         }
607     }
608 
609     /**
610      * Returns rotation amount or angle in radians around the rotation axis
611      * associated to this instance.
612      *
613      * @return Rotation angle in radians.
614      * @throws RotationException Raised if numerical instabilities happen.
615      *                           Because internal matrix will always be well-defined (orthogonal and
616      *                           determinant equal to 1), this exception will rarely happen.
617      */
618     @Override
619     public double getRotationAngle() throws RotationException {
620 
621         // obtain rotation axis
622         final var axis = getRotationAxis();
623 
624         try {
625             final var axisMatrix = new Matrix(1, ROTATION3D_INHOM_MATRIX_COLS);
626             axisMatrix.setSubmatrix(0, 0, 0,
627                     ROTATION3D_INHOM_MATRIX_COLS - 1, axis);
628 
629             final var decomposer = new SingularValueDecomposer(axisMatrix);
630             decomposer.decompose();
631 
632             final var v = decomposer.getV();
633 
634             // because axisMatrix has rank 1, its null-space will contain a
635             // two-dimensional space (two vectors) perpendicular to axisMatrix
636             final var perpendicular = v.getSubmatrix(0, 2, 2, 2);
637             final var normPerpendicular = Utils.normF(perpendicular);
638 
639             // use internal matrix to rotate perpendicular vector
640             final var rotated = internalMatrix.multiplyAndReturnNew(perpendicular);
641             final var normRotated = Utils.normF(rotated);
642 
643             // normalize vectors
644             perpendicular.multiplyByScalar(1.0 / normPerpendicular);
645             rotated.multiplyByScalar(1.0 / normRotated);
646 
647             // their dot product is the cosine of their angle
648             // (we transpose rotated matrix to compute dot product)
649             rotated.transpose();
650             rotated.multiply(perpendicular);
651             final var dotProduct = rotated.getElementAtIndex(0);
652 
653             final var theta = Math.acos(dotProduct);
654 
655             // we need to determine sign of theta, for that reason we instantiate
656             // two camera rotations with theta and -theta using the same rotation
657             // axis and check for the rotation matrix that produces less error
658             final var rotation1 = new MatrixRotation3D(axis, theta);
659             final var rotation2 = new MatrixRotation3D(axis, -theta);
660 
661             final var rotationMatrix1 = rotation1.internalMatrix;
662             final var rotationMatrix2 = rotation2.internalMatrix;
663 
664             // normalize rotation matrices for their comparison
665             final var norm1 = Utils.normF(rotationMatrix1);
666             final var norm2 = Utils.normF(rotationMatrix2);
667             rotationMatrix1.multiplyByScalar(1.0 / norm1);
668             rotationMatrix2.multiplyByScalar(1.0 / norm2);
669 
670             // normalize current internal matrix for its comparison
671             final var internalNorm = Utils.normF(internalMatrix);
672             final var internal = internalMatrix.multiplyByScalarAndReturnNew(1.0 / internalNorm);
673 
674             // compare matrices
675             rotationMatrix1.subtract(internal);
676             rotationMatrix2.subtract(internal);
677 
678             // now rotation matrices 1 and 2 contain the difference
679             final var normDiff1 = Utils.normF(rotationMatrix1);
680             final var normDiff2 = Utils.normF(rotationMatrix2);
681 
682             // pick the rotation matrix that produces less error
683             if (normDiff1 < normDiff2) {
684                 // positive theta
685                 return theta;
686             } else {
687                 // negative theta
688                 return -theta;
689             }
690         } catch (final AlgebraException e) {
691             throw new RotationException(e);
692         }
693     }
694 
695     /**
696      * Returns a 3D rotation which is inverse to this instance.
697      * In other words, the combination of this rotation with its inverse
698      * produces no change.
699      *
700      * @return Inverse 3D rotation.
701      */
702     @Override
703     public MatrixRotation3D inverseRotationAndReturnNew() {
704         final var result = new MatrixRotation3D();
705         inverseRotation(result);
706         return result;
707     }
708 
709     /**
710      * Sets into provided MatrixRotation3D instance a rotation inverse to this
711      * instance.
712      * The combination of this rotation with its inverse produces no change.
713      *
714      * @param result Instance where inverse rotation will be set.
715      */
716     public void inverseRotation(final MatrixRotation3D result) {
717         try {
718             result.internalMatrix = Utils.inverse(internalMatrix);
719         } catch (final AlgebraException ignore) {
720             // matrix should always be invertible
721         }
722     }
723 
724     /**
725      * Sets into provided MatrixRotation3D instance a rotation inverse to this
726      * instance.
727      * The combination of this rotation with its inverse produces no change.
728      *
729      * @param result Instance where inverse rotation will be set.
730      */
731     @Override
732     public void inverseRotation(final Rotation3D result) {
733         if (result instanceof MatrixRotation3D matrixResult) {
734             inverseRotation(matrixResult);
735         } else if (result instanceof AxisRotation3D) {
736             MatrixRotation3D rot = new MatrixRotation3D();
737             inverseRotation(rot);
738             try {
739                 result.fromMatrix(rot.asInhomogeneousMatrix());
740             } catch (final InvalidRotationMatrixException ignore) {
741                 // never happens
742             }
743         }
744     }
745 
746     /**
747      * Reverses the rotation of this instance.
748      */
749     @Override
750     public void inverseRotation() {
751         inverseRotation(this);
752     }
753 
754 
755     /**
756      * Returns this 3D rotation instance expressed as a 3x3 inhomogeneous
757      * matrix.
758      * This is equivalent to call getInternalMatrix().
759      *
760      * @return Rotation matrix expressed in inhomogeneous coordinates.
761      */
762     @Override
763     public Matrix asInhomogeneousMatrix() {
764         return getInternalMatrix();
765     }
766 
767     /**
768      * Sets into provided Matrix instance this 3D rotation expressed as a
769      * 3x3 inhomogeneous matrix.
770      *
771      * @param result Matrix where rotation will be set.
772      * @throws IllegalArgumentException Raised if provided instance does not
773      *                                  have size 3x3.
774      */
775     @Override
776     public void asInhomogeneousMatrix(final Matrix result) {
777         if (result.getRows() != ROTATION3D_INHOM_MATRIX_ROWS || result.getColumns() != ROTATION3D_INHOM_MATRIX_COLS) {
778             throw new IllegalArgumentException();
779         }
780 
781         result.copyFrom(internalMatrix);
782     }
783 
784     /**
785      * Returns this 3D rotation instance expressed as a 4x4 homogeneous matrix.
786      *
787      * @return Rotation matrix expressed in homogeneous coordinates.
788      */
789     @Override
790     public Matrix asHomogeneousMatrix() {
791         Matrix result = null;
792         try {
793             result = Matrix.identity(ROTATION3D_HOM_MATRIX_ROWS, ROTATION3D_HOM_MATRIX_COLS);
794             // sets into 3x3 top-left sub-matrix the internal matrix of this
795             // instance, the remaining part will continue to be the identity
796             result.setSubmatrix(0, 0, ROTATION3D_INHOM_MATRIX_ROWS - 1,
797                     ROTATION3D_INHOM_MATRIX_COLS - 1, internalMatrix);
798         } catch (final WrongSizeException ignore) {
799             // never happens
800         }
801         return result;
802     }
803 
804     /**
805      * Sets into provided Matrix instance this 3D rotation expressed as a
806      * 4x4 homogeneous matrix.
807      *
808      * @param result Matrix where rotation will be set.
809      * @throws IllegalArgumentException Raised if provided instance does not
810      *                                  have size 4x4.
811      */
812     @Override
813     public void asHomogeneousMatrix(final Matrix result) {
814         if (result.getRows() != ROTATION3D_HOM_MATRIX_ROWS || result.getColumns() != ROTATION3D_HOM_MATRIX_COLS) {
815             throw new IllegalArgumentException();
816         }
817 
818         result.initialize(0.0);
819         // sets into 3x3 top-left sub-matrix the internal matrix of this instance
820         result.setSubmatrix(0, 0, ROTATION3D_INHOM_MATRIX_ROWS - 1,
821                 ROTATION3D_INHOM_MATRIX_COLS - 1, internalMatrix);
822         // set las element to 1.0 (to be like the identity
823         result.setElementAt(ROTATION3D_HOM_MATRIX_ROWS - 1, ROTATION3D_HOM_MATRIX_COLS - 1, 1.0);
824     }
825 
826     /**
827      * Sets amount of rotation from provided inhomogeneous rotation matrix.
828      * Provided matrix must be orthogonal (i.e. squared, non-singular, it's
829      * transpose must be its inverse) and must have determinant equal to 1.
830      * Provided matrix must also have size 3x3.
831      *
832      * @param m         Provided rotation matrix.
833      * @param threshold Threshold to determine whether matrix is orthonormal.
834      * @throws InvalidRotationMatrixException Raised if provided matrix is not
835      *                                        valid (has wrong size, or it is not orthonormal).
836      * @throws IllegalArgumentException       Raised if provided threshold is
837      *                                        negative.
838      *                                        {@link #isValidRotationMatrix(Matrix)}.
839      */
840     @Override
841     public void fromInhomogeneousMatrix(final Matrix m, final double threshold) throws InvalidRotationMatrixException {
842         setInternalMatrix(m, threshold);
843     }
844 
845     /**
846      * Sets amount of rotation from provided homogeneous rotation matrix.
847      * Provided matrix must be orthogonal (i.e. squared, non-singular, it's
848      * transpose must be its inverse) and must have determinant equal to 1.
849      * Provided matrix must also have size 4x4, and its last row and column must
850      * be zero, except for element in last row and column which must be 1.
851      *
852      * @param m         Provided rotation matrix.
853      * @param threshold Threshold to determine whether matrix is orthonormal.
854      * @throws InvalidRotationMatrixException Raised if provided matrix is not
855      *                                        valid (has wrong size, or it is not orthonormal).
856      * @throws IllegalArgumentException       Raised if provided threshold is
857      *                                        negative.
858      *                                        {@link #isValidRotationMatrix(Matrix)}.
859      */
860     @Override
861     public void fromHomogeneousMatrix(final Matrix m, final double threshold) throws InvalidRotationMatrixException {
862         if (m.getRows() != ROTATION3D_HOM_MATRIX_ROWS || m.getColumns() != ROTATION3D_HOM_MATRIX_COLS) {
863             throw new InvalidRotationMatrixException();
864         }
865         if (!isValidRotationMatrix(m, threshold)) {
866             throw new InvalidRotationMatrixException();
867         }
868         if (Math.abs(m.getElementAt(3, 0)) > threshold
869                 || Math.abs(m.getElementAt(3, 1)) > threshold
870                 || Math.abs(m.getElementAt(3, 2)) > threshold
871                 || Math.abs(m.getElementAt(0, 3)) > threshold
872                 || Math.abs(m.getElementAt(1, 3)) > threshold
873                 || Math.abs(m.getElementAt(2, 3)) > threshold
874                 || Math.abs(m.getElementAt(3, 3) - 1.0) > threshold) {
875             throw new InvalidRotationMatrixException();
876         }
877 
878         internalMatrix = m.getSubmatrix(0, 0, ROTATION3D_INHOM_MATRIX_ROWS - 1,
879                 ROTATION3D_INHOM_MATRIX_COLS - 1);
880     }
881 
882     /**
883      * Rotates a 3D point using the origin of coordinates as the axis of
884      * rotation.
885      * Point will be rotated by the amount of rotation contained in this
886      * instance.
887      *
888      * @param inputPoint  Input point to be rotated.
889      * @param resultPoint Rotated point.
890      */
891     @Override
892     public void rotate(final Point3D inputPoint, final Point3D resultPoint) {
893         try {
894             final var r = asHomogeneousMatrix();
895             final var p = new Matrix(Point3D.POINT3D_HOMOGENEOUS_COORDINATES_LENGTH, 1);
896 
897             // to increase accuracy
898             inputPoint.normalize();
899             p.setElementAt(0, 0, inputPoint.getHomX());
900             p.setElementAt(1, 0, inputPoint.getHomY());
901             p.setElementAt(2, 0, inputPoint.getHomZ());
902             p.setElementAt(3, 0, inputPoint.getHomW());
903 
904             // Rotated point below is R * p
905             r.multiply(p);
906 
907             resultPoint.setHomogeneousCoordinates(r.getElementAt(0, 0), r.getElementAt(1, 0),
908                     r.getElementAt(2, 0), r.getElementAt(3, 0));
909         } catch (final WrongSizeException ignore) {
910             // never happens
911         }
912     }
913 
914     /**
915      * Combines provided rotation with this rotation and returns the result as
916      * a new MatrixRotation3D instance.
917      *
918      * @param rotation Input rotation to be combined.
919      * @return Combined rotation, which is equal to the multiplication of the
920      * internal matrix of provided rotation with the internal matrix of this
921      * instance.
922      */
923     public MatrixRotation3D combineAndReturnNew(final MatrixRotation3D rotation) {
924         final var result = new MatrixRotation3D();
925         combine(this, rotation, result);
926         return result;
927     }
928 
929     /**
930      * Combines provided rotation with this rotation and returns the result as
931      * a new MatrixRotation3D instance.
932      *
933      * @param rotation Input rotation to be combined.
934      * @return Combined rotation, which is equal to the multiplication of the
935      * internal matrix of provided rotation with the internal matrix of this
936      * instance.
937      */
938     @Override
939     public Rotation3D combineAndReturnNew(final Rotation3D rotation) {
940         if (rotation instanceof MatrixRotation3D matrixRotation) {
941             return combineAndReturnNew(matrixRotation);
942         } else {
943             return combineAndReturnNew(new MatrixRotation3D(rotation));
944         }
945     }
946 
947     /**
948      * Combines provided rotation into this rotation resulting in the
949      * multiplication of the internal matrices of both rotations.
950      *
951      * @param rotation Input rotation to be combined.
952      */
953     public void combine(final MatrixRotation3D rotation) {
954         combine(this, rotation, this);
955     }
956 
957     /**
958      * Combines provided rotation into this rotation resulting in the
959      * multiplication of the internal matrices of both rotations.
960      *
961      * @param rotation Input rotation to be combined.
962      */
963     @Override
964     public void combine(final Rotation3D rotation) {
965         if (rotation instanceof MatrixRotation3D matrixRotation) {
966             combine(matrixRotation);
967         } else {
968             combine(new MatrixRotation3D(rotation));
969         }
970     }
971 
972     /**
973      * Combines the rotation of instances rot1 and rot1 into provided result
974      * instance.
975      *
976      * @param rot1   1st input rotation.
977      * @param rot2   2nd input rotation.
978      * @param result Combined rotation, which is equal to the multiplication of
979      *               the internal matrix of provided rotation with the internal matrix of this
980      *               instance.
981      */
982     public static void combine(
983             final MatrixRotation3D rot1, final MatrixRotation3D rot2, final MatrixRotation3D result) {
984         try {
985             result.internalMatrix = rot1.internalMatrix.multiplyAndReturnNew(rot2.internalMatrix);
986         } catch (final WrongSizeException ignore) {
987             // never happens
988         }
989     }
990 
991     /**
992      * Sets values of this rotation from a 3D matrix rotation.
993      *
994      * @param rot 3D matrix rotation to set values from.
995      */
996     @Override
997     public void fromRotation(final MatrixRotation3D rot) {
998         rot.internalMatrix.copyTo(internalMatrix);
999     }
1000 
1001     /**
1002      * Sets values of this rotation from a quaternion.
1003      *
1004      * @param q a quaternion to set values from.
1005      */
1006     @Override
1007     public void fromRotation(final Quaternion q) {
1008         q.toMatrixRotation(internalMatrix);
1009     }
1010 
1011     /**
1012      * Converts this 3D rotation into a matrix rotation storing the result
1013      * into provided instance.
1014      *
1015      * @param result instance where result wil be stored.
1016      */
1017     @Override
1018     public void toMatrixRotation(final MatrixRotation3D result) {
1019         internalMatrix.copyTo(result.internalMatrix);
1020     }
1021 
1022     /**
1023      * Converts this 3D rotation into an axis rotation storing the result into
1024      * provided instance.
1025      *
1026      * @param result instance where result will be stored.
1027      */
1028     @Override
1029     public void toAxisRotation(final AxisRotation3D result) {
1030         try {
1031             result.fromInhomogeneousMatrix(internalMatrix);
1032         } catch (final InvalidRotationMatrixException ignore) {
1033             // never thrown
1034         }
1035     }
1036 
1037     /**
1038      * Converts this 3D rotation into a quaternion storing the result into
1039      * provided instance.
1040      *
1041      * @param result instance where result will be stored.
1042      */
1043     @Override
1044     public void toQuaternion(final Quaternion result) {
1045         Quaternion.matrixRotationToQuaternion(internalMatrix, result);
1046     }
1047 }