Source code for pyfar.classes.orientations

from scipy.spatial.transform import Rotation
import numpy as np
import warnings

import pyfar as pf
if np.__version__ < '2.0.0':
    from numpy import VisibleDeprecationWarning
else:
    from numpy.exceptions import VisibleDeprecationWarning

# this warning needs to be caught and appears if numpy array are generated
# from nested lists containing lists of unequal lengths,
#  e.g., [[1, 0, 0], [1, 0]]
warnings.filterwarnings("error", category=VisibleDeprecationWarning)


[docs] class Orientations(Rotation): """ This class for Orientations in the three-dimensional space, is a subclass of :py:class:`scipy:scipy.spatial.transform.Rotation` and equally based on quaternions of shape (N, 4). It inherits all methods of the Rotation class and adds the creation from perpendicular view and up vectors through :py:func:`~from_view_up` and a convenient plot function :py:func:`~show`. An orientation can be visualized with the triple of view, up and right vectors and it is tied to the object's local coordinate system. Alternatively the object's orientation can be illustrated with help of the right hand: Thumb (view), forefinger (up) and middle finger (right). Examples -------- >>> from pyfar import Orientations >>> views = [[1, 0, 0], [2, 0, 0]] >>> ups = [[0, 1, 0], [0, -2, 0]] >>> orientations = Orientations.from_view_up(views, ups) Visualize orientations at certain positions: >>> positions = [[0, 0.5, 0], [0, -0.5, 0]] >>> orientations.show(positions) Rotate first element of orientations: >>> from scipy.spatial.transform import Rotation >>> rot_x45 = Rotation.from_euler('x', 45, degrees=True) >>> orientations[1] = orientations[1] * rot_x45 >>> orientations.show(positions) To create `Orientations` objects use ``from_...`` methods. ``Orientations(...)`` is not supposed to be instantiated directly. Attributes ---------- quat : array_like, shape (N, 4) or (4,) Each row is a (possibly non-unit norm) quaternion in scalar-last (x, y, z, w) format. Each quaternion will be normalized to unit norm. """ def __init__(self, quat=None, normalize=True, copy=True, **kwargs): if quat is None: quat = np.array([0., 0., 0., 1.]) super().__init__(quat, copy=copy, normalize=normalize, **kwargs)
[docs] @classmethod def from_view_up(cls, views, ups): """Initialize Orientations from a view an up vector. Orientations are internally stored as quaternions for better spherical linear interpolation (SLERP) and spherical harmonics operations. More intuitionally, they can be expressed as view and up vectors which cannot be collinear. In this case, they are restricted to be perpendicular to minimize rounding errors. Parameters ---------- views : array_like, shape (N, 3) or (3,), Coordinates A single vector or a stack of vectors, giving the look-direction of an object in three-dimensional space, e.g. from a listener, or the acoustic axis of a loudspeaker, or the direction of a main lobe. Views can also be passed as a Coordinates object. ups : array_like, shape (N, 3) or (3,), Coordinates A single vector or a stack of vectors, giving the up-direction of an object, which is usually the up-direction in world-space. Views can also be passed as a Coordinates object. Returns ------- orientations : Orientations Object containing the orientations represented by quaternions. """ # init views and up try: views = np.atleast_2d(views).astype(np.float64) ups = np.atleast_2d(ups).astype(np.float64) except VisibleDeprecationWarning as exc: raise ValueError( "Expected `views` and `ups` to have shape (N, 3)") from exc # check views and ups if (views.ndim > 2 or views.shape[-1] != 3 or ups.ndim > 2 or ups.shape[-1] != 3): raise ValueError(f"Expected `views` and `ups` to have shape (N, 3)" f" or (3,), got {views.shape}") if views.shape == ups.shape: pass elif views.shape[0] > 1 and ups.shape[0] == 1: ups = np.repeat(ups, views.shape[0], axis=0) elif ups.shape[0] > 1 and views.shape[0] == 1: views = np.repeat(views, ups.shape[0], axis=0) else: raise ValueError("Expected 1:1, 1:N or N:1 `views` and `ups` " f"not M:N, got {views.shape} and {ups.shape}") if not (np.all(np.linalg.norm(views, axis=1)) and np.all(np.linalg.norm(ups, axis=1))): raise ValueError("View and Up Vectors must have a length.") if not np.allclose(0, np.einsum('ij,kj->k', views, ups)): raise ValueError("View and Up vectors must be perpendicular.") # Assuming that the direction of the cross product is defined # by the right-hand rule rights = np.cross(views, ups) rotation_matrix = np.asarray([views, ups, rights]) rotation_matrix = np.swapaxes(rotation_matrix, 0, 1) return super().from_matrix(rotation_matrix)
[docs] def show(self, positions=None, show_views=True, show_ups=True, show_rights=True, **kwargs): """ Visualize Orientations as triples of view (red), up (green) and right (blue) vectors in a quiver plot. Parameters ---------- positions : array_like, shape (O, 3), O is len(self) These are the positions of each vector triple. If not provided, all triples are positioned in the origin of the coordinate system. show_views: bool select wether to show the view vectors or not. The default is True. show_ups: bool select wether to show the up vectors or not. The default is True. show_rights: bool select wether to show the right vectors or not. The default is True. Returns ------- ax : :py:class:`~mpl_toolkits.mplot3d.axes3d.Axes3D` The axis used for the plot. """ if positions is None: positions = np.zeros((self.as_quat().shape[0], 3)) positions = np.atleast_2d(positions).astype(np.float64) if positions.shape[0] != self.as_quat().shape[0]: raise ValueError("If provided, there must be the same number" "of positions as orientations.") # Create view, up and right vectors from Rotation object views, ups, rights = self.as_view_up_right() kwargs.pop('color', None) ax = None if show_views: ax = pf.plot.quiver( positions, views, color=pf.plot.color('r'), **kwargs) if show_ups: ax = pf.plot.quiver( positions, ups, ax=ax, color=pf.plot.color('g'), **kwargs) if show_rights: ax = pf.plot.quiver( positions, rights, ax=ax, color=pf.plot.color('b'), **kwargs)
[docs] def as_view_up_right(self): """Get Orientations as a view, up, and right vector. Orientations are internally stored as quaternions for better spherical linear interpolation (SLERP) and spherical harmonics operations. More intuitionally, they can be expressed as view and and up of vectors which cannot be collinear. In this case are restricted to be perpendicular to minimize rounding errors. Returns ---------- vector_triple: ndarray, shape (N, 3), normalized vectors - views, see :py:func:`Orientations.from_view_up` - ups, see :py:func:`Orientations.from_view_up` - rights, see :py:func:`Orientations.from_view_up` A single vector or a stack of vectors, pointing to the right of the object, constructed as a cross product of ups and rights. """ # Apply self as a Rotation (base class) on eye i.e. generate orientions # as rotations relative to standard basis in 3d vector_triple = super().as_matrix() if vector_triple.ndim == 3: return np.swapaxes(vector_triple, 0, 1) return vector_triple
[docs] def copy(self): """Return a deep copy of the Orientations object.""" return self.from_quat(self.as_quat())
def _encode(self): """Return object in a proper encoding format.""" # Use public interface of the scipy super-class to prevent # error in case of chaning super-class implementations return {'quat': self.as_quat()} @classmethod def _decode(cls, obj_dict): """Decode object based on its respective `_encode` counterpart.""" return cls.from_quat(obj_dict['quat']) def __setitem__(self, idx, val): """ Assign orientations(s) at given index(es) from object. Parameters ---------- idx : see NumPy Indexing val : array_like quaternion(s), shape (N, 4) or (4,) """ if isinstance(val, Orientations): val = val.as_quat() quat = np.atleast_2d(val) if quat.ndim > 2 or quat.shape[-1] != 4: raise ValueError(f"Expected assigned value to have shape" f" or (1, 4), got {quat.shape}") quats = self.as_quat() quats[idx] = quat self = super().from_quat(quats) def __eq__(self, other): """Check for equality of two objects.""" return np.array_equal(self.as_quat(), other.as_quat())