Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,18 @@ spineps sample -i scan.nii.gz --model-semantic t2w --model-instance instance --n
| `Segmentation_Model_NNunet` | `SegmentationModelNNunet` |
| `Segmentation_Model_Unet3D` | `SegmentationModelUnet3D` |

### Removed

| Removed | Why / what to do instead |
| --- | --- |
| `--model-semantic auto` (CLI) and `process_dataset(model_semantic=None)` | The auto-selection it depended on (`spineps.seg_utils.find_best_matching_model`) was never implemented and always raised `NotImplementedError`. Name a model explicitly. |
| `spineps.seg_utils.find_best_matching_model` | See above. |
| `spineps.utils.image` (vendored spinalcordtoolbox `Image`) and `spineps.utils.generate_disc_labels` | Standalone disc-label export, wired to no entry point. Derive disc labels from the vertebra mask with `TPTBox` instead. |
| `spineps.architectures_new.unet2D` | The instance model is 3D only; `PLNet(do2D=True)` now raises. |
| `spineps.example` scripts | Never shipped in the wheel; see the README for usage examples. |
| `spineps.seg_pipeline.pipeline_revision` | Centroid metadata no longer records a git revision (see below). |
| `SPINEPS_TURN_OF_CITATION_REMINDER` | Renamed to `SPINEPS_NO_CITATION_REMINDER` (and it now actually works). |

`process_dataset`, `get_semantic_model`, `get_instance_model`, `get_labeling_model`, `predict_semantic_mask`,
`predict_instance_mask` and the other phase functions keep their names.

Expand All @@ -95,3 +107,45 @@ spineps sample -i scan.nii.gz --model-semantic t2w --model-instance instance --n
(semantic sliding-window step), and `--tta` / `--no-tta` (toggle test-time mirroring; `SegmentationModel.set_test_time_augmentation(...)` in Python).
- Clearer errors: invalid paths / missing models now raise `FileNotFoundError` / `ValueError` instead of bare
`AssertionError`, and `spineps sample -h` / `dataset -h` no longer crash.

## Fixed in 2.0 — output may change

These are bug fixes, so results can differ from 1.x. Each was wrong before.

- **Endplate labels reach the semantic mask.** The superior/inferior split was computed and then thrown away
by a binarising `extract_label`, so the `msk` output carried endplate voxels labelled `1` instead of
`Vertebral_Body_Endplate_Superior` (52) / `_Inferior` (53). If you worked around this, remove the workaround.
- **The semantic bounding-box clean keeps what it incorporates.** It grew a region to take in nearby connected
components and then cropped to the *largest* component's box anyway, deleting the rest. Spines split across
components (gaps, implants) keep more of the mask now.
- **No phantom disc.** `detect_and_solve_merged_vertebra` offset every voxel including the background, adding a
volume-sized fake IVD to the height-sorted list the split-C2 heuristic reads.
- **Small connected-component cleaning actually runs.** With `only_delete=False`, the neighbourhood mask was
destroyed before it was used, so nothing was ever deleted or relabelled despite the log saying otherwise.
- **Merged-corpus splitting.** `get_separating_components` returned its two parts *after* dilating them in
place, so they overlapped and the separating plane was derived from smeared centers of mass.
- **Incompatible models stop the run.** `process_dataset` logged "stop program" and then carried on; it now
raises `ValueError` unless `ignore_model_compatibility=True` / `--ignore-model-compatibility`.
- **Non-overlapping instance partners.** The couple search detected two partners that agree with the anchor but
not with each other, logged that it was skipping them, and used both anyway.
- **Version metadata.** `ctd.info["version"]` is the installed package version. It used to shell out to `git`
with no working directory, so it recorded whatever repository you happened to be standing in (or
`"Version not found"`), and `ctd.info["revision"]` is gone.
- **Labeling no longer crashes** on an empty instance mask, or with `disable_c1=False` and no subregion mask.
- **`import spineps` no longer creates a directory** inside the installed package; the fallback models folder is
created on demand.

## Faster and smaller

No output change -- pinned by voxel-identical regression tests.

- The instance phase keeps each cutout prediction where it lives instead of in a dense
`(n_vertebrae, 3, *volume)` array, and compares candidates on their overlapping region only.
- The endplate splitter runs per vertebra on numpy arrays and grows each dilation by one voxel per round
instead of re-dilating from scratch.
- `clean_cc_artifacts` builds connected components one label at a time and works inside each component's
bounding box.
- The input volume is read from disk once per image instead of up to three times.

On a 24-vertebra, 8.1M-voxel whole-spine volume: instance phase 2.30s -> 1.00s (peak RSS +313MB -> +157MB),
combined post-processing 3.34s -> 0.59s.
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,9 @@ spineps sample -i <path-to-nifty> --model-semantic <model_name> --model-instance
(replacing `<model_name>` with the model you want to use). You can also call SPINEPS from Python — see
[Using the Code](#using-the-code).

SPINEPS prints a short citation reminder on first use and at exit. Set `SPINEPS_NO_CITATION_REMINDER=1`
(or `true`/`yes`/`on`) to silence it.

### Issues

- import issues: try installing via the requirements again, somethings it doesn't install everything
Expand Down Expand Up @@ -276,7 +279,7 @@ To that end, we are using TPTBox (see https://github.com/Hendrik-code/TPTBox)
| argument | explanation |
| :--- | --------- |
| --directory, -i, -d | Absolute path to the dataset directory, preferably a BIDS dataset (required) |
| --model-semantic, -ms | The model used for the semantic segmentation, or `auto` to select automatically by modality (default: t2w) |
| --model-semantic, -ms | The model used for the semantic segmentation (default: t2w) |
| --model-instance, -mv, -mi | The model used for the vertebra instance segmentation (default: instance) |
| --model-labeling, -ml | The (optional) VERIDAH model used for vertebra labeling (default: t2w_labeling) |
| --rawdata-name, -rn | Sets the name of the rawdata folder of the dataset (default: "rawdata")
Expand Down Expand Up @@ -320,9 +323,11 @@ In the subregion segmentation:
| 47 | Inferior_Articular_Left |
| 48 | Inferior_Articular_Right |
| 49 | Vertebra_Corpus_border |
| 52 | Vertebral_Body_Endplate_Superior |
| 53 | Vertebral_Body_Endplate_Inferior |
| 60 | Spinal_Cord |
| 61 | Spinal_Canal |
| 62 | Endplate |
| 62 | Endplate (only where the plate could not be assigned to a vertebra) |
| 100 | Vertebra_Disc |
| 26 | Sacrum |

Expand Down
6 changes: 1 addition & 5 deletions docs/api/architectures.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,14 @@ Network architectures and the vertebra label definitions used by the models.

## spineps.architectures_new.pl_unet

The newer PyTorch Lightning U-Net wrapper (2D or 3D), used by [`spineps.seg_model`](models.md).
The newer PyTorch Lightning U-Net wrapper, used by [`spineps.seg_model`](models.md).

::: spineps.architectures_new.pl_unet

## spineps.architectures_new.unet3D

::: spineps.architectures_new.unet3D

## spineps.architectures_new.unet2D

::: spineps.architectures_new.unet2D

## spineps.architectures_new.dice

::: spineps.architectures_new.dice
6 changes: 1 addition & 5 deletions docs/api/utils.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Utilities

Image processing, the vertebra-labeling path solver, disc labeling and other helpers.
Image processing, the vertebra-labeling path solver and other helpers.

## spineps.utils.resolution

Expand All @@ -17,10 +17,6 @@ behaviour is consistent across MRI and CT resolutions.

::: spineps.utils.find_min_cost_path

## spineps.utils.generate_disc_labels

::: spineps.utils.generate_disc_labels

## spineps.utils.filepaths

::: spineps.utils.filepaths
Expand Down
1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@ exclude = [
"dist",
"node_modules",
"venv",
"spineps/utils/image.py", # vendored from spinalcordtoolbox
".toml",
]
line-length = 140
Expand Down
24 changes: 14 additions & 10 deletions spineps/architectures_new/pl_unet.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
from torch.optim import Adam, lr_scheduler

from .dice import MemoryEfficientSoftDiceLoss
from .unet2D import Unet2D
from .unet3D import Unet3D


Expand Down Expand Up @@ -42,36 +41,41 @@ def _tb_logger(module: pl.LightningModule) -> TensorBoardLogger:


class PLNet(pl.LightningModule):
"""LightningModule training a 2D or 3D U-Net with a combined cross-entropy, Dice and L2 loss.
"""LightningModule training a 3D U-Net with a combined cross-entropy, Dice and L2 loss.

Wraps :class:`Unet2D` or :class:`Unet3D` and handles the training/validation loops, loss computation,
Dice metric logging and optimizer configuration.
Wraps :class:`Unet3D` and handles the training/validation loops, loss computation, Dice metric
logging and optimizer configuration.
"""

def __init__(self, opt: Namespace | None = None, do2D: bool = False, num_channels=11, dim=8, *args: Any, **kwargs: Any) -> None: # ruff: ignore[unused-method-argument]
# The previous suppression comment here used a directive form ruff does not recognise, so it never
# silenced anything; ARG002 needs a real noqa.
def __init__(self, opt: Namespace | None = None, do2D: bool = False, num_channels=11, dim=8, *args: Any, **kwargs: Any) -> None: # noqa: ARG002
"""Build the network and configure losses, metrics and training hyperparameters.

Args:
opt (Namespace): Configuration namespace providing ``channelwise``, ``n_epoch``, ``lr``,
``lr_end_factor``, ``l2_reg_w`` and ``dsc_loss_w``.
do2D (bool): If ``True``, use the 2D U-Net; otherwise the 3D U-Net.
do2D (bool): Kept so 2.x checkpoints still restore their saved hyperparameters. Only 3D is
supported; passing ``True`` raises.
*args (Any): Unused positional arguments.
**kwargs (Any): Unused keyword arguments.
"""
if opt is None:
opt = Namespace(**kwargs)

if do2D:
raise NotImplementedError("PLNet only supports the 3D U-Net; the 2D variant was removed in SPINEPS 2.0")

super().__init__()
self.save_hyperparameters()
arch = Unet2D if do2D else Unet3D
self.network = arch(
self.network = Unet3D(
dim=dim,
dim_mults=(1, 2, 4, 8),
out_dim=4,
channels=1 if not opt.channelwise else num_channels,
)

self.do2D = do2D
self.do2D = False
self.n_epoch = opt.n_epoch
self.start_lr = opt.lr
self.linear_end_factor = opt.lr_end_factor
Expand Down Expand Up @@ -314,4 +318,4 @@ def __str__(self):
Returns:
str: ``"Unet_2D"`` or ``"Unet_3D"``.
"""
return f"Unet_{'2D' if self.do2D else '3D'}"
return "Unet_3D"
Loading
Loading