"""Homography abstraction.
A homography transformation can be represented be the following change-of-frame matrix
$$
F=\\begin{bmatrix}
C_{n\\times n}&T_{n\\times 1}\\\\
P_{1\\times n}&1
\\end{bmatrix}
$$
Different transformation types can be distinguished based on the properties of the translation vector $T$, change-of-basis matrix $C$ and projection vector $P$
* translation:
* $C$ is the identity matrix and $P$ are all zeros
* proper rigid (euclidian) transformation:
* $P$ are all zeros
* $C$ is any orthogonal matrix ($C^T=C^{-1}$) with determinant $\\det{C}=+1$
* preserves angles, distances and handedness
* translation + rotation
* rigid (euclidian) transformation:
* $P$ are all zeros
* $C$ is any orthogonal matrix ($C^T=C^{-1}$)
* preserves angles and distances
* proper rigid transformation + reflection
* similarity transformation:
* $P$ are all zeros
* $C=rA$ where A any orthogonal matrix ($A^T=A^{-1}$) and $r>0$
* preserves angles and ratios between distances
* rigid transformation + isotropic scaling
* affine transformation:
* $P$ are all zeros
* $C$ is any invertible matrix (i.e. linear transformation)
* preserves parallelism
* similarity transformation + non-isotropic scaling and shear
* projective transformation (homography):
* $C$ is any invertible matrix (i.e. linear transformation)
* preserves collinearity
* affine transformation + projection
"""
from typing import Optional
import numpy
from .base import Transformation
from .types import TransformationType
[docs]
def type_from_matrix(matrix: numpy.ndarray) -> TransformationType:
"""
:param matrix: shape `(N+1, N+1)` or `(K, N+1, N+1)`
:returns: transformation type
"""
N = matrix.shape[0] - 1
if matrix.shape != (N + 1, N + 1):
raise ValueError("must be a square matrix")
if numpy.allclose(matrix, numpy.identity(N + 1)):
return TransformationType.identity
if matrix[-1, -1] != 1 or not numpy.allclose(matrix[-1, 0:-1], 0):
return TransformationType.projective
C = matrix[:-1, :-1]
if numpy.allclose(C, numpy.identity(N)):
if numpy.allclose(matrix[-1, :-1], 0):
return TransformationType.translation
else:
return TransformationType.rigid
C_T = C.T
C_I = numpy.linalg.inv(C)
if numpy.allclose(numpy.linalg.det(C), 1, rtol=1e-7):
return TransformationType.rigid
C_I[C_I == 0] = 1
r = C_T / C_I
if numpy.allclose(r, r.mean()):
return TransformationType.similarity
return TransformationType.affine
[docs]
class Homography(Transformation, register=False):
def __init__(
self,
passive_matrix: numpy.ndarray,
transfo_type: Optional[TransformationType] = None,
) -> None:
if transfo_type is None:
transfo_type = type_from_matrix(passive_matrix)
self._passive_matrix = passive_matrix
self._active = None
super().__init__(transfo_type)
@property
def passive_matrix(self) -> numpy.ndarray:
return self._passive_matrix
[docs]
def as_parameters(self) -> numpy.ndarray:
return params_from_trans(self)
def __matmul__(self, other: Transformation) -> Transformation:
if isinstance(other, Homography):
if self.passive_matrix.shape == other.passive_matrix.shape:
return Homography(self.passive_matrix @ other.passive_matrix)
else:
raise TypeError("Homographies must have same dimensions")
else:
raise TypeError(
"Concatenating Homography and non-homography is not possible"
)
"""
Conversions between matrix representation of transformation and tuple of parameters of transformation
Identity: ()
translation: (shift_x, shift_y)
proper_rigid: (angle, shift_x, shift_y)
affine: (matrix, translation)
projective: flattened version of matrix
"""
[docs]
def params_from_trans(transformation: Homography) -> list:
if transformation.transformation_type == "identity":
return []
elif transformation.transformation_type == "translation":
return list(transformation.passive_matrix[1::-1, 2])
elif transformation.transformation_type == "rigid":
return [
numpy.arccos(transformation.passive_matrix[0, 0]),
transformation.passive_matrix[1, 2],
transformation.passive_matrix[0, 2],
]
elif (
transformation.transformation_type == "affine"
or transformation.transformation_type == "similarity"
):
return list(transformation.passive_matrix[0:2].flatten())
elif transformation.transformation_type == "projective":
return list(transformation.passive_matrix.flatten())
else:
raise NotImplementedError
[docs]
def matrix_from_params(
params: list, transformation_type: TransformationType
) -> numpy.ndarray:
if transformation_type == "identity":
return numpy.identity(3)
elif transformation_type == "translation":
matrix = numpy.identity(3)
matrix[0:2, 2] = params[::-1]
return matrix
elif transformation_type == "rigid":
cos, sin = numpy.cos(params[0]), numpy.sin(params[0])
matrix = numpy.asarray(
[[cos, -sin, params[2]], [sin, cos, params[1]], [0, 0, 1]]
)
return matrix
elif transformation_type == "affine":
matrix = numpy.identity(3)
matrix[0:2] = numpy.reshape(params, (2, 3))
return matrix
elif transformation_type == "projective":
return numpy.reshape(params, (3, 3))
else:
raise NotImplementedError
[docs]
def reverse_indices(matrix: numpy.ndarray):
N = matrix.shape[0]
if matrix.ndim != 2 and matrix.shape != (N, N):
raise ValueError("Input must be square matrix")
switched = matrix.copy()
switched[0:-1, 0:-1] = switched[0:-1, 0:-1].T
switched[:-1, -1] = switched[:-1, -1][::-1]
switched[-1, :-1] = switched[-1, :-1][::-1]
return switched