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 }