Skip to content
Merged
34 changes: 32 additions & 2 deletions TPTBox/core/internal/elastic_deform.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,43 @@
import time

# pip install elasticdeform
import elasticdeform # See https://github.com/gvtulder/elasticdeform/issues/24 to install this for >2.x
import numpy as np
from numpy.typing import NDArray

from TPTBox import NII


def _elasticdeform_install_hint() -> str:
"""Build an install hint tailored to the currently active NumPy version.

The PyPI ``elasticdeform`` wheel is compiled against NumPy 1.x and fails
to import under NumPy 2.x. The fix is to install from source so the C
extension is rebuilt against whatever NumPy is available in the current
environment. Reporting the detected NumPy version makes it obvious to the
reader why the wheel broke and lets us suggest an install line that
references the concrete environment they are on.

See https://github.com/gvtulder/elasticdeform/issues/24 for context.
"""
numpy_version = np.__version__
numpy_major = int(numpy_version.split(".", 1)[0]) if numpy_version[:1].isdigit() else 0
tarball = "https://github.com/gvtulder/elasticdeform/archive/refs/tags/v0.5.1.tar.gz"
if numpy_major >= 2:
return (
f"elasticdeform could not be imported (NumPy {numpy_version} detected). "
"The published wheel is built against NumPy 1.x and cannot load under "
"NumPy 2.x. Rebuild from source against your current NumPy with:\n"
f" pip install --no-binary :all: --no-build-isolation --force-reinstall --no-deps {tarball}\n"
"See https://github.com/gvtulder/elasticdeform/issues/24 for details."
)
return f"elasticdeform could not be imported (NumPy {numpy_version} detected). Install it with:\n pip install elasticdeform\n"


try:
import elasticdeform # noqa: E402 - kept after the helper so the message can be built
except ImportError as _exc:
raise ImportError(_elasticdeform_install_hint()) from _exc


def deformed_nii(
nii_dic: dict[str, NII],
sigma: float | None = None,
Expand Down
4 changes: 3 additions & 1 deletion TPTBox/core/internal/train_nnUnet/train.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ class Config:
) # default=["2d", "3d_fullres", "3d_lowres","3d_cascade_fullres"],
num_processes: tuple[int] = (4,) # [32] # [8, 4, 8]
verbose = False
nnUNetTrainer: str = "nnUNetTrainer" # noqa: N815

@property
def plans(self) -> str:
Expand Down Expand Up @@ -285,7 +286,7 @@ def _train_fold(self, fold: int | str):
dataset_name_or_id=self.cfg.dataset_folder,
configuration="3d_fullres",
fold=fold,
trainer_class_name="nnUNetTrainer",
trainer_class_name=self.cfg.nnUNetTrainer,
plans_identifier=self.cfg.plans,
num_iterations_per_epoch=self.cfg.num_iterations_per_epoch,
num_epochs=self.cfg.num_epochs,
Expand Down Expand Up @@ -338,6 +339,7 @@ def run(self) -> None:
ds = self._load_dataset_json()

self.cfg.overwrite_target_spacing = ds.get("spacing", self.cfg.overwrite_target_spacing)
self.cfg.nnUNetTrainer = ds.get("nnUNetTrainer", self.cfg.nnUNetTrainer)

self._preprocess()

Expand Down
2 changes: 1 addition & 1 deletion TPTBox/core/poi_fun/poi_global.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ def save(
verbose: Emit a save log message. Defaults to ``True``.
"""
if Path(out_path).name.endswith("mrk.json"):
logging.on_warning("use save_mrk to save .mrk.json files")
log.on_warning("use save_mrk to save .mrk.json files")
return self.save_mrk(out_path)
return save_poi(
self, out_path, make_parents, additional_info, save_hint=save_hint, resample_reference=resample_reference, verbose=verbose
Expand Down
128 changes: 119 additions & 9 deletions TPTBox/registration/__init__.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,139 @@
from __future__ import annotations

from typing import Any

# ---------------------------------------------------------------------------
# Some of the registration entry points require ``hf-deepali`` (and therefore
# also PyTorch). ``hf-deepali`` is an *optional* dependency: importing this
# package must succeed even when it is missing, and only *using* one of the
# deepali-backed classes should surface an error.
#
# For each optional class we try to import it. On failure we replace it with a
# stub that raises a clear ``ImportError`` the first time the caller touches it
# (instantiation *or* attribute access). Users then get "install hf-deepali
# (also needs PyTorch)" instead of a vague ``NameError`` from Python.
# ---------------------------------------------------------------------------


def _make_missing_deepali_stub(name: str, exc: BaseException):
"""Return a class placeholder for a deepali-backed entry point.

Any instantiation or attribute access raises an ``ImportError`` explaining
that ``hf-deepali`` (and PyTorch) must be installed. ``isinstance``/subclass
checks are safe – the stub is a plain class.
"""
original_error = str(exc) or exc.__class__.__name__

class _MissingDeepali:
__name__ = name
__qualname__ = name
_tptbox_optional_dep = "hf-deepali"
_tptbox_import_error = original_error

def __init__(self, *_args: Any, **_kwargs: Any) -> None: # noqa: D401
raise ImportError(
f"`{name}` requires the optional dependency `hf-deepali` "
f"(which in turn requires PyTorch). Install both with:\n"
f" pip install torch hf-deepali\n"
f"Original import error was: {original_error}"
)

def __class_getitem__(cls, item): # keep type-annotations happy
return cls

def __getattr__(self, item):
raise ImportError(
f"`{name}.{item}` requires `hf-deepali` (and PyTorch). "
f"Install with: pip install torch hf-deepali\n"
f"Original import error was: {original_error}"
)

_MissingDeepali.__name__ = name
_MissingDeepali.__qualname__ = name
return _MissingDeepali


def _make_missing_deepali_func(name: str, exc: BaseException):
"""Return a callable stub for a deepali-backed helper function."""
original_error = str(exc) or exc.__class__.__name__

def _stub(*_args: Any, **_kwargs: Any) -> Any:
raise ImportError(
f"`{name}()` requires the optional dependency `hf-deepali` "
f"(which in turn requires PyTorch). Install both with:\n"
f" pip install torch hf-deepali\n"
f"Original import error was: {original_error}"
)

_stub.__name__ = name
_stub.__qualname__ = name
return _stub


# --- SITK point registration (no deepali needed) ---------------------------
try:
from ._ridged_points.point_registration import (
Point_Registration,
ridged_points_from_poi,
ridged_points_from_subreg_vert,
)
except ImportError as _e_sitk: # SimpleITK missing - very unlikely, still guard.
Point_Registration = _make_missing_deepali_stub("Point_Registration", _e_sitk) # type: ignore[misc,assignment]
ridged_points_from_poi = _make_missing_deepali_func("ridged_points_from_poi", _e_sitk) # type: ignore[assignment]
ridged_points_from_subreg_vert = _make_missing_deepali_func("ridged_points_from_subreg_vert", _e_sitk) # type: ignore[assignment]

# --- Deepali closed-form point registration --------------------------------
try:
from ._ridged_points.point_registration import Point_Registration, ridged_points_from_poi, ridged_points_from_subreg_vert
from ._ridged_points.deepali_point_registration import (
Deepali_Point_Registration,
ridged_points_from_poi_deepali,
ridged_points_from_subreg_vert_deepali,
)
except ImportError as _e_deep_pt:
Deepali_Point_Registration = _make_missing_deepali_stub("Deepali_Point_Registration", _e_deep_pt) # type: ignore[misc,assignment]
ridged_points_from_poi_deepali = _make_missing_deepali_func("ridged_points_from_poi_deepali", _e_deep_pt) # type: ignore[assignment]
ridged_points_from_subreg_vert_deepali = _make_missing_deepali_func( # type: ignore[assignment]
"ridged_points_from_subreg_vert_deepali", _e_deep_pt
)

# --- General deepali image registration ------------------------------------
try:
from ._deepali.deepali_model import General_Registration
except ImportError as _e_gen:
General_Registration = _make_missing_deepali_stub("General_Registration", _e_gen) # type: ignore[misc,assignment]

except ImportError:
pass
try:
from TPTBox.registration._deformable.deformable_reg import Deformable_Registration
from TPTBox.registration._deformable.multilabel_segmentation import Template_Registration
except ImportError as _e_def:
Deformable_Registration = _make_missing_deepali_stub("Deformable_Registration", _e_def) # type: ignore[misc,assignment]

try:
from ._deepali.spine_rigid_elements_reg import Rigid_Elements_Registration
except ImportError:
pass
except ImportError as _e_rer:
Rigid_Elements_Registration = _make_missing_deepali_stub("Rigid_Elements_Registration", _e_rer) # type: ignore[misc,assignment]

# --- Template registrations (deformable + optional pre-reg) ----------------
try:
from ._deepali.deepali_model import General_Registration
from TPTBox.registration._deformable.multilabel_segmentation import Template_Registration
except ImportError as _e_tpl:
Template_Registration = _make_missing_deepali_stub("Template_Registration", _e_tpl) # type: ignore[misc,assignment]

try:
from ._deformable.multilabel_segmentation import Template_Registration2
except ImportError as _e_tpl2:
Template_Registration2 = _make_missing_deepali_stub("Template_Registration2", _e_tpl2) # type: ignore[misc,assignment]


except ImportError:
pass
__all__ = [
"Deepali_Point_Registration",
"Deformable_Registration",
"General_Registration",
"Point_Registration",
"Rigid_Elements_Registration",
"Template_Registration",
"Template_Registration2",
"ridged_points_from_poi",
"ridged_points_from_poi_deepali",
"ridged_points_from_subreg_vert",
"ridged_points_from_subreg_vert_deepali",
]
Loading
Loading