Source code for nornir_imageregistration.transforms.base

"""
Created on Oct 18, 2012

@author: Jamesan
"""
import abc
from abc import ABC, abstractmethod
from typing import Any, Callable

import numpy as np
from numpy.typing import NDArray, ArrayLike
import scipy.spatial

import nornir_imageregistration
from nornir_imageregistration.transforms.transform_type import TransformType


[docs] class ITransform(ABC):
[docs] @abstractmethod def Transform(self, point: NDArray[np.floating], **kwargs) -> NDArray: """Map points from the mapped space to fixed space. Nornir is gradually transitioning to a source space to target space naming convention.""" raise NotImplementedError()
[docs] @abstractmethod def InverseTransform(self, point: NDArray[np.floating], **kwargs) -> NDArray: """Map points from the fixed space to mapped space. Nornir is gradually transitioning to a target space to source space naming convention.""" raise NotImplementedError()
[docs] @abc.abstractmethod def Load(self, TransformString: str, pixelSpacing=None): """ Creates an instance of the transform from the TransformString """ raise NotImplementedError()
[docs] @abc.abstractmethod def ToITKString(self) -> str: """Serialize the transform to an ITK-compatible string representation.""" raise NotImplementedError()
@property @abc.abstractmethod def type(self) -> TransformType: raise NotImplementedError()
[docs] class ITransformChangeEvents(ABC):
[docs] @abstractmethod def AddOnChangeEventListener(self, func: Callable): """ Call func whenever the transform changes """ raise NotImplementedError()
[docs] @abstractmethod def RemoveOnChangeEventListener(self, func: Callable): """ Stop calling func whenever the transform changes """ raise NotImplementedError()
[docs] class ITransformTranslation(ABC):
[docs] @abstractmethod def TranslateFixed(self, offset: NDArray): """Translate all fixed points by the specified amount""" raise NotImplementedError()
[docs] @abstractmethod def TranslateWarped(self, offset: NDArray): """Translate all warped points by the specified amount""" raise NotImplementedError()
class ITransformSourceRotation(ABC): @abstractmethod def RotateSourcePoints(self, rangle: float, rotation_center: NDArray[np.floating] | None): """Rotate all warped points by the specified amount If rotation center is not specified the transform chooses""" raise NotImplementedError() class ITransformTargetRotation(ABC): @abstractmethod def RotateTargetPoints(self, rangle: float, rotation_center: NDArray[np.floating] | None): """Rotate all fixed points by the specified amount If rotation center is not specified the transform chooses""" raise NotImplementedError()
[docs] class ITransformScaling(ABC): """Supports scaling target and source space together (changing image downsample level for example)"""
[docs] @abstractmethod def Scale(self, scalar: float) -> None: """Scale both spaces by the specified amount""" raise NotImplementedError()
class ITransfomFlip(ABC): """Supports flipping the target and source space independently of each other""" def Flip(self): """Flip the target and source space independently of each other""" raise NotImplementedError() # Correct spelling; ITransfomFlip is deprecated. ITransformFlip = ITransfomFlip class ITransformRelativeScaling(ABC): """Supports scaling of target space or source space independently of each other""" @abstractmethod def ScaleFixed(self, scalar: float) -> None: """Scale all fixed points by the specified amount""" raise NotImplementedError() @abstractmethod def ScaleWarped(self, scalar: float) -> None: """Scale all warped points by the specified amount""" raise NotImplementedError()
[docs] class IDiscreteTransform(ITransform, ABC): @property @abc.abstractmethod def MappedBoundingBox(self) -> nornir_imageregistration.Rectangle: """Bounding box of mapped space points (source-space alias)""" raise NotImplementedError() @property @abc.abstractmethod def FixedBoundingBox(self) -> nornir_imageregistration.Rectangle: """Bounding box of fixed space points (target-space alias)""" raise NotImplementedError() @property @abc.abstractmethod def TargetBoundingBox(self) -> nornir_imageregistration.Rectangle: """Bounding box of target space points""" raise NotImplementedError()
class IGridTransform(ITransform, ABC): @property @abc.abstractmethod def grid(self) -> nornir_imageregistration.IGrid: raise NotImplementedError()
[docs] class IControlPoints(ABC): """Interface for transforms that use control points""" @property @abc.abstractmethod def SourcePoints(self) -> NDArray: """The source points of the transform. Order matches the results of SourcePoints and points""" raise NotImplementedError() @property @abc.abstractmethod def TargetPoints(self) -> NDArray: """The target points of the transform. Order matches the results of SourcePoints and points""" raise NotImplementedError() @property @abc.abstractmethod def points(self) -> NDArray: """Points is a 4xN array of corresponding control points in this format [[TargetY, TargetX, SourceY, SourceX],]. Order matches the results of SourcePoints and TargetPoints""" raise NotImplementedError()
[docs] @abc.abstractmethod def NearestFixedPoint(self, points: NDArray) -> tuple[float | NDArray[np.floating], int | NDArray[np.integer]]: """ Return the fixed points nearest to the query points :return: Distance, Index """ raise NotImplementedError()
[docs] @abc.abstractmethod def NearestWarpedPoint(self, points: NDArray) -> tuple[float | NDArray[np.floating], int | NDArray[np.integer]]: """ Return the warped points nearest to the query points :return: Distance, Index """ raise NotImplementedError()
[docs] @abc.abstractmethod def GetPointPairsInTargetRect(self, bounds: nornir_imageregistration.Rectangle) -> NDArray[np.floating]: """Return the point pairs inside the rectangle defined in target space""" raise NotImplementedError()
[docs] @abc.abstractmethod def GetPointPairsInSourceRect(self, bounds: nornir_imageregistration.Rectangle) -> NDArray[np.floating]: """Return the point pairs inside the rectangle defined in source space""" raise NotImplementedError()
[docs] @abc.abstractmethod def PointPairsToWarpedPoints(self, points: NDArray[np.floating]) -> NDArray[np.floating]: """Return the warped points from a set of target-source point pairs""" raise NotImplementedError()
[docs] @abc.abstractmethod def PointPairsToTargetPoints(self, points: NDArray[np.floating]) -> NDArray[np.floating]: """Return the target points from a set of target-source point pairs""" raise NotImplementedError()
@property @abc.abstractmethod def NumControlPoints(self) -> int: raise NotImplementedError() @property @abc.abstractmethod def TargetBoundingBox(self) -> nornir_imageregistration.Rectangle: """Bounding box of target space points""" raise NotImplementedError() @property @abc.abstractmethod def SourceBoundingBox(self) -> nornir_imageregistration.Rectangle: """Bounding box of source space points""" raise NotImplementedError()
class ITriangulatedTargetSpace(ABC): @property @abc.abstractmethod def target_space_trianglulation(self) -> scipy.spatial.Delaunay: raise NotImplementedError() class ITriangulatedSourceSpace(ABC): @property @abc.abstractmethod def source_space_trianglulation(self) -> scipy.spatial.Delaunay: raise NotImplementedError() class IControlPointAddRemove(ABC): """Interface for control point based transforms that can add/remove control points""" @abc.abstractmethod def AddPoint(self, pointpair: NDArray[np.floating]) -> int: raise NotImplementedError() @abc.abstractmethod def AddPoints(self, new_points: NDArray[np.floating]): raise NotImplementedError() @abc.abstractmethod def RemovePoint(self, index: int | NDArray[np.integer]): raise NotImplementedError() class ITargetSpaceControlPointEdit(ABC): """Transforms where the source space side of control points can be moved. Originally added for grid transforms where source points are unmovable""" @abc.abstractmethod def UpdateTargetPointsByIndex( self, index: int | NDArray[np.integer], points: NDArray[np.floating], *, remove_duplicates: bool = True, ) -> int | NDArray[np.integer]: """Move target-space control points at *index*. :param remove_duplicates: When True (default), collapse coincident control points after the write and return the nearest remaining index. When False, skip uniqueness: coincident points can remain; Delaunay/RBF can later fail or remap rows. Use False only for a bounded interactive sequence (drag, registration apply) where the caller keeps indices stable and remeshes when idle. :return: The index of the edited points after any collapse. """ raise NotImplementedError() @abc.abstractmethod def UpdateTargetPointsByPosition( self, old_points: NDArray[np.floating], new_points: NDArray[np.floating], *, remove_duplicates: bool = True, ) -> int | NDArray[np.integer]: """Move the points closest to *old_points* to positions at *new_points*. :param remove_duplicates: See :meth:`UpdateTargetPointsByIndex`. :return: The index of the edited points after any collapse. """ raise NotImplementedError() class ISourceSpaceControlPointEdit(ABC): """Transforms where the source space side of control points can be moved""" @abc.abstractmethod def UpdateSourcePointsByIndex( self, index: int | NDArray[np.integer], points: NDArray[np.floating], *, remove_duplicates: bool = True, ) -> int | NDArray[np.integer]: """Move source-space control points at *index*. :param remove_duplicates: When True (default), collapse coincident control points after the write and return the nearest remaining index. When False, skip uniqueness: coincident points can remain; Delaunay/RBF can later fail or remap rows. Use False only for a bounded interactive sequence (drag, registration apply) where the caller keeps indices stable and remeshes when idle. :return: The index of the edited points after any collapse. """ raise NotImplementedError() @abc.abstractmethod def UpdateSourcePointsByPosition( self, old_points: NDArray[np.floating], new_points: NDArray[np.floating], *, remove_duplicates: bool = True, ) -> int | NDArray[np.integer]: """Move the points closest to *old_points* to positions at *new_points*. :param remove_duplicates: See :meth:`UpdateSourcePointsByIndex`. :return: The index of the edited points after any collapse. """ raise NotImplementedError() class IControlPointEdit(ITargetSpaceControlPointEdit, ISourceSpaceControlPointEdit, ABC): """Control point transforms where source and target control points can be edited""" @abc.abstractmethod def UpdatePointPair(self, index: int, pointpair: NDArray[np.floating]): raise NotImplementedError() @abc.abstractmethod def RemovePoint(self, index: int | NDArray[np.integer]): raise NotImplementedError()
[docs] class IRigidTransform(ITransform, ABC): """A transform that encodes a rigid transformation: The order of operations should be: 1. Scaling 2. Rotation 3. Translation 4. Flip """ @property @abc.abstractmethod def angle(self) -> float: """Angle of rotation in radians""" raise NotImplementedError() @property @abc.abstractmethod def target_offset(self) -> NDArray[np.floating]: """Translation from source to target space""" raise NotImplementedError() @property @abc.abstractmethod def source_space_center_of_rotation(self) -> NDArray[np.floating]: """Center of rotation in source space""" raise NotImplementedError() @property @abc.abstractmethod def flip_ud(self) -> bool: """Whether to flip the Y axis""" raise NotImplementedError() @property @abc.abstractmethod def scalar(self) -> float: """Scaling factor""" raise NotImplementedError()
[docs] @abc.abstractmethod def GetRigidState(self) -> dict[str, Any]: """Snapshot the registration parameters for a later in-place restore. Interactive editors share one model instance across views and mutate it in place, so undoing a gesture needs a parameter snapshot rather than a reference to the model. """ raise NotImplementedError()
[docs] @abc.abstractmethod def SetRigidState(self, state: dict[str, Any]) -> None: """Restore parameters from GetRigidState onto this instance. Must preserve object identity and change subscribers, unlike ``__setstate__``, which is for unpickling a fresh object. """ raise NotImplementedError()
class Base(ITransform, ITransformTranslation, ABC): """Base class of all transforms""" pass