From c5859c447d6ec77366ddfcbf8af3b9adce20abca Mon Sep 17 00:00:00 2001 From: nicmuenster Date: Wed, 28 Jan 2026 13:55:46 +0100 Subject: [PATCH 1/8] Vhanged workflow for setting the masking value in the skull stripping process to either a global custom value for all inputs or the minimum value within the input image --- .../brain_extraction/brain_extractor.py | 41 +++++++++++++++++-- .../brain_extraction/synthstrip.py | 9 ++-- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/brainles_preprocessing/brain_extraction/brain_extractor.py b/brainles_preprocessing/brain_extraction/brain_extractor.py index 03a716b..1c63709 100644 --- a/brainles_preprocessing/brain_extraction/brain_extractor.py +++ b/brainles_preprocessing/brain_extraction/brain_extractor.py @@ -4,6 +4,7 @@ from pathlib import Path from typing import Optional, Union from enum import Enum +import numpy as np from auxiliary.io import read_image, write_image from brainles_hd_bet import run_hd_bet @@ -14,7 +15,23 @@ class Mode(Enum): ACCURATE = "accurate" -class BrainExtractor: +class BrainExtractor(ABC): + def __init__( + self, + masking_value: Optional[Union[int, float]] = None, + ): + """ + Base class for skull stripping medical images using brain masks. + + Subclasses should implement the `extract` method to generate a skull stripped image + based on the provided input image and mask. + """ + # Just as in the defacer, masking value is a global value defined across all images and modalities + # If no value is passed, the minimum of a given input image is chosen + # TODO: Consider extending this to modality-specific masking values in the future, this should + # probably be implemented as a property of the the specific modality + self.masking_value = masking_value + @abstractmethod def extract( self, @@ -63,8 +80,17 @@ def apply_mask( if input_data.shape != mask_data.shape: raise ValueError("Input image and mask must have the same dimensions.") - # Mask and save it - masked_data = input_data * mask_data + # check whether a global masking value was passed, otherwise choose minimum + if self.masking_value is None: + current_masking_value = np.min(input_data) + else: + current_masking_value = ( + np.array(self.masking_value).astype(input_data.dtype).item() + ) + # Apply mask (element-wise either input or masking value) + masked_data = np.where( + mask_data.astype(bool), input_data, current_masking_value + ) try: write_image( @@ -78,6 +104,15 @@ def apply_mask( class HDBetExtractor(BrainExtractor): + def __init__(self, masking_value: Optional[Union[int, float]] = None): + """ + Brain extraction HDBet implementation. + + Args: + masking_value (Optional[Union[int, float]], optional): global value to be inserted in the masked areas. Default is None which leads to the minimum of each respective image. + """ + super().__init__(masking_value=masking_value) + def extract( self, input_image_path: Union[str, Path], diff --git a/brainles_preprocessing/brain_extraction/synthstrip.py b/brainles_preprocessing/brain_extraction/synthstrip.py index 0ea3135..eed97d6 100644 --- a/brainles_preprocessing/brain_extraction/synthstrip.py +++ b/brainles_preprocessing/brain_extraction/synthstrip.py @@ -21,7 +21,9 @@ class SynthStripExtractor(BrainExtractor): - def __init__(self, border: int = 1): + def __init__( + self, border: int = 1, masking_value: Optional[Union[int, float]] = None + ): """ Brain extraction using SynthStrip with preprocessing conforming to model requirements. @@ -31,9 +33,10 @@ def __init__(self, border: int = 1): Args: border (int): Mask border threshold in mm. Defaults to 1. - """ + masking_value (Optional[Union[int, float]], optional): global value to be inserted in the masked areas. Default is None which leads to the minimum of each respective image. - super().__init__() + """ + super().__init__(masking_value=masking_value) self.border = border def _setup_model(self, device: torch.device) -> StripModel: From 2395565cef4b4fd3a40ca618cdfa86dcc70bd15b Mon Sep 17 00:00:00 2001 From: "brainless-bot[bot]" <153751247+brainless-bot[bot]@users.noreply.github.com> Date: Wed, 28 Jan 2026 13:17:08 +0000 Subject: [PATCH 2/8] Autoformat with black --- brainles_preprocessing/registration/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/brainles_preprocessing/registration/__init__.py b/brainles_preprocessing/registration/__init__.py index e38d97d..9eea05d 100644 --- a/brainles_preprocessing/registration/__init__.py +++ b/brainles_preprocessing/registration/__init__.py @@ -1,6 +1,5 @@ import warnings - try: from .ANTs.ANTs import ANTsRegistrator except ImportError: @@ -11,7 +10,6 @@ from .niftyreg.niftyreg import NiftyRegRegistrator - try: from .elastix.elastix import ElastixRegistrator except ImportError: From 776cf77d0d102f936932495790a1cc2affeb538b Mon Sep 17 00:00:00 2001 From: neuronflow <7048826+neuronflow@users.noreply.github.com> Date: Wed, 28 Jan 2026 14:31:26 +0100 Subject: [PATCH 3/8] Update brainles_preprocessing/brain_extraction/brain_extractor.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- brainles_preprocessing/brain_extraction/brain_extractor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/brainles_preprocessing/brain_extraction/brain_extractor.py b/brainles_preprocessing/brain_extraction/brain_extractor.py index 1c63709..fb662d6 100644 --- a/brainles_preprocessing/brain_extraction/brain_extractor.py +++ b/brainles_preprocessing/brain_extraction/brain_extractor.py @@ -29,7 +29,7 @@ def __init__( # Just as in the defacer, masking value is a global value defined across all images and modalities # If no value is passed, the minimum of a given input image is chosen # TODO: Consider extending this to modality-specific masking values in the future, this should - # probably be implemented as a property of the the specific modality + # probably be implemented as a property of the specific modality self.masking_value = masking_value @abstractmethod From fb51adedef10c9eed119599abd3bb273227fda26 Mon Sep 17 00:00:00 2001 From: nicmuenster Date: Fri, 24 Jul 2026 12:47:53 +0200 Subject: [PATCH 4/8] cleaned up after failed merger --- .../brain_extraction/brain_extractor.py | 72 ------------------- .../brain_extraction/hd_bet.py | 11 +++ 2 files changed, 11 insertions(+), 72 deletions(-) diff --git a/brainles_preprocessing/brain_extraction/brain_extractor.py b/brainles_preprocessing/brain_extraction/brain_extractor.py index 3da9c2d..a5ad289 100644 --- a/brainles_preprocessing/brain_extraction/brain_extractor.py +++ b/brainles_preprocessing/brain_extraction/brain_extractor.py @@ -30,7 +30,6 @@ def __init__( # probably be implemented as a property of the specific modality self.masking_value = masking_value -class BrainExtractor(ABC): @abstractmethod def extract( self, @@ -102,74 +101,3 @@ def apply_mask( raise RuntimeError(f"Error writing output file: {e}") from e -class HDBetExtractor(BrainExtractor): - def __init__(self, masking_value: Optional[Union[int, float]] = None): - """ - Brain extraction HDBet implementation. - - Args: - masking_value (Optional[Union[int, float]], optional): global value to be inserted in the masked areas. Default is None which leads to the minimum of each respective image. - """ - super().__init__(masking_value=masking_value) - - def extract( - self, - input_image_path: Union[str, Path], - masked_image_path: Union[str, Path], - brain_mask_path: Union[str, Path], - mode: Union[str, Mode] = Mode.ACCURATE, - device: Optional[Union[int, str]] = 0, - do_tta: bool = True, - **kwargs, - ) -> None: - # GPU + accurate + TTA - """ - Skull-strips images with HD-BET and generates a skull-stripped file and mask. - - Args: - input_image_path (str or Path): Path to the input image. - masked_image_path (str or Path): Path where the brain-extracted image will be saved. - brain_mask_path (str or Path): Path where the brain mask will be saved. - mode (str or Mode): Extraction mode ('fast' or 'accurate'). - device (str or int): Device to use for computation (e.g., 0 for GPU 0, 'cpu' for CPU). - do_tta (bool): whether to do test time data augmentation by mirroring along all axes. - """ - - # Ensure mode is a Mode enum instance - if isinstance(mode, str): - try: - mode_enum = Mode(mode.lower()) - except ValueError: - raise ValueError(f"'{mode}' is not a valid Mode.") - elif isinstance(mode, Mode): - mode_enum = mode - else: - raise TypeError("Mode must be a string or a Mode enum instance.") - - # Run HD-BET - run_hd_bet( - mri_fnames=[str(input_image_path)], - output_fnames=[str(masked_image_path)], - mode=mode_enum.value, - device=device, - # TODO consider postprocessing - postprocess=False, - do_tta=do_tta, - keep_mask=True, - overwrite=True, - ) - - # Construct the path to the generated mask - masked_image_path = Path(masked_image_path) - hdbet_mask_path = masked_image_path.with_name( - masked_image_path.name.replace(".nii.gz", "_mask.nii.gz") - ) - - if hdbet_mask_path.resolve() != Path(brain_mask_path).resolve(): - try: - shutil.copyfile( - src=str(hdbet_mask_path), - dst=str(brain_mask_path), - ) - except Exception as e: - raise RuntimeError(f"Error copying mask file: {e}") from e diff --git a/brainles_preprocessing/brain_extraction/hd_bet.py b/brainles_preprocessing/brain_extraction/hd_bet.py index cd631ea..c806d26 100644 --- a/brainles_preprocessing/brain_extraction/hd_bet.py +++ b/brainles_preprocessing/brain_extraction/hd_bet.py @@ -13,6 +13,15 @@ class Mode(Enum): class HDBetExtractor(BrainExtractor): + def __init__(self, masking_value: Optional[Union[int, float]] = None): + """ + Brain extraction HDBet implementation. + + Args: + masking_value (Optional[Union[int, float]], optional): global value to be inserted in the masked areas. Default is None which leads to the minimum of each respective image. + """ + super().__init__(masking_value=masking_value) + def extract( self, input_image_path: Union[str, Path], @@ -74,3 +83,5 @@ def extract( ) except Exception as e: raise RuntimeError(f"Error copying mask file: {e}") from e + + From fc7e640d73db0893b908edfff2c2320da9217370 Mon Sep 17 00:00:00 2001 From: "brainless-bot[bot]" <153751247+brainless-bot[bot]@users.noreply.github.com> Date: Fri, 24 Jul 2026 10:49:55 +0000 Subject: [PATCH 5/8] Autoformat with black --- brainles_preprocessing/brain_extraction/brain_extractor.py | 2 -- brainles_preprocessing/brain_extraction/hd_bet.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/brainles_preprocessing/brain_extraction/brain_extractor.py b/brainles_preprocessing/brain_extraction/brain_extractor.py index a5ad289..af85511 100644 --- a/brainles_preprocessing/brain_extraction/brain_extractor.py +++ b/brainles_preprocessing/brain_extraction/brain_extractor.py @@ -99,5 +99,3 @@ def apply_mask( ) except Exception as e: raise RuntimeError(f"Error writing output file: {e}") from e - - diff --git a/brainles_preprocessing/brain_extraction/hd_bet.py b/brainles_preprocessing/brain_extraction/hd_bet.py index c806d26..7391efb 100644 --- a/brainles_preprocessing/brain_extraction/hd_bet.py +++ b/brainles_preprocessing/brain_extraction/hd_bet.py @@ -83,5 +83,3 @@ def extract( ) except Exception as e: raise RuntimeError(f"Error copying mask file: {e}") from e - - From 6568378e95ad6647e47dad55bed9b96373a082b0 Mon Sep 17 00:00:00 2001 From: nicmuenster Date: Fri, 24 Jul 2026 14:45:26 +0200 Subject: [PATCH 6/8] Removed mode from brain_extractor.py as it is only needed in hd_bet.py --- brainles_preprocessing/brain_extraction/brain_extractor.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/brainles_preprocessing/brain_extraction/brain_extractor.py b/brainles_preprocessing/brain_extraction/brain_extractor.py index af85511..cbacb2c 100644 --- a/brainles_preprocessing/brain_extraction/brain_extractor.py +++ b/brainles_preprocessing/brain_extraction/brain_extractor.py @@ -8,11 +8,6 @@ from auxiliary.io import read_image, write_image -class Mode(Enum): - FAST = "fast" - ACCURATE = "accurate" - - class BrainExtractor(ABC): def __init__( self, @@ -45,7 +40,6 @@ def extract( input_image_path (str or Path): Path to the input image. masked_image_path (str or Path): Path where the brain-extracted image will be saved. brain_mask_path (str or Path): Path where the brain mask will be saved. - mode (str or Mode): Extraction mode. **kwargs: Additional keyword arguments. """ pass From d94e07831f23f2b4c01c2589bcb098f5066249f2 Mon Sep 17 00:00:00 2001 From: nicmuenster Date: Fri, 24 Jul 2026 15:15:36 +0200 Subject: [PATCH 7/8] changed synthstrip extract function to also use the the defined masking value --- brainles_preprocessing/brain_extraction/synthstrip.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/brainles_preprocessing/brain_extraction/synthstrip.py b/brainles_preprocessing/brain_extraction/synthstrip.py index eed97d6..bb5b1d1 100644 --- a/brainles_preprocessing/brain_extraction/synthstrip.py +++ b/brainles_preprocessing/brain_extraction/synthstrip.py @@ -222,8 +222,14 @@ def extract( # write the masked output img_data = image.get_fdata() - bg = np.min([0, img_data.min()]) - img_data[mask == 0] = bg + # check whether a global masking value was passed, otherwise choose minimum + if self.masking_value is None: + current_masking_value = np.min(img_data) + else: + current_masking_value = ( + np.array(self.masking_value).astype(img_data.dtype).item() + ) + img_data[mask == 0] = current_masking_value Nifti1Image(img_data, image.affine, image.header).to_filename( masked_image_path, ) From 861e8e164b24a93b23fb1718d298034169994e80 Mon Sep 17 00:00:00 2001 From: nicmuenster Date: Fri, 24 Jul 2026 15:58:11 +0200 Subject: [PATCH 8/8] removed enum import --- brainles_preprocessing/brain_extraction/brain_extractor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/brainles_preprocessing/brain_extraction/brain_extractor.py b/brainles_preprocessing/brain_extraction/brain_extractor.py index cbacb2c..6a10f8e 100644 --- a/brainles_preprocessing/brain_extraction/brain_extractor.py +++ b/brainles_preprocessing/brain_extraction/brain_extractor.py @@ -2,7 +2,6 @@ from abc import abstractmethod, ABC from pathlib import Path from typing import Optional, Union -from enum import Enum import numpy as np from auxiliary.io import read_image, write_image