Source code for intensity_normalization.base_cli

"""CLI base class for normalization/preprocessing methods
Author: Jacob Reinhold <jcreinhold@gmail.com>
Created on: 06 Jun 2021
"""

from __future__ import annotations

__all__ = ["CLIMixin", "DirectoryCLI", "setup_log", "SingleImageCLI"]

import abc
import argparse
import logging
import pathlib
import sys
import typing

import pymedio.image as mioi

import intensity_normalization as intnorm
import intensity_normalization.typing as intnormt
import intensity_normalization.util.io as intnormio
from intensity_normalization import __version__ as int_norm_version

logger = logging.getLogger(__name__)

T = typing.TypeVar("T")


[docs]def setup_log(verbosity: int) -> None: """set logger with verbosity logging level and message""" if verbosity == 1: level = logging.getLevelName("INFO") elif verbosity >= 2: level = logging.getLevelName("DEBUG") else: level = logging.getLevelName("WARNING") fmt = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" logging.basicConfig(format=fmt, level=level) logging.captureWarnings(True)
[docs]class CLIMixin(metaclass=abc.ABCMeta): def __str__(self) -> str: return self.__class__.__name__
[docs] @staticmethod @abc.abstractmethod def description() -> str: raise NotImplementedError
[docs] @staticmethod @abc.abstractmethod def name() -> str: raise NotImplementedError
[docs] @staticmethod @abc.abstractmethod def fullname() -> str: raise NotImplementedError
[docs] def append_name_to_file( self, filepath: intnormt.PathLike, alternate_path: intnormt.PathLike | None = None, ) -> pathlib.Path: path, base, ext = intnormio.split_filename(filepath) if alternate_path is not None: path = pathlib.Path(alternate_path).resolve() assert path.is_dir() new_path: pathlib.Path = path / (base + f"_{self.name()}" + ext) return new_path
[docs] @classmethod @abc.abstractmethod def get_parent_parser( cls, desc: str, valid_modalities: frozenset[str] = intnorm.VALID_MODALITIES, **kwargs: typing.Any, ) -> argparse.ArgumentParser: raise NotImplementedError
[docs] @staticmethod def add_method_specific_arguments( parent_parser: argparse.ArgumentParser, ) -> argparse.ArgumentParser: return parent_parser
[docs] @classmethod def parser(cls) -> argparse.ArgumentParser: parser = cls.get_parent_parser(cls.description()) parser = cls.add_method_specific_arguments(parser) return parser
[docs] @classmethod def main( cls, parser: argparse.ArgumentParser ) -> typing.Callable[[intnormt.ArgType], int]: def _main(args: intnormt.ArgType = None) -> int: if args is None: if len(sys.argv) == 2 and sys.argv[1] == "--version": print(f"intensity-normalization version {int_norm_version}") return 0 args = parser.parse_args() elif isinstance(args, list): args = parser.parse_args(args) else: raise ValueError("args must be None or a list of strings to parse") if args.version: print(f"intensity-normalization version {int_norm_version}") setup_log(args.verbosity) cls_instance = cls.from_argparse_args(args) cls_instance.call_from_argparse_args(args) return 0 return _main
[docs] @classmethod @abc.abstractmethod def from_argparse_args(cls: typing.Type[T], args: argparse.Namespace) -> T: raise NotImplementedError
[docs] @abc.abstractmethod def call_from_argparse_args( self, args: argparse.Namespace, /, **kwargs: typing.Any ) -> None: raise NotImplementedError
[docs] @staticmethod def load_image(image_path: intnormt.PathLike) -> mioi.Image: return mioi.Image.from_path(image_path)
[docs]class SingleImageCLI(CLIMixin, metaclass=abc.ABCMeta): @abc.abstractmethod def __call__( self, image: intnormt.ImageLike, /, mask: intnormt.ImageLike | None, *, modality: intnormt.Modality = intnormt.Modality.T1, **kwargs: typing.Any, ) -> typing.Any: raise NotImplementedError
[docs] @classmethod def get_parent_parser( cls, desc: str, valid_modalities: frozenset[str] = intnorm.VALID_MODALITIES, **kwargs: typing.Any, ) -> argparse.ArgumentParser: parser = argparse.ArgumentParser( description=desc, formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) parser.add_argument( "image", type=intnormt.file_path(), help="Path of image to process.", ) parser.add_argument( "-m", "--mask", type=intnormt.file_path(), default=None, help="Path of foreground mask for image.", ) parser.add_argument( "-o", "--output", type=intnormt.save_file_path(), default=None, help="Path to save the processed image.", ) parser.add_argument( "-mo", "--modality", type=str, default="t1", choices=valid_modalities, help="Modality of the image.", ) parser.add_argument( "-v", "--verbosity", action="count", default=0, help="Increase output verbosity (e.g., -vv is more than -v).", ) parser.add_argument( "--version", action="store_true", help="Print the version of intensity-normalization.", ) return parser
[docs] def call_from_argparse_args( self, args: argparse.Namespace, /, **kwargs: typing.Any ) -> None: image = self.load_image(args.image) mask: intnormt.ImageLike | None if hasattr(args, "mask") and args.mask is not None: mask = self.load_image(args.mask) else: mask = None out = self(image, mask) if args.output is None: args.output = self.append_name_to_file(args.image) logger.debug(f"Saving output: {args.output}") if hasattr(out, "save"): out.save(args.output) elif hasattr(out, "to_filename"): out.to_filename(args.output) else: raise ValueError("Unexpected image type")
[docs]class DirectoryCLI(CLIMixin, metaclass=abc.ABCMeta):
[docs] @classmethod def get_parent_parser( cls, desc: str, valid_modalities: frozenset[str] = intnorm.VALID_MODALITIES, **kwargs: typing.Any, ) -> argparse.ArgumentParser: parser = argparse.ArgumentParser( description=desc, formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) parser.add_argument( "image_dir", type=intnormt.dir_path(), help="Path of directory containing images to normalize.", ) parser.add_argument( "-m", "--mask-dir", type=intnormt.dir_path(), default=None, help="Path of directory of foreground masks corresponding to images.", ) parser.add_argument( "-o", "--output-dir", type=intnormt.dir_path(), default=None, help="Path of directory in which to save normalized images.", ) parser.add_argument( "-mo", "--modality", type=str, default="t1", choices=intnorm.VALID_MODALITIES, help="Modality of the images.", ) parser.add_argument( "-e", "--extension", type=str, default="nii*", help="Extension of images.", ) parser.add_argument( "-v", "--verbosity", action="count", default=0, help="Increase output verbosity (e.g., -vv is more than -v).", ) parser.add_argument( "--version", action="store_true", help="Print the version of intensity-normalization.", ) return parser
[docs] @abc.abstractmethod def call_from_argparse_args( self, args: argparse.Namespace, /, **kwargs: typing.Any ) -> None: raise NotImplementedError