from __future__ import division, print_function, absolute_import
import re
import warnings
import numpy as np
import scipy.linalg
from scipy._lib._util import check_random_state
_AXIS_TO_IND = {'x': 0, 'y': 1, 'z': 2}
def _elementary_basis_vector(axis):
b = np.zeros(3)
b[_AXIS_TO_IND[axis]] = 1
return b
def _compute_euler_from_dcm(dcm, seq, extrinsic=False):
# The algorithm assumes intrinsic frame transformations. For representation
# the paper uses transformation matrices, which are transpose of the
# direction cosine matrices used by our Rotation class.
# Adapt the algorithm for our case by
# 1. Instead of transposing our representation, use the transpose of the
# O matrix as defined in the paper, and be careful to swap indices
# 2. Reversing both axis sequence and angles for extrinsic rotations
if extrinsic:
seq = seq[::-1]
if dcm.ndim == 2:
dcm = dcm[None, :, :]
num_rotations = dcm.shape[0]
# Step 0
# Algorithm assumes axes as column vectors, here we use 1D vectors
n1 = _elementary_basis_vector(seq[0])
n2 = _elementary_basis_vector(seq[1])
n3 = _elementary_basis_vector(seq[2])
# Step 2
sl = np.dot(np.cross(n1, n2), n3)
cl = np.dot(n1, n3)
# angle offset is lambda from the paper referenced in [2] from docstring of
# `as_euler` function
offset = np.arctan2(sl, cl)
c = np.vstack((n2, np.cross(n1, n2), n1))
# Step 3
rot = np.array([
[1, 0, 0],
[0, cl, sl],
[0, -sl, cl],
])
res = np.einsum('...ij,...jk->...ik', c, dcm)
dcm_transformed = np.einsum('...ij,...jk->...ik', res, c.T.dot(rot))
# Step 4
angles = np.empty((num_rotations, 3))
# Ensure less than unit norm
positive_unity = dcm_transformed[:, 2, 2] > 1
negative_unity = dcm_transformed[:, 2, 2] < -1
dcm_transformed[positive_unity, 2, 2] = 1
dcm_transformed[negative_unity, 2, 2] = -1
angles[:, 1] = np.arccos(dcm_transformed[:, 2, 2])
# Steps 5, 6
eps = 1e-7
safe1 = (np.abs(angles[:, 1]) >= eps)
safe2 = (np.abs(angles[:, 1] - np.pi) >= eps)
# Step 4 (Completion)
angles[:, 1] += offset
# 5b
safe_mask = np.logical_and(safe1, safe2)
angles[safe_mask, 0] = np.arctan2(dcm_transformed[safe_mask, 0, 2],
-dcm_transformed[safe_mask, 1, 2])
angles[safe_mask, 2] = np.arctan2(dcm_transformed[safe_mask, 2, 0],
dcm_transformed[safe_mask, 2, 1])
if extrinsic:
# For extrinsic, set first angle to zero so that after reversal we
# ensure that third angle is zero
# 6a
angles[~safe_mask, 0] = 0
# 6b
angles[~safe1, 2] = np.arctan2(
dcm_transformed[~safe1, 1, 0] - dcm_transformed[~safe1, 0, 1],
dcm_transformed[~safe1, 0, 0] + dcm_transformed[~safe1, 1, 1]
)
# 6c
angles[~safe2, 2] = -np.arctan2(
dcm_transformed[~safe2, 1, 0] + dcm_transformed[~safe2, 0, 1],
dcm_transformed[~safe2, 0, 0] - dcm_transformed[~safe2, 1, 1]
)
else:
# For instrinsic, set third angle to zero
# 6a
angles[~safe_mask, 2] = 0
# 6b
angles[~safe1, 0] = np.arctan2(
dcm_transformed[~safe1, 1, 0] - dcm_transformed[~safe1, 0, 1],
dcm_transformed[~safe1, 0, 0] + dcm_transformed[~safe1, 1, 1]
)
# 6c
angles[~safe2, 0] = np.arctan2(
dcm_transformed[~safe2, 1, 0] + dcm_transformed[~safe2, 0, 1],
dcm_transformed[~safe2, 0, 0] - dcm_transformed[~safe2, 1, 1]
)
# Step 7
if seq[0] == seq[2]:
# lambda = 0, so we can only ensure angle2 -> [0, pi]
adjust_mask = np.logical_or(angles[:, 1] < 0, angles[:, 1] > np.pi)
else:
# lambda = + or - pi/2, so we can ensure angle2 -> [-pi/2, pi/2]
adjust_mask = np.logical_or(angles[:, 1] < -np.pi / 2,
angles[:, 1] > np.pi / 2)
# Dont adjust gimbal locked angle sequences
adjust_mask = np.logical_and(adjust_mask, safe_mask)
angles[adjust_mask, 0] += np.pi
angles[adjust_mask, 1] = 2 * offset - angles[adjust_mask, 1]
angles[adjust_mask, 2] -= np.pi
angles[angles < -np.pi] += 2 * np.pi
angles[angles > np.pi] -= 2 * np.pi
# Step 8
if not np.all(safe_mask):
warnings.warn("Gimbal lock detected. Setting third angle to zero since"
" it is not possible to uniquely determine all angles.")
# Reverse role of extrinsic and intrinsic rotations, but let third angle be
# zero for gimbal locked cases
if extrinsic:
angles = angles[:, ::-1]
return angles
def _make_elementary_quat(axis, angles):
quat = np.zeros((angles.shape[0], 4))
quat[:, 3] = np.cos(angles / 2)
quat[:, _AXIS_TO_IND[axis]] = np.sin(angles / 2)
return quat
def _compose_quat(p, q):
product = np.empty((max(p.shape[0], q.shape[0]), 4))
product[:, 3] = p[:, 3] * q[:, 3] - np.sum(p[:, :3] * q[:, :3], axis=1)
product[:, :3] = (p[:, None, 3] * q[:, :3] + q[:, None, 3] * p[:, :3] +
np.cross(p[:, :3], q[:, :3]))
return product
def _elementary_quat_compose(seq, angles, intrinsic=False):
result = _make_elementary_quat(seq[0], angles[:, 0])
for idx, axis in enumerate(seq[1:], start=1):
if intrinsic:
result = _compose_quat(
result,
_make_elementary_quat(axis, angles[:, idx]))
else:
result = _compose_quat(
_make_elementary_quat(axis, angles[:, idx]),
result)
return result
class Rotation(object):
"""Rotation in 3 dimensions.
This class provides an interface to initialize from and represent rotations
with:
- Quaternions
- Direction Cosine Matrices
- Rotation Vectors
- Euler angles
The following operations on rotations are supported:
- Application on vectors
- Rotation Composition
- Rotation Inversion
- Rotation Indexing
Indexing within a rotation is supported since multiple rotation transforms
can be stored within a single `Rotation` instance.
To create `Rotation` objects use ``from_...`` methods (see examples below).
``Rotation(...)`` is not supposed to be instantiated directly.
Methods
-------
__len__
from_quat
from_dcm
from_rotvec
from_euler
as_quat
as_dcm
as_rotvec
as_euler
apply
__mul__
inv
__getitem__
random
match_vectors
See Also
--------
Slerp
Notes
-----
.. versionadded: 1.2.0
Examples
--------
>>> from scipy.spatial.transform import Rotation as R
A `Rotation` instance can be initialized in any of the above formats and
converted to any of the others. The underlying object is independent of the
representation used for initialization.
Consider a counter-clockwise rotation of 90 degrees about the z-axis. This
corresponds to the following quaternion (in scalar-last format):
>>> r = R.from_quat([0, 0, np.sin(np.pi/4), np.cos(np.pi/4)])
The rotation can be expressed in any of the other formats:
>>> r.as_dcm()
array([[ 2.22044605e-16, -1.00000000e+00, 0.00000000e+00],
[ 1.00000000e+00, 2.22044605e-16, 0.00000000e+00],
[ 0.00000000e+00, 0.00000000e+00, 1.00000000e+00]])
>>> r.as_rotvec()
array([0. , 0. , 1.57079633])
>>> r.as_euler('zyx', degrees=True)
array([90., 0., 0.])
The same rotation can be initialized using a direction cosine matrix:
>>> r = R.from_dcm(np.array([
... [0, -1, 0],
... [1, 0, 0],
... [0, 0, 1]]))
Representation in other formats:
>>> r.as_quat()
array([0. , 0. , 0.70710678, 0.70710678])
>>> r.as_rotvec()
array([0. , 0. , 1.57079633])
>>> r.as_euler('zyx', degrees=True)
array([90., 0., 0.])
The rotation vector corresponding to this rotation is given by:
>>> r = R.from_rotvec(np.pi/2 * np.array([0, 0, 1]))
Representation in other formats:
>>> r.as_quat()
array([0. , 0. , 0.70710678, 0.70710678])
>>> r.as_dcm()
array([[ 2.22044605e-16, -1.00000000e+00, 0.00000000e+00],
[ 1.00000000e+00, 2.22044605e-16, 0.00000000e+00],
[ 0.00000000e+00, 0.00000000e+00, 1.00000000e+00]])
>>> r.as_euler('zyx', degrees=True)
array([90., 0., 0.])
The ``from_euler`` method is quite flexible in the range of input formats
it supports. Here we initialize a single rotation about a single axis:
>>> r = R.from_euler('z', 90, degrees=True)
Again, the object is representation independent and can be converted to any
other format:
>>> r.as_quat()
array([0. , 0. , 0.70710678, 0.70710678])
>>> r.as_dcm()
array([[ 2.22044605e-16, -1.00000000e+00, 0.00000000e+00],
[ 1.00000000e+00, 2.22044605e-16, 0.00000000e+00],
[ 0.00000000e+00, 0.00000000e+00, 1.00000000e+00]])
>>> r.as_rotvec()
array([0. , 0. , 1.57079633])
It is also possible to initialize multiple rotations in a single instance
using any of the `from_...` functions. Here we initialize a stack of 3
rotations using the ``from_euler`` method:
>>> r = R.from_euler('zyx', [
... [90, 0, 0],
... [0, 45, 0],
... [45, 60, 30]], degrees=True)
The other representations also now return a stack of 3 rotations. For
example:
>>> r.as_quat()
array([[0. , 0. , 0.70710678, 0.70710678],
[0. , 0.38268343, 0. , 0.92387953],
[0.39190384, 0.36042341, 0.43967974, 0.72331741]])
Applying the above rotations onto a vector:
>>> v = [1, 2, 3]
>>> r.apply(v)
array([[-2. , 1. , 3. ],
[ 2.82842712, 2. , 1.41421356],
[ 2.24452282, 0.78093109, 2.89002836]])
A `Rotation` instance can be indexed and sliced as if it were a single
1D array or list:
>>> r.as_quat()
array([[0. , 0. , 0.70710678, 0.70710678],
[0. , 0.38268343, 0. , 0.92387953],
[0.39190384, 0.36042341, 0.43967974, 0.72331741]])
>>> p = r[0]
>>> p.as_dcm()
array([[ 2.22044605e-16, -1.00000000e+00, 0.00000000e+00],
[ 1.00000000e+00, 2.22044605e-16, 0.00000000e+00],
[ 0.00000000e+00, 0.00000000e+00, 1.00000000e+00]])
>>> q = r[1:3]
>>> q.as_quat()
array([[0. , 0.38268343, 0. , 0.92387953],
[0.39190384, 0.36042341, 0.43967974, 0.72331741]])
Multiple rotations can be composed using the ``*`` operator:
>>> r1 = R.from_euler('z', 90, degrees=True)
>>> r2 = R.from_rotvec([np.pi/4, 0, 0])
>>> v = [1, 2, 3]
>>> r2.apply(r1.apply(v))
array([-2. , -1.41421356, 2.82842712])
Loading ...