Skip to content

Commit e6c139c

Browse files
authored
Merge pull request #90 from yasinzaii/main
Code Updates and Cleaning, Bug Fixes
2 parents 26dc3b8 + a707c4e commit e6c139c

File tree

10 files changed

+696
-417
lines changed

10 files changed

+696
-417
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88
<!-- [![codecov](https://codecov.io/gh/BrainLesion/brainles-preprocessing/graph/badge.svg?token=A7FWUKO9Y4)](https://codecov.io/gh/BrainLesion/brainles-preprocessing) -->
99
<!-- [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) -->
1010

11-
`BrainLes preprocessing` is a comprehensive tool for preprocessing tasks in biomedical imaging, with a focus on (but not limited to) multi-modal brain MRI. It can be used to build to build modular preprocessing pipelines:
11+
`BrainLes preprocessing` is a comprehensive tool for preprocessing tasks in biomedical imaging, with a focus on (but not limited to) multi-modal brain MRI. It can be used to build modular preprocessing pipelines:
1212

1313
This includes **normalization**, **co-registration**, **atlas registration** and **skulstripping / brain extraction**.
1414

15-
BrainLes is written `backend-agnostic` meaning it allows to swap the registration and brain extration tools.
15+
BrainLes is written `backend-agnostic` meaning it allows to swap the registration and brain extraction tools.
1616

1717
<!-- TODO mention defacing -->
1818

Lines changed: 102 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,90 +1,146 @@
11
# TODO add typing and docs
2-
from abc import abstractmethod
2+
import shutil
3+
from abc import ABC, abstractmethod
34
from pathlib import Path
4-
from shutil import copyfile
5+
from typing import Optional, Union
6+
from enum import Enum
57

68
from auxiliary.nifti.io import read_nifti, write_nifti
7-
from auxiliary.turbopath import name_extractor
89
from brainles_hd_bet import run_hd_bet
910

1011

12+
class Mode(Enum):
13+
FAST = "fast"
14+
ACCURATE = "accurate"
15+
16+
1117
class BrainExtractor:
1218
@abstractmethod
1319
def extract(
1420
self,
15-
input_image_path: str,
16-
masked_image_path: str,
17-
brain_mask_path: str,
18-
log_file_path: str,
19-
# TODO convert mode to enum
20-
mode: str,
21+
input_image_path: Union[str, Path],
22+
masked_image_path: Union[str, Path],
23+
brain_mask_path: Union[str, Path],
24+
log_file_path: Optional[Union[str, Path]],
25+
mode: Union[str, Mode],
26+
**kwargs,
2127
) -> None:
28+
"""
29+
Abstract method to extract the brain from an input image.
30+
31+
Args:
32+
input_image_path (str or Path): Path to the input image.
33+
masked_image_path (str or Path): Path where the brain-extracted image will be saved.
34+
brain_mask_path (str or Path): Path where the brain mask will be saved.
35+
log_file_path (str or Path, Optional): Path to the log file.
36+
mode (str or Mode): Extraction mode.
37+
**kwargs: Additional keyword arguments.
38+
"""
2239
pass
2340

2441
def apply_mask(
2542
self,
26-
input_image_path: Path,
27-
mask_path: Path,
28-
bet_image_path: Path,
43+
input_image_path: Union[str, Path],
44+
mask_path: Union[str, Path],
45+
bet_image_path: Union[str, Path],
2946
) -> None:
3047
"""
3148
Apply a brain mask to an input image.
3249
3350
Args:
34-
input_image_path (str): Path to the input image (NIfTI format).
35-
mask_path (str): Path to the brain mask image (NIfTI format).
36-
bet_image_path (str): Path to save the resulting masked image (NIfTI format).
51+
input_image_path (str or Path): Path to the input image (NIfTI format).
52+
mask_path (str or Path): Path to the brain mask image (NIfTI format).
53+
bet_image_path (str or Path): Path to save the resulting masked image (NIfTI format).
3754
"""
3855

39-
# read data
40-
input_data = read_nifti(input_image_path)
41-
mask_data = read_nifti(mask_path)
56+
try:
57+
# Read data
58+
input_data = read_nifti(str(input_image_path))
59+
mask_data = read_nifti(str(mask_path))
60+
except FileNotFoundError as e:
61+
raise FileNotFoundError(f"File not found: {e.filename}") from e
62+
except Exception as e:
63+
raise RuntimeError(f"Error reading files: {e}") from e
64+
65+
# Check that the input and mask have the same shape
66+
if input_data.shape != mask_data.shape:
67+
raise ValueError("Input image and mask must have the same dimensions.")
4268

43-
# mask and save it
69+
# Mask and save it
4470
masked_data = input_data * mask_data
4571

46-
write_nifti(
47-
input_array=masked_data,
48-
output_nifti_path=bet_image_path,
49-
reference_nifti_path=input_image_path,
50-
create_parent_directory=True,
51-
)
72+
try:
73+
write_nifti(
74+
input_array=masked_data,
75+
output_nifti_path=str(bet_image_path),
76+
reference_nifti_path=str(input_image_path),
77+
create_parent_directory=True,
78+
)
79+
except Exception as e:
80+
raise RuntimeError(f"Error writing output file: {e}") from e
5281

5382

5483
class HDBetExtractor(BrainExtractor):
5584
def extract(
5685
self,
57-
input_image_path: str,
58-
masked_image_path: str,
59-
brain_mask_path: str,
60-
log_file_path: str = None,
86+
input_image_path: Union[str, Path],
87+
masked_image_path: Union[str, Path],
88+
brain_mask_path: Union[str, Path],
89+
log_file_path: Optional[Union[str, Path]] = None,
6190
# TODO convert mode to enum
62-
mode: str = "accurate",
63-
device: int | str = 0,
64-
do_tta: bool = True,
91+
mode: Union[str, Mode] = Mode.ACCURATE,
92+
device: Optional[Union[int, str]] = 0,
93+
do_tta: Optional[bool] = True,
6594
) -> None:
6695
# GPU + accurate + TTA
67-
"""skullstrips images with HD-BET generates a skullstripped file and mask"""
96+
"""
97+
Skull-strips images with HD-BET and generates a skull-stripped file and mask.
98+
99+
Args:
100+
input_image_path (str or Path): Path to the input image.
101+
masked_image_path (str or Path): Path where the brain-extracted image will be saved.
102+
brain_mask_path (str or Path): Path where the brain mask will be saved.
103+
log_file_path (str or Path, Optional): Path to the log file.
104+
mode (str or Mode): Extraction mode ('fast' or 'accurate').
105+
device (str or int): Device to use for computation (e.g., 0 for GPU 0, 'cpu' for CPU).
106+
do_tta (bool): whether to do test time data augmentation by mirroring along all axes.
107+
"""
108+
109+
# Ensure mode is a Mode enum instance
110+
if isinstance(mode, str):
111+
try:
112+
mode_enum = Mode(mode.lower())
113+
except ValueError:
114+
raise ValueError(f"'{mode}' is not a valid Mode.")
115+
elif isinstance(mode, Mode):
116+
mode_enum = mode
117+
else:
118+
raise TypeError("Mode must be a string or a Mode enum instance.")
119+
120+
# Run HD-BET
68121
run_hd_bet(
69-
mri_fnames=[input_image_path],
70-
output_fnames=[masked_image_path],
71-
# device=0,
72-
# TODO consider postprocessing
73-
# postprocess=False,
74-
mode=mode,
122+
mri_fnames=[str(input_image_path)],
123+
output_fnames=[str(masked_image_path)],
124+
mode=mode_enum.value,
75125
device=device,
126+
# TODO consider postprocessing
76127
postprocess=False,
77128
do_tta=do_tta,
78129
keep_mask=True,
79130
overwrite=True,
80131
)
81132

82-
hdbet_mask_path = (
83-
Path(masked_image_path).parent
84-
/ f"{name_extractor(masked_image_path)}_mask.nii.gz"
133+
# Construct the path to the generated mask
134+
masked_image_path = Path(masked_image_path)
135+
hdbet_mask_path = masked_image_path.with_name(
136+
masked_image_path.name.replace(".nii.gz", "_mask.nii.gz")
85137
)
138+
86139
if hdbet_mask_path.resolve() != Path(brain_mask_path).resolve():
87-
copyfile(
88-
src=hdbet_mask_path,
89-
dst=brain_mask_path,
90-
)
140+
try:
141+
shutil.copyfile(
142+
src=str(hdbet_mask_path),
143+
dst=str(brain_mask_path),
144+
)
145+
except Exception as e:
146+
raise RuntimeError(f"Error copying mask file: {e}") from e
Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,75 @@
1-
from abc import abstractmethod
1+
from abc import ABC, abstractmethod
22
from pathlib import Path
3+
from typing import Union
34

45
from auxiliary.nifti.io import read_nifti, write_nifti
56

67

7-
class Defacer:
8+
class Defacer(ABC):
9+
"""
10+
Base class for defacing medical images using brain masks.
11+
12+
Subclasses should implement the `deface` method to generate a defaced image
13+
based on the provided input image and mask.
14+
"""
15+
816
@abstractmethod
917
def deface(
1018
self,
11-
input_image_path: Path,
12-
mask_image_path: Path,
19+
input_image_path: Union[str, Path],
20+
mask_image_path: Union[str, Path],
1321
) -> None:
22+
"""
23+
Generate a defacing mask provided an input image.
24+
25+
Args:
26+
input_image_path (str or Path): Path to the input image (NIfTI format).
27+
mask_image_path (str or Path): Path to the output mask image (NIfTI format).
28+
"""
1429
pass
1530

1631
def apply_mask(
1732
self,
18-
input_image_path: str,
19-
mask_path: str,
20-
defaced_image_path: str,
33+
input_image_path: Union[str, Path],
34+
mask_path: Union[str, Path],
35+
defaced_image_path: Union[str, Path],
2136
) -> None:
2237
"""
2338
Apply a brain mask to an input image.
2439
2540
Args:
26-
input_image_path (str): Path to the input image (NIfTI format).
27-
mask_path (str): Path to the brain mask image (NIfTI format).
28-
defaced_image_path (str): Path to save the resulting defaced image (NIfTI format).
41+
input_image_path (str or Path): Path to the input image (NIfTI format).
42+
mask_path (str or Path): Path to the brain mask image (NIfTI format).
43+
defaced_image_path (str or Path): Path to save the resulting defaced image (NIfTI format).
2944
"""
3045

31-
# read data
32-
input_data = read_nifti(input_image_path)
33-
mask_data = read_nifti(mask_path)
46+
if not input_image_path.is_file():
47+
raise FileNotFoundError(
48+
f"Input image file does not exist: {input_image_path}"
49+
)
50+
if not mask_path.is_file():
51+
raise FileNotFoundError(f"Mask file does not exist: {mask_path}")
52+
53+
try:
54+
# Read data
55+
input_data = read_nifti(str(input_image_path))
56+
mask_data = read_nifti(str(mask_path))
57+
except Exception as e:
58+
raise RuntimeError(
59+
f"An error occurred while reading input files: {e}"
60+
) from e
61+
62+
# Check that the input and mask have the same shape
63+
if input_data.shape != mask_data.shape:
64+
raise ValueError("Input image and mask must have the same dimensions.")
3465

35-
# mask and save it
66+
# Apply mask (element-wise multiplication)
3667
masked_data = input_data * mask_data
3768

69+
# Save the defaced image
3870
write_nifti(
3971
input_array=masked_data,
40-
output_nifti_path=defaced_image_path,
41-
reference_nifti_path=input_image_path,
72+
output_nifti_path=str(defaced_image_path),
73+
reference_nifti_path=str(input_image_path),
4274
create_parent_directory=True,
4375
)

brainles_preprocessing/defacing/quickshear/quickshear.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
from pathlib import Path
2+
from typing import Union
23

34
import nibabel as nib
5+
46
from brainles_preprocessing.defacing.defacer import Defacer
57
from brainles_preprocessing.defacing.quickshear.nipy_quickshear import run_quickshear
6-
from numpy.typing import NDArray
78
from auxiliary.nifti.io import write_nifti
89

910

@@ -37,17 +38,26 @@ def __init__(self, buffer: float = 10.0):
3738
super().__init__()
3839
self.buffer = buffer
3940

40-
def deface(self, mask_image_path: Path, bet_img_path: Path) -> None:
41-
"""Deface image using Quickshear algorithm
41+
def deface(
42+
self,
43+
input_image_path: Union[str, Path],
44+
mask_image_path: Union[str, Path],
45+
) -> None:
46+
"""
47+
Generate a defacing mask using Quickshear algorithm.
48+
49+
Note:
50+
The input image must be a brain-extracted (skull-stripped) image.
4251
4352
Args:
44-
bet_img_path (Path): Path to the brain extracted image
53+
input_image_path (str or Path): Path to the brain-extracted input image.
54+
mask_image_path (str or Path): Path to save the generated mask image.
4555
"""
4656

47-
bet_img = nib.load(bet_img_path)
57+
bet_img = nib.load(str(input_image_path))
4858
mask = run_quickshear(bet_img=bet_img, buffer=self.buffer)
4959
write_nifti(
5060
input_array=mask,
51-
output_nifti_path=mask_image_path,
52-
reference_nifti_path=bet_img_path,
61+
output_nifti_path=str(mask_image_path),
62+
reference_nifti_path=str(input_image_path),
5363
)

0 commit comments

Comments
 (0)