193 lines
5.2 KiB
Python
193 lines
5.2 KiB
Python
|
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)
|