Package transformnd
transformnd
A library providing an API for coordinate transformations,
as well as some common transforms.
The goal is to allow downstream applications which require such transformations
(e.g. image registration) to be generic over anything inheriting from Transform.
The base classes and utilities are very lightweight with few dependencies, for use as an API; additional transforms and features use extras.
Heavily inspired by/ cribbed directly from Philipp Schlegel's work in navis; co-developed with xform as a red team prototype.
N coordinates in D dimensions are given as a numpy array of shape (N, D).
Transform subclasses which are restricted to certain dimensionalities
can specify this in their ndim class variable.
Instances of Transform subclasses can further restrict their ndim.
Use self._validate_coords(coords) in __call__ to ensure the coordinates
are of valid type and dimensions.
Additionally, transformnd provides an interface for transforming types other than NxD numpy arrays,
and implements these adapters for a few common types.
See the tutorial here.
Implemented transforms
- Identity (
Identity) - Translation (
Translate) - Scale (
Scale) - Reflection (
Reflect) - Affine (
Affine)- Can be composed efficiently with
@operator
- Can be composed efficiently with
- Moving Least Squares, affine (
MovingLeastSquares)- uses
movingleastsquaresextra
- uses
- Thin Plate Splines (
ThinPlateSplines)- uses
thinplatesplinesextra
- uses
Arbitrary transforms can be composed into a TransformSequence with transform1 | transform2.
A graph of transforms between defined spaces can be traversed using the TransformGraph.
Implemented adapters
- Numpy arrays of shape
(…, D, …)(ReshapeAdapter) meshio.Mesh(MeshAdapter)pandas.DataFrame(DataFrameAdapter)- Takes a subset of columns as a coordinate array
- Geometries from
shapely(GeometryAdapter) - Objects composed of transformable attributes (
AttrAdapter).
Additional transforms and adapters
Contributions of additional transforms and adapters are welcome! Even if they're only thin wrappers around an external library, the downstream ecosystem benefits from a consistent API.
Such external transformation libraries should be specified as "extras",
and be contained in a submodule so that they are not immediately imported
with transformnd.
Dependencies for new adapters do not need to be included in transformnd's dependencies,
but should be specified in the requirements.txt for tests.
Alternatively, consider adopting transformnd's base classes in your own library,
and have your transformation instantly compatible for downstream users.
Expand source code
"""
.. include:: ../../README.md
"""
from .base import Transform, TransformSequence, TransformWrapper
from .util import SpaceRef, TransformSignature, check_ndim
from .version import version as __version__ # noqa: F401
from .version import version_tuple as __version_info__ # noqa: F401
__all__ = [
"Transform",
"TransformSequence",
"TransformWrapper",
"TransformSignature",
"SpaceRef",
"check_ndim",
]
Sub-modules
transformnd.adapters-
Adapters for transforming objects which are not well-behaved numpy arrays …
transformnd.base-
Base classes and wrappers for transforms.
transformnd.graph-
Bridging transforms between known spaces.
transformnd.transforms-
Implementations of some common transforms.
transformnd.util-
Utilities used elsewhere in the package.
transformnd.version
Functions
def check_ndim(given_ndim: int, supported_ndim: Optional[Set[int]])-
Raise a ValueError if dimensionality is unsupported.
Parameters
given_ndim:int- The dimensionality to check.
supported_ndim:Optional[Set[int]]- Which dimensions are supported. If None, the check passes.
Raises
ValueError- If supported dimensions are defined and given_ndim is not in them.
Expand source code
def check_ndim(given_ndim: int, supported_ndim: Optional[Set[int]]): """Raise a ValueError if dimensionality is unsupported. Parameters ---------- given_ndim : int The dimensionality to check. supported_ndim : Optional[Set[int]] Which dimensions are supported. If None, the check passes. Raises ------ ValueError If supported dimensions are defined and given_ndim is not in them. """ if supported_ndim is not None and given_ndim not in supported_ndim: raise ValueError( f"Transform supported for {format_dims(supported_ndim)}, not {given_ndim}" )
Classes
class Transform (*, spaces: SpaceTuple = (None, None))-
Helper class that provides a standard way to create an ABC using inheritance.
Base class for transformations.
Parameters
spaces:tuple[SpaceRef, SpaceRef]- Optional source and target spaces
Expand source code
class Transform(ABC): ndim: Optional[Set[int]] = None def __init__( self, *, spaces: SpaceTuple = (None, None), ): """Base class for transformations. Parameters ---------- spaces : tuple[SpaceRef, SpaceRef] Optional source and target spaces """ self.spaces = spaces @property def source_space(self): return self.spaces[0] @property def target_space(self): return self.spaces[1] def _validate_coords(self, coords) -> np.ndarray: """Check that dimension of coords are supported. Also ensure that coords is a 2D numpy array. Parameters ---------- coords : np.ndarray NxD array of N D-dimensional coordinates. Raises ------ ValueError If dimensions are not supported. """ coords = np.asarray(coords) if coords.ndim != 2: raise ValueError("Coords must be a 2D array") check_ndim(coords.shape[1], self.ndim) return coords @abstractmethod def apply(self, coords: np.ndarray) -> np.ndarray: """Apply transformation. Parameters ---------- coords : np.ndarray NxD array of N D-dimensional coordinates. Returns ------- np.ndarray Transformed coordinates in the same shape. """ pass def __invert__(self) -> Transform: """Invert transformation if possible. Returns ------- Transform Inverted transformation. """ return NotImplemented def __or__(self, other) -> TransformSequence: """Compose transformations into a sequence. If other is a TransformSequence, prepend this transform to the others. Parameters ---------- other : Transform Returns ------- TransformSequence """ if not isinstance(other, Transform): return NotImplemented transforms = get_transform_list(self) + get_transform_list(other) return TransformSequence( transforms, spaces=(self.source_space, other.target_space), ) def __ror__(self, other) -> TransformSequence: """Compose transformations into a sequence. If other is a TransformSequence, append this transform to the others. Parameters ---------- other : Transform Returns ------- TransformSequence """ if not isinstance(other, Transform): return NotImplemented transforms = get_transform_list(other) + get_transform_list(self) return TransformSequence( transforms, spaces=(other.source_space, self.target_space), ) def __str__(self) -> str: cls_name = type(self).__name__ src = space_str(self.source_space) tgt = space_str(self.target_space) return f"{cls_name}[{src}->{tgt}]"Ancestors
- abc.ABC
Subclasses
- TransformSequence
- TransformWrapper
- Affine
- MovingLeastSquares
- Reflect
- Identity
- Scale
- Translate
- ThinPlateSplines
Class variables
var ndim : Optional[Set[int]]
Instance variables
var source_space-
Expand source code
@property def source_space(self): return self.spaces[0] var target_space-
Expand source code
@property def target_space(self): return self.spaces[1]
Methods
def apply(self, coords: np.ndarray) ‑> numpy.ndarray-
Apply transformation.
Parameters
coords:np.ndarray- NxD array of N D-dimensional coordinates.
Returns
np.ndarray- Transformed coordinates in the same shape.
Expand source code
@abstractmethod def apply(self, coords: np.ndarray) -> np.ndarray: """Apply transformation. Parameters ---------- coords : np.ndarray NxD array of N D-dimensional coordinates. Returns ------- np.ndarray Transformed coordinates in the same shape. """ pass
class TransformSequence (transforms: Sequence[Transform], *, spaces: SpaceTuple = (None, None))-
Helper class that provides a standard way to create an ABC using inheritance.
Combine transforms by chaining them.
Also checks for consistent dimensionality and space references, inferring if None.
Parameters
transforms:List[Transform]- Items which are a TransformSequences will each still be treated as a single transform.
spaces:tuple[SpaceRef, SpaceRef]- Optional source and target spaces. Can also be inferred from the first and last transforms.
Raises
ValueError- If spaces are incompatible.
Expand source code
class TransformSequence(Transform, Sequence[Transform]): def __init__( self, transforms: Sequence[Transform], *, spaces: SpaceTuple = (None, None), ) -> None: """Combine transforms by chaining them. Also checks for consistent dimensionality and space references, inferring if None. Parameters ---------- transforms : List[Transform] Items which are a TransformSequences will each still be treated as a single transform. spaces : tuple[SpaceRef, SpaceRef] Optional source and target spaces. Can also be inferred from the first and last transforms. Raises ------ ValueError If spaces are incompatible. """ ts = infer_spaces(transforms, *spaces) super().__init__( spaces=( ts[0].source_space, ts[-1].target_space, ), ) self.transforms: List[Transform] = ts self.ndim = None for t in self.transforms: self.ndim = dim_intersection(self.ndim, t.ndim) if self.ndim is not None and len(self.ndim) == 0: raise ValueError("Transforms have incompatible dimensionalities") def __iter__(self) -> Iterator[Transform]: """Iterate through component transforms. Yields ------- Transform """ yield from self.transforms def __len__(self) -> int: """Number of transforms. Returns ------- int """ return len(self.transforms) def __invert__(self) -> Transform: try: transforms = [~t for t in reversed(self.transforms)] except NotImplementedError: return NotImplemented return type(self)( transforms, spaces=self.spaces[::-1], ) def apply(self, coords: np.ndarray) -> np.ndarray: for t in self.transforms: coords = t.apply(coords) return coords def list_spaces(self, skip_none=False) -> List[SpaceRef]: """List spaces in this transform. Parameters ---------- skip_none : bool, optional Whether to skip undefined spaces, default False. Returns ------- List[SpaceRef] """ spaces = [self.source_space] + [t.target_space for t in self.transforms] if skip_none: spaces = [s for s in spaces if s is not None] return spaces def __str__(self) -> str: cls_name = type(self).__name__ spaces_str = "->".join(space_str(s) for s in self.list_spaces()) return f"{cls_name}[{spaces_str}]" def __getitem__(self, idx: Union[slice, int]): if isinstance(idx, int): return self.transforms[idx] return type(self)(self.transforms[idx])Ancestors
- Transform
- abc.ABC
- collections.abc.Sequence
- collections.abc.Reversible
- collections.abc.Collection
- collections.abc.Sized
- collections.abc.Iterable
- collections.abc.Container
- typing.Generic
Methods
def list_spaces(self, skip_none=False) ‑> List[Hashable]-
List spaces in this transform.
Parameters
skip_none:bool, optional- Whether to skip undefined spaces, default False.
Returns
List[SpaceRef]
Expand source code
def list_spaces(self, skip_none=False) -> List[SpaceRef]: """List spaces in this transform. Parameters ---------- skip_none : bool, optional Whether to skip undefined spaces, default False. Returns ------- List[SpaceRef] """ spaces = [self.source_space] + [t.target_space for t in self.transforms] if skip_none: spaces = [s for s in spaces if s is not None] return spaces
Inherited members
class TransformWrapper (fn: TransformSignature, ndim: Optional[Union[Set[int], int]] = None, *, spaces: SpaceTuple = (None, None))-
Helper class that provides a standard way to create an ABC using inheritance.
Wrapper around an arbitrary function.
Callable should take and return an identically-shaped NxD numpy array of N D-dimensional coordinates.
Parameters
fn:TransformSignature- Callable.
spaces:tuple[SpaceRef, SpaceRef]- Optional source and target spaces
Expand source code
class TransformWrapper(Transform): def __init__( self, fn: TransformSignature, ndim: Optional[Union[Set[int], int]] = None, *, spaces: SpaceTuple = (None, None), ): """Wrapper around an arbitrary function. Callable should take and return an identically-shaped NxD numpy array of N D-dimensional coordinates. Parameters ---------- fn : TransformSignature Callable. spaces : tuple[SpaceRef, SpaceRef] Optional source and target spaces """ super().__init__(spaces=spaces) self.fn = fn if ndim is not None: if isinstance(ndim, int): self.ndim = {ndim} else: self.ndim = set(ndim) def apply(self, coords: np.ndarray) -> np.ndarray: self._validate_coords(coords) return self.fn(coords)Ancestors
- Transform
- abc.ABC
Inherited members