diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 38c7294..693b2ad 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -19,10 +19,10 @@ jobs: runs-on: ${{ matrix.operating-system }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} diff --git a/LAST_CHANGELOG.md b/LAST_CHANGELOG.md index 6fad604..9162463 100644 --- a/LAST_CHANGELOG.md +++ b/LAST_CHANGELOG.md @@ -2,12 +2,8 @@ ## Software -* Release of the HSF finetuning pipeline, -* Bug fixes and optimizations, -* Updated dependencies. +* Option to override already segmented mris. ## Models -* Models trained on hippocampal subfields from Clark et al. (2023) dataset (https://doi.org/10.1038/s41597-023-02449-9), -* Models are now hosted on HuggingFace, -* Bug fixes and optimizations. +* No changelog. diff --git a/README.rst b/README.rst index 8f1e7e9..4d68e7d 100644 --- a/README.rst +++ b/README.rst @@ -220,6 +220,10 @@ Changelogs HSF --- +**Version 1.2.1** + +* Option to override already segmented mris. + **Version 1.2.0** * Released finetuning scripts, diff --git a/docs/about/release-notes.md b/docs/about/release-notes.md index cd412f9..5ca30e5 100644 --- a/docs/about/release-notes.md +++ b/docs/about/release-notes.md @@ -16,6 +16,10 @@ Current maintainers: ## HSF +### Version 1.2.1 (2024-03-29) + +* Added an option to override already segmented mris. + ### Version 1.2.0 (2024-02-06) * Released finetuning scripts, diff --git a/docs/user-guide/configuration.md b/docs/user-guide/configuration.md index 96153bf..3b0e5bc 100644 --- a/docs/user-guide/configuration.md +++ b/docs/user-guide/configuration.md @@ -55,6 +55,7 @@ I/O are managed through the `files.*` arguments. Default parameters are defined - `files.path` and `files.pattern` are mandatory arguments and respectively define where to search for MRIs, and how to find them through a `glob()` pattern. - `files.mask_pattern` defines how to find brain extraction masks for registration purposes (see [ROILoc documentation](user-guide/roiloc.md)). - `files.output_dir` defines where to store temporary files in a relative subject directory. +- `files.overwrite` defines whether to overwrite existing segmentations. The following example will recursively search all `*T2w.nii.gz` files in the `~Datasets/MRI/` folder, for search a `*T2w_bet_mask.nii.gz` located next to each T2w images: diff --git a/hsf/__init__.py b/hsf/__init__.py index 58d478a..3f262a6 100644 --- a/hsf/__init__.py +++ b/hsf/__init__.py @@ -1 +1 @@ -__version__ = '1.2.0' +__version__ = '1.2.1' diff --git a/hsf/conf/files/default.yaml b/hsf/conf/files/default.yaml index a6f29e5..c1fcce8 100644 --- a/hsf/conf/files/default.yaml +++ b/hsf/conf/files/default.yaml @@ -2,3 +2,4 @@ path: ??? pattern: ??? mask_pattern: "*mask.nii.gz" output_dir: "hsf_outputs" +overwrite: false diff --git a/hsf/factory.py b/hsf/factory.py index 8e008a5..f906943 100644 --- a/hsf/factory.py +++ b/hsf/factory.py @@ -1,6 +1,6 @@ import logging from pathlib import Path, PosixPath -from typing import Generator, Optional +from typing import Generator, List, Optional import ants import hydra @@ -144,6 +144,45 @@ def save(mri: PosixPath, hippocampus: PosixPath, hard_pred: torch.Tensor, log.info(f"Saved segmentation in native space to {str(output_path)}") +def filter_mris(mris: List[PosixPath], overwrite: bool) -> List[PosixPath]: + """ + Filter mris. + + Args: + mris (List[PosixPath]): List of MRI paths. + overwrite (bool): Overwrite existing segmentations. + + Returns: + List[PosixPath]: List of filtered MRI paths. + """ + + def _get_segmentations(mri: PosixPath) -> List[PosixPath]: + extensions = "".join(mri.suffixes) + stem = mri.name.replace(extensions, "") + segmentations = list(mri.parent.glob(f"{stem}*_hippocampus_seg.nii.gz")) + + if len(segmentations) > 2: + log.warning( + f"Found {len(segmentations)} segmentations for {mri}. " + "As HSF produces only two files per MRI, this might indicate a misconfiguration." + ) + + if len(segmentations) > 0: + log.info( + f"Found {len(segmentations)} segmentations for {mri}. " + "Skipping segmentation. If you want to overwrite, set overwrite=True." + ) + + return segmentations + + mris = [mri for mri in mris if not mri.name.endswith("_seg.nii.gz")] + if overwrite: + return mris + + mris = [mri for mri in mris if len(_get_segmentations(mri)) == 0] + return mris + + @hydra.main(config_path="conf", config_name="config", version_base="1.1") def main(cfg: DictConfig) -> None: fetch_models(cfg.segmentation.models_path, cfg.segmentation.models) @@ -158,6 +197,13 @@ def main(cfg: DictConfig) -> None: ) % bs == 0, "test_time_num_aug+1 must be a multiple of batch_size for deepsparse" mris = load_from_config(cfg.files.path, cfg.files.pattern) + _n = len(mris) + + mris = filter_mris(mris, cfg.files.overwrite) + + if len(mris) == 0 and _n > 0: + log.info("No new MRI found. Skipping segmentation.") + return log.warning( "Please be aware that the segmentation is highly dependant on ROILoc to locate both hippocampi.\nROILoc will be run using the following configuration.\nIf the segmentation is of bad quality, please tune your ROILoc settings (e.g. ``margin``)." diff --git a/pyproject.toml b/pyproject.toml index cb6ab6f..1e3d8fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "HSF" -version = "1.2.0" +version = "1.2.1" description = "A simple yet exhaustive segmentation tool of the Hippocampal Subfields in T1w and T2w MRIs." authors = ["Clément POIRET "] license = "MIT" diff --git a/tests/mri/tse.nii.gz b/tests/mri/sub0_tse.nii.gz similarity index 100% rename from tests/mri/tse.nii.gz rename to tests/mri/sub0_tse.nii.gz diff --git a/tests/mri/sub1_tse.nii.gz b/tests/mri/sub1_tse.nii.gz new file mode 100755 index 0000000..8961e7c Binary files /dev/null and b/tests/mri/sub1_tse.nii.gz differ diff --git a/tests/mri/sub1_tse_left_hippocampus_seg.nii.gz b/tests/mri/sub1_tse_left_hippocampus_seg.nii.gz new file mode 100755 index 0000000..8961e7c Binary files /dev/null and b/tests/mri/sub1_tse_left_hippocampus_seg.nii.gz differ diff --git a/tests/mri/sub1_tse_right_hippocampus_seg.nii.gz b/tests/mri/sub1_tse_right_hippocampus_seg.nii.gz new file mode 100755 index 0000000..8961e7c Binary files /dev/null and b/tests/mri/sub1_tse_right_hippocampus_seg.nii.gz differ diff --git a/tests/test_hsf.py b/tests/test_hsf.py index 1a7d853..c9713d8 100644 --- a/tests/test_hsf.py +++ b/tests/test_hsf.py @@ -2,6 +2,10 @@ from pathlib import Path import ants +import pytest +import torch +from omegaconf import DictConfig + import hsf.engines import hsf.factory import hsf.fetch_models @@ -9,14 +13,11 @@ import hsf.roiloc_wrapper import hsf.segment import hsf.uncertainty -import pytest -import torch from hsf import __version__ -from omegaconf import DictConfig def test_version(): - assert __version__ == '1.2.0' + assert __version__ == '1.2.1' # SETUP FIXTURES @@ -30,7 +31,7 @@ def models_path(tmpdir_factory): tmpdir_path = Path(tmpdir_path) # Copy sample mri - shutil.copy("tests/mri/tse.nii.gz", tmpdir_path / "tse.nii.gz") + shutil.copy("tests/mri/sub0_tse.nii.gz", tmpdir_path / "sub0_tse.nii.gz") shutil.copy("tests/mri/mask.nii.gz", tmpdir_path / "mask.nii.gz") # Download model @@ -47,9 +48,10 @@ def config(models_path): configuration = { "files": { "path": str(models_path), - "pattern": "tse.nii.gz", + "pattern": "sub*_tse.nii.gz", "mask_pattern": None, "output_dir": "hsf_outputs", + "overwrite": False }, "hardware": { "engine": "onnxruntime", @@ -140,7 +142,7 @@ def test_main_compute_uncertainty(models_path): soft_pred = torch.randn(5, 6, 448, 30, 448) soft_pred = torch.softmax(soft_pred, dim=1) - hsf.factory.compute_uncertainty(models_path / "tse.nii.gz", soft_pred) + hsf.factory.compute_uncertainty(models_path / "sub0_tse.nii.gz", soft_pred) # fetch_models @@ -160,7 +162,7 @@ def test_fetch_models(models_path, config): # # ROILoc def test_roiloc(models_path): """Tests that we can locate and save hippocampi.""" - mris = hsf.roiloc_wrapper.load_from_config(models_path, "tse.nii.gz") + mris = hsf.roiloc_wrapper.load_from_config(models_path, "sub0_tse.nii.gz") assert mris mri, mask = hsf.roiloc_wrapper.get_mri(mris[0], mask_pattern="mask.nii.gz") @@ -186,7 +188,7 @@ def test_roiloc(models_path): # Segmentation def test_segment(models_path, config, deepsparse_inference_engines): """Tests that we can segment and save a hippocampus.""" - mri = models_path / "tse_right_hippocampus.nii.gz" + mri = models_path / "sub0_tse_right_hippocampus.nii.gz" sub = hsf.segment.mri_to_subject(mri) sub = [sub, sub] @@ -210,7 +212,7 @@ def test_multispectrality(models_path): "output_dir": str(models_path) }, "multispectrality": { - "pattern": "tse.nii.gz", + "pattern": "sub0_tse.nii.gz", "same_space": False, "registration": { "type_of_transform": "AffineFast" @@ -218,9 +220,9 @@ def test_multispectrality(models_path): } }) - mri = hsf.roiloc_wrapper.load_from_config(models_path, "tse.nii.gz")[0] + mri = hsf.roiloc_wrapper.load_from_config(models_path, "sub0_tse.nii.gz")[0] second_contrast = hsf.multispectrality.get_second_contrast( - mri, "tse.nii.gz") + mri, "sub0_tse.nii.gz") registered = hsf.multispectrality.register( mri, second_contrast,