Source code for compas.geometry.transformations.transformation


"""
This library for transformations partly derived and was re-implemented from the
following online resources:

    * http://www.lfd.uci.edu/~gohlke/code/transformations.py.html
    * http://www.euclideanspace.com/maths/geometry/rotations/
    * http://code.activestate.com/recipes/578108-determinant-of-matrix-of-any-order/
    * http://blog.acipo.com/matrix-inversion-in-javascript/

Many thanks to Christoph Gohlke, Martin John Baker, Sachin Joglekar and Andrew
Ippoliti for providing code and documentation.
"""
import math

from compas.base import Base

from compas.geometry import multiply_matrices
from compas.geometry import transpose_matrix

from compas.geometry.transformations import basis_vectors_from_matrix
from compas.geometry.transformations import decompose_matrix
from compas.geometry.transformations import identity_matrix
from compas.geometry.transformations import matrix_determinant
from compas.geometry.transformations import matrix_from_euler_angles
from compas.geometry.transformations import matrix_from_frame
from compas.geometry.transformations import matrix_from_translation
from compas.geometry.transformations import matrix_inverse
from compas.geometry.transformations import translation_from_matrix


__all__ = ['Transformation']


[docs]class Transformation(Base): """The ``Transformation`` represents a 4x4 transformation matrix. It is the base class for transformations like :class:`Rotation`, :class:`Translation`, :class:`Scale`, :class:`Reflection`, :class:`Projection` and :class:`Shear`. The class allows to concatenate Transformations by multiplication, to calculate the inverse transformation and to decompose a transformation into its components of rotation, translation, scale, shear, and perspective. The matrix follows the row-major order, such that translation components x, y, z are in the right column of the matrix, i.e. ``M[0][3], M[1][3], M[2][3] = x, y, z``. Parameters ---------- matrix : list of list of float, optional The 4x4 transformation matrix. Examples -------- >>> from compas.geometry import Frame >>> f1 = Frame([1, 1, 1], [0.68, 0.68, 0.27], [-0.67, 0.73, -0.15]) >>> T = Transformation.from_frame(f1) >>> Sc, Sh, R, Tl, P = T.decomposed() >>> Tinv = T.inverse() """
[docs] def __init__(self, matrix=None): """Construct a transformation from a 4x4 transformation matrix. """ super(Transformation, self).__init__() if not matrix: matrix = identity_matrix(4) self.matrix = matrix
def __mul__(self, other): return self.concatenated(other) def __imul__(self, other): return self.concatenated(other) def __getitem__(self, key): i, j = key return self.matrix[i][j] def __setitem__(self, key, value): i, j = key self.matrix[i][j] = value def __iter__(self): return iter(self.matrix) def __eq__(self, other, tol=1e-05): try: A = self.matrix B = other.matrix for i in range(4): for j in range(4): if math.fabs(A[i][j] - B[i][j]) > tol: return False return True except BaseException: return False def __repr__(self): return "Transformation({})".format(self.matrix) def __len__(self): return len(self.matrix)
[docs] def copy(self): """Returns a copy of the transformation. """ cls = type(self) matrix = [ self.matrix[0][:], self.matrix[1][:], self.matrix[2][:], self.matrix[3][:]] return cls(matrix)
@property def data(self): """Return a ``Transformation`` object's to a data dict. Returns ------- dict A dictionary with a transformation matrix stored under the key "matrix". Examples -------- >>> matrix = [[1, 0, 0, 3], [0, 1, 0, 4], [0, 0, 1, 5], [0, 0, 0, 1]] >>> data = {'matrix': matrix} >>> T = Transformation.from_data(data) >>> T.data == data True """ return {'matrix': self.matrix} @data.setter def data(self, data): self.matrix = data['matrix']
[docs] @classmethod def from_data(cls, data): """Creates a ``Transformation`` from a data dict. Parameters ---------- data : :obj:`dict` A dictionary with a transformation matrix stored under the key "matrix". Returns ------- Transformation The ``Transformation`` object. Examples -------- >>> matrix = [[1, 0, 0, 3], [0, 1, 0, 4], [0, 0, 1, 5], [0, 0, 0, 1]] >>> data = {'matrix': matrix} >>> T = Transformation.from_data(data) """ return cls(data['matrix'])
[docs] def to_data(self): """Convert a ``Transformation`` object to a data dict. Returns ------- dict A dictionary with a transformation matrix stored under the key "matrix". Examples -------- >>> matrix = [[1, 0, 0, 3], [0, 1, 0, 4], [0, 0, 1, 5], [0, 0, 0, 1]] >>> data = {'matrix': matrix} >>> T = Transformation.from_data(data) >>> T.to_data() == data True """ return self.data
[docs] @classmethod def from_matrix(cls, matrix): """Creates a ``Transformation`` from a 4x4 matrix-like object. Parameters ---------- matrix : 4x4 matrix-like The 4x4 transformation matrix. Returns ------- Transformation The ``Transformation`` object. """ return cls(matrix)
[docs] @classmethod def from_list(cls, numbers): """Creates a ``Transformation`` from a list of 16 numbers. Parameters ---------- numbers : :obj:`list` of :obj:`float` A list of 16 numbers Returns ------- Transformation The ``Transformation`` object. Examples -------- >>> numbers = [1, 0, 0, 3, 0, 1, 0, 4, 0, 0, 1, 5, 0, 0, 0, 1] >>> T = Transformation.from_list(numbers) Notes ----- Since the transformation matrix follows the row-major order, the translational components must be at the list's indices 3, 7, 11. """ matrix = identity_matrix(4) for i in range(4): for j in range(4): matrix[i][j] = float(numbers[i * 4 + j]) return cls(matrix)
[docs] @classmethod def from_euler_angles(cls, euler_angles, static=True, axes='xyz', point=[0, 0, 0]): """Construct a transformation from a rotation represented by Euler angles. Parameters ---------- euler_angles : list of float Three numbers that represent the angles of rotations about the defined axes. static : bool, optional If true the rotations are applied to a static frame. If not, to a rotational. Defaults to ``True``. axes : str, optional A 3 character string specifying the order of the axes. Defaults to ``'xyz'``. point : list of float, optional The point of the frame. Defaults to ``[0, 0, 0]``. Returns ------- :class:`compas.geometry.Transformation` The constructed transformation. """ R = matrix_from_euler_angles(euler_angles, static, axes) T = matrix_from_translation(point) M = multiply_matrices(T, R) return cls.from_matrix(M)
# should not one of the two just have a "to" function
[docs] @classmethod def from_frame(cls, frame): """Computes a transformation from world XY to frame. Parameters ---------- frame : :class:`Frame` A frame describing the targeted Cartesian coordinate system. Returns ------- Transformation The ``Transformation`` object. Examples -------- >>> from compas.geometry import Frame >>> f1 = Frame([1, 1, 1], [0.68, 0.68, 0.27], [-0.67, 0.73, -0.15]) >>> T = Transformation.from_frame(f1) >>> f2 = Frame.from_transformation(T) >>> f1 == f2 True Notes ----- It is the same as from_frame_to_frame(Frame.worldXY(), frame). """ return cls(matrix_from_frame(frame))
[docs] @classmethod def from_frame_to_frame(cls, frame_from, frame_to): """Computes a transformation between two frames. This transformation allows to transform geometry from one Cartesian coordinate system defined by "frame_from" to another Cartesian coordinate system defined by "frame_to". Parameters ---------- frame_from : :class:`Frame` A frame defining the original Cartesian coordinate system. frame_to : :class:`Frame` A frame defining the targeted Cartesian coordinate system. Returns ------- Transformation The ``Transformation`` object representing a change of basis. Examples -------- >>> from compas.geometry import Frame >>> f1 = Frame([2, 2, 2], [0.12, 0.58, 0.81], [-0.80, 0.53, -0.26]) >>> f2 = Frame([1, 1, 1], [0.68, 0.68, 0.27], [-0.67, 0.73, -0.15]) >>> T = Transformation.from_frame_to_frame(f1, f2) >>> f1.transform(T) >>> f1 == f2 True """ T1 = cls.from_frame(frame_from) T2 = cls.from_frame(frame_to) return cls(multiply_matrices(T2.matrix, matrix_inverse(T1.matrix)))
[docs] @classmethod def from_change_of_basis(cls, frame_from, frame_to): """Computes a change of basis transformation between two frames. A basis change is essentially a remapping of geometry from one coordinate system to another. Parameters ---------- frame_from : :class:`Frame` A frame defining the original Cartesian coordinate system. frame_to : :class:`Frame` A frame defining the targeted Cartesian coordinate system. Examples -------- >>> from compas.geometry import Point, Frame >>> f1 = Frame([2, 2, 2], [0.12, 0.58, 0.81], [-0.80, 0.53, -0.26]) >>> f2 = Frame([1, 1, 1], [0.68, 0.68, 0.27], [-0.67, 0.73, -0.15]) >>> T = Transformation.from_change_of_basis(f1, f2) >>> p_f1 = Point(1, 1, 1) # point in f1 >>> p_f1.transformed(T) # point represented in f2 Point(1.395, 0.955, 1.934) >>> Frame.local_to_local_coordinates(f1, f2, p_f1) Point(1.395, 0.955, 1.934) """ T1 = cls.from_frame(frame_from) T2 = cls.from_frame(frame_to) return cls(multiply_matrices(matrix_inverse(T2.matrix), T1.matrix))
@property def scale(self): """The scale component of the transformation matrix. Returns ------- compas.geometry.Scale The scale component of the transformation. """ S, H, R, T, P = self.decomposed() return S @property def shear(self): """The shear component of the transformation matrix. Returns ------- compas.geometry.Shear The shear component of the transformation. """ S, H, R, T, P = self.decomposed() return H @property def rotation(self): """The rotation component of the transformation matrix. Returns ------- compas.geometry.Rotation The rotation component of the transformation. """ S, H, R, T, P = self.decomposed() return R @property def translation(self): """The translation component of the transformation matrix. Returns ------- compas.geometry.Translation The translation component of the transformation. """ S, H, R, T, P = self.decomposed() return T @property def projection(self): """The projection component of the transformation matrix. Returns ------- compas.geometry.Projection The projectionn component of the transformation. """ S, H, R, T, P = self.decomposed() return P @property def translation_vector(self): from compas.geometry import Vector vector = translation_from_matrix(self.matrix) return Vector(*vector) @property def basis_vectors(self): """The basis vectors from the rotation component of the transformation matrix. Returns ------- tuple of :class:`compas.geometry.Vector` The basis vectors of the rotation component of the tranformation. """ from compas.geometry import Vector x, y = basis_vectors_from_matrix(self.rotation.matrix) return Vector(*x), Vector(*y) @property def list(self): """Flattens the 4x4 transformation matrix into a list of 16 numbers. Returns ------- list The transformation matrix as a flattened list in row-major order. """ return [a for c in self.matrix for a in c] @property def determinant(self): """The determinant of the matrix of the transformation. Returns ------- float The determinant of the matrix of this transformation. """ return matrix_determinant(self.matrix)
[docs] def transpose(self): """Transpose the matrix of this transformation. Returns ------- None The transformation is transposed in-place. """ self.matrix = transpose_matrix(self.matrix)
[docs] def transposed(self): """Create a transposed copy of this transformation. Returns ------- Transformation The transposed transformation object. """ T = self.copy() T.transpose() return T
[docs] def invert(self): """Invert this transformation.""" self.matrix = matrix_inverse(self.matrix)
[docs] def inverse(self): """Returns the inverse transformation. Returns ------- Transformation The inverse transformation. Examples -------- >>> from compas.geometry import Frame >>> f = Frame([1, 1, 1], [0.68, 0.68, 0.27], [-0.67, 0.73, -0.15]) >>> T = Transformation.from_frame(f) >>> I = Transformation(identity_matrix(4)) >>> I == T * T.inverse() True """ T = self.copy() T.invert() return T
inverted = inverse
[docs] def decomposed(self): """Decompose the ``Transformation`` into its ``Scale``, ``Shear``, ``Rotation``, ``Translation`` and ``Projection`` components. Returns ------- 5-tuple of Transformation The scale, shear, rotation, translation, and projection components of the current transformation. Examples -------- >>> trans1 = [1, 2, 3] >>> angle1 = [-2.142, 1.141, -0.142] >>> scale1 = [0.123, 2, 0.5] >>> T1 = Translation.from_vector(trans1) >>> R1 = Rotation.from_euler_angles(angle1) >>> S1 = Scale.from_factors(scale1) >>> M = T1 * R1 * S1 >>> S, H, R, T, P = M.decomposed() >>> S1 == S True >>> R1 == R True >>> T1 == T True """ from compas.geometry import Scale # noqa: F811 from compas.geometry import Shear from compas.geometry import Rotation # noqa: F811 from compas.geometry import Translation # noqa: F811 from compas.geometry import Projection s, h, a, t, p = decompose_matrix(self.matrix) S = Scale.from_factors(s) H = Shear.from_entries(h) R = Rotation.from_euler_angles(a, static=True, axes='xyz') T = Translation.from_vector(t) P = Projection.from_entries(p) return S, H, R, T, P
[docs] def concatenate(self, other): """Concatenate another transformation to this transformation. Parameters ---------- other: :class:`compas.geometry.Transformation` The transformation object to concatenate. Returns ------- None This transformation object is changed in-place. Notes ----- Rz * Ry * Rx means that Rx is first transformation, Ry second, and Rz third. """ self.matrix = multiply_matrices(self.matrix, other.matrix)
[docs] def concatenated(self, other): """Concatenate two transformations into one ``Transformation``. Parameters ---------- other : :class:`compas.geometry.Transformation` The transformation object to concatenate. Returns ------- T : :class:`compas.geometry.Transformation` The new transformation that is the concatenation of this one and the other. Notes ----- Rz * Ry * Rx means that Rx is first transformation, Ry second, and Rz third. """ cls = type(self) if isinstance(other, cls): return cls(multiply_matrices(self.matrix, other.matrix)) return Transformation(multiply_matrices(self.matrix, other.matrix))
# ============================================================================== # Main # ============================================================================== if __name__ == "__main__": from compas.geometry import Translation # noqa: F401 from compas.geometry import Rotation # noqa: F401 from compas.geometry import Scale # noqa: F401 from compas.geometry import Frame # noqa: F401 import doctest doctest.testmod(globs=globals()) # world = Frame.worldXY() # frame = Frame([1.0, 1.0, 1.0], [0, 0, -1], [1, 0, 0]) # X1 = Transformation.from_frame_to_frame(world, frame) # X2 = Transformation.from_frame(frame) # X3 = Transformation.from_change_of_basis(frame, world) # print(X1.matrix) # print(X2.matrix) # print(X3.matrix) # trans1 = [1, 2, 3] # angle1 = [-2.142, 1.141, -0.142] # scale1 = [0.123, 2, 0.5] # T1 = Translation.from_vector(trans1) # R1 = Rotation.from_euler_angles(angle1) # S1 = Scale.from_factors(scale1) # M = T1 * R1 * S1 # S, H, R, T, P = M.decomposed() # print(S1 == S) # print(R1 == R) # print(T1 == T) # S, H, R, T, P = X3.decomposed() # print(S) # print(H) # print(R) # print(T) # print(P)