"""Transformation functions for 3D graphics."""
import math
import numpy as np
from scipy.spatial.transform import Rotation as Rot # type: ignore
# axis sequences for Euler angles
_NEXT_AXIS = [1, 2, 0, 1]
# map axes strings to/from tuples of inner axis, parity, repetition, frame
_AXES2TUPLE = {
"sxyz": (0, 0, 0, 0),
"sxyx": (0, 0, 1, 0),
"sxzy": (0, 1, 0, 0),
"sxzx": (0, 1, 1, 0),
"syzx": (1, 0, 0, 0),
"syzy": (1, 0, 1, 0),
"syxz": (1, 1, 0, 0),
"syxy": (1, 1, 1, 0),
"szxy": (2, 0, 0, 0),
"szxz": (2, 0, 1, 0),
"szyx": (2, 1, 0, 0),
"szyz": (2, 1, 1, 0),
"rzyx": (0, 0, 0, 1),
"rxyx": (0, 0, 1, 1),
"ryzx": (0, 1, 0, 1),
"rxzx": (0, 1, 1, 1),
"rxzy": (1, 0, 0, 1),
"ryzy": (1, 0, 1, 1),
"rzxy": (1, 1, 0, 1),
"ryxy": (1, 1, 1, 1),
"ryxz": (2, 0, 0, 1),
"rzxz": (2, 0, 1, 1),
"rxyz": (2, 1, 0, 1),
"rzyz": (2, 1, 1, 1),
}
_TUPLE2AXES = {v: k for k, v in _AXES2TUPLE.items()}
[docs]
def euler_matrix(ai, aj, ak, *, axes="sxyz"):
"""
Return homogeneous rotation matrix from Euler angles and axis sequence.
Parameters
----------
ai : float
First Euler angle in radians.
aj : float
Second Euler angle in radians.
ak : float
Third Euler angle in radians.
axes : str or tuple, optional
One of 24 axis sequences as string or encoded tuple. Default is 'sxyz'.
Returns
-------
ndarray (4, 4)
Homogeneous rotation matrix.
Notes
-----
Code modified from the work of Christoph Gohlke:
http://www.lfd.uci.edu/~gohlke/code/transformations.py.html
Examples
--------
>>> import numpy
>>> R = euler_matrix(1, 2, 3, axes='syxz')
>>> numpy.allclose(numpy.sum(R[0]), -1.34786452)
True
>>> R = euler_matrix(1, 2, 3, axes=(0, 1, 0, 1))
>>> numpy.allclose(numpy.sum(R[0]), -0.383436184)
True
>>> ai, aj, ak = (4.0*math.pi) * (numpy.random.random(3) - 0.5)
>>> for axes in _AXES2TUPLE.keys():
... _ = euler_matrix(ai, aj, ak, axes=axes)
>>> for axes in _TUPLE2AXES.keys():
... _ = euler_matrix(ai, aj, ak, axes=axes)
"""
try:
firstaxis, parity, repetition, frame = _AXES2TUPLE[axes]
except (AttributeError, KeyError):
firstaxis, parity, repetition, frame = axes
i = firstaxis
j = _NEXT_AXIS[i + parity]
k = _NEXT_AXIS[i - parity + 1]
if frame:
ai, ak = ak, ai
if parity:
ai, aj, ak = -ai, -aj, -ak
si, sj, sk = math.sin(ai), math.sin(aj), math.sin(ak)
ci, cj, ck = math.cos(ai), math.cos(aj), math.cos(ak)
cc, cs = ci * ck, ci * sk
sc, ss = si * ck, si * sk
M = np.identity(4)
if repetition:
M[i, i] = cj
M[i, j] = sj * si
M[i, k] = sj * ci
M[j, i] = sj * sk
M[j, j] = -cj * ss + cc
M[j, k] = -cj * cs - sc
M[k, i] = -sj * ck
M[k, j] = cj * sc + cs
M[k, k] = cj * cc - ss
else:
M[i, i] = cj * ck
M[i, j] = sj * sc - cs
M[i, k] = sj * cc + ss
M[j, i] = cj * sk
M[j, j] = sj * ss + cc
M[j, k] = sj * cs - sc
M[k, i] = -sj
M[k, j] = cj * si
M[k, k] = cj * ci
return M
[docs]
def sphere2cart(r, theta, phi):
"""
Convert spherical coordinates to Cartesian coordinates.
Parameters
----------
r : array_like
Radius.
theta : array_like
Inclination or polar angle.
phi : array_like
Azimuth angle.
Returns
-------
x : array
X coordinate(s) in Cartesian space.
y : array
Y coordinate(s) in Cartesian space.
z : array
Z coordinate(s) in Cartesian space.
Notes
-----
Imagine a sphere with center (0,0,0). Orient it with the z axis
running south-north, the y axis running west-east and the x axis
from posterior to anterior. `theta` (the inclination angle) is the
angle to rotate from the z-axis (the zenith) around the y-axis,
towards the x axis. Thus the rotation is counter-clockwise from the
point of view of positive y. `phi` (azimuth) gives the angle of
rotation around the z-axis towards the y axis. The rotation is
counter-clockwise from the point of view of positive z.
Equivalently, given a point P on the sphere, with coordinates x, y,
z, `theta` is the angle between P and the z-axis, and `phi` is
the angle between the projection of P onto the XY plane, and the X
axis.
Geographical nomenclature designates theta as 'co-latitude', and phi
as 'longitude'.
See these pages for more details:
* http://en.wikipedia.org/wiki/Spherical_coordinate_system
* http://mathworld.wolfram.com/SphericalCoordinates.html
We have deliberately named this function ``sphere2cart`` rather than
``sph2cart`` to distinguish it from the Matlab function of that
name, which uses a different convention.
"""
sin_theta = np.sin(theta)
x = r * np.cos(phi) * sin_theta
y = r * np.sin(phi) * sin_theta
z = r * np.cos(theta)
x, y, z = np.broadcast_arrays(x, y, z)
return x, y, z
[docs]
def cart2sphere(x, y, z):
r"""
Convert Cartesian coordinates to spherical coordinates.
Parameters
----------
x : array_like
X coordinate in Cartesian space.
y : array_like
Y coordinate in Cartesian space.
z : array_like
Z coordinate in Cartesian space.
Returns
-------
r : array
Radius.
theta : array
Inclination (polar) angle in range [0, π].
phi : array
Azimuth angle in range [-π, π].
Notes
-----
Uses the same convention as sphere2cart. The inclination angle theta
is in range [0, π] and the azimuth angle phi is in range [-π, π].
$0\le\theta\mathrm{(theta)}\le\pi$ and $-\pi\le\phi\mathrm{(phi)}\le\pi$
See sphere2cart for detailed description of the coordinate convention.
"""
r = np.sqrt(x * x + y * y + z * z)
theta = np.arccos(
np.divide(z, r, where=r > 0, out=np.full_like(z, np.nan, dtype=np.float64))
)
theta = np.where(r > 0, theta, 0.0)
phi = np.arctan2(y, x)
r, theta, phi = np.broadcast_arrays(r, theta, phi)
return r, theta, phi
def _apply_actor_transform(matrix, actor, transfomation_type):
"""
Apply a transformation matrix to an actor based on the given mode.
Parameters
----------
matrix : ndarray (4, 4)
Homogeneous transformation matrix to be applied.
actor : Actor
The actor to which the transformation will be applied.
transfomation_type : str
Type of transformation to apply to the actor. Can be either
`relative` or `absolute`.
- The `relative` transformation multiplies the current transformation matrix
with the new matrix.
- The `absolute` transformation sets the actor's transformation matrix to the
new matrix.
"""
if actor is None:
return
if transfomation_type not in ["relative", "absolute"]:
raise ValueError(
"transfomation_type should be either `relative` or `absolute`."
)
if transfomation_type == "relative":
actor.local.matrix = matrix @ actor.local.matrix
else:
actor.local.matrix = matrix
[docs]
def translate(translation, *, actor=None, transfomation_type="relative"):
"""
Create a transformation matrix for translation.
Parameters
----------
translation : ndarray (3,)
Translation vector in x, y and z directions.
actor : Actor, optional
If provided, the transformation will be applied to this actor.
transfomation_type : str, optional
Type of transformation to apply to the actor. Can be either
`relative` or `absolute`.
- The `relative` transformation multiplies the current transformation matrix
with the new translation matrix.
- The `absolute` transformation sets the actor's transformation matrix to the
new translation matrix.
Returns
-------
ndarray (4, 4)
Homogeneous transformation matrix with translation parameters in the
last column.
Examples
--------
>>> import numpy as np; import fury
>>> tran = np.array([0.3, 0.2, 0.25])
>>> transform = fury.transform.translate(tran)
>>> transform
array([[1. , 0. , 0. , 0.3 ],
[0. , 1. , 0. , 0.2 ],
[0. , 0. , 1. , 0.25],
[0. , 0. , 0. , 1. ]])
"""
iden = np.identity(4)
translation = np.append(translation, 0).reshape(-1, 1)
t = np.array(
[[0, 0, 0, 1], [0, 0, 0, 1], [0, 0, 0, 1], [0, 0, 0, 1]],
np.float32,
)
translation = np.multiply(t, translation)
translation = np.add(iden, translation)
_apply_actor_transform(translation, actor, transfomation_type)
return translation
[docs]
def rotate(quat, *, actor=None, transfomation_type="relative"):
"""
Create a transformation matrix for rotation using quaternion.
Parameters
----------
quat : ndarray (4,)
Rotation quaternion in form [x, y, z, w].
actor : Actor, optional
If provided, the transformation will be applied to this actor.
transfomation_type : str, optional
Type of transformation to apply to the actor. Can be either
`relative` or `absolute`.
- The `relative` transformation multiplies the current transformation matrix
with the new rotation matrix.
- The `absolute` transformation sets the actor's transformation matrix to the
new rotation matrix.
Returns
-------
ndarray (4, 4)
Homogeneous transformation matrix for rotation.
Examples
--------
>>> import numpy as np; import fury
>>> quat = np.array([0.259, 0.0, 0.0, 0.966])
>>> rotation = fury.transform.rotate(quat)
>>> rotation
array([[ 1. , 0. , 0. , 0. ],
[ 0. , 0.86586979, -0.50026944, 0. ],
[ 0. , 0.50026944, 0.86586979, 0. ],
[ 0. , 0. , 0. , 1. ]])
"""
iden = np.identity(3)
rotation_mat = Rot.from_quat(quat).as_matrix()
iden = np.append(iden, [[0, 0, 0]]).reshape(-1, 3)
rotation_mat = np.dot(iden, rotation_mat)
iden = np.array([[0, 0, 0, 1]]).reshape(-1, 1)
rotation_mat = np.concatenate((rotation_mat, iden), axis=1)
_apply_actor_transform(rotation_mat, actor, transfomation_type)
return rotation_mat
[docs]
def scale(scales, *, actor=None, transfomation_type="relative"):
"""
Create a transformation matrix for scaling.
Parameters
----------
scales : ndarray (3,)
Scale factors for x, y and z directions.
actor : Actor, optional
If provided, the transformation will be applied to this actor.
transfomation_type : str, optional
Type of transformation to apply to the actor. Can be either
`relative` or `absolute`.
- The `relative` transformation multiplies the current transformation matrix
with the new scaling matrix.
- The `absolute` transformation sets the actor's transformation matrix
to the new scaling matrix.
Returns
-------
ndarray (4, 4)
Homogeneous transformation matrix with scale factors along the diagonal.
Examples
--------
>>> import numpy as np; import fury
>>> scales = np.array([2.0, 1.0, 0.5])
>>> transform = fury.transform.scale(scales)
>>> transform
array([[2. , 0. , 0. , 0. ],
[0. , 1. , 0. , 0. ],
[0. , 0. , 0.5, 0. ],
[0. , 0. , 0. , 1. ]])
"""
scale_mat = np.identity(4)
scales = np.append(scales, [1])
for i in range(len(scales)):
scale_mat[i][i] = scales[i]
_apply_actor_transform(scale_mat, actor, transfomation_type)
return scale_mat