diff --git a/brainles_preprocessing/modality.py b/brainles_preprocessing/modality.py index f6a1e86..648e94c 100644 --- a/brainles_preprocessing/modality.py +++ b/brainles_preprocessing/modality.py @@ -5,6 +5,7 @@ from typing import Dict, Optional, Union from auxiliary.io import read_image, write_image +from loguru import logger from brainles_preprocessing.brain_extraction.brain_extractor import BrainExtractor from brainles_preprocessing.constants import Atlas, PreprocessorSteps @@ -17,8 +18,6 @@ from brainles_preprocessing.registration.registrator import Registrator from brainles_preprocessing.utils.zenodo import verify_or_download_atlases -logger = logging.getLogger(__name__) - class Modality: """ diff --git a/brainles_preprocessing/preprocessor/atlas_centric_preprocessor.py b/brainles_preprocessing/preprocessor/atlas_centric_preprocessor.py index 18e9021..ccfe83b 100644 --- a/brainles_preprocessing/preprocessor/atlas_centric_preprocessor.py +++ b/brainles_preprocessing/preprocessor/atlas_centric_preprocessor.py @@ -2,6 +2,8 @@ from pathlib import Path from typing import List, Optional, Union +from loguru import logger + from brainles_preprocessing.brain_extraction.brain_extractor import BrainExtractor from brainles_preprocessing.constants import Atlas, PreprocessorSteps from brainles_preprocessing.defacing import Defacer, QuickshearDefacer @@ -9,12 +11,8 @@ from brainles_preprocessing.n4_bias_correction import N4BiasCorrector from brainles_preprocessing.preprocessor.preprocessor import BasePreprocessor from brainles_preprocessing.registration.registrator import Registrator -from brainles_preprocessing.utils.logging_utils import LoggingManager from brainles_preprocessing.utils.zenodo import verify_or_download_atlases -logging_man = LoggingManager(name=__name__) -logger = logging_man.get_logger() - class AtlasCentricPreprocessor(BasePreprocessor): """ @@ -101,97 +99,102 @@ def run( Results are saved in the specified directories, allowing for modular and configurable output storage. """ - logging_man._set_log_file(log_file) - logger.info(f"{' Starting preprocessing ':=^80}") - logger.info(f"Logs are saved to {logging_man.log_file_handler.baseFilename}") - modality_names = ", ".join( - [modality.modality_name for modality in self.moving_modalities] - ) - logger.info( - f"Received center modality: {self.center_modality.modality_name} " - f"and moving modalities: {modality_names}" - ) - - # Co-register moving modalities to center modality - logger.info(f"{' Starting Coregistration ':-^80}") - self.run_coregistration( - save_dir_coregistration=save_dir_coregistration, - ) - logger.info( - f"Coregistration complete. Output saved to {save_dir_coregistration}" - ) - - # Register center modality to atlas - logger.info(f"{' Starting atlas registration ':-^80}") - self.run_atlas_registration( - save_dir_atlas_registration=save_dir_atlas_registration, - ) - logger.info( - f"Transformations complete. Output saved to {save_dir_atlas_registration}" - ) + logger_id = self._add_log_file_handler(log_file) + try: + logger.info(f"{' Starting preprocessing ':=^80}") + modality_names = ", ".join( + [modality.modality_name for modality in self.moving_modalities] + ) + logger.info( + f"Received center modality: {self.center_modality.modality_name} " + f"and moving modalities: {modality_names}" + ) - # Optional: additional correction in atlas space - logger.info(f"{' Checking optional atlas correction ':-^80}") - self.run_atlas_correction( - save_dir_atlas_correction=save_dir_atlas_correction, - ) + # Co-register moving modalities to center modality + logger.info(f"{' Starting Coregistration ':-^80}") + self.run_coregistration( + save_dir_coregistration=save_dir_coregistration, + ) + logger.info( + f"Coregistration complete. Output saved to {save_dir_coregistration}" + ) - # Optional: N4 bias correction - logger.info(f"{' Checking optional N4 bias correction ':-^80}") - self.run_n4_bias_correction( - save_dir_n4_bias_correction=save_dir_n4_bias_correction, - ) + # Register center modality to atlas + logger.info(f"{' Starting atlas registration ':-^80}") + self.run_atlas_registration( + save_dir_atlas_registration=save_dir_atlas_registration, + ) + logger.info( + f"Transformations complete. Output saved to {save_dir_atlas_registration}" + ) - # Now we save images that are not skullstripped (current image = atlas registered or atlas registered + corrected) - logger.info("Saving non skull-stripped images...") - for modality in self.all_modalities: - if modality.raw_skull_output_path: - modality.save_current_image( - modality.raw_skull_output_path, - normalization=False, - ) - if modality.normalized_skull_output_path: - modality.save_current_image( - modality.normalized_skull_output_path, - normalization=True, - ) + # Optional: additional correction in atlas space + logger.info(f"{' Checking optional atlas correction ':-^80}") + self.run_atlas_correction( + save_dir_atlas_correction=save_dir_atlas_correction, + ) - # Optional: Brain extraction - logger.info(f"{' Checking optional brain extraction ':-^80}") - self.run_brain_extraction( - save_dir_brain_extraction=save_dir_brain_extraction, - ) + # Optional: N4 bias correction + logger.info(f"{' Checking optional N4 bias correction ':-^80}") + self.run_n4_bias_correction( + save_dir_n4_bias_correction=save_dir_n4_bias_correction, + ) - # Defacing - logger.info(f"{' Checking optional defacing ':-^80}") - self.run_defacing( - save_dir_defacing=save_dir_defacing, - ) + # Now we save images that are not skullstripped (current image = atlas registered or atlas registered + corrected) + logger.info("Saving non skull-stripped images...") + for modality in self.all_modalities: + if modality.raw_skull_output_path: + modality.save_current_image( + modality.raw_skull_output_path, + normalization=False, + ) + if modality.normalized_skull_output_path: + modality.save_current_image( + modality.normalized_skull_output_path, + normalization=True, + ) + + # Optional: Brain extraction + logger.info(f"{' Checking optional brain extraction ':-^80}") + self.run_brain_extraction( + save_dir_brain_extraction=save_dir_brain_extraction, + ) - # move to separate method - if save_dir_transformations: - save_dir_transformations = Path(save_dir_transformations) + # Defacing + logger.info(f"{' Checking optional defacing ':-^80}") + self.run_defacing( + save_dir_defacing=save_dir_defacing, + ) - # Save transformation matrices - logger.info(f"Saving transformation matrices to {save_dir_transformations}") - for modality in self.all_modalities: + # move to separate method + if save_dir_transformations: + save_dir_transformations = Path(save_dir_transformations) - modality_transformations_dir = ( - save_dir_transformations / modality.modality_name + # Save transformation matrices + logger.info( + f"Saving transformation matrices to {save_dir_transformations}" ) - modality_transformations_dir.mkdir(exist_ok=True, parents=True) - for step, path in modality.transformation_paths.items(): - if path is not None: - shutil.copyfile( - src=str(path.absolute()), - dst=str( - modality_transformations_dir - / f"{step.value}_{path.name}" - ), - ) - - # End - logger.info(f"{' Preprocessing complete ':=^80}") + for modality in self.all_modalities: + + modality_transformations_dir = ( + save_dir_transformations / modality.modality_name + ) + modality_transformations_dir.mkdir(exist_ok=True, parents=True) + for step, path in modality.transformation_paths.items(): + if path is not None: + shutil.copyfile( + src=str(path.absolute()), + dst=str( + modality_transformations_dir + / f"{step.value}_{path.name}" + ), + ) + + # End + logger.info(f"{' Preprocessing complete ':=^80}") + finally: + # Remove log file handler if it was added + logger.remove(logger_id) def run_atlas_registration( self, save_dir_atlas_registration: Optional[Union[str, Path]] = None diff --git a/brainles_preprocessing/preprocessor/native_space_preprocessor.py b/brainles_preprocessing/preprocessor/native_space_preprocessor.py index 62cb578..e9ecee6 100644 --- a/brainles_preprocessing/preprocessor/native_space_preprocessor.py +++ b/brainles_preprocessing/preprocessor/native_space_preprocessor.py @@ -2,12 +2,10 @@ from pathlib import Path from typing import Optional, Union +from loguru import logger + from brainles_preprocessing.defacing import QuickshearDefacer from brainles_preprocessing.preprocessor.preprocessor import BasePreprocessor -from brainles_preprocessing.utils.logging_utils import LoggingManager - -logging_man = LoggingManager(name=__name__) -logger = logging_man.get_logger() class NativeSpacePreprocessor(BasePreprocessor): @@ -57,81 +55,85 @@ def run( Results are saved in the specified directories, allowing for modular and configurable output storage. """ - logging_man._set_log_file(log_file) - logger.info(f"{' Starting preprocessing ':=^80}") - logger.info(f"Logs are saved to {logging_man.log_file_handler.baseFilename}") - modality_names = ", ".join( - [modality.modality_name for modality in self.moving_modalities] - ) - logger.info( - f"Received center modality: {self.center_modality.modality_name} " - f"and moving modalities: {modality_names}" - ) - - # Co-register moving modalities to center modality - logger.info(f"{' Starting Coregistration ':-^80}") - self.run_coregistration( - save_dir_coregistration=save_dir_coregistration, - ) - logger.info( - f"Coregistration complete. Output saved to {save_dir_coregistration}" - ) - # Optional: N4 bias correction - logger.info(f"{' Checking optional N4 bias correction ':-^80}") - self.run_n4_bias_correction( - save_dir_n4_bias_correction=save_dir_n4_bias_correction, - ) - - # Now we save images that are not skullstripped (current image = atlas registered or atlas registered + corrected) - logger.info("Saving non skull-stripped images...") - for modality in self.all_modalities: - if modality.raw_skull_output_path: - modality.save_current_image( - modality.raw_skull_output_path, - normalization=False, - ) - if modality.normalized_skull_output_path: - modality.save_current_image( - modality.normalized_skull_output_path, - normalization=True, - ) + logger_id = self._add_log_file_handler(log_file) + try: + logger.info(f"{' Starting preprocessing ':=^80}") + modality_names = ", ".join( + [modality.modality_name for modality in self.moving_modalities] + ) + logger.info( + f"Received center modality: {self.center_modality.modality_name} " + f"and moving modalities: {modality_names}" + ) - # Optional: Brain extraction - logger.info(f"{' Checking optional brain extraction ':-^80}") - self.run_brain_extraction( - save_dir_brain_extraction=save_dir_brain_extraction, - ) + # Co-register moving modalities to center modality + logger.info(f"{' Starting Coregistration ':-^80}") + self.run_coregistration( + save_dir_coregistration=save_dir_coregistration, + ) + logger.info( + f"Coregistration complete. Output saved to {save_dir_coregistration}" + ) + # Optional: N4 bias correction + logger.info(f"{' Checking optional N4 bias correction ':-^80}") + self.run_n4_bias_correction( + save_dir_n4_bias_correction=save_dir_n4_bias_correction, + ) - # Defacing - logger.info(f"{' Checking optional defacing ':-^80}") - self.run_defacing( - save_dir_defacing=save_dir_defacing, - ) + # Now we save images that are not skullstripped (current image = atlas registered or atlas registered + corrected) + logger.info("Saving non skull-stripped images...") + for modality in self.all_modalities: + if modality.raw_skull_output_path: + modality.save_current_image( + modality.raw_skull_output_path, + normalization=False, + ) + if modality.normalized_skull_output_path: + modality.save_current_image( + modality.normalized_skull_output_path, + normalization=True, + ) + + # Optional: Brain extraction + logger.info(f"{' Checking optional brain extraction ':-^80}") + self.run_brain_extraction( + save_dir_brain_extraction=save_dir_brain_extraction, + ) - # move to separate method - if save_dir_transformations: - save_dir_transformations = Path(save_dir_transformations) + # Defacing + logger.info(f"{' Checking optional defacing ':-^80}") + self.run_defacing( + save_dir_defacing=save_dir_defacing, + ) - # Save transformation matrices - logger.info(f"Saving transformation matrices to {save_dir_transformations}") - for modality in self.all_modalities: + # move to separate method + if save_dir_transformations: + save_dir_transformations = Path(save_dir_transformations) - modality_transformations_dir = ( - save_dir_transformations / modality.modality_name + # Save transformation matrices + logger.info( + f"Saving transformation matrices to {save_dir_transformations}" ) - modality_transformations_dir.mkdir(exist_ok=True, parents=True) - for step, path in modality.transformation_paths.items(): - if path is not None: - shutil.copyfile( - src=str(path.absolute()), - dst=str( - modality_transformations_dir - / f"{step.value}_{path.name}" - ), - ) - - # End - logger.info(f"{' Preprocessing complete ':=^80}") + for modality in self.all_modalities: + + modality_transformations_dir = ( + save_dir_transformations / modality.modality_name + ) + modality_transformations_dir.mkdir(exist_ok=True, parents=True) + for step, path in modality.transformation_paths.items(): + if path is not None: + shutil.copyfile( + src=str(path.absolute()), + dst=str( + modality_transformations_dir + / f"{step.value}_{path.name}" + ), + ) + + # End + logger.info(f"{' Preprocessing complete ':=^80}") + finally: + logger.remove(logger_id) def run_defacing( self, save_dir_defacing: Optional[Union[str, Path]] = None diff --git a/brainles_preprocessing/preprocessor/preprocessor.py b/brainles_preprocessing/preprocessor/preprocessor.py index b460185..7c69397 100644 --- a/brainles_preprocessing/preprocessor/preprocessor.py +++ b/brainles_preprocessing/preprocessor/preprocessor.py @@ -5,10 +5,12 @@ import warnings from abc import ABC, abstractmethod from collections import Counter -from functools import wraps +from datetime import datetime from pathlib import Path from typing import List, Optional, Union +from loguru import logger + from brainles_preprocessing.brain_extraction.brain_extractor import ( BrainExtractor, HDBetExtractor, @@ -23,10 +25,6 @@ from brainles_preprocessing.registration import ANTsRegistrator from brainles_preprocessing.registration.registrator import Registrator from brainles_preprocessing.utils.citation_reminder import citation_reminder -from brainles_preprocessing.utils.logging_utils import LoggingManager - -logging_man = LoggingManager(name=__name__) -logger = logging_man.get_logger() class BasePreprocessor(ABC): @@ -46,6 +44,28 @@ class BasePreprocessor(ABC): """ + def _add_log_file_handler(self, log_file: Optional[Path | str]) -> int: + """ + Add a log file handler to the logger. + + Args: + log_file (Optional[Path | str]): Path to the log file. If None, a default log file will be created (brainles_preprocessing_{timestamp}.log). + + Returns: + int: The logger id + """ + log_file = Path( + log_file + if log_file + else f"brainles_preprocessing_{datetime.now().strftime('%Y-%m-%d_T%H-%M-%S.%f')}.log" + ) + logger_id = logger.add(log_file, level="DEBUG", catch=True) + logger.info( + f"Logging console logs and further debug information to: {log_file.absolute()}" + ) + + return logger_id + @citation_reminder def __init__( self, @@ -59,7 +79,6 @@ def __init__( use_gpu: Optional[bool] = None, limit_cuda_visible_devices: Optional[str] = None, ): - logging_man._setup_logger() if not isinstance(center_modality, CenterModality): warnings.warn( @@ -146,18 +165,6 @@ def _cuda_is_available() -> bool: except (subprocess.CalledProcessError, FileNotFoundError): return False - def ensure_remove_log_file_handler(func): - @wraps(func) - def wrapper(*args, **kwargs): - try: - return func(*args, **kwargs) - finally: - self = args[0] - if isinstance(self, BasePreprocessor): - logging_man.remove_log_file_handler() - - return wrapper - @property def all_modalities(self) -> List[Modality]: """ @@ -173,7 +180,6 @@ def requires_defacing(self) -> bool: return any(modality.requires_deface for modality in self.all_modalities) @abstractmethod - @ensure_remove_log_file_handler def run(self, *args, **kwargs): """ Execute the preprocessing pipeline, encompassing diff --git a/brainles_preprocessing/transform.py b/brainles_preprocessing/transform.py index a4c819b..4447c90 100644 --- a/brainles_preprocessing/transform.py +++ b/brainles_preprocessing/transform.py @@ -1,12 +1,10 @@ from pathlib import Path from typing import Optional, Union +from loguru import logger + from brainles_preprocessing.registration import ANTsRegistrator from brainles_preprocessing.registration.registrator import Registrator -from brainles_preprocessing.utils.logging_utils import LoggingManager - -logging_man = LoggingManager(name=__name__) -logger = logging_man.get_logger() class Transform: diff --git a/brainles_preprocessing/utils/logging_utils.py b/brainles_preprocessing/utils/logging_utils.py deleted file mode 100644 index 04652dc..0000000 --- a/brainles_preprocessing/utils/logging_utils.py +++ /dev/null @@ -1,131 +0,0 @@ -import sys -import signal -import logging -import traceback - -from datetime import datetime -from pathlib import Path -from typing import Optional, Union - - -class LoggingManager: - """ - Manages logging configurations for the application or library. - """ - - def __init__(self, name: str, log_file_path: Optional[Union[str, Path]] = None): - self.name = name - self.log_file_path = log_file_path - - # Create and configure the custom logger - self.logger = logging.getLogger(self.name) - self.log_file_handler = None - - # Disable log propagation - self.logger.propagate = False - - # Set up the logger - self._setup_logger() - - # Set up the logger file if provided - if log_file_path: - self._set_log_file(log_file_path) - - def get_logger(self) -> logging.Logger: - """ - Returns the logger instance. - """ - return self.logger - - def _setup_logger(self): - """ - Sets up the custom logger and overwrites system hooks to add logging for exceptions and signals. - """ - # Configure the custom logger if it hasn't been configured yet - if not self.logger.handlers: - self.logger.setLevel(logging.INFO) - formatter = logging.Formatter( - "[%(levelname)s | %(name)s] %(asctime)s: %(message)s", - "%Y-%m-%dT%H:%M:%S%z", - ) - - # Add a console handler - console_handler = logging.StreamHandler() - console_handler.setFormatter(formatter) - self.logger.addHandler(console_handler) - - # Overwrite system hooks to log exceptions and signals (caution advised) - sys.excepthook = self.exception_handler - signal.signal(signal.SIGINT, self.signal_handler) - signal.signal(signal.SIGTERM, self.signal_handler) - - def _set_log_file(self, log_file: str = None) -> None: - """ - Sets the log file handler for the logger. - - Args: - log_file (str | Path, optional): Log file path. If not provided, a timestamped log file is created. - """ - - # Remove existing log file handler if present - if self.log_file_handler: - self.remove_log_file_handler() - - # Ensure parent directories exist - log_file = Path( - log_file - if log_file - else f"brainles_preprocessing_{datetime.now().strftime('%Y-%m-%d_T%H-%M-%S.%f')}.log" - ) - log_file.parent.mkdir(parents=True, exist_ok=True) - - # Create and add the file handler - self.log_file_handler = logging.FileHandler(str(log_file)) - self.log_file_handler.setFormatter( - logging.Formatter( - "[%(levelname)-8s | %(name)s | %(module)-15s | L%(lineno)-5d] %(asctime)s: %(message)s", - "%Y-%m-%dT%H:%M:%S%z", - ) - ) - self.logger.addHandler(self.log_file_handler) - self.log_file_path = log_file - - def remove_log_file_handler(self) -> None: - """ - Removes the log file handler from the logger. - """ - if self.log_file_handler: - self.logger.removeHandler(self.log_file_handler) - self.log_file_handler.close() - self.log_file_handler = None - self.log_file_path = None - - # overwrite system hooks to log exceptions and signals (SIGINT, SIGTERM) - #! NOTE: This will note work in Jupyter Notebooks, (Without extra setup) see https://stackoverflow.com/a/70469055: - def exception_handler(self, exception_type, value, tb): - """Handle exceptions - - Args: - exception_type (Exception): Exception type - exception (Exception): Exception - traceback (Traceback): Traceback - """ - self.logger.error( - "".join(traceback.format_exception(exception_type, value, tb)) - ) - - if issubclass(exception_type, SystemExit): - # add specific code if exception was a system exit - sys.exit(value.code) - - def signal_handler(self, sig, frame): - """ - Handles signals by logging them and exiting. - - Args: - sig (int): Signal number - frame (FrameType): Current stack frame - """ - signame = signal.Signals(sig).name - self.logger.error(f"Received signal {sig} ({signame}), exiting...") - sys.exit(0) diff --git a/brainles_preprocessing/utils/zenodo.py b/brainles_preprocessing/utils/zenodo.py index 5b978fb..538787b 100644 --- a/brainles_preprocessing/utils/zenodo.py +++ b/brainles_preprocessing/utils/zenodo.py @@ -8,14 +8,9 @@ from typing import Dict, List, Tuple import requests +from loguru import logger from rich.progress import Progress, SpinnerColumn, TextColumn -from brainles_preprocessing.utils.logging_utils import LoggingManager - - -logging_man = LoggingManager(name=__name__) -logger = logging_man.get_logger() - ZENODO_RECORD_URL = "https://zenodo.org/api/records/15236131" ATLASES_FOLDER = Path(__file__).parent.parent / "registration" / "atlases" ATLASES_DIR_PATTERN = "atlases_v*.*.*" diff --git a/pyproject.toml b/pyproject.toml index a2f68ca..f520a3f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,6 +72,7 @@ rich = "^13.6.0" # optional registration backends itk-elastix = { version = "^0.20.0", optional = true } picsl_greedy = { version = "^0.0.6", optional = true } +loguru = "^0.7.3" [tool.poetry.extras]