Skip to content

Commit 3f8ea1d

Browse files
authored
Merge pull request #75 from BrainLesion/30-feature-request-we-want-logging
30 feature request we want logging
2 parents 07c9bf5 + 8251522 commit 3f8ea1d

File tree

3 files changed

+147
-9
lines changed

3 files changed

+147
-9
lines changed

brainles_preprocessing/preprocessor.py

Lines changed: 145 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
1+
from functools import wraps
2+
import logging
13
import os
4+
from pathlib import Path
25
import shutil
6+
import signal
37
import subprocess
8+
import sys
49
import tempfile
10+
import traceback
11+
from datetime import datetime
512
from typing import List, Optional
613

714
from auxiliary.turbopath import turbopath
@@ -10,6 +17,8 @@
1017
from .modality import Modality
1118
from .registration.registrator import Registrator
1219

20+
logger = logging.getLogger(__name__)
21+
1322

1423
class Preprocessor:
1524
"""
@@ -39,6 +48,8 @@ def __init__(
3948
use_gpu: Optional[bool] = None,
4049
limit_cuda_visible_devices: Optional[str] = None,
4150
):
51+
self._setup_logger()
52+
4253
self.center_modality = center_modality
4354
self.moving_modalities = moving_modalities
4455
self.atlas_image_path = turbopath(atlas_image_path)
@@ -93,16 +104,94 @@ def _cuda_is_available():
93104
except (subprocess.CalledProcessError, FileNotFoundError):
94105
return False
95106

107+
def ensure_remove_log_file_handler(func):
108+
@wraps(func)
109+
def wrapper(*args, **kwargs):
110+
try:
111+
return func(*args, **kwargs)
112+
finally:
113+
self = args[0]
114+
if isinstance(self, Preprocessor) and self.log_file_handler:
115+
logging.getLogger().removeHandler(self.log_file_handler)
116+
117+
return wrapper
118+
119+
def _set_log_file(self, log_file: Optional[str | Path]) -> None:
120+
"""Set the log file and remove the file handler from a potential previous run.
121+
122+
Args:
123+
log_file (str | Path): log file path
124+
"""
125+
if self.log_file_handler:
126+
logging.getLogger().removeHandler(self.log_file_handler)
127+
128+
# ensure parent directories exists
129+
log_file = Path(
130+
log_file
131+
if log_file
132+
else f"brainles_preprocessing_{datetime.now().isoformat()}.log"
133+
)
134+
log_file.parent.mkdir(parents=True, exist_ok=True)
135+
136+
self.log_file_handler = logging.FileHandler(log_file)
137+
self.log_file_handler.setFormatter(
138+
logging.Formatter(
139+
"[%(levelname)-8s | %(module)-15s | L%(lineno)-5d] %(asctime)s: %(message)s",
140+
"%Y-%m-%dT%H:%M:%S%z",
141+
)
142+
)
143+
144+
# Add the file handler to the !root! logger
145+
logging.getLogger().addHandler(self.log_file_handler)
146+
147+
def _setup_logger(self):
148+
"""Setup the logger and overwrite system hooks to add logging for exceptions and signals."""
149+
150+
logging.basicConfig(
151+
format="[%(levelname)s] %(asctime)s: %(message)s",
152+
datefmt="%Y-%m-%dT%H:%M:%S%z",
153+
level=logging.INFO,
154+
)
155+
self.log_file_handler = None
156+
157+
# overwrite system hooks to log exceptions and signals (SIGINT, SIGTERM)
158+
#! NOTE: This will note work in Jupyter Notebooks, (Without extra setup) see https://stackoverflow.com/a/70469055:
159+
def exception_handler(exception_type, value, tb):
160+
"""Handle exceptions
161+
162+
Args:
163+
exception_type (Exception): Exception type
164+
exception (Exception): Exception
165+
traceback (Traceback): Traceback
166+
"""
167+
logger.error("".join(traceback.format_exception(exception_type, value, tb)))
168+
169+
if issubclass(exception_type, SystemExit):
170+
# add specific code if exception was a system exit
171+
sys.exit(value.code)
172+
173+
def signal_handler(sig, frame):
174+
signame = signal.Signals(sig).name
175+
logger.error(f"Received signal {sig} ({signame}), exiting...")
176+
sys.exit(0)
177+
178+
sys.excepthook = exception_handler
179+
180+
signal.signal(signal.SIGINT, signal_handler)
181+
signal.signal(signal.SIGTERM, signal_handler)
182+
96183
@property
97184
def all_modalities(self):
98185
return [self.center_modality] + self.moving_modalities
99186

187+
@ensure_remove_log_file_handler
100188
def run(
101189
self,
102190
save_dir_coregistration: Optional[str] = None,
103191
save_dir_atlas_registration: Optional[str] = None,
104192
save_dir_atlas_correction: Optional[str] = None,
105193
save_dir_brain_extraction: Optional[str] = None,
194+
log_file: Optional[str] = None,
106195
):
107196
"""
108197
Execute the preprocessing pipeline, encompassing coregistration, atlas-based registration,
@@ -113,6 +202,7 @@ def run(
113202
save_dir_atlas_registration (str, optional): Directory path to save atlas registration results.
114203
save_dir_atlas_correction (str, optional): Directory path to save atlas correction results.
115204
save_dir_brain_extraction (str, optional): Directory path to save brain extraction results.
205+
log_file (str, optional): Path to save the log file. Defaults to a timestamped file in the current directory.
116206
117207
This method orchestrates the entire preprocessing workflow by sequentially performing:
118208
@@ -123,12 +213,26 @@ def run(
123213
124214
Results are saved in the specified directories, allowing for modular and configurable output storage.
125215
"""
216+
self._set_log_file(log_file=log_file)
217+
logger.info(f"{' Starting preprocessing ':=^80}")
218+
logger.info(f"Logs are saved to {self.log_file_handler.baseFilename}")
219+
logger.info(
220+
f"Received center modality: {self.center_modality.modality_name} and moving modalities: {', '.join([modality.modality_name for modality in self.moving_modalities])}"
221+
)
222+
223+
logger.info(f"{' Starting Coregistration ':-^80}")
224+
126225
# Coregister moving modalities to center modality
127226
coregistration_dir = os.path.join(self.temp_folder, "coregistration")
128227
os.makedirs(coregistration_dir, exist_ok=True)
129-
228+
logger.info(
229+
f"Coregistering {len(self.moving_modalities)} moving modalities to center modality..."
230+
)
130231
for moving_modality in self.moving_modalities:
131232
file_name = f"co__{self.center_modality.modality_name}__{moving_modality.modality_name}"
233+
logger.info(
234+
f"Registering modality {moving_modality.modality_name} (file={file_name}) to center modality..."
235+
)
132236
moving_modality.register(
133237
registrator=self.registrator,
134238
fixed_image_path=self.center_modality.current,
@@ -148,19 +252,31 @@ def run(
148252
src=coregistration_dir,
149253
save_dir=save_dir_coregistration,
150254
)
255+
logger.info(
256+
f"Coregistration complete. Output saved to {save_dir_coregistration}"
257+
)
151258

152259
# Register center modality to atlas
260+
logger.info(f"{' Starting atlas registration ':-^80}")
261+
logger.info(f"Registering center modality to atlas...")
153262
center_file_name = f"atlas__{self.center_modality.modality_name}"
154263
transformation_matrix = self.center_modality.register(
155264
registrator=self.registrator,
156265
fixed_image_path=self.atlas_image_path,
157266
registration_dir=self.atlas_dir,
158267
moving_image_name=center_file_name,
159268
)
269+
logger.info(f"Atlas registration complete. Output saved to {self.atlas_dir}")
160270

161271
# Transform moving modalities to atlas
272+
logger.info(
273+
f"Transforming {len(self.moving_modalities)} moving modalities to atlas space..."
274+
)
162275
for moving_modality in self.moving_modalities:
163276
moving_file_name = f"atlas__{moving_modality.modality_name}"
277+
logger.info(
278+
f"Transforming modality {moving_modality.modality_name} (file={moving_file_name}) to atlas space..."
279+
)
164280
moving_modality.transform(
165281
registrator=self.registrator,
166282
fixed_image_path=self.atlas_image_path,
@@ -172,36 +288,49 @@ def run(
172288
src=self.atlas_dir,
173289
save_dir=save_dir_atlas_registration,
174290
)
291+
logger.info(
292+
f"Transformations complete. Output saved to {save_dir_atlas_registration}"
293+
)
175294

176295
# Optional: additional correction in atlas space
296+
logger.info(f"{' Checking optional atlas correction ':-^80}")
177297
atlas_correction_dir = os.path.join(self.temp_folder, "atlas-correction")
178298
os.makedirs(atlas_correction_dir, exist_ok=True)
179299

180300
for moving_modality in self.moving_modalities:
181-
if moving_modality.atlas_correction is True:
301+
if moving_modality.atlas_correction:
302+
logger.info(
303+
f"Applying optional atlas correction for modality {moving_modality.modality_name}"
304+
)
182305
moving_file_name = f"atlas_corrected__{self.center_modality.modality_name}__{moving_modality.modality_name}"
183306
moving_modality.register(
184307
registrator=self.registrator,
185308
fixed_image_path=self.center_modality.current,
186309
registration_dir=atlas_correction_dir,
187310
moving_image_name=moving_file_name,
188311
)
312+
else:
313+
logger.info("Skipping optional atlas correction.")
189314

190-
if self.center_modality.atlas_correction is True:
315+
if self.center_modality.atlas_correction:
191316
shutil.copyfile(
192317
src=self.center_modality.current,
193318
dst=os.path.join(
194319
atlas_correction_dir,
195320
f"atlas_corrected__{self.center_modality.modality_name}.nii.gz",
196321
),
197322
)
323+
logger.info(
324+
f"Atlas correction complete. Output saved to {save_dir_atlas_correction}"
325+
)
198326

199327
self._save_output(
200328
src=atlas_correction_dir,
201329
save_dir=save_dir_atlas_correction,
202330
)
203331

204332
# now we save images that are not skullstripped
333+
logger.info("Saving non skull-stripped images...")
205334
for modality in self.all_modalities:
206335
if modality.raw_skull_output_path is not None:
207336
modality.save_current_image(
@@ -213,21 +342,25 @@ def run(
213342
modality.normalized_skull_output_path,
214343
normalization=True,
215344
)
216-
217345
# Optional: Brain extraction
346+
logger.info(f"{' Checking optional brain extraction ':-^80}")
218347
brain_extraction = any(modality.bet for modality in self.all_modalities)
219348
# print("brain extraction: ", brain_extraction)
220349

221350
if brain_extraction:
351+
logger.info("Starting brain extraction...")
222352
bet_dir = os.path.join(self.temp_folder, "brain-extraction")
223353
os.makedirs(bet_dir, exist_ok=True)
224354
brain_masked_dir = os.path.join(bet_dir, "brain_masked")
225355
os.makedirs(brain_masked_dir, exist_ok=True)
226-
356+
logger.info("Extracting brain region for center modality...")
227357
atlas_mask = self.center_modality.extract_brain_region(
228358
brain_extractor=self.brain_extractor, bet_dir_path=bet_dir
229359
)
230360
for moving_modality in self.moving_modalities:
361+
logger.info(
362+
f"Applying brain mask to {moving_modality.modality_name}..."
363+
)
231364
moving_modality.apply_mask(
232365
brain_extractor=self.brain_extractor,
233366
brain_masked_dir_path=brain_masked_dir,
@@ -238,8 +371,14 @@ def run(
238371
src=bet_dir,
239372
save_dir=save_dir_brain_extraction,
240373
)
374+
logger.info(
375+
f"Brain extraction complete. Output saved to {save_dir_brain_extraction}"
376+
)
377+
else:
378+
logger.info("Skipping optional brain extraction.")
241379

242380
# now we save images that are skullstripped
381+
logger.info("Saving skull-stripped images...")
243382
for modality in self.all_modalities:
244383
if modality.raw_bet_output_path is not None:
245384
modality.save_current_image(
@@ -251,6 +390,7 @@ def run(
251390
modality.normalized_bet_output_path,
252391
normalization=True,
253392
)
393+
logger.info(f"{' Preprocessing complete ':=^80}")
254394

255395
def _save_output(
256396
self,

example/example_modality_centric_preprocessor.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
from brainles_preprocessing.preprocessor import Preprocessor
99
from brainles_preprocessing.registration import (
1010
ANTsRegistrator,
11-
NiftyRegRegistrator,
12-
eRegRegistrator,
11+
# NiftyRegRegistrator,
12+
# eRegRegistrator,
1313
)
1414

1515

temporary_directory/.gitignore

Lines changed: 0 additions & 2 deletions
This file was deleted.

0 commit comments

Comments
 (0)