Source code for sigima.proc.image.transformations
# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
"""
Geometry transformations module
===============================
This module provides a unified interface for applying geometric transformations
to both geometry results (:class:`sigima.objects.GeometryResult`) and ROI objects
using the shape coordinate system (:mod:`sigima.objects.shape`).
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
import numpy as np
from sigima.objects.scalar import GeometryResult, KindShape
from sigima.objects.shape import (
CircleCoordinates,
EllipseCoordinates,
PointCoordinates,
PolygonCoordinates,
RectangleCoordinates,
SegmentCoordinates,
)
if TYPE_CHECKING:
from sigima.objects import CircularROI, ImageObj, PolygonalROI, RectangularROI
__all__ = [
"GeometryTransformer",
"transformer",
]
[docs]
class GeometryTransformer:
"""
Singleton class for applying transformations to geometry objects.
Provides a unified interface for transforming both GeometryResult and ROI
objects using the shape coordinate system.
"""
_instance: GeometryTransformer | None = None
def __new__(cls) -> GeometryTransformer:
"""Ensure singleton pattern."""
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self) -> None:
"""Initialize the transformer (only once due to singleton)."""
if self._initialized: # pylint: disable=access-member-before-definition
return
# Mapping from GeometryResult kinds to shape coordinate classes
self._geometry_shape_map: dict[
KindShape,
type[
RectangleCoordinates
| CircleCoordinates
| PolygonCoordinates
| SegmentCoordinates
],
] = {
KindShape.POINT: PointCoordinates,
KindShape.RECTANGLE: RectangleCoordinates,
KindShape.CIRCLE: CircleCoordinates,
KindShape.ELLIPSE: EllipseCoordinates,
KindShape.POLYGON: PolygonCoordinates,
KindShape.SEGMENT: SegmentCoordinates,
KindShape.MARKER: PointCoordinates,
}
# Mapping from ROI types to shape coordinate classes (lazy loaded)
self._roi_shape_map: dict[type, type] = {}
self._initialized = True
def _get_roi_shape_map(
self,
) -> dict[
type[CircularROI | RectangularROI | PolygonalROI],
type[RectangleCoordinates | CircleCoordinates | PolygonCoordinates],
]:
"""Lazy load ROI shape mapping to avoid circular imports."""
if not self._roi_shape_map:
# pylint: disable=import-outside-toplevel
from sigima.objects.image import CircularROI, PolygonalROI, RectangularROI
self._roi_shape_map = {
RectangularROI: RectangleCoordinates,
CircularROI: CircleCoordinates,
PolygonalROI: PolygonCoordinates,
}
return self._roi_shape_map
[docs]
def transform_geometry(
self, geometry: GeometryResult, operation: str, **kwargs: Any
) -> GeometryResult:
"""
Transform a GeometryResult and return a new one.
Args:
geometry: The GeometryResult to transform.
operation: Operation name ('rotate', 'translate', 'fliph', 'flipv',
'transpose', 'scale').
**kwargs: Operation-specific parameters.
Returns:
New GeometryResult with transformed coordinates.
Raises:
ValueError: If operation is unknown or geometry kind is unsupported.
"""
coord_class = self._geometry_shape_map.get(geometry.kind)
if coord_class is None:
raise ValueError(f"Unsupported geometry kind: {geometry.kind}")
# Transform each row of coordinates
transformed_coords = []
for row in geometry.coords:
# Create coordinate object for this row
shape_coords = coord_class(row.copy())
# Apply transformation
self._apply_operation(shape_coords, operation, **kwargs)
transformed_coords.append(shape_coords.data)
# Create new GeometryResult with transformed coordinates
return GeometryResult(
title=geometry.title,
kind=geometry.kind,
coords=np.array(transformed_coords),
roi_indices=(
geometry.roi_indices.copy()
if geometry.roi_indices is not None
else None
),
attrs=geometry.attrs.copy(),
func_name=geometry.func_name,
)
[docs]
def transform_single_roi(
self,
single_roi: RectangularROI | CircularROI | PolygonalROI,
operation: str,
**kwargs: Any,
) -> None:
"""
Transform ROI coordinates inplace.
Args:
single_roi: ROI object with .coords attribute.
operation: Operation name.
**kwargs: Operation-specific parameters.
Raises:
ValueError: If ROI type is unsupported or operation is unknown.
"""
roi_shape_map = self._get_roi_shape_map()
coord_class = roi_shape_map.get(type(single_roi))
if coord_class is None:
raise ValueError(f"Unsupported ROI type: {type(single_roi)}")
# Create shape coordinates and transform
shape_coords = coord_class(single_roi.coords.copy())
self._apply_operation(shape_coords, operation, **kwargs)
# Update ROI coordinates inplace
single_roi.coords[:] = shape_coords.data
[docs]
def transform_roi(self, image: ImageObj, operation: str, **kwargs: Any) -> None:
"""
Transform all ROI coordinates in an ImageObj inplace.
Args:
image: Image object whose ROI coordinates will be transformed.
operation: Operation name.
**kwargs: Operation-specific parameters.
"""
if image.roi is None or image.roi.is_empty():
return
# Import here to avoid circular imports
# pylint: disable=import-outside-toplevel
from sigima.objects.image import ImageROI
# Determine ROI type and set up appropriate classes
new_roi = ImageROI()
# Transform each single ROI
for single_roi in image.roi.single_rois:
coords = single_roi.coords.copy()
roi_class = single_roi.__class__
# Create shape coordinates and transform
roi_shape_map = self._get_roi_shape_map()
coord_class = roi_shape_map.get(roi_class)
if coord_class is None:
raise ValueError(f"Unsupported ROI type: {roi_class}")
shape_coords = coord_class(coords)
self._apply_operation(shape_coords, operation, **kwargs)
new_coords = shape_coords.data
new_single_roi = roi_class(new_coords, single_roi.indices, single_roi.title)
new_roi.add_roi(new_single_roi)
image.roi = new_roi
def _apply_operation(
self,
shape_coords: (
PointCoordinates
| RectangleCoordinates
| CircleCoordinates
| EllipseCoordinates
| PolygonCoordinates
| SegmentCoordinates
),
operation: str,
**kwargs: Any,
) -> None:
"""
Apply the specified operation to shape coordinates.
Args:
shape_coords: Shape coordinate object to transform.
operation: Operation name.
**kwargs: Operation-specific parameters.
Raises:
ValueError: If operation is unknown.
"""
if operation == "rotate":
angle = kwargs.get("angle", 0)
center = kwargs.get("center", (0, 0))
shape_coords.rotate(angle, center)
elif operation == "translate":
dx = kwargs.get("dx", 0)
dy = kwargs.get("dy", 0)
shape_coords.translate(dx, dy)
elif operation == "fliph":
cx = kwargs.get("cx", 0.0)
shape_coords.fliph(cx)
elif operation == "flipv":
cy = kwargs.get("cy", 0.0)
shape_coords.flipv(cy)
elif operation == "transpose":
shape_coords.transpose()
elif operation == "scale":
sx = kwargs.get("sx", 1.0)
sy = kwargs.get("sy", 1.0)
center = kwargs.get("center", (0, 0))
shape_coords.scale(sx, sy, center)
else:
raise ValueError(f"Unknown operation: {operation}")
# Convenience methods for common operations
[docs]
def rotate(
self,
obj: GeometryResult | RectangularROI | CircularROI | PolygonalROI,
angle: float,
center: tuple[float, float],
) -> GeometryResult | None:
"""
Rotate geometry or ROI by given angle around center.
Args:
obj: GeometryResult or single ROI object.
angle: Rotation angle in radians.
center: Center of rotation (x, y).
Returns:
New GeometryResult if input was GeometryResult, None if ROI (inplace).
"""
if isinstance(obj, GeometryResult):
return self.transform_geometry(obj, "rotate", angle=angle, center=center)
self.transform_single_roi(obj, "rotate", angle=angle, center=center)
return None
[docs]
def translate(
self,
obj: GeometryResult | RectangularROI | CircularROI | PolygonalROI,
dx: float,
dy: float,
) -> GeometryResult | None:
"""
Translate geometry or ROI by given offset.
Args:
obj: GeometryResult or single ROI object.
dx: Translation in x direction.
dy: Translation in y direction.
Returns:
New GeometryResult if input was GeometryResult, None if ROI (inplace).
"""
if isinstance(obj, GeometryResult):
return self.transform_geometry(obj, "translate", dx=dx, dy=dy)
self.transform_single_roi(obj, "translate", dx=dx, dy=dy)
return None
[docs]
def fliph(
self,
obj: GeometryResult | RectangularROI | CircularROI | PolygonalROI,
cx: float,
) -> GeometryResult | None:
"""
Flip geometry or ROI horizontally around given x-coordinate.
Args:
obj: GeometryResult or single ROI object.
cx: X-coordinate of flip axis.
Returns:
New GeometryResult if input was GeometryResult, None if ROI (inplace).
"""
if isinstance(obj, GeometryResult):
return self.transform_geometry(obj, "fliph", cx=cx)
self.transform_single_roi(obj, "fliph", cx=cx)
return None
[docs]
def flipv(
self,
obj: GeometryResult | RectangularROI | CircularROI | PolygonalROI,
cy: float,
) -> GeometryResult | None:
"""
Flip geometry or ROI vertically around given y-coordinate.
Args:
obj: GeometryResult or single ROI object.
cy: Y-coordinate of flip axis.
Returns:
New GeometryResult if input was GeometryResult, None if ROI (inplace).
"""
if isinstance(obj, GeometryResult):
return self.transform_geometry(obj, "flipv", cy=cy)
self.transform_single_roi(obj, "flipv", cy=cy)
return None
[docs]
def transpose(
self, obj: GeometryResult | RectangularROI | CircularROI | PolygonalROI
) -> GeometryResult | None:
"""
Transpose geometry or ROI (swap x and y coordinates).
Args:
obj: GeometryResult or single ROI object.
Returns:
New GeometryResult if input was GeometryResult, None if ROI (inplace).
"""
if isinstance(obj, GeometryResult):
return self.transform_geometry(obj, "transpose")
self.transform_single_roi(obj, "transpose")
return None
[docs]
def scale(
self,
obj: GeometryResult | RectangularROI | CircularROI | PolygonalROI,
sx: float,
sy: float,
center: tuple[float, float],
) -> GeometryResult | None:
"""
Scale geometry or ROI by given factors around center.
Args:
obj: GeometryResult or single ROI object.
sx: Scale factor in x direction.
sy: Scale factor in y direction.
center: Center of scaling (x, y).
Returns:
New GeometryResult if input was GeometryResult, None if ROI (inplace).
"""
if isinstance(obj, GeometryResult):
return self.transform_geometry(obj, "scale", sx=sx, sy=sy, center=center)
self.transform_single_roi(obj, "scale", sx=sx, sy=sy, center=center)
return None
#: Global singleton instance of GeometryTransformer for applying geometric
#: transformations to geometry results and ROI objects.
transformer = GeometryTransformer()