Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
e79de84
Move bet and deface back to modality to support non atlas centric pip…
MarcelRosier Jul 11, 2025
fad39c6
Create preprocessor baseclass and atlas/ native centric classes
MarcelRosier Jul 11, 2025
2c9fb77
Adapt and add example
MarcelRosier Jul 11, 2025
e83a3da
Adapt test to new naming
MarcelRosier Jul 11, 2025
d542c8b
Generalize example
MarcelRosier Jul 14, 2025
de3d33a
Improve docstrings and formatting
MarcelRosier Jul 14, 2025
e93e5f8
Merge branch 'main' into 121-feature-non-atlas-centric-preprocessing-…
MarcelRosier Jul 14, 2025
e1da9ab
Fix isinstance check
MarcelRosier Jul 14, 2025
b75e02a
Move bet method back to center modality
MarcelRosier Jul 15, 2025
7f7c625
Move run bet up to base class
MarcelRosier Jul 15, 2025
ebf034a
Update examples
MarcelRosier Jul 15, 2025
295f305
Add option to force atlas reg in quickshear defacing
MarcelRosier Jul 15, 2025
54ea78c
Cleanup import
MarcelRosier Jul 15, 2025
26f39a6
Make atlas reg during defacing optional
MarcelRosier Jul 15, 2025
c64d117
Generalize hardcoded paths
MarcelRosier Jul 15, 2025
8c18b29
Merge branch 'main' into 121-feature-non-atlas-centric-preprocessing-…
neuronflow Jul 15, 2025
e792e44
Merge branch 'main' into 121-feature-non-atlas-centric-preprocessing-…
MarcelRosier Jul 16, 2025
3470a89
Move force atlas reg option to quickshear class
MarcelRosier Jul 16, 2025
862ce8b
Improve docstring
MarcelRosier Jul 16, 2025
c335375
Generalize to use any registrator for deface atlas reg
MarcelRosier Jul 16, 2025
aba0d9b
Fix wrong parameter
MarcelRosier Jul 16, 2025
2e7cd7a
Rm legacy parameter
MarcelRosier Jul 16, 2025
f85ad5f
Unify inverse_transform interface
MarcelRosier Jul 16, 2025
7caf251
Merge branch '121-feature-non-atlas-centric-preprocessing-pipeline' o…
MarcelRosier Jul 16, 2025
db5ac99
Merge branch 'main' into 121-feature-non-atlas-centric-preprocessing-…
neuronflow Jul 16, 2025
674f86c
Add validation that input for defacing exists
MarcelRosier Jul 16, 2025
5940d70
Add option to specific atlas for deface registration
MarcelRosier Jul 17, 2025
cfe0ec6
Cleanup imports and fix docstring
MarcelRosier Jul 17, 2025
7d1c4d3
Remove unused imports
MarcelRosier Jul 17, 2025
9444341
Update brainles_preprocessing/modality.py
neuronflow Jul 17, 2025
8527153
Change misleading outputdir name
MarcelRosier Jul 17, 2025
2bc0f3b
Merge branch '121-feature-non-atlas-centric-preprocessing-pipeline' o…
MarcelRosier Jul 17, 2025
b1ce517
Autoformat with black
brainless-bot[bot] Jul 17, 2025
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
12 changes: 11 additions & 1 deletion brainles_preprocessing/defacing/quickshear/quickshear.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from brainles_preprocessing.defacing.defacer import Defacer
from brainles_preprocessing.defacing.quickshear.nipy_quickshear import run_quickshear
from brainles_preprocessing.constants import Atlas


class QuickshearDefacer(Defacer):
Expand All @@ -29,14 +30,23 @@ class QuickshearDefacer(Defacer):
```
"""

def __init__(self, buffer: float = 10.0):
def __init__(
self,
buffer: float = 10.0,
force_atlas_registration: bool = True,
atlas_image_path: Union[str, Path, Atlas] = Atlas.SRI24,
):
"""Initialize Quickshear defacer

Args:
buffer (float, optional): buffer parameter from quickshear algorithm. Defaults to 10.0.
force_atlas_registration (bool, optional): If True, forces atlas registration of the BET mask before defacing to potentially boost quickshear performance. Defaults to True.
atlas_image_path (Union[str, Path, Atlas], optional): Path to the atlas image or an Atlas enum value that will be used for the optional atlas registrations. Defaults to Atlas.SRI24.
"""
super().__init__()
self.buffer = buffer
self.force_atlas_registration = force_atlas_registration
self.atlas_image_path = atlas_image_path

def deface(
self,
Expand Down
159 changes: 89 additions & 70 deletions brainles_preprocessing/modality.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@
from auxiliary.io import read_image, write_image

from brainles_preprocessing.brain_extraction.brain_extractor import BrainExtractor
from brainles_preprocessing.constants import PreprocessorSteps
from brainles_preprocessing.constants import Atlas, PreprocessorSteps
from brainles_preprocessing.defacing import Defacer, QuickshearDefacer
from brainles_preprocessing.normalization.normalizer_base import Normalizer
from brainles_preprocessing.registration import ( # TODO: this will throw warnings if ANTs or NiftyReg are not installed, not ideal
ANTsRegistrator,
NiftyRegRegistrator,
)
from brainles_preprocessing.registration.registrator import Registrator
from brainles_preprocessing.utils.zenodo import verify_or_download_atlases

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -295,7 +296,7 @@ def apply_bet_mask(
if self.bet:
mask_path = Path(mask_path)
bet_dir = Path(bet_dir)
bet_img = bet_dir / f"atlas__{self.modality_name}_bet.nii.gz"
bet_img = bet_dir / f"{self.modality_name}_bet.nii.gz"

brain_extractor.apply_mask(
input_image_path=self.current,
Expand Down Expand Up @@ -324,14 +325,27 @@ def apply_deface_mask(
if self.requires_deface:
mask_path = Path(mask_path)
deface_dir = Path(deface_dir)
defaced_img = deface_dir / f"atlas__{self.modality_name}_defaced.nii.gz"
input_img = self.steps[
(
PreprocessorSteps.ATLAS_CORRECTED
if self.atlas_correction
else PreprocessorSteps.ATLAS_REGISTERED
defaced_img = deface_dir / f"{self.modality_name}_defaced.nii.gz"

# For Atlas centric preprocessing, we use the atlas corrected or registered image as input
# For Native space preprocessing, we use the coregistered image as input
input_img = (
self.steps[
(
PreprocessorSteps.ATLAS_CORRECTED
if self.atlas_correction
else PreprocessorSteps.ATLAS_REGISTERED
)
]
or self.steps[PreprocessorSteps.COREGISTERED]
)

if input_img is None:
raise ValueError(
"Input image for defacing is missing. Ensure that the required preprocessing steps "
"have been performed before defacing."
)
]

defacer.apply_mask(
input_image_path=input_img,
mask_path=mask_path,
Expand Down Expand Up @@ -428,65 +442,40 @@ def extract_brain_region(
"Legacy method. Please Migrate to use the CenterModality Class. Will be removed in future versions.",
category=DeprecationWarning,
)

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"
bet = bet_dir_path / f"{self.modality_name}_bet.nii.gz"
mask_path = bet_dir_path / f"{self.modality_name}_brain_mask.nii.gz"

brain_extractor.extract(
input_image_path=self.current,
masked_image_path=atlas_bet_cm,
masked_image_path=bet,
brain_mask_path=mask_path,
log_file_path=bet_log,
)

# 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
self.steps[PreprocessorSteps.BET] = bet

if self.bet:
self.current = atlas_bet_cm
self.current = bet
return mask_path

def deface(
self,
defacer,
defaced_dir_path: Union[str, Path],
) -> Path:
registrator: Optional[Registrator] = None,
) -> Path | None:
"""
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:
defacer (Defacer): The defacer object.
defaced_dir_path (str or Path): Directory to store defacing results.

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=DeprecationWarning,
raise RuntimeError(
"The 'deface' method has been deprecated and moved to the CenterModality class as its only supposed to be called once from the CenterModality. "
"Please update your code to use the 'CenterModality.deface()' method instead."
)
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],
)
return atlas_mask_path
else:
logger.warning(
"Defacing method not implemented yet. Skipping defacing for this modality."
)
return None

def save_current_image(
self,
Expand Down Expand Up @@ -612,6 +601,7 @@ def extract_brain_region(
bet_dir_path: Union[str, Path],
) -> Path:
"""

Extract the brain region using the specified brain extractor.

Args:
Expand All @@ -621,66 +611,95 @@ def extract_brain_region(
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"
bet = bet_dir_path / f"{self.modality_name}_bet.nii.gz"
mask_path = bet_dir_path / f"{self.modality_name}_brain_mask.nii.gz"

brain_extractor.extract(
input_image_path=self.current,
masked_image_path=atlas_bet_cm,
masked_image_path=bet,
brain_mask_path=mask_path,
log_file_path=bet_log,
)

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
self.steps[PreprocessorSteps.BET] = atlas_bet_cm
self.steps[PreprocessorSteps.BET] = bet

if self.bet:
self.current = atlas_bet_cm
self.current = bet
return mask_path

def deface(
self,
defacer,
defacer: Defacer,
defaced_dir_path: Union[str, Path],
) -> Path:
registrator: Optional[Registrator] = None,
) -> Path | None:
"""
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.
registrator (Registrator, optional): The registrator object for atlas registration.

Returns:
Path: Path to the extracted brain mask.
Path | None: Path to the defacing mask if successful, None otherwise.
"""

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"
)
mask_path = defaced_dir_path / f"{self.modality_name}_deface_mask.nii.gz"

defacer.deface(
mask_image_path=atlas_mask_path,
input_image_path=self.steps[PreprocessorSteps.BET],
)
if self.steps.get(PreprocessorSteps.BET, None) is None:
raise ValueError(
"Brain extraction must be performed before defacing. "
"Please run brain extraction first."
)

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.defacing_mask_output_path,
if defacer.force_atlas_registration and registrator is not None:
logger.info("Forcing atlas registration before defacing as requested.")
atlas_bet = defaced_dir_path / "atlas_bet.nii.gz"
atlas_bet_M = defaced_dir_path / "M_atlas_bet"

# resolve atlas image path
if isinstance(defacer.atlas_image_path, Atlas):
atlas_folder = verify_or_download_atlases()
atlas_image_path = atlas_folder / defacer.atlas_image_path.value
else:
atlas_image_path = Path(defacer.atlas_image_path)

registrator.register(
fixed_image_path=atlas_image_path,
moving_image_path=self.steps[PreprocessorSteps.BET],
transformed_image_path=atlas_bet,
matrix_path=atlas_bet_M,
log_file_path=defaced_dir_path / "atlas_bet.log",
)

deface_mask_atlas = defaced_dir_path / "deface_mask_atlas.nii.gz"
defacer.deface(
input_image_path=atlas_bet,
mask_image_path=deface_mask_atlas,
)

registrator.inverse_transform(
fixed_image_path=self.steps[PreprocessorSteps.BET],
moving_image_path=deface_mask_atlas,
transformed_image_path=mask_path,
matrix_path=atlas_bet_M,
log_file_path=defaced_dir_path / "inverse_transform.log",
)
else:
defacer.deface(
input_image_path=self.steps[PreprocessorSteps.BET],
mask_image_path=mask_path,
)

return atlas_mask_path
return mask_path
else:
logger.warning(
"Defacing method not implemented yet. Skipping defacing for this modality."
Expand Down
15 changes: 15 additions & 0 deletions brainles_preprocessing/preprocessor/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import warnings
from .atlas_centric_preprocessor import AtlasCentricPreprocessor
from .native_space_preprocessor import NativeSpacePreprocessor


# Deprecation warning for Preprocessor alias, added to ensure backward compatibility.
class Preprocessor(AtlasCentricPreprocessor):
def __init__(self, *args, **kwargs):
warnings.warn(
"Preprocessor has been renamed to AtlasCentricPreprocessor and is deprecated."
"The alias will be removed in future releases, please migrate to AtlasCentricPreprocessor.",
DeprecationWarning,
stacklevel=2,
)
super().__init__(*args, **kwargs)
Loading