From 37d3fe03badc75f53b2dfd9b6247e5a599d05f1b Mon Sep 17 00:00:00 2001 From: Marcel Rosier Date: Wed, 6 Nov 2024 11:22:32 +0100 Subject: [PATCH 01/10] Adding CenterModality subclass to allow elegantly saving masks --- brainles_preprocessing/modality.py | 149 ++++++++++++++++++++++++++++- 1 file changed, 148 insertions(+), 1 deletion(-) diff --git a/brainles_preprocessing/modality.py b/brainles_preprocessing/modality.py index 1fb1859..7452072 100644 --- a/brainles_preprocessing/modality.py +++ b/brainles_preprocessing/modality.py @@ -1,5 +1,6 @@ import logging import shutil +import warnings from pathlib import Path from typing import Optional, Union @@ -346,6 +347,8 @@ def extract_brain_region( bet_dir_path: Union[str, Path], ) -> Path: """ + WARNING: Legacy method. Please Migrate to use the CenterModality Class. Will be removed in future versions. + Extract the brain region using the specified brain extractor. Args: @@ -355,6 +358,13 @@ def extract_brain_region( Returns: Path: Path to the extracted brain mask. """ + + warnings.warn( + "Legacy method. Please Migrate to use the CenterModality Class. Will be removed in future versions.", + category=FutureWarning, + stacklevel=2, + ) + bet_dir_path = Path(bet_dir_path) bet_log = bet_dir_path / "brain-extraction.log" @@ -382,6 +392,8 @@ def deface( defaced_dir_path: Union[str, Path], ) -> Path: """ + WARNING: Legacy method. Please Migrate to use the CenterModality Class. Will be removed in future versions. + Deface the current modality using the specified defacer. Args: @@ -391,7 +403,11 @@ def deface( Returns: Path: Path to the extracted brain mask. """ - + warnings.warn( + "Legacy method. Please Migrate to use the CenterModality class. Will be removed in future versions.", + category=FutureWarning, + stacklevel=2, + ) if isinstance(defacer, QuickshearDefacer): defaced_dir_path = Path(defaced_dir_path) @@ -443,3 +459,134 @@ def save_current_image( src=str(self.current), dst=str(output_path), ) + + +class CenterModality(Modality): + + def __init__( + self, + modality_name: str, + input_path: Union[str, Path], + normalizer: Optional[Normalizer] = None, + raw_bet_output_path: Optional[Union[str, Path]] = None, + raw_skull_output_path: Optional[Union[str, Path]] = None, + raw_defaced_output_path: Optional[Union[str, Path]] = None, + normalized_bet_output_path: Optional[Union[str, Path]] = None, + normalized_skull_output_path: Optional[Union[str, Path]] = None, + normalized_defaced_output_path: Optional[Union[str, Path]] = None, + atlas_correction: bool = True, + bet_mask_output: Optional[Union[str, Path]] = None, + deface_mask_output: Optional[Union[str, Path]] = None, + ) -> None: + super().__init__( + modality_name=modality_name, + input_path=input_path, + normalizer=normalizer, + raw_bet_output_path=raw_bet_output_path, + raw_skull_output_path=raw_skull_output_path, + raw_defaced_output_path=raw_defaced_output_path, + normalized_bet_output_path=normalized_bet_output_path, + normalized_skull_output_path=normalized_skull_output_path, + normalized_defaced_output_path=normalized_defaced_output_path, + atlas_correction=atlas_correction, + ) + # Only for CenterModality + self.bet_mask_output = Path(bet_mask_output) if bet_mask_output else None + self.deface_mask_output = ( + Path(deface_mask_output) if deface_mask_output else None + ) + + def extract_brain_region( + self, + brain_extractor: BrainExtractor, + bet_dir_path: Union[str, Path], + ) -> Path: + """ + Extract the brain region using the specified brain extractor. + + Args: + brain_extractor (BrainExtractor): The brain extractor object. + bet_dir_path (str or Path): Directory to store brain extraction results. + + Returns: + Path: Path to the extracted brain mask. + """ + bet_dir_path = Path(bet_dir_path) + bet_log = bet_dir_path / "brain-extraction.log" + + atlas_bet_cm = bet_dir_path / f"atlas__{self.modality_name}_bet.nii.gz" + mask_path = bet_dir_path / f"atlas__{self.modality_name}_brain_mask.nii.gz" + + brain_extractor.extract( + input_image_path=self.current, + masked_image_path=atlas_bet_cm, + brain_mask_path=mask_path, + log_file_path=bet_log, + ) + + if self.bet_mask_output: + logger.debug(f"Saving bet mask to {self.bet_mask_output}") + self.save_mask(mask_path=mask_path, output_path=self.bet_mask_output) + + # always temporarily store bet image for center modality, since e.g. quickshear defacing could require it + # down the line even if the user does not wish to save the bet image + self.steps[PreprocessorSteps.BET] = atlas_bet_cm + + if self.bet: + self.current = atlas_bet_cm + return mask_path + + def deface( + self, + defacer, + defaced_dir_path: Union[str, Path], + ) -> Path: + """ + Deface the current modality using the specified defacer. + + Args: + defacer (Defacer): The defacer object. + defaced_dir_path (str or Path): Directory to store defacing results. + + Returns: + Path: Path to the extracted brain mask. + """ + + if isinstance(defacer, QuickshearDefacer): + + defaced_dir_path = Path(defaced_dir_path) + atlas_mask_path = ( + defaced_dir_path / f"atlas__{self.modality_name}_deface_mask.nii.gz" + ) + + defacer.deface( + mask_image_path=atlas_mask_path, + input_image_path=self.steps[PreprocessorSteps.BET], + ) + + if self.deface_mask_output: + logger.debug(f"Saving deface mask to {self.deface_mask_output}") + self.save_mask( + mask_path=atlas_mask_path, output_path=self.deface_mask_output + ) + + return atlas_mask_path + else: + logger.warning( + "Defacing method not implemented yet. Skipping defacing for this modality." + ) + return None + + def save_mask(self, mask_path: Union[str, Path], output_path: Path) -> None: + """ + Save the mask to the specified output path. + + Args: + mask_path (Union[str, Path]): Mask NifTI file path. + output_path (Path): Output NifTI file path. + """ + output_path.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile( + src=str(mask_path), + dst=str(output_path), + ) From 1884089d4753ce2f75d331ac1364b5aa3858b4b4 Mon Sep 17 00:00:00 2001 From: Marcel Rosier Date: Wed, 6 Nov 2024 11:27:02 +0100 Subject: [PATCH 02/10] Update docs and add warning to sue center modality class --- brainles_preprocessing/preprocessor.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/brainles_preprocessing/preprocessor.py b/brainles_preprocessing/preprocessor.py index dceea46..145e4be 100644 --- a/brainles_preprocessing/preprocessor.py +++ b/brainles_preprocessing/preprocessor.py @@ -10,12 +10,13 @@ from functools import wraps from pathlib import Path from typing import List, Optional, Union +import warnings from brainles_preprocessing.constants import PreprocessorSteps from brainles_preprocessing.defacing import Defacer, QuickshearDefacer from .brain_extraction.brain_extractor import BrainExtractor, HDBetExtractor -from .modality import Modality +from .modality import Modality, CenterModality from .registration import ANTsRegistrator from .registration.registrator import Registrator @@ -30,7 +31,7 @@ class Preprocessor: Preprocesses medical image modalities using coregistration, normalization, brain extraction, and more. Args: - center_modality (Modality): The central modality for coregistration. + center_modality (CenterModality): The central modality for coregistration. moving_modalities (List[Modality]): List of modalities to be coregistered to the central modality. registrator (Registrator): The registrator object for coregistration and registration to the atlas. brain_extractor (Optional[BrainExtractor]): The brain extractor object for brain extraction. @@ -44,7 +45,7 @@ class Preprocessor: def __init__( self, - center_modality: Modality, + center_modality: CenterModality, moving_modalities: List[Modality], registrator: Registrator = None, brain_extractor: Optional[BrainExtractor] = None, @@ -56,6 +57,14 @@ def __init__( ): logging_man._setup_logger() + if not isinstance(center_modality, CenterModality): + warnings.warn( + "Center modality should be of type CenterModality instead of Modality to allow for more features, e.g. saving bet and deface masks. " + "Support for using Modality for the Center Modality will be deprecated in future versions. " + "Note: Moving modalities should still be of type Modality.", + category=FutureWarning, + stacklevel=2, + ) self.center_modality = center_modality self.moving_modalities = moving_modalities From 205dc3bae2cbc035f7e4701bd6dcb417843a0ae5 Mon Sep 17 00:00:00 2001 From: Marcel Rosier Date: Wed, 6 Nov 2024 15:24:02 +0100 Subject: [PATCH 03/10] Change warning to deprecation --- brainles_preprocessing/modality.py | 6 ++---- brainles_preprocessing/preprocessor.py | 3 +-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/brainles_preprocessing/modality.py b/brainles_preprocessing/modality.py index 7452072..e1d4547 100644 --- a/brainles_preprocessing/modality.py +++ b/brainles_preprocessing/modality.py @@ -361,8 +361,7 @@ def extract_brain_region( warnings.warn( "Legacy method. Please Migrate to use the CenterModality Class. Will be removed in future versions.", - category=FutureWarning, - stacklevel=2, + category=DeprecationWarning, ) bet_dir_path = Path(bet_dir_path) @@ -405,8 +404,7 @@ def deface( """ warnings.warn( "Legacy method. Please Migrate to use the CenterModality class. Will be removed in future versions.", - category=FutureWarning, - stacklevel=2, + category=DeprecationWarning, ) if isinstance(defacer, QuickshearDefacer): diff --git a/brainles_preprocessing/preprocessor.py b/brainles_preprocessing/preprocessor.py index 145e4be..2da241a 100644 --- a/brainles_preprocessing/preprocessor.py +++ b/brainles_preprocessing/preprocessor.py @@ -62,8 +62,7 @@ def __init__( "Center modality should be of type CenterModality instead of Modality to allow for more features, e.g. saving bet and deface masks. " "Support for using Modality for the Center Modality will be deprecated in future versions. " "Note: Moving modalities should still be of type Modality.", - category=FutureWarning, - stacklevel=2, + category=DeprecationWarning, ) self.center_modality = center_modality self.moving_modalities = moving_modalities From c4de0f523df708adf7ef88b15aef28fb0ad86301 Mon Sep 17 00:00:00 2001 From: Marcel Rosier Date: Wed, 6 Nov 2024 15:24:17 +0100 Subject: [PATCH 04/10] Update example to use CenterModality --- README.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 32ac653..3b36419 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,7 @@ This includes **normalization**, **co-registration**, **atlas registration** and **skulstripping / brain extraction**. -BrainLes is written `backend-agnostic` meaning it allows to swap the registration and brain extraction tools. - - +BrainLes is written `backend-agnostic` meaning it allows to swap the registration, brain extraction tools and defacing tools. @@ -32,7 +30,7 @@ pip install brainles-preprocessing A minimal example to register (to the standard atlas using ANTs) and skull strip (using HDBet) a t1c image (center modality) with 1 moving modality (flair) could look like this: ```python from pathlib import Path -from brainles_preprocessing.modality import Modality +from brainles_preprocessing.modality import Modality, CenterModality from brainles_preprocessing.normalization.percentile_normalizer import ( PercentileNormalizer, ) @@ -48,8 +46,8 @@ percentile_normalizer = PercentileNormalizer( upper_limit=1, ) -# define modalities -center = Modality( +# define center and moving modalities +center = CenterModality( modality_name="t1c", input_path=patient_folder / "t1c.nii.gz", normalizer=percentile_normalizer, From 4d27031cf6e9de96452a0beac2b95dbaa28fdf35 Mon Sep 17 00:00:00 2001 From: Marcel Rosier Date: Wed, 6 Nov 2024 15:42:27 +0100 Subject: [PATCH 05/10] Add class docstrings for CenterModality --- brainles_preprocessing/modality.py | 42 ++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/brainles_preprocessing/modality.py b/brainles_preprocessing/modality.py index e1d4547..407ae2b 100644 --- a/brainles_preprocessing/modality.py +++ b/brainles_preprocessing/modality.py @@ -460,6 +460,48 @@ def save_current_image( class CenterModality(Modality): + """ + Represents a medical image center modality with associated preprocessing information. + + Args: + modality_name (str): Name of the modality, e.g., "T1", "T2", "FLAIR". + input_path (str or Path): Path to the input modality data. + normalizer (Normalizer, optional): An optional normalizer for intensity normalization. + raw_bet_output_path (str or Path, optional): Path to save the raw brain extracted modality data. + raw_skull_output_path (str or Path, optional): Path to save the raw modality data with skull. + raw_defaced_output_path (str or Path, optional): Path to save the raw defaced modality data. + normalized_bet_output_path (str or Path, optional): Path to save the normalized brain extracted modality data. Requires a normalizer. + normalized_skull_output_path (str or Path, optional): Path to save the normalized modality data with skull. Requires a normalizer. + normalized_defaced_output_path (str or Path, optional): Path to save the normalized defaced modality data. Requires a normalizer. + atlas_correction (bool, optional): Indicates whether atlas correction should be performed. + bet_mask_output (str or Path, optional): Path to save the brain extraction mask. + deface_mask_output (str or Path, optional): Path to save the defacing mask. + + Attributes: + modality_name (str): Name of the modality. + input_path (str or Path): Path to the input modality data. + normalizer (Normalizer, optional): An optional normalizer for intensity normalization. + raw_bet_output_path (str or Path, optional): Path to save the raw brain extracted modality data. + raw_skull_output_path (str or Path, optional): Path to save the raw modality data with skull. + raw_defaced_output_path (str or Path, optional): Path to save the raw defaced modality data. + normalized_bet_output_path (str or Path, optional): Path to save the normalized brain extracted modality data. Requires a normalizer. + normalized_skull_output_path (str or Path, optional): Path to save the normalized modality data with skull. Requires a normalizer. + normalized_defaced_output_path (str or Path, optional): Path to save the normalized defaced modality data. Requires a normalizer. + bet (bool): Indicates whether brain extraction is enabled. + atlas_correction (bool): Indicates whether atlas correction should be performed. + bet_mask_output (Path, optional): Path to save the brain extraction mask. + deface_mask_output (Path, optional): Path to save the defacing mask. + + Example: + >>> t1_modality = CenterModality( + ... modality_name="T1", + ... input_path="/path/to/input_t1.nii", + ... normalizer=PercentileNormalizer(), + ... raw_bet_output_path="/path/to/raw_bet_t1.nii", + ... normalized_bet_output_path="/path/to/norm_bet_t1.nii", + ... bet_mask_output="/path/to/bet_mask_t1.nii", + ... ) + """ def __init__( self, From 921961269ce412e02f5b34c60aafdf882a69a2e2 Mon Sep 17 00:00:00 2001 From: Marcel Rosier Date: Wed, 6 Nov 2024 17:04:31 +0100 Subject: [PATCH 06/10] Simplify imports --- brainles_preprocessing/normalization/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/brainles_preprocessing/normalization/__init__.py b/brainles_preprocessing/normalization/__init__.py index e69de29..bf20950 100644 --- a/brainles_preprocessing/normalization/__init__.py +++ b/brainles_preprocessing/normalization/__init__.py @@ -0,0 +1,3 @@ +from .normalizer_base import Normalizer +from .percentile_normalizer import PercentileNormalizer +from .windowing_normalizer import WindowingNormalizer From 790275be0b25b52bf7773aa29470975e837f173e Mon Sep 17 00:00:00 2001 From: Marcel Rosier Date: Wed, 6 Nov 2024 17:06:29 +0100 Subject: [PATCH 07/10] Add mask outputs to example --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 3b36419..0de5103 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,9 @@ center = CenterModality( raw_bet_output_path="patient/raw_bet_dir/t1c_bet_raw.nii.gz", normalized_skull_output_path="patient/norm_skull_dir/t1c_skull_normalized.nii.gz", normalized_bet_output_path="patient/norm_bet_dir/t1c_bet_normalized.nii.gz", + # specify output paths for the brain extraction and defacing masks + bet_mask_output="patient/masks/t1c_bet_mask.nii.gz", + deface_mask_output="patient/masks/t1c_deface_mask.nii.gz", ) moving_modalities = [ From bf4af58a237675291fe0f64e0aba0a2a29c1b28f Mon Sep 17 00:00:00 2001 From: neuronflow <7048826+neuronflow@users.noreply.github.com> Date: Wed, 6 Nov 2024 17:08:26 +0100 Subject: [PATCH 08/10] Update README.md wording change --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0de5103..4cc1442 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ center = CenterModality( normalized_bet_output_path="patient/norm_bet_dir/t1c_bet_normalized.nii.gz", # specify output paths for the brain extraction and defacing masks bet_mask_output="patient/masks/t1c_bet_mask.nii.gz", - deface_mask_output="patient/masks/t1c_deface_mask.nii.gz", + deface_mask_output="patient/masks/t1c_defacing_mask.nii.gz", ) moving_modalities = [ From ddf7ad314dee58fdad656df6ff3efebf25b0733c Mon Sep 17 00:00:00 2001 From: Marcel Rosier Date: Wed, 6 Nov 2024 17:13:10 +0100 Subject: [PATCH 09/10] Fix inconsistent naming --- README.md | 4 ++-- brainles_preprocessing/modality.py | 35 ++++++++++++++++-------------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 4cc1442..1fec20e 100644 --- a/README.md +++ b/README.md @@ -57,8 +57,8 @@ center = CenterModality( normalized_skull_output_path="patient/norm_skull_dir/t1c_skull_normalized.nii.gz", normalized_bet_output_path="patient/norm_bet_dir/t1c_bet_normalized.nii.gz", # specify output paths for the brain extraction and defacing masks - bet_mask_output="patient/masks/t1c_bet_mask.nii.gz", - deface_mask_output="patient/masks/t1c_defacing_mask.nii.gz", + bet_mask_output_path="patient/masks/t1c_bet_mask.nii.gz", + defacing_mask_output_path="patient/masks/t1c_defacing_mask.nii.gz", ) moving_modalities = [ diff --git a/brainles_preprocessing/modality.py b/brainles_preprocessing/modality.py index 407ae2b..0ddd88d 100644 --- a/brainles_preprocessing/modality.py +++ b/brainles_preprocessing/modality.py @@ -474,8 +474,8 @@ class CenterModality(Modality): normalized_skull_output_path (str or Path, optional): Path to save the normalized modality data with skull. Requires a normalizer. normalized_defaced_output_path (str or Path, optional): Path to save the normalized defaced modality data. Requires a normalizer. atlas_correction (bool, optional): Indicates whether atlas correction should be performed. - bet_mask_output (str or Path, optional): Path to save the brain extraction mask. - deface_mask_output (str or Path, optional): Path to save the defacing mask. + bet_mask_output_path (str or Path, optional): Path to save the brain extraction mask. + defacing_mask_output_path (str or Path, optional): Path to save the defacing mask. Attributes: modality_name (str): Name of the modality. @@ -489,8 +489,8 @@ class CenterModality(Modality): normalized_defaced_output_path (str or Path, optional): Path to save the normalized defaced modality data. Requires a normalizer. bet (bool): Indicates whether brain extraction is enabled. atlas_correction (bool): Indicates whether atlas correction should be performed. - bet_mask_output (Path, optional): Path to save the brain extraction mask. - deface_mask_output (Path, optional): Path to save the defacing mask. + bet_mask_output_path (Path, optional): Path to save the brain extraction mask. + defacing_mask_output_path (Path, optional): Path to save the defacing mask. Example: >>> t1_modality = CenterModality( @@ -499,7 +499,7 @@ class CenterModality(Modality): ... normalizer=PercentileNormalizer(), ... raw_bet_output_path="/path/to/raw_bet_t1.nii", ... normalized_bet_output_path="/path/to/norm_bet_t1.nii", - ... bet_mask_output="/path/to/bet_mask_t1.nii", + ... bet_mask_output_path="/path/to/bet_mask_t1.nii", ... ) """ @@ -515,8 +515,8 @@ def __init__( normalized_skull_output_path: Optional[Union[str, Path]] = None, normalized_defaced_output_path: Optional[Union[str, Path]] = None, atlas_correction: bool = True, - bet_mask_output: Optional[Union[str, Path]] = None, - deface_mask_output: Optional[Union[str, Path]] = None, + bet_mask_output_path: Optional[Union[str, Path]] = None, + defacing_mask_output_path: Optional[Union[str, Path]] = None, ) -> None: super().__init__( modality_name=modality_name, @@ -531,9 +531,11 @@ def __init__( atlas_correction=atlas_correction, ) # Only for CenterModality - self.bet_mask_output = Path(bet_mask_output) if bet_mask_output else None - self.deface_mask_output = ( - Path(deface_mask_output) if deface_mask_output else None + self.bet_mask_output_path = ( + Path(bet_mask_output_path) if bet_mask_output_path else None + ) + self.defacing_mask_output_path = ( + Path(defacing_mask_output_path) if defacing_mask_output_path else None ) def extract_brain_region( @@ -564,9 +566,9 @@ def extract_brain_region( log_file_path=bet_log, ) - if self.bet_mask_output: - logger.debug(f"Saving bet mask to {self.bet_mask_output}") - self.save_mask(mask_path=mask_path, output_path=self.bet_mask_output) + if self.bet_mask_output_path: + logger.debug(f"Saving bet mask to {self.bet_mask_output_path}") + self.save_mask(mask_path=mask_path, output_path=self.bet_mask_output_path) # always temporarily store bet image for center modality, since e.g. quickshear defacing could require it # down the line even if the user does not wish to save the bet image @@ -604,10 +606,11 @@ def deface( input_image_path=self.steps[PreprocessorSteps.BET], ) - if self.deface_mask_output: - logger.debug(f"Saving deface mask to {self.deface_mask_output}") + if self.defacing_mask_output_path: + logger.debug(f"Saving deface mask to {self.defacing_mask_output_path}") self.save_mask( - mask_path=atlas_mask_path, output_path=self.deface_mask_output + mask_path=atlas_mask_path, + output_path=self.defacing_mask_output_path, ) return atlas_mask_path From 233857c687db3700f103df880d9b4f7a622683e8 Mon Sep 17 00:00:00 2001 From: neuronflow <7048826+neuronflow@users.noreply.github.com> Date: Wed, 6 Nov 2024 18:02:21 +0100 Subject: [PATCH 10/10] Update README.md --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 1fec20e..d5363c5 100644 --- a/README.md +++ b/README.md @@ -108,15 +108,15 @@ We provide a (WIP) documentation. Have a look [here](https://brainles-preprocess ## FAQ Please credit the authors by citing their work. +### Registration +We currently provide support for [ANTs](https://github.com/ANTsX/ANTs) (default), [Niftyreg](https://github.com/KCL-BMEIS/niftyreg) (Linux), eReg (experimental) + ### Atlas Reference We provide the SRI-24 atlas from this [publication](https://www.ncbi.nlm.nih.gov/pmc/articles/PMC2915788/). -However, custom atlases can be supplied. +However, custom atlases in NIfTI format are supported. ### Brain extraction We currently provide support for [HD-BET](https://github.com/MIC-DKFZ/HD-BET). -### Registration -We currently provide support for [ANTs](https://github.com/ANTsX/ANTs) (default), [Niftyreg](https://github.com/KCL-BMEIS/niftyreg) (Linux), eReg (experimental) - ### Defacing -We currently provide support for [Quickshear](https://github.com/nipy/quickshear) +We currently provide support for [Quickshear](https://github.com/nipy/quickshear).