"""
Submodule for calculating affine transforms between planar coordinate systems.
"""
import logging
import sys
import numpy as np
import scipy.linalg
from matplotlib.transforms import Affine2D
logging.getLogger(__name__).addHandler(logging.NullHandler())
logger = logging.getLogger(__name__)
__RCOND__ = [-1, None][sys.version_info >= (3, 7)] # 3.6 will fail with lapack error
def _pad(x):
x = np.array(x)
return np.hstack([x, np.ones((x.shape[0], 1))])
def _unpad(x):
x = np.array(x)
return x[:, :-1]
[docs]def corners(size):
x0, x1, y0, y1 = np.array([[0, 0], [*size]]).T.flatten()
return np.array([[x0, y0], [x1, y0], [x1, y1], [x0, y1]])
[docs]def affine_from_AB(X, Y):
"""
Create an affine transforamtion matrix based on two sets of coordinates.
note
-----
* This is an augmented matrix, and includes the translation component
"""
X, Y = np.array(X), np.array(Y)
if np.isclose(X, Y).all():
return compose_affine2d()
if not np.isfinite(X).all() and np.isfinite(Y).all():
msg = "Inputs contain missing values; cannot construct affine matrix."
raise AssertionError(msg)
assert X.shape == Y.shape
# least squares X * A = Y
A, res, rank, s = np.linalg.lstsq(_pad(X), _pad(Y), rcond=__RCOND__)
A[np.isclose(A, 0.0)] = 0.0
return A.T
[docs]def translate(x=0, y=0):
"""
Generate a 2D affine translation matrix.
"""
T = np.eye(3)
T[:-1, -1] = np.array([x, y])
return T
[docs]def rotate(theta=0, degrees=True):
"""
Generate a 2D affine rotation matrix.
Uses clockwise rotations.
"""
θ = np.deg2rad(theta)
R = np.eye(3)
R[[0, 1], [0, 1]] = np.cos(θ)
R[[1, 0], [0, 1]] = -np.sin(θ), np.sin(θ)
return R
[docs]def zoom(x=1, y=1):
"""
Generate a 2D affine zoom matrix.
"""
Z = np.eye(3)
Z[0, 0] = x
Z[1, 1] = y
return Z
[docs]def shear(x=0, y=0):
"""
Generate a 2D affine shear matrix.
"""
S = np.eye(3)
S[0, 1] = x
S[1, 0] = y
return S
[docs]def compose_affine2d(T=translate(0, 0), Z=zoom(1, 1), R=rotate(0)):
"""
Compose an affine transformation matrix based on translation, zoom and rotation
components.
Parameters
-----------
T, Z, R : :class:`numpy.ndarray`
Component affine transfrom matricies for translation, zoom/scaling and rotation.
Returns
---------
A : :class:`numpy.ndarray`
"""
A = T @ Z @ R
return A
[docs]def decompose_affine2d(A):
"""
Decompose an affine transform into components using a polar transform.
Returns
--------
T, Z, R : :class:`numpy.ndarray`
Note
-----
This decomposes the transform into the sequence rotation - zoom - translation.
To recompose this transform,
"""
T = np.eye(3)
T[:-1, -1] = A[:-1, -1]
M = A.copy()
M[:-1, -1] = 0.0
R, Z = scipy.linalg.polar(M, side="left")
for arr in [T, Z, R]:
arr[np.isclose(arr, 0)] = 0.0
return T, Z, R