"""Base classes and data structures for the adapter system.
This module contains the core abstractions that all adapters share:
- SliceInfo: Standardized slice information
- RegistrationData: Complete registration data container
- Abstract base classes for Anchoring, Deformation, and Damage providers
"""
from __future__ import annotations
import math
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Tuple
import numpy as np
# =============================================================================
# Type Aliases
# =============================================================================
# Takes (x_coords, y_coords) arrays and returns (x_warped, y_warped)
DeformationFunction = Callable[[np.ndarray, np.ndarray], Tuple[np.ndarray, np.ndarray]]
def calculate_physical_dimensions(anchoring: List[float]) -> Tuple[int, int]:
"""Return physical plane dimensions (width, height) from an anchoring vector.
The +1 ensures the integer dimension fully covers the continuous extent of
the plane (i.e. ceil-like rounding), which is needed for image loading and
mask creation.
"""
u = anchoring[3:6]
v = anchoring[6:9]
return int(math.hypot(*u)) + 1, int(math.hypot(*v)) + 1
# =============================================================================
# Data Classes
# =============================================================================
@dataclass
class SliceInfo:
"""Standardized slice information from any registration source.
This is the common data structure that all components produce and consume.
Components can add or modify fields as data flows through the pipeline.
"""
# Section identifier (filename or number)
section_id: str
section_number: int
# Registration dimensions (in registration space)
width: int
height: int
# Anchoring: 9 values defining the 3D plane in atlas space
# [ox, oy, oz, ux, uy, uz, vx, vy, vz]
# o = origin, u = horizontal vector, v = vertical vector
anchoring: List[float]
# Non-linear deformation function (optional)
# Takes (x, y) coordinate arrays and returns warped (x', y') arrays
# This is the INVERSE direction: maps from deformed space back to original
# Used by quantification (transform_vec)
deformation: Optional[DeformationFunction] = None
# Forward deformation function (optional)
# Maps from original space to deformed space
# Used by volume projection (forwardtransform_vec)
forward_deformation: Optional[DeformationFunction] = None
# Damage/exclusion mask (optional)
# 2D boolean array where True = damaged/excluded
damage_mask: Optional[np.ndarray] = None
# Additional metadata from various sources
metadata: Dict[str, Any] = field(default_factory=dict)
@property
def physical_dimensions(self) -> Tuple[int, int]:
"""Physical plane dimensions (width, height) derived from anchoring vectors.
The +1 ensures the integer dimension fully covers the continuous extent of the
plane (i.e. ceil-like rounding), which is needed for image loading and mask
creation. For continuous-valued computations such as volume sampling, use the
raw anchoring vector norms directly.
"""
return calculate_physical_dimensions(self.anchoring)
[docs]
@dataclass
class RegistrationData:
"""Complete registration data from an alignment file."""
slices: List[SliceInfo]
grid_spacing: Optional[int] = None
metadata: Dict[str, Any] = field(default_factory=dict)
# =============================================================================
# Abstract Base Classes
# =============================================================================
class AnchoringLoader(ABC):
"""Loads basic slice info and linear registration (anchoring).
This is the foundation - it provides section identification,
dimensions, and the linear transformation to atlas space.
Examples: QuickNII, DeepSlice, ABBA
"""
name: str = "base"
file_extensions: List[str] = []
@abstractmethod
def load(self, path: str) -> RegistrationData:
"""Load registration data from a file.
Returns RegistrationData with slices containing:
- section_id, section_number
- width, height
- anchoring
Deformation and damage_mask should be None (added by providers).
"""
pass
@classmethod
def can_handle(cls, path: str) -> bool:
"""Check if this loader can handle the given file."""
ext = Path(path).suffix.lower()
return ext in cls.file_extensions
class DeformationProvider(ABC):
"""Adds non-linear deformation to slice info.
Takes RegistrationData and adds deformation functions to each slice.
Examples: VisuAlign markers, displacement fields, B-spline transforms
"""
name: str = "base"
@abstractmethod
def apply(self, data: RegistrationData) -> RegistrationData:
"""Add deformation functions to all slices.
Args:
data: RegistrationData with basic anchoring.
Returns:
RegistrationData with deformation functions added.
"""
pass
class DamageProvider(ABC):
"""Adds damage/exclusion masks to slice info.
Takes RegistrationData and adds damage masks to each slice.
Examples: QCAlign grid, manual ROI masks, tissue detection
"""
name: str = "base"
@abstractmethod
def apply(self, data: RegistrationData) -> RegistrationData:
"""Add damage masks to all slices.
Args:
data: RegistrationData (may already have deformation).
Returns:
RegistrationData with damage masks added.
"""
pass