''' Functions for working with zooms (scales) Terms used in function names: * *mat* : array shape (3, 3) (3D non-homogenous coordinates) * *aff* : affine array shape (4, 4) (3D homogenous coordinates) * *zfdir* : zooms encoded by factor scalar and direction vector ''' import numpy as np from .utils import normalized_vector def zfdir2mat(factor, direction=None): """Return matrix to scale by factor around origin in direction. Use factor == -1 for point symmetry. Parameters ---------- factor : scalar factor to zoom by (see `direction`) direction : None or array-like shape (3,), optional If None, simply apply uniform scaling by `factor`. Otherwise, apply scaling along direction given by vector `direction`. We convert direction to a :term:`unit vector` before application. Returns ------- mat : array shape (3,3) 3x3 transformation matrix implementing zooms Examples -------- >>> v = (np.random.rand(3, 5) - 0.5) * 20.0 >>> S = zfdir2mat(-1.234) >>> np.allclose(np.dot(S, v), -1.234*v) True >>> factor = np.random.random() * 10 - 5 >>> direct = np.random.random(3) - 0.5 >>> S = zfdir2mat(factor, direct) """ if direction is None: # uniform scaling return np.diag([factor] * 3) # nonuniform scaling direction = normalized_vector(direction) factor = 1.0 - factor M = np.eye(3) M -= factor * np.outer(direction, direction) return M def zfdir2aff(factor, direction=None, origin=None): """Return affine to scale by `factor` around `origin` in `direction`. Use factor -1 for point symmetry. Parameters ---------- factor : scalar factor to zoom by (see direction) direction : None or array-like shape (3,) If None, simply apply uniform scaling by `factor`. Otherwise, apply scaling along direction given by vector `direction`. We convert direction to a :term:`unit vector` before application. origin : None or array-like shape (3,) point at which to apply implied zooms Returns ------- aff : array shape (4,4) 4x4 transformation matrix implementing zooms Examples -------- >>> v = (np.random.rand(3, 5) - 0.5) * 20.0 >>> S = zfdir2aff(-1.234)[:3,:3] >>> np.allclose(np.dot(S, v), -1.234*v) True >>> factor = np.random.random() * 10 - 5 >>> direct = np.random.random(3) - 0.5 >>> origin = np.random.random(3) - 0.5 >>> S = zfdir2aff(factor, None, origin) >>> S = zfdir2aff(factor, direct, origin) """ M = np.eye(4) M[:3,:3] = zfdir2mat(factor, direction) if origin is None: return M if direction is None: M[:3, 3] = origin M[:3, 3] *= 1.0 - factor return M # nonuniform scaling direction = normalized_vector(direction) M[:3, 3] = ((1-factor) * np.dot(origin, direction)) * direction return M def mat2zfdir(mat): """Return scaling factor and direction from zoom (scaling) matrix Parameters ---------- mat : array-like shape (3,3) 3x3 zoom matrix Returns ------- factor : scalar zoom (scale) factor as for ``zfdir2mat`` direction : None or array, shape (3,) direction of zoom as for ``zfdir2mat``. None if scaling is uniform. Examples -------- Roundtrip may not generate same factor, direction, but the generated transformation matrices will be the same >>> factor = np.random.random() * 10 - 5 >>> S0 = zfdir2mat(factor, None) >>> f2, d2 = mat2zfdir(S0) >>> S1 = zfdir2mat(f2, d2) >>> np.allclose(S0, S1) True >>> direct = np.random.random(3) - 0.5 >>> S0 = zfdir2mat(factor, direct) >>> f2, d2 = mat2zfdir(S0) >>> S1 = zfdir2mat(f2, d2) >>> np.allclose(S0, S1) True """ mat = np.asarray(mat, dtype=np.float64) factor = np.trace(mat) - 2.0 # direction: unit eigenvector corresponding to eigenvalue factor l, V = np.linalg.eig(mat) near_factors, = np.nonzero(abs(np.real(l.squeeze()) - factor) < 1e-8) if near_factors.size == 0: # uniform scaling factor = (factor + 2.0) / 3.0 return factor, None direction = np.real(V[:, near_factors[0]]) return factor, normalized_vector(direction) def aff2zfdir(aff): """Return scaling factor, direction and origin from scaling matrix. Parameters ---------- aff : array-like shape (4,4) 4x4 :term:`affine transformation` matrix. Returns ------- factor : scalar zoom (scale) factor as for ``zfdir2mat`` direction : None or array, shape (3,) direction of zoom as for ``zfdir2mat``. None if scaling is uniform. origin : array, shape (3,) origin of zooms Examples -------- >>> factor = np.random.random() * 10 - 5 >>> direct = np.random.random(3) - 0.5 >>> origin = np.random.random(3) - 0.5 >>> S0 = zfdir2aff(factor) >>> f2, d2, o2 = aff2zfdir(S0) >>> np.allclose(S0, zfdir2aff(f2, d2, o2)) True >>> S0 = zfdir2aff(factor, direct) >>> f2, d2, o2 = aff2zfdir(S0) >>> np.allclose(S0, zfdir2aff(f2, d2, o2)) True >>> S0 = zfdir2aff(factor, direct, origin) """ M = np.asarray(aff, dtype=np.float64) factor, direction = mat2zfdir(M[:3,:3]) # origin: any eigenvector corresponding to eigenvalue 1 l, V = np.linalg.eig(M) near_1, = np.nonzero(abs(np.real(l.squeeze()) - 1.0) < 1e-8) if near_1.size == 0: raise ValueError("no eigenvector corresponding to eigenvalue 1") origin = np.real(V[:, near_1[-1]]).squeeze() origin = origin[:3] / origin[3] return factor, direction, origin