Module transformnd.transforms.affine
Rigid transformations implemented as affine multiplications.
Expand source code
"""
Rigid transformations implemented as affine multiplications.
"""
from __future__ import annotations
from typing import Container, Optional, Tuple, Union
import numpy as np
from numpy.typing import ArrayLike
from ..base import SpaceTuple, Transform
from ..util import is_square, none_eq
def arg_as_array(arg: ArrayLike, ndim: Optional[int]):
if np.isscalar(arg):
if ndim is None:
raise ValueError("Argument must be array-like or ndim must be given")
return np.full(ndim, arg)
arr = np.asarray(arg)
if ndim is not None and len(arr) != ndim:
raise ValueError("Mismatch between ndim and length of argument")
return arr
class Affine(Transform):
def __init__(
self,
matrix: ArrayLike,
*,
spaces: SpaceTuple = (None, None),
):
"""Affine transformation using an augmented matrix.
Matrix must have shape (D+1, D+1) or (D, D+1);
the bottom row is assumed to be [0, 0, ..., 0, 1].
Parameters
----------
matrix : ArrayLike
Affine transformation matrix.
spaces : tuple[SpaceRef, SpaceRef]
Optional source and target spaces
Raises
------
ValueError
Malformed matrix.
"""
super().__init__(spaces=spaces)
m = np.asarray(matrix)
if m.ndim != 2:
raise ValueError("Transformation matrix must be 2D")
if m.shape[1] == m.shape[0] + 1:
base = np.eye(m.shape[1])
base[:-1, :] = m
m = base
elif not is_square(m):
raise ValueError("Transformation matrix must be square")
self.matrix = m
self.ndim = {len(self.matrix) - 1}
def apply(self, coords: np.ndarray) -> np.ndarray:
coords = self._validate_coords(coords)
# todo: replace with writing into full ones?
coords = np.concatenate(
[coords, np.ones((coords.shape[0], 1), dtype=coords.dtype)], axis=1
)
return (self.matrix @ coords.T).T[:, :-1]
def __invert__(self) -> Transform:
return type(self)(
np.linalg.inv(self.matrix),
spaces=self.spaces[::-1],
)
def __matmul__(self, rhs: Affine) -> Affine:
"""Compose two affine transforms by matrix multiplication.
Parameters
----------
rhs : AffineTransform
Returns
-------
AffineTransform
Raises
------
ValueError
Incompatible transforms.
"""
if not isinstance(rhs, Affine):
return NotImplemented
if self.matrix.shape != rhs.matrix.shape:
raise ValueError(
"Cannot multiply affine matrices of different dimensionality"
)
if not none_eq(self.target_space, rhs.source_space):
raise ValueError("Affine transforms do not share a space")
return Affine(
self.matrix @ rhs.matrix,
spaces=(self.source_space, rhs.target_space),
)
@classmethod
def from_linear_map(
cls,
linear_map: ArrayLike,
translation=0,
*,
spaces: SpaceTuple = (None, None),
) -> Affine:
"""Create an augmented affine matrix from a linear map,
with an optional translation.
Parameters
----------
linear_map : ArrayLike
Shape (D, D)
translation : ArrayLike, optional
Translation to add to the matrix, by default None
spaces : tuple[SpaceRef, SpaceRef]
Optional source and target spaces
Returns
-------
AffineTransform
"""
lin_map = np.asarray(linear_map)
side = len(lin_map) + 1
matrix = np.eye(side, dtype=lin_map.dtype)
matrix[:-1, :] = lin_map
matrix[:-1, -1] = translation
return cls(matrix, spaces=spaces)
@classmethod
def identity(
cls,
ndim: int,
*,
spaces: SpaceTuple = (None, None),
) -> Affine:
"""Create an identity affine transformation.
Parameters
----------
ndim : int
spaces : tuple[SpaceRef, SpaceRef]
Optional source and target spaces
Returns
-------
AffineTransform
"""
return cls(
np.eye(ndim + 1),
spaces=spaces,
)
@classmethod
def translation(
cls,
translation: ArrayLike,
ndim: Optional[int] = None,
*,
spaces: SpaceTuple = (None, None),
) -> Affine:
"""Create an affine translation.
Parameters
----------
translation : ArrayLike
If scalar, broadcast to ndim.
ndim : int, optional
If translation is scalar, how many dims to use.
spaces : tuple[SpaceRef, SpaceRef]
Optional source and target spaces
Returns
-------
AffineTransform
"""
t = arg_as_array(translation, ndim)
m = np.eye(len(t) + 1, dtype=t.dtype)
m[:-1, -1] = t
return cls(m, spaces=spaces)
@classmethod
def scaling(
cls,
scale: ArrayLike,
ndim: Optional[int] = None,
*,
spaces: SpaceTuple = (None, None),
) -> Affine:
"""Create an affine scaling.
Parameters
----------
scale : ArrayLike
If scalar, broadcast to ndim.
ndim : Optional[int], optional
If scale is scalar, how many dimensions to use
spaces : tuple[SpaceRef, SpaceRef]
Optional source and target spaces
Returns
-------
AffineTransform
[description]
"""
s = arg_as_array(scale, ndim)
m = np.eye(len(s) + 1, dtype=s.dtype)
m[:-1, :-1] *= s
return cls(m, spaces=spaces)
@classmethod
def reflection(
cls,
axis: Union[int, Container[int]],
ndim: int,
*,
spaces: SpaceTuple = (None, None),
) -> Affine:
"""Create an affine reflection.
Parameters
----------
axis : Union[int, Container[int]]
A single axis or multiple to reflect in.
ndim : int
How many dimensions to work in.
spaces : tuple[SpaceRef, SpaceRef]
Optional source and target spaces
Returns
-------
AffineTransform
"""
if np.isscalar(axis):
axis = [axis] # type: ignore
values = [-1 if idx in axis else 1 for idx in range(ndim)] # type:ignore
m = np.eye(ndim + 1)
m[:-1, :-1] *= values
return cls(m, spaces=spaces)
@classmethod
def rotation2(
cls,
rotation: float,
degrees=True,
clockwise=False,
*,
spaces: SpaceTuple = (None, None),
) -> Affine:
"""Create a 2D affine rotation.
Parameters
----------
rotation : float
Angle to rotate.
degrees : bool, optional
Whether rotation is in degrees (rather than radians), by default True
clockwise : bool, optional
Whether rotation is clockwise, by default False
spaces : tuple[SpaceRef, SpaceRef]
Optional source and target spaces
Returns
-------
AffineTransform
"""
if degrees:
rotation = np.deg2rad(rotation)
if clockwise:
rotation *= -1
vals = [
[np.cos(rotation), -np.sin(rotation)],
[np.sin(rotation), np.cos(rotation)],
]
m = np.eye(3)
m[:-1, :-1] = vals
return cls(m, spaces=spaces)
@classmethod
def rotation3(
cls,
rotation: Union[float, Tuple[float, float, float]],
degrees=True,
clockwise=False,
order=(0, 1, 2),
*,
spaces: SpaceTuple = (None, None),
) -> Affine:
"""Create a 3D affine rotation.
Parameters
----------
rotation : Union[float, Tuple[float, float, float]]
Either a single rotation for all axes, or 1 for each.
degrees : bool, optional
Whether rotation is in degrees (rather than radians), by default True
clockwise : bool, optional
Whether rotation is clockwise, by default False
order : tuple, optional
What order to apply the rotations, by default (0, 1, 2)
spaces : tuple[SpaceRef, SpaceRef]
Optional source and target spaces
Returns
-------
AffineTransform
Raises
------
ValueError
Incompatible order.
"""
if np.isscalar(rotation):
r = np.array([rotation] * 3)
else:
r = np.asarray(rotation)
if degrees:
r = np.deg2rad(r)
if clockwise:
r *= -1
if len(order) != 3 or set(order) != {0, 1, 2}:
raise ValueError("Order must contain only 0, 1, 2 in any order.")
order = list(order)
rots = [
np.array(
[
[1, 0, 0],
[0, np.cos(r[0]), -np.sin(r[0])],
[0, np.sin(r[0]), np.cos(r[0])],
]
),
np.array(
[
[np.cos(r[1]), 0, np.sin(r[1])],
[0, 1, 0],
[-np.sin(r[1]), 0, np.cos(r[1])],
]
),
np.array(
[
[np.cos(r[2]), -np.sin(r[2]), 0],
[np.sin(r[2]), np.cos(r[2]), 0],
[0, 0, 1],
]
),
]
rot = rots[order.pop(0)]
rot @= rots[order.pop(0)]
rot @= rots[order.pop(0)]
m = np.eye(4)
m[:-1, :-1] = rot
return cls(m, spaces=spaces)
@classmethod
def shearing(
cls,
factor: Union[float, np.ndarray],
ndim: Optional[int] = None,
*,
spaces: SpaceTuple = (None, None),
) -> Affine:
"""Create an affine shear.
`factor` can be a scalar to broadcast to all dimensions,
or a D-length list of D-1 lists.
The first inner list contains the shear factors in the first dimension
for all *but* the first dimension.
The second inner list contains the shear factors in the second dimension
for all the *but* the second dimension, etc.
Parameters
----------
factor : Union[float, np.ndarray]
Shear scale factors; see above for more details.
ndim : Optional[int], optional
If factor is scalar, broadcast to this many dimensions, by default None
spaces : tuple[SpaceRef, SpaceRef]
Optional source and target spaces
Returns
-------
AffineTransform
Raises
------
ValueError
Incompatible factor.
"""
if np.isscalar(factor):
if ndim is None:
raise ValueError("If factor is scalar, ndim must be defined")
s = np.full((ndim, ndim - 1), factor)
else:
s = np.asarray(factor)
if s.shape[0] != s.shape[1] + 1:
raise ValueError("Factor must be of shape (D, D-1)")
ndim = s.shape[0]
m = np.eye(ndim, dtype=s.dtype)
for col_idx in range(m.shape[1]):
it = iter(factor[col_idx]) # type: ignore
for row_idx in range(m.shape[0] - 1):
if m[row_idx, col_idx] == 0:
m[row_idx, col_idx] = next(it)
return cls(m, spaces=spaces)
Functions
def arg_as_array(arg: ArrayLike, ndim: Optional[int])-
Expand source code
def arg_as_array(arg: ArrayLike, ndim: Optional[int]): if np.isscalar(arg): if ndim is None: raise ValueError("Argument must be array-like or ndim must be given") return np.full(ndim, arg) arr = np.asarray(arg) if ndim is not None and len(arr) != ndim: raise ValueError("Mismatch between ndim and length of argument") return arr
Classes
class Affine (matrix: ArrayLike, *, spaces: SpaceTuple = (None, None))-
Helper class that provides a standard way to create an ABC using inheritance.
Affine transformation using an augmented matrix.
Matrix must have shape (D+1, D+1) or (D, D+1); the bottom row is assumed to be [0, 0, …, 0, 1].
Parameters
matrix:ArrayLike- Affine transformation matrix.
spaces:tuple[SpaceRef, SpaceRef]- Optional source and target spaces
Raises
ValueError- Malformed matrix.
Expand source code
class Affine(Transform): def __init__( self, matrix: ArrayLike, *, spaces: SpaceTuple = (None, None), ): """Affine transformation using an augmented matrix. Matrix must have shape (D+1, D+1) or (D, D+1); the bottom row is assumed to be [0, 0, ..., 0, 1]. Parameters ---------- matrix : ArrayLike Affine transformation matrix. spaces : tuple[SpaceRef, SpaceRef] Optional source and target spaces Raises ------ ValueError Malformed matrix. """ super().__init__(spaces=spaces) m = np.asarray(matrix) if m.ndim != 2: raise ValueError("Transformation matrix must be 2D") if m.shape[1] == m.shape[0] + 1: base = np.eye(m.shape[1]) base[:-1, :] = m m = base elif not is_square(m): raise ValueError("Transformation matrix must be square") self.matrix = m self.ndim = {len(self.matrix) - 1} def apply(self, coords: np.ndarray) -> np.ndarray: coords = self._validate_coords(coords) # todo: replace with writing into full ones? coords = np.concatenate( [coords, np.ones((coords.shape[0], 1), dtype=coords.dtype)], axis=1 ) return (self.matrix @ coords.T).T[:, :-1] def __invert__(self) -> Transform: return type(self)( np.linalg.inv(self.matrix), spaces=self.spaces[::-1], ) def __matmul__(self, rhs: Affine) -> Affine: """Compose two affine transforms by matrix multiplication. Parameters ---------- rhs : AffineTransform Returns ------- AffineTransform Raises ------ ValueError Incompatible transforms. """ if not isinstance(rhs, Affine): return NotImplemented if self.matrix.shape != rhs.matrix.shape: raise ValueError( "Cannot multiply affine matrices of different dimensionality" ) if not none_eq(self.target_space, rhs.source_space): raise ValueError("Affine transforms do not share a space") return Affine( self.matrix @ rhs.matrix, spaces=(self.source_space, rhs.target_space), ) @classmethod def from_linear_map( cls, linear_map: ArrayLike, translation=0, *, spaces: SpaceTuple = (None, None), ) -> Affine: """Create an augmented affine matrix from a linear map, with an optional translation. Parameters ---------- linear_map : ArrayLike Shape (D, D) translation : ArrayLike, optional Translation to add to the matrix, by default None spaces : tuple[SpaceRef, SpaceRef] Optional source and target spaces Returns ------- AffineTransform """ lin_map = np.asarray(linear_map) side = len(lin_map) + 1 matrix = np.eye(side, dtype=lin_map.dtype) matrix[:-1, :] = lin_map matrix[:-1, -1] = translation return cls(matrix, spaces=spaces) @classmethod def identity( cls, ndim: int, *, spaces: SpaceTuple = (None, None), ) -> Affine: """Create an identity affine transformation. Parameters ---------- ndim : int spaces : tuple[SpaceRef, SpaceRef] Optional source and target spaces Returns ------- AffineTransform """ return cls( np.eye(ndim + 1), spaces=spaces, ) @classmethod def translation( cls, translation: ArrayLike, ndim: Optional[int] = None, *, spaces: SpaceTuple = (None, None), ) -> Affine: """Create an affine translation. Parameters ---------- translation : ArrayLike If scalar, broadcast to ndim. ndim : int, optional If translation is scalar, how many dims to use. spaces : tuple[SpaceRef, SpaceRef] Optional source and target spaces Returns ------- AffineTransform """ t = arg_as_array(translation, ndim) m = np.eye(len(t) + 1, dtype=t.dtype) m[:-1, -1] = t return cls(m, spaces=spaces) @classmethod def scaling( cls, scale: ArrayLike, ndim: Optional[int] = None, *, spaces: SpaceTuple = (None, None), ) -> Affine: """Create an affine scaling. Parameters ---------- scale : ArrayLike If scalar, broadcast to ndim. ndim : Optional[int], optional If scale is scalar, how many dimensions to use spaces : tuple[SpaceRef, SpaceRef] Optional source and target spaces Returns ------- AffineTransform [description] """ s = arg_as_array(scale, ndim) m = np.eye(len(s) + 1, dtype=s.dtype) m[:-1, :-1] *= s return cls(m, spaces=spaces) @classmethod def reflection( cls, axis: Union[int, Container[int]], ndim: int, *, spaces: SpaceTuple = (None, None), ) -> Affine: """Create an affine reflection. Parameters ---------- axis : Union[int, Container[int]] A single axis or multiple to reflect in. ndim : int How many dimensions to work in. spaces : tuple[SpaceRef, SpaceRef] Optional source and target spaces Returns ------- AffineTransform """ if np.isscalar(axis): axis = [axis] # type: ignore values = [-1 if idx in axis else 1 for idx in range(ndim)] # type:ignore m = np.eye(ndim + 1) m[:-1, :-1] *= values return cls(m, spaces=spaces) @classmethod def rotation2( cls, rotation: float, degrees=True, clockwise=False, *, spaces: SpaceTuple = (None, None), ) -> Affine: """Create a 2D affine rotation. Parameters ---------- rotation : float Angle to rotate. degrees : bool, optional Whether rotation is in degrees (rather than radians), by default True clockwise : bool, optional Whether rotation is clockwise, by default False spaces : tuple[SpaceRef, SpaceRef] Optional source and target spaces Returns ------- AffineTransform """ if degrees: rotation = np.deg2rad(rotation) if clockwise: rotation *= -1 vals = [ [np.cos(rotation), -np.sin(rotation)], [np.sin(rotation), np.cos(rotation)], ] m = np.eye(3) m[:-1, :-1] = vals return cls(m, spaces=spaces) @classmethod def rotation3( cls, rotation: Union[float, Tuple[float, float, float]], degrees=True, clockwise=False, order=(0, 1, 2), *, spaces: SpaceTuple = (None, None), ) -> Affine: """Create a 3D affine rotation. Parameters ---------- rotation : Union[float, Tuple[float, float, float]] Either a single rotation for all axes, or 1 for each. degrees : bool, optional Whether rotation is in degrees (rather than radians), by default True clockwise : bool, optional Whether rotation is clockwise, by default False order : tuple, optional What order to apply the rotations, by default (0, 1, 2) spaces : tuple[SpaceRef, SpaceRef] Optional source and target spaces Returns ------- AffineTransform Raises ------ ValueError Incompatible order. """ if np.isscalar(rotation): r = np.array([rotation] * 3) else: r = np.asarray(rotation) if degrees: r = np.deg2rad(r) if clockwise: r *= -1 if len(order) != 3 or set(order) != {0, 1, 2}: raise ValueError("Order must contain only 0, 1, 2 in any order.") order = list(order) rots = [ np.array( [ [1, 0, 0], [0, np.cos(r[0]), -np.sin(r[0])], [0, np.sin(r[0]), np.cos(r[0])], ] ), np.array( [ [np.cos(r[1]), 0, np.sin(r[1])], [0, 1, 0], [-np.sin(r[1]), 0, np.cos(r[1])], ] ), np.array( [ [np.cos(r[2]), -np.sin(r[2]), 0], [np.sin(r[2]), np.cos(r[2]), 0], [0, 0, 1], ] ), ] rot = rots[order.pop(0)] rot @= rots[order.pop(0)] rot @= rots[order.pop(0)] m = np.eye(4) m[:-1, :-1] = rot return cls(m, spaces=spaces) @classmethod def shearing( cls, factor: Union[float, np.ndarray], ndim: Optional[int] = None, *, spaces: SpaceTuple = (None, None), ) -> Affine: """Create an affine shear. `factor` can be a scalar to broadcast to all dimensions, or a D-length list of D-1 lists. The first inner list contains the shear factors in the first dimension for all *but* the first dimension. The second inner list contains the shear factors in the second dimension for all the *but* the second dimension, etc. Parameters ---------- factor : Union[float, np.ndarray] Shear scale factors; see above for more details. ndim : Optional[int], optional If factor is scalar, broadcast to this many dimensions, by default None spaces : tuple[SpaceRef, SpaceRef] Optional source and target spaces Returns ------- AffineTransform Raises ------ ValueError Incompatible factor. """ if np.isscalar(factor): if ndim is None: raise ValueError("If factor is scalar, ndim must be defined") s = np.full((ndim, ndim - 1), factor) else: s = np.asarray(factor) if s.shape[0] != s.shape[1] + 1: raise ValueError("Factor must be of shape (D, D-1)") ndim = s.shape[0] m = np.eye(ndim, dtype=s.dtype) for col_idx in range(m.shape[1]): it = iter(factor[col_idx]) # type: ignore for row_idx in range(m.shape[0] - 1): if m[row_idx, col_idx] == 0: m[row_idx, col_idx] = next(it) return cls(m, spaces=spaces)Ancestors
- Transform
- abc.ABC
Static methods
def from_linear_map(linear_map: ArrayLike, translation=0, *, spaces: SpaceTuple = (None, None)) ‑> Affine-
Create an augmented affine matrix from a linear map, with an optional translation.
Parameters
linear_map:ArrayLike- Shape (D, D)
translation:ArrayLike, optional- Translation to add to the matrix, by default None
spaces:tuple[SpaceRef, SpaceRef]- Optional source and target spaces
Returns
AffineTransform
Expand source code
@classmethod def from_linear_map( cls, linear_map: ArrayLike, translation=0, *, spaces: SpaceTuple = (None, None), ) -> Affine: """Create an augmented affine matrix from a linear map, with an optional translation. Parameters ---------- linear_map : ArrayLike Shape (D, D) translation : ArrayLike, optional Translation to add to the matrix, by default None spaces : tuple[SpaceRef, SpaceRef] Optional source and target spaces Returns ------- AffineTransform """ lin_map = np.asarray(linear_map) side = len(lin_map) + 1 matrix = np.eye(side, dtype=lin_map.dtype) matrix[:-1, :] = lin_map matrix[:-1, -1] = translation return cls(matrix, spaces=spaces) def identity(ndim: int, *, spaces: SpaceTuple = (None, None)) ‑> Affine-
Create an identity affine transformation.
Parameters
ndim:intspaces:tuple[SpaceRef, SpaceRef]- Optional source and target spaces
Returns
AffineTransform
Expand source code
@classmethod def identity( cls, ndim: int, *, spaces: SpaceTuple = (None, None), ) -> Affine: """Create an identity affine transformation. Parameters ---------- ndim : int spaces : tuple[SpaceRef, SpaceRef] Optional source and target spaces Returns ------- AffineTransform """ return cls( np.eye(ndim + 1), spaces=spaces, ) def reflection(axis: Union[int, Container[int]], ndim: int, *, spaces: SpaceTuple = (None, None)) ‑> Affine-
Create an affine reflection.
Parameters
axis:Union[int, Container[int]]- A single axis or multiple to reflect in.
ndim:int- How many dimensions to work in.
spaces:tuple[SpaceRef, SpaceRef]- Optional source and target spaces
Returns
AffineTransform
Expand source code
@classmethod def reflection( cls, axis: Union[int, Container[int]], ndim: int, *, spaces: SpaceTuple = (None, None), ) -> Affine: """Create an affine reflection. Parameters ---------- axis : Union[int, Container[int]] A single axis or multiple to reflect in. ndim : int How many dimensions to work in. spaces : tuple[SpaceRef, SpaceRef] Optional source and target spaces Returns ------- AffineTransform """ if np.isscalar(axis): axis = [axis] # type: ignore values = [-1 if idx in axis else 1 for idx in range(ndim)] # type:ignore m = np.eye(ndim + 1) m[:-1, :-1] *= values return cls(m, spaces=spaces) def rotation2(rotation: float, degrees=True, clockwise=False, *, spaces: SpaceTuple = (None, None)) ‑> Affine-
Create a 2D affine rotation.
Parameters
rotation:float- Angle to rotate.
degrees:bool, optional- Whether rotation is in degrees (rather than radians), by default True
clockwise:bool, optional- Whether rotation is clockwise, by default False
spaces:tuple[SpaceRef, SpaceRef]- Optional source and target spaces
Returns
AffineTransform
Expand source code
@classmethod def rotation2( cls, rotation: float, degrees=True, clockwise=False, *, spaces: SpaceTuple = (None, None), ) -> Affine: """Create a 2D affine rotation. Parameters ---------- rotation : float Angle to rotate. degrees : bool, optional Whether rotation is in degrees (rather than radians), by default True clockwise : bool, optional Whether rotation is clockwise, by default False spaces : tuple[SpaceRef, SpaceRef] Optional source and target spaces Returns ------- AffineTransform """ if degrees: rotation = np.deg2rad(rotation) if clockwise: rotation *= -1 vals = [ [np.cos(rotation), -np.sin(rotation)], [np.sin(rotation), np.cos(rotation)], ] m = np.eye(3) m[:-1, :-1] = vals return cls(m, spaces=spaces) def rotation3(rotation: Union[float, Tuple[float, float, float]], degrees=True, clockwise=False, order=(0, 1, 2), *, spaces: SpaceTuple = (None, None)) ‑> Affine-
Create a 3D affine rotation.
Parameters
rotation:Union[float, Tuple[float, float, float]]- Either a single rotation for all axes, or 1 for each.
degrees:bool, optional- Whether rotation is in degrees (rather than radians), by default True
clockwise:bool, optional- Whether rotation is clockwise, by default False
order:tuple, optional- What order to apply the rotations, by default (0, 1, 2)
spaces:tuple[SpaceRef, SpaceRef]- Optional source and target spaces
Returns
AffineTransform
Raises
ValueError- Incompatible order.
Expand source code
@classmethod def rotation3( cls, rotation: Union[float, Tuple[float, float, float]], degrees=True, clockwise=False, order=(0, 1, 2), *, spaces: SpaceTuple = (None, None), ) -> Affine: """Create a 3D affine rotation. Parameters ---------- rotation : Union[float, Tuple[float, float, float]] Either a single rotation for all axes, or 1 for each. degrees : bool, optional Whether rotation is in degrees (rather than radians), by default True clockwise : bool, optional Whether rotation is clockwise, by default False order : tuple, optional What order to apply the rotations, by default (0, 1, 2) spaces : tuple[SpaceRef, SpaceRef] Optional source and target spaces Returns ------- AffineTransform Raises ------ ValueError Incompatible order. """ if np.isscalar(rotation): r = np.array([rotation] * 3) else: r = np.asarray(rotation) if degrees: r = np.deg2rad(r) if clockwise: r *= -1 if len(order) != 3 or set(order) != {0, 1, 2}: raise ValueError("Order must contain only 0, 1, 2 in any order.") order = list(order) rots = [ np.array( [ [1, 0, 0], [0, np.cos(r[0]), -np.sin(r[0])], [0, np.sin(r[0]), np.cos(r[0])], ] ), np.array( [ [np.cos(r[1]), 0, np.sin(r[1])], [0, 1, 0], [-np.sin(r[1]), 0, np.cos(r[1])], ] ), np.array( [ [np.cos(r[2]), -np.sin(r[2]), 0], [np.sin(r[2]), np.cos(r[2]), 0], [0, 0, 1], ] ), ] rot = rots[order.pop(0)] rot @= rots[order.pop(0)] rot @= rots[order.pop(0)] m = np.eye(4) m[:-1, :-1] = rot return cls(m, spaces=spaces) def scaling(scale: ArrayLike, ndim: Optional[int] = None, *, spaces: SpaceTuple = (None, None)) ‑> Affine-
Create an affine scaling.
Parameters
scale:ArrayLike- If scalar, broadcast to ndim.
ndim:Optional[int], optional- If scale is scalar, how many dimensions to use
spaces:tuple[SpaceRef, SpaceRef]- Optional source and target spaces
Returns
AffineTransform- [description]
Expand source code
@classmethod def scaling( cls, scale: ArrayLike, ndim: Optional[int] = None, *, spaces: SpaceTuple = (None, None), ) -> Affine: """Create an affine scaling. Parameters ---------- scale : ArrayLike If scalar, broadcast to ndim. ndim : Optional[int], optional If scale is scalar, how many dimensions to use spaces : tuple[SpaceRef, SpaceRef] Optional source and target spaces Returns ------- AffineTransform [description] """ s = arg_as_array(scale, ndim) m = np.eye(len(s) + 1, dtype=s.dtype) m[:-1, :-1] *= s return cls(m, spaces=spaces) def shearing(factor: Union[float, np.ndarray], ndim: Optional[int] = None, *, spaces: SpaceTuple = (None, None)) ‑> Affine-
Create an affine shear.
factorcan be a scalar to broadcast to all dimensions, or a D-length list of D-1 lists. The first inner list contains the shear factors in the first dimension for all but the first dimension. The second inner list contains the shear factors in the second dimension for all the but the second dimension, etc.Parameters
factor:Union[float, np.ndarray]- Shear scale factors; see above for more details.
ndim:Optional[int], optional- If factor is scalar, broadcast to this many dimensions, by default None
spaces:tuple[SpaceRef, SpaceRef]- Optional source and target spaces
Returns
AffineTransform
Raises
ValueError- Incompatible factor.
Expand source code
@classmethod def shearing( cls, factor: Union[float, np.ndarray], ndim: Optional[int] = None, *, spaces: SpaceTuple = (None, None), ) -> Affine: """Create an affine shear. `factor` can be a scalar to broadcast to all dimensions, or a D-length list of D-1 lists. The first inner list contains the shear factors in the first dimension for all *but* the first dimension. The second inner list contains the shear factors in the second dimension for all the *but* the second dimension, etc. Parameters ---------- factor : Union[float, np.ndarray] Shear scale factors; see above for more details. ndim : Optional[int], optional If factor is scalar, broadcast to this many dimensions, by default None spaces : tuple[SpaceRef, SpaceRef] Optional source and target spaces Returns ------- AffineTransform Raises ------ ValueError Incompatible factor. """ if np.isscalar(factor): if ndim is None: raise ValueError("If factor is scalar, ndim must be defined") s = np.full((ndim, ndim - 1), factor) else: s = np.asarray(factor) if s.shape[0] != s.shape[1] + 1: raise ValueError("Factor must be of shape (D, D-1)") ndim = s.shape[0] m = np.eye(ndim, dtype=s.dtype) for col_idx in range(m.shape[1]): it = iter(factor[col_idx]) # type: ignore for row_idx in range(m.shape[0] - 1): if m[row_idx, col_idx] == 0: m[row_idx, col_idx] = next(it) return cls(m, spaces=spaces) def translation(translation: ArrayLike, ndim: Optional[int] = None, *, spaces: SpaceTuple = (None, None)) ‑> Affine-
Create an affine translation.
Parameters
translation:ArrayLike- If scalar, broadcast to ndim.
ndim:int, optional- If translation is scalar, how many dims to use.
spaces:tuple[SpaceRef, SpaceRef]- Optional source and target spaces
Returns
AffineTransform
Expand source code
@classmethod def translation( cls, translation: ArrayLike, ndim: Optional[int] = None, *, spaces: SpaceTuple = (None, None), ) -> Affine: """Create an affine translation. Parameters ---------- translation : ArrayLike If scalar, broadcast to ndim. ndim : int, optional If translation is scalar, how many dims to use. spaces : tuple[SpaceRef, SpaceRef] Optional source and target spaces Returns ------- AffineTransform """ t = arg_as_array(translation, ndim) m = np.eye(len(t) + 1, dtype=t.dtype) m[:-1, -1] = t return cls(m, spaces=spaces)
Inherited members