Source code for hazenlib

"""
Welcome to the hazen Command Line Interface

The following Tasks are available:
- ACR phantom:
acr_snr | acr_slice_position | acr_slice_thickness | acr_spatial_resolution | acr_uniformity | acr_ghosting | acr_geometric_accuracy
- MagNET Test Objects:
snr | snr_map | slice_position | slice_width | spatial_resolution | uniformity | ghosting
- Caliber phantom:
relaxometry

All tasks can be run by executing 'hazen <task> <folder>'. Optional flags are available for the Tasks; see the General
Options section below. The 'acr_snr' and 'snr' Tasks have additional optional flags, also detailed below.

Usage:
    hazen <task> <folder> [options]
    hazen snr <folder> [--measured_slice_width=<mm>] [--coil=<head or body>] [options]
    hazen acr_snr <folder> [--measured_slice_width=<mm>] [--subtract=<folder2>] [options]
    hazen relaxometry <folder> --calc=<T1> --plate_number=<4> [options]

    hazen -h | --help
    hazen --version

General Options: available for all Tasks
    --report                     Whether to generate visualisation of the measurement steps.
    --output=<path>              Provide a folder where report images are to be saved.
    --verbose                    Whether to provide additional metadata about the calculation in the result (slice position and relaxometry tasks)
    --log=<level>                Set the level of logging based on severity. Available levels are "debug", "warning", "error", "critical", with "info" as default.

acr_snr & snr Task options:
    --measured_slice_width=<mm>  Provide a slice width to be used for SNR measurement, by default it is parsed from the DICOM (optional for acr_snr and snr)
    --subtract=<folder2>         Provide a second folder path to calculate SNR by subtraction for the ACR phantom (optional for acr_snr)

relaxometry Task options:
    --calc=<n>                   Choose 'T1' or 'T2' for relaxometry measurement (required)
    --plate_number=<n>           Which plate to use for measurement: 4 or 5 (required)
"""

import sys
import json
import inspect
import logging
import importlib

from docopt import docopt
from hazenlib.utils import get_dicom_files
from hazenlib._version import __version__

"""
Hazen is designed to measure the same parameters from multiple images.
While some tasks require a set of multiple images (within the same folder),
such as slice position, SNR and all ACR tasks,
the majority of the calculations are performed on a single image at a time,
and bulk processing all images in the input folder with the same task.

In Sep 2023 a design decision was made to pass the minimum number of files
to the task.run() functions.
Below is a list of the single image tasks where the task.run() will be called
on each image in the folder, while other tasks are being passed ALL image files.
"""
single_image_tasks = [
    "ghosting",
    "uniformity",
    "spatial_resolution",
    "slice_width",
    "snr_map",
]


[docs]def init_task(selected_task, files, report, report_dir, **kwargs): """Initialise object of the correct HazenTask class Args: selected_task (string): name of task script/module to load files (list): list of filepaths to DICOM images report (bool): whether to generate report images report_dir (string): path to folder to save report images to kwargs: any other key word arguments Returns: an object of the specified HazenTask class """ task_module = importlib.import_module(f"hazenlib.tasks.{selected_task}") try: task = getattr(task_module, selected_task.capitalize())( input_data=files, report=report, report_dir=report_dir, **kwargs ) except: class_list = [ cls.__name__ for _, cls in inspect.getmembers( sys.modules[task_module.__name__], lambda x: inspect.isclass(x) and (x.__module__ == task_module.__name__), ) ] if len(class_list) == 1: task = getattr(task_module, class_list[0])( input_data=files, report=report, report_dir=report_dir, **kwargs ) else: raise Exception( f"Task {task_module} has multiple class definitions: {class_list}" ) return task
[docs]def main(): """Main entrypoint to hazen""" arguments = docopt(__doc__, version=__version__) files = get_dicom_files(arguments["<folder>"]) # Set common options log_levels = { "critical": logging.CRITICAL, "debug": logging.DEBUG, "info": logging.INFO, "warning": logging.WARNING, "error": logging.ERROR, } if arguments["--log"] in log_levels.keys(): level = log_levels[arguments["--log"]] logging.getLogger().setLevel(level) else: # logging.basicConfig() logging.getLogger().setLevel(logging.INFO) report = arguments["--report"] report_dir = arguments["--output"] if arguments["--output"] else None verbose = arguments["--verbose"] # Parse the task and optional arguments: if arguments["snr"] or arguments["<task>"] == "snr": selected_task = "snr" task = init_task( selected_task, files, report, report_dir, measured_slice_width=arguments["--measured_slice_width"], coil=arguments["--coil"], ) result = task.run() elif arguments["acr_snr"] or arguments["<task>"] == "acr_snr": selected_task = "acr_snr" task = init_task( selected_task, files, report, report_dir, subtract=arguments["--subtract"], measured_slice_width=arguments["--measured_slice_width"], ) result = task.run() elif arguments["relaxometry"] or arguments["<task>"] == "relaxometry": selected_task = "relaxometry" task = init_task(selected_task, files, report, report_dir) result = task.run( calc=arguments["--calc"], plate_number=arguments["--plate_number"], verbose=arguments["--verbose"], ) else: selected_task = arguments["<task>"] if selected_task in single_image_tasks: # Ghosting, Uniformity, Spatial resolution, SNR map, Slice width for file in files: task = init_task(selected_task, [file], report, report_dir) result = task.run() result_string = json.dumps(result, indent=2) print(result_string) return else: # Slice Position task, all ACR tasks except SNR task = init_task(selected_task, files, report, report_dir, verbose=verbose) result = task.run() result_string = json.dumps(result, indent=2) print(result_string)
if __name__ == "__main__": main()