import numpy as np from .. import util from .. import caching from .. import transformations as tr class Transform(object): """ Class for caching metadata associated with 4x4 transformations. The transformation matrix is used to define relevant properties for the voxels, including pitch and origin. """ def __init__(self, matrix): """ Initialize with a transform Parameters ----------- matrix : (4, 4) float Homogeneous transformation matrix """ matrix = np.asanyarray(matrix, dtype=np.float64) if matrix.shape != (4, 4): raise ValueError('matrix must be 4x4!') if not np.all(matrix[3, :] == [0, 0, 0, 1]): raise ValueError('matrix not a valid transformation matrix') # store matrix as data self._data = caching.tracked_array(matrix, dtype=np.float64) # dump cache when matrix changes self._cache = caching.Cache(id_function=self._data.fast_hash) def md5(self): """ Get the MD5 hash of the current transformation matrix. Returns ------------ md5 : str Hash of transformation matrix """ return self._data.md5() def crc(self): """ Get the zlib.adler32 hash of the current transformation matrix. Returns ------------ crc : str Hash of transformation matrix """ return self._data.crc() @property def translation(self): """ Get the translation component of the matrix Returns ------------ translation : (3,) float Cartesian translation """ return self._data[:3, 3] @property def matrix(self): """ Get the homogeneous transformation matrix. Returns ------------- matrix : (4, 4) float Transformation matrix """ return self._data @matrix.setter def matrix(self, data): """ Set the homogeneous transformation matrix. Parameters ------------- matrix : (4, 4) float Transformation matrix """ data = np.asanyarray(data, dtype=np.float64) if data.shape != (4, 4): raise ValueError('matrix must be (4, 4)!') self._data = caching.tracked_array(data, dtype=np.float64) @caching.cache_decorator def scale(self): """ Get the scale factor of the current transformation. Returns ------------- scale : (3,) float Scale factor from the matrix """ # get the current transformation matrix = self.matrix # get the (3,) diagonal of the rotation component scale = np.diag(matrix[:3, :3]) if not util.allclose( matrix[:3, :3], scale * np.eye(3), scale * 1e-6 + 1e-8): raise RuntimeError( 'scale ill-defined because transform features ' 'a shear or rotation') return scale @caching.cache_decorator def pitch(self): scale = self.scale if not util.allclose( scale[0], scale[1:], np.max(np.abs(scale)) * 1e-6 + 1e-8): raise RuntimeError( 'pitch ill-defined because transform features ' 'non-uniform scaling.') return scale @caching.cache_decorator def unit_volume(self): """Volume of a transformed unit cube.""" return np.linalg.det(self._data[:3, :3]) def apply_transform(self, matrix): """Mutate the transform in-place and return self.""" self.matrix = np.matmul(matrix, self.matrix) return self def apply_translation(self, translation): """Mutate the transform in-place and return self.""" self.matrix[:3, 3] += translation return self def apply_scale(self, scale): """Mutate the transform in-place and return self.""" self.matrix[:3] *= scale return self def transform_points(self, points): """ Apply the transformation to points (not in-place). Parameters ---------- points: (n, 3) float Points in cartesian space Returns ---------- transformed : (n, 3) float Points transformed by matrix """ if self.is_identity: return points.copy() return tr.transform_points( points.reshape(-1, 3), self.matrix).reshape(points.shape) def inverse_transform_points(self, points): """Apply the inverse transformation to points (not in-place).""" if self.is_identity: return points return tr.transform_points( points.reshape(-1, 3), self.inverse_matrix).reshape(points.shape) @caching.cache_decorator def inverse_matrix(self): inv = np.linalg.inv(self.matrix) inv.flags.writeable = False return inv def copy(self): return Transform(self._data.copy()) @caching.cache_decorator def is_identity(self): """ Flags this transformation being sufficiently close to eye(4). """ return util.allclose(self.matrix, np.eye(4), 1e-8)