From 69c602d656c0d14f3787dedefab9179cf9a939bd Mon Sep 17 00:00:00 2001 From: sophie22 Date: Mon, 3 Jul 2023 14:04:21 +0100 Subject: [PATCH 01/27] move hazen's main function to hazen.py --- hazenlib/__init__.py | 173 --------------------------------------- hazenlib/hazen.py | 188 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 188 insertions(+), 173 deletions(-) create mode 100644 hazenlib/hazen.py diff --git a/hazenlib/__init__.py b/hazenlib/__init__.py index f4f98aa4..868f103f 100644 --- a/hazenlib/__init__.py +++ b/hazenlib/__init__.py @@ -1,111 +1,8 @@ -""" -MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM -MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWWWNNNNNNNWWWWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM -MMMMMMMMMMMMMMMMMMMMMMMMMMMWWNNNNXXXXXXXXXXXXXXXKKKXNNWWMMMMMMMMMMMMMMMMMMMMMMMM -MMMMMMMMMMMMMMMMMMMMWWNNXKK0OOO0XNXK0KKXXXXKXNNX0OOOO0KKXXNNWMMMMMMMMMMMMMMMMMMM -MMMMMMMMMMMMMMMMWNXXK0kddxOkxO0KXXKXXXXXXXXXXXNXXXKOxdxkOO00KXWMMMMMMMMMMMMMMMMM -MMMMMMMMMMMMWWNKOxdxxxxxxxO0KNXKXNXKKK0KKKXKKKKKK0kkOkxolodxkk0XWMMMMMMMMMMMMMMM -MMMMMMMMMWNXXKOOkkxxxkOddOXNXKOOO0KKXXXNNWWNXK00OxdkXNX0kdc:coox0WMMMMMMMMMMMMMM -MMMMMMMWNKOOkxxxxxxkOOOOO00kkO0KNNWWNX0OkO0OxxO000KXXNNNWNXOdc:ldkNMMMMMMMMMMMMM -MMMMMMWKOxoc:lddxxxxkO0KKK0OkxkO00Okxlccclooooddooodxxk0KXXNWXkxddxKWMMMMMMMMMMM -MMMMMNklc:;:clooxxxkkkkO00OOOxc,,;;,,,,;;;;::looddxxkdoloxk0KXXNX0xx0WMMMMMMMMMM -MMMMNx:,;coxOXNNWWNNXXXXKOkxdl:,...';:clodkOKXNNNNNNWNXOdoodkkOKNWN0kONMMMMMMMMM -MMMNx:coxO0KKK0OkkkO0KXXKOkxoc;'.',;coxkO0KKKKKKKKKXNWWWX0kdddxkOKNWWKKXWMMMMMMM -MMW0odkOkkkxddxO0XXXXXKK0Odoooc'...,:ccccccccloodxxkOO0KXNNKkxxxxkOKXNXXXNMMMMMM -MNOdxxxdodxk0XNNXKKKXXX0OO00kc......,coxkkOkxxddoooddxxkk0XNNXOxddxkOO0KXXXWMMMM -XxloddoodOXNNXKKKXNWXOkOKX0l'.........;clldkO0KKKKKK0OOkkkO0KNNXOxddxkkO0KXKNWMM -OolllldOKXX0O0XWMWKxoxKNXk;............',;llcldxk0KNWWWWNK0OOOKXNXOxddxkkOKXKXWM -dc:cldk0OkkKNWWWXkox0XXKd'................;oxdlccldxOXNWMMWNX0OOKXNXOxddxxk0XKXM -kc:oxdxddOXWNXKOdx0NNXKl....................lKXKOkdoodxOKXNWMWX0OOKXNKOkxdxOXNXN -Kloxllld0K0KK0doONWNNKl......................:ONWWWNXKOkk0KKXWMNKOOOKNX0kxxk0NXX -W0o:,:xOxdk00xxKWWWWXl........................,o0XNWWMWNXXXKKNWMWKOkkOKNX0Ok0NXX -MWKl:ldlcx0OkOXWMWMXl....,:loxkxdlcc:::::;,'....,lkKXNWNNNXKKXNWMN0kxkOXWX00KNXX -MMMXxc;;cxkxkKWMMMXl..',:okOO0KKKKK00KXXXXK0Okdc,.'lkKKKKXNXKKXNMMXOxxkOXXOO0NXX -MMMMXl,;codx0WMMMXl'',:ldO0KXXNWWNNNNNNXXNNNNNNX0kl,,lk0OO0XXKKXWMWKkxxk0KOOOKXX -MMMMWO:;lookXMWWNd,;loxOXNNNNNKKNWWNXKKXXXNNWWNNNX0d,.'lxkkOKK0KNWMXkxkkOOkOOKKX -MMMMMXoclcxKWWNNk;';:clodk0KNWNXNWNK000kkdoddkKXXXOoc,..,lkkO000XNWKkxkkOkkOO00K -MMMMMXocclOXWWNO:'.',,,'.',:xXWNXOkkko:,''.'':ooloxxol:...cxkOOOKNW0dxxkOkxkO00K -MMMMMXdc:oO0XNXo.,:ccldoc,..,d0KOdodko:;::::;:lxklcdOOkc...:xkkkOXN0dxxkOxxkO0OK -MMMMMWOc;ok0KXKl'::;:oddol;..cOOkdokOd:cccclooooxd:;dKX0l'..:dxdkKX0ddxxkdxkOOON -MMMMMMNx;;oxk00l',;;,..;ddl,.;xOkdodOkdc;,,locxOdldOKWWNO:...:ol:oOkodkxddxOOOKW -MMMMMMMNx;;loxkd;,;;:cldkdc;'lKNN0xOXKOkkk0K000K0OKWNNXX0c.. 'c:';xxlododk0OO0WM -MMMMMMMMNOo:;:colodc:lddl;''cKWWNKkxO0OxxkKXNNNXKXNNNNNXkc.. .,;.'cc::cok0OkONMM -MMMMMMMMMMNOo;,;:xXKOxdoc;,c0WWWNX000KKK0KXXNXXXNWNNXXN0c.....,,..'..;lxxdoONMMM -MMMMMMMMMMMMW0d:;dKXNX0kdll0WMWWNXKXXXXXXXKKKXNNWWWWNXK0o...........;:c::dKWMMMM -MMMMMMMMMMMMMMW0:cOKNWXOllONWWNKK0KXXNNNNNNNXXKKXXXXXKKx:........',,;;:dKWMMMMMM -MMMMMMMMMMMMMMMWx:oO0Kk:,oKNWWXOkO00O0XNWWWWNXK000000Od:'.......'';ld0NMMMMMMMMM -MMMMMMMMMMMMMMMM0;,lxxc',o0XNNKdlOKOxxxOKXNNNNX0Okolxo,....',''cdkKWMMMMMMMMMMMM -MMMMMMMMMMMMMMMXo..:ddc,',lkkxl;,:lldK0xxk0XXXK0Odc',;'''...'',xWMMMMMMMMMMMMMMM -MMMMMMMMMMMMMMMNk:.'dko:'',;;;:;',cloxO0K0OO000Odcc:....'....''oNMMMMMMMMMMMMMMM -MMMMMMMMMMMMMMMMNo..coc;''',;;;:,,,,,,;lxxolodxoc;;c,..;,'.''..oNMMMMMMMMMMMMMMM -MMMMMMMMMMMMMMMMWO;.';:;:;;:od:cc:;;::,',,;,;;;;,,,c;.'::;'.,,..ckXMMMMMMMMMMMMM -MMMMMMMMMMMMMMMMM0:..,:;:clccl;,:,,,,''...':lc::loclc',::,..'.. .,oKWMMMMMMMMMM -MMMMMMMMMMMMMMMMWk'':lc,,,,coooooddl;......:occlllld:.,:,...... 'l0WMMMMMMMM -MMMMMMMMMMMMMMMMNd';loc:;'',:cccclc:,.......:;,cc:cl,'cc;....... .lONMMMMMM -MMMMMMMMMMMMMMMMXl';cccc;;:,'.. ..''''.....,c;:l:cc,,:c:,'.''..... .;d0NMMM -MMMMMMMMMMMMMMMMKc'',:loc::;,...'c;''...'',:;';lcc;,c:,,,.''.. .... ....;d0W -MMMMMMMMMMMMMMWXxlc;',ll:;;:;...;l;''',;:c:,..':;,;;;'.'..''. ............ .: -MMMMMMMMMMMMN0o;:do;',;,;ooc:,',,:;',:::cc;.'',:,.,,,'..................... . -MMMMMMMMMN0d;...,lc,'.':odl::;;:,';;;clc:;,'..,,''''.'...................... . -MMMMMMWKd;. ....;cc'.:odollc;::'.';,;l:,,;,.',.'',,.',,,'...................... -MMMMWKd,..........;;..,:ooloccol;',:;,lc,,',';:,;:'...''.........''''''''''..''' -MMMW0d:'..........',..';cclooxxdooll::oc;,...:c',:,.,:;'.''''..''',;;,,,,,,,;ccl -MMMWN0d:;,'............';:clodxxxxlc:;:;:,..;;'''::':c,,,;;,',,,;;,;;,'',;ldk00K -MMMMMWN0koc:,,'''..''''.';;;lldkxo::;;;,''.','..,:,''',,,,',,,:::::;;;;;cdKWWMMM -MMMMMMMMMWXKOxlc;,...','...,:;coc;;;;'''''.....,:,...''''',;::clcclddxk0XWMMMMMM -MMMMMMMMMMMMMWNN0d:,........''';;..,,........';;,'','',;;:codxkOO0KNWWMMMMMMMMMM -MMMMMMMMMMMMMMMMMWX0xl;,...........',...,...':;;;;;::codk0XNWWMMMMMMMMMMMMMMMMMM -MMMMMMMMMMMMMMMMMMMMMWXOdc,'..',,,,,;;,,,'.,;ccldkOOKXNWMMMMMMMMMMMMMMMMMMMMMMMM -MMMMMMMMMMMMMMMMMMMMMMMMWN0xoc:cccccldkxxkOOO0KXNWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM -MMMMMMMMMMMMMMMMMMMMMMMMMMMMN0xc;;::cxXMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM - - - - -`7MMF' `7MMF' - MM MM - MM MM ,6"Yb. M""MMV .gP"Ya 7MMpMMMb. - MMmmmmmmMM 8) MM ' AMV ,M' Yb MM MM - MM MM ,pm9MM AMV 8M~~~~~' MM MM - MM MM 8M MM AMV ,YM. , MM MM -.JMML. .JMML.`Moo9^Yo.AMMmmmM `Mbmmd'.JMML JMML. - - - -Welcome to the Hazen Command Line Interface -Usage: - hazen [--measured_slice_width=] [--subtract=] [--report] [--output=] - [--calc_t1 | --calc_t2] [--plate_number=] [--show_template_fit] - [--show_relax_fits] [--show_rois] [--log=] [--verbose] - hazen -h|--help - hazen --version -Options: -Report is an optional argument needed if you want to get a plot of your results. -'Calc_t1', 'calc_t', 'plate_number=', 'show_template_fit', 'show_relax_fits', 'show_rois', 'verbose' are optional arguments for the relaxometry function. -'Measured_slice_width' is an optional argument for the SNR function. -'Log' is an optional argument that allows users to set the severity of the logs. - snr | slice_position | slice_width | spatial_resolution | uniformity | ghosting | relaxometry | snr_map | - acr_ghosting | acr_uniformity | acr_spatial_resolution | acr_slice_thickness | acr_snr | acr_slice_position | acr_geometric_accuracy - - --report - -""" -import importlib -import inspect -import logging -import sys -import pprint -import os import numpy as np import pydicom -from docopt import docopt -from hazenlib.logger import logger -from hazenlib.HazenTask import HazenTask -from hazenlib.tools import is_dicom_file, get_dicom_files from hazenlib._version import __version__ -import hazenlib.exceptions EXCLUDED_FILES = ['.DS_Store'] @@ -333,73 +230,3 @@ def parse_relaxometry_data(task, arguments, dicom_objects, **relaxometry_args) -def main(): - arguments = docopt(__doc__, version=__version__) - task_module = importlib.import_module(f"hazenlib.tasks.{arguments['']}") - files = get_dicom_files(arguments['']) - pp = pprint.PrettyPrinter(indent=4, depth=1, width=1) - - 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) - - class_list = [cls 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: - raise Exception(f'Task {task_module} has multiple class definitions: {class_list}') - - task = getattr(task_module, class_list[0].__name__)(data_paths=files, - report=arguments['--report'], - # TODO: Is this necessary? See HazenTask __init__() - report_dir=[arguments['--output'] if arguments[ - '--output'] else os.path.join(os.getcwd(), 'report')][0]) - - if not arguments[''] == 'snr' and arguments['--measured_slice_width']: - raise Exception("the (--measured_slice_width) option can only be used with snr") - elif not arguments[''] == 'acr_snr' and arguments['--subtract']: - raise Exception("the (--subtract) option can only be used with acr_snr") - elif arguments[''] == 'snr' and arguments['--measured_slice_width']: - measured_slice_width = float(arguments['--measured_slice_width']) - logger.info(f'Calculating SNR with measured slice width {measured_slice_width}') - result = task.run(measured_slice_width) - elif arguments[''] == 'acr_snr': - acr_snr_cli_args = {'--subtract'} - - acr_snr_args = {} - for key in acr_snr_cli_args: - acr_snr_args[key[2:]] = arguments[key] - - result = task.run(**acr_snr_args) - # TODO: Refactor Relaxometry task into HazenTask object Relaxometry not currently converted to HazenTask object - - # this task accessible in the CLI using the old syntax until it can be refactored - elif arguments[''] == 'relaxometry': - task = importlib.import_module(f"hazenlib.{arguments['']}") - dicom_objects = [pydicom.read_file(x, force=True) for x in files if is_dicom_file(x)] - result = parse_relaxometry_data(task, arguments, dicom_objects, report=True) - else: - result = task.run() - - return pp.pformat(result) - - -def entry_point(): - result = main() - print(result) - - -if __name__ == "__main__": - entry_point() diff --git a/hazenlib/hazen.py b/hazenlib/hazen.py new file mode 100644 index 00000000..295321f1 --- /dev/null +++ b/hazenlib/hazen.py @@ -0,0 +1,188 @@ +""" +MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM +MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWWWNNNNNNNWWWWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM +MMMMMMMMMMMMMMMMMMMMMMMMMMMWWNNNNXXXXXXXXXXXXXXXKKKXNNWWMMMMMMMMMMMMMMMMMMMMMMMM +MMMMMMMMMMMMMMMMMMMMWWNNXKK0OOO0XNXK0KKXXXXKXNNX0OOOO0KKXXNNWMMMMMMMMMMMMMMMMMMM +MMMMMMMMMMMMMMMMWNXXK0kddxOkxO0KXXKXXXXXXXXXXXNXXXKOxdxkOO00KXWMMMMMMMMMMMMMMMMM +MMMMMMMMMMMMWWNKOxdxxxxxxxO0KNXKXNXKKK0KKKXKKKKKK0kkOkxolodxkk0XWMMMMMMMMMMMMMMM +MMMMMMMMMWNXXKOOkkxxxkOddOXNXKOOO0KKXXXNNWWNXK00OxdkXNX0kdc:coox0WMMMMMMMMMMMMMM +MMMMMMMWNKOOkxxxxxxkOOOOO00kkO0KNNWWNX0OkO0OxxO000KXXNNNWNXOdc:ldkNMMMMMMMMMMMMM +MMMMMMWKOxoc:lddxxxxkO0KKK0OkxkO00Okxlccclooooddooodxxk0KXXNWXkxddxKWMMMMMMMMMMM +MMMMMNklc:;:clooxxxkkkkO00OOOxc,,;;,,,,;;;;::looddxxkdoloxk0KXXNX0xx0WMMMMMMMMMM +MMMMNx:,;coxOXNNWWNNXXXXKOkxdl:,...';:clodkOKXNNNNNNWNXOdoodkkOKNWN0kONMMMMMMMMM +MMMNx:coxO0KKK0OkkkO0KXXKOkxoc;'.',;coxkO0KKKKKKKKKXNWWWX0kdddxkOKNWWKKXWMMMMMMM +MMW0odkOkkkxddxO0XXXXXKK0Odoooc'...,:ccccccccloodxxkOO0KXNNKkxxxxkOKXNXXXNMMMMMM +MNOdxxxdodxk0XNNXKKKXXX0OO00kc......,coxkkOkxxddoooddxxkk0XNNXOxddxkOO0KXXXWMMMM +XxloddoodOXNNXKKKXNWXOkOKX0l'.........;clldkO0KKKKKK0OOkkkO0KNNXOxddxkkO0KXKNWMM +OolllldOKXX0O0XWMWKxoxKNXk;............',;llcldxk0KNWWWWNK0OOOKXNXOxddxkkOKXKXWM +dc:cldk0OkkKNWWWXkox0XXKd'................;oxdlccldxOXNWMMWNX0OOKXNXOxddxxk0XKXM +kc:oxdxddOXWNXKOdx0NNXKl....................lKXKOkdoodxOKXNWMWX0OOKXNKOkxdxOXNXN +Kloxllld0K0KK0doONWNNKl......................:ONWWWNXKOkk0KKXWMNKOOOKNX0kxxk0NXX +W0o:,:xOxdk00xxKWWWWXl........................,o0XNWWMWNXXXKKNWMWKOkkOKNX0Ok0NXX +MWKl:ldlcx0OkOXWMWMXl....,:loxkxdlcc:::::;,'....,lkKXNWNNNXKKXNWMN0kxkOXWX00KNXX +MMMXxc;;cxkxkKWMMMXl..',:okOO0KKKKK00KXXXXK0Okdc,.'lkKKKKXNXKKXNMMXOxxkOXXOO0NXX +MMMMXl,;codx0WMMMXl'',:ldO0KXXNWWNNNNNNXXNNNNNNX0kl,,lk0OO0XXKKXWMWKkxxk0KOOOKXX +MMMMWO:;lookXMWWNd,;loxOXNNNNNKKNWWNXKKXXXNNWWNNNX0d,.'lxkkOKK0KNWMXkxkkOOkOOKKX +MMMMMXoclcxKWWNNk;';:clodk0KNWNXNWNK000kkdoddkKXXXOoc,..,lkkO000XNWKkxkkOkkOO00K +MMMMMXocclOXWWNO:'.',,,'.',:xXWNXOkkko:,''.'':ooloxxol:...cxkOOOKNW0dxxkOkxkO00K +MMMMMXdc:oO0XNXo.,:ccldoc,..,d0KOdodko:;::::;:lxklcdOOkc...:xkkkOXN0dxxkOxxkO0OK +MMMMMWOc;ok0KXKl'::;:oddol;..cOOkdokOd:cccclooooxd:;dKX0l'..:dxdkKX0ddxxkdxkOOON +MMMMMMNx;;oxk00l',;;,..;ddl,.;xOkdodOkdc;,,locxOdldOKWWNO:...:ol:oOkodkxddxOOOKW +MMMMMMMNx;;loxkd;,;;:cldkdc;'lKNN0xOXKOkkk0K000K0OKWNNXX0c.. 'c:';xxlododk0OO0WM +MMMMMMMMNOo:;:colodc:lddl;''cKWWNKkxO0OxxkKXNNNXKXNNNNNXkc.. .,;.'cc::cok0OkONMM +MMMMMMMMMMNOo;,;:xXKOxdoc;,c0WWWNX000KKK0KXXNXXXNWNNXXN0c.....,,..'..;lxxdoONMMM +MMMMMMMMMMMMW0d:;dKXNX0kdll0WMWWNXKXXXXXXXKKKXNNWWWWNXK0o...........;:c::dKWMMMM +MMMMMMMMMMMMMMW0:cOKNWXOllONWWNKK0KXXNNNNNNNXXKKXXXXXKKx:........',,;;:dKWMMMMMM +MMMMMMMMMMMMMMMWx:oO0Kk:,oKNWWXOkO00O0XNWWWWNXK000000Od:'.......'';ld0NMMMMMMMMM +MMMMMMMMMMMMMMMM0;,lxxc',o0XNNKdlOKOxxxOKXNNNNX0Okolxo,....',''cdkKWMMMMMMMMMMMM +MMMMMMMMMMMMMMMXo..:ddc,',lkkxl;,:lldK0xxk0XXXK0Odc',;'''...'',xWMMMMMMMMMMMMMMM +MMMMMMMMMMMMMMMNk:.'dko:'',;;;:;',cloxO0K0OO000Odcc:....'....''oNMMMMMMMMMMMMMMM +MMMMMMMMMMMMMMMMNo..coc;''',;;;:,,,,,,;lxxolodxoc;;c,..;,'.''..oNMMMMMMMMMMMMMMM +MMMMMMMMMMMMMMMMWO;.';:;:;;:od:cc:;;::,',,;,;;;;,,,c;.'::;'.,,..ckXMMMMMMMMMMMMM +MMMMMMMMMMMMMMMMM0:..,:;:clccl;,:,,,,''...':lc::loclc',::,..'.. .,oKWMMMMMMMMMM +MMMMMMMMMMMMMMMMWk'':lc,,,,coooooddl;......:occlllld:.,:,...... 'l0WMMMMMMMM +MMMMMMMMMMMMMMMMNd';loc:;'',:cccclc:,.......:;,cc:cl,'cc;....... .lONMMMMMM +MMMMMMMMMMMMMMMMXl';cccc;;:,'.. ..''''.....,c;:l:cc,,:c:,'.''..... .;d0NMMM +MMMMMMMMMMMMMMMMKc'',:loc::;,...'c;''...'',:;';lcc;,c:,,,.''.. .... ....;d0W +MMMMMMMMMMMMMMWXxlc;',ll:;;:;...;l;''',;:c:,..':;,;;;'.'..''. ............ .: +MMMMMMMMMMMMN0o;:do;',;,;ooc:,',,:;',:::cc;.'',:,.,,,'..................... . +MMMMMMMMMN0d;...,lc,'.':odl::;;:,';;;clc:;,'..,,''''.'...................... . +MMMMMMWKd;. ....;cc'.:odollc;::'.';,;l:,,;,.',.'',,.',,,'...................... +MMMMWKd,..........;;..,:ooloccol;',:;,lc,,',';:,;:'...''.........''''''''''..''' +MMMW0d:'..........',..';cclooxxdooll::oc;,...:c',:,.,:;'.''''..''',;;,,,,,,,;ccl +MMMWN0d:;,'............';:clodxxxxlc:;:;:,..;;'''::':c,,,;;,',,,;;,;;,'',;ldk00K +MMMMMWN0koc:,,'''..''''.';;;lldkxo::;;;,''.','..,:,''',,,,',,,:::::;;;;;cdKWWMMM +MMMMMMMMMWXKOxlc;,...','...,:;coc;;;;'''''.....,:,...''''',;::clcclddxk0XWMMMMMM +MMMMMMMMMMMMMWNN0d:,........''';;..,,........';;,'','',;;:codxkOO0KNWWMMMMMMMMMM +MMMMMMMMMMMMMMMMMWX0xl;,...........',...,...':;;;;;::codk0XNWWMMMMMMMMMMMMMMMMMM +MMMMMMMMMMMMMMMMMMMMMWXOdc,'..',,,,,;;,,,'.,;ccldkOOKXNWMMMMMMMMMMMMMMMMMMMMMMMM +MMMMMMMMMMMMMMMMMMMMMMMMWN0xoc:cccccldkxxkOOO0KXNWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM +MMMMMMMMMMMMMMMMMMMMMMMMMMMMN0xc;;::cxXMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM + + +`7MMF' `7MMF' + MM MM + MM MM ,6"Yb. M""MMV .gP"Ya 7MMpMMMb. + MMmmmmmmMM 8) MM ' AMV ,M' Yb MM MM + MM MM ,pm9MM AMV 8M~~~~~' MM MM + MM MM 8M MM AMV ,YM. , MM MM +.JMML. .JMML.`Moo9^Yo.AMMmmmM `Mbmmd'.JMML JMML. + + + +Welcome to the Hazen Command Line Interface +Usage: + hazen [--measured_slice_width=] [--subtract=] [--report] [--output=] + [--calc_t1 | --calc_t2] [--plate_number=] [--show_template_fit] + [--show_relax_fits] [--show_rois] [--log=] [--verbose] + hazen -h|--help + hazen --version +Options: +Report is an optional argument needed if you want to get a plot of your results. +'Calc_t1', 'calc_t', 'plate_number=', 'show_template_fit', 'show_relax_fits', 'show_rois', 'verbose' are optional arguments for the relaxometry function. +'Measured_slice_width' is an optional argument for the SNR function. +'Log' is an optional argument that allows users to set the severity of the logs. + snr | slice_position | slice_width | spatial_resolution | uniformity | ghosting | relaxometry | snr_map | + acr_ghosting | acr_uniformity | acr_spatial_resolution | acr_slice_thickness | acr_snr | acr_slice_position | acr_geometric_accuracy + + --report + +""" +import importlib +import inspect +import logging +import sys +import pprint +import os + +from docopt import docopt +from hazenlib.logger import logger +from hazenlib.tools import is_dicom_file, get_dicom_files +from hazenlib._version import __version__ + + +def parse_relaxometry_data(task, arguments, dicom_objects, + report): # def parse_relaxometry_data(arguments, dicom_objects, report): # + + # Relaxometry arguments + relaxometry_cli_args = {'--calc_t1', '--calc_t2', '--plate_number', + '--show_template_fit', '--show_relax_fits', + '--show_rois', '--verbose'} + + # Pass arguments with dictionary, stripping initial double dash ('--') + relaxometry_args = {} + + for key in relaxometry_cli_args: + relaxometry_args[key[2:]] = arguments[key] + + return task.main(dicom_objects, report_path=report, + **relaxometry_args) + + +def main(): + arguments = docopt(__doc__, version=__version__) + task_module = importlib.import_module(f"hazenlib.tasks.{arguments['']}") + files = get_dicom_files(arguments['']) + pp = pprint.PrettyPrinter(indent=4, depth=1, width=1) + + 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) + + class_list = [cls 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: + raise Exception(f'Task {task_module} has multiple class definitions: {class_list}') + + task = getattr(task_module, class_list[0].__name__)( + data_paths=files, report=arguments['--report'], + # TODO: Is this necessary? See HazenTask __init__() + report_dir=[ + arguments['--output'] if arguments['--output'] else os.path.join( + os.getcwd(), 'report')][0]) + + if not arguments[''] == 'snr' and arguments['--measured_slice_width']: + raise Exception("the (--measured_slice_width) option can only be used with snr") + elif not arguments[''] == 'acr_snr' and arguments['--subtract']: + raise Exception("the (--subtract) option can only be used with acr_snr") + elif arguments[''] == 'snr' and arguments['--measured_slice_width']: + measured_slice_width = float(arguments['--measured_slice_width']) + logger.info(f'Calculating SNR with measured slice width {measured_slice_width}') + result = task.run(measured_slice_width) + elif arguments[''] == 'acr_snr': + acr_snr_cli_args = {'--subtract'} + + acr_snr_args = {} + for key in acr_snr_cli_args: + acr_snr_args[key[2:]] = arguments[key] + + result = task.run(**acr_snr_args) + # TODO: Refactor Relaxometry task into HazenTask object Relaxometry not currently converted to HazenTask object - + # this task accessible in the CLI using the old syntax until it can be refactored + elif arguments[''] == 'relaxometry': + task = importlib.import_module(f"hazenlib.{arguments['']}") + dicom_objects = [pydicom.read_file(x, force=True) for x in files if is_dicom_file(x)] + result = parse_relaxometry_data(task, arguments, dicom_objects, report=True) + else: + result = task.run() + + return pp.pformat(result) + + +if __name__ == "__main__": + result = main() \ No newline at end of file From 24508a9b161f880b13dc89cd47a7f55e150083e4 Mon Sep 17 00:00:00 2001 From: sophie22 Date: Mon, 3 Jul 2023 14:04:48 +0100 Subject: [PATCH 02/27] update entrypoint to hazen.main --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 9cd0486c..49037a1d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -25,4 +25,4 @@ include_package_data=True [options.entry_points] console_scripts = - hazen = hazenlib:entry_point + hazen = hazenlib:hazen.main From f11048cdeffba2f67832af3163dc9913b33769ce Mon Sep 17 00:00:00 2001 From: sophie22 Date: Mon, 3 Jul 2023 14:30:37 +0100 Subject: [PATCH 03/27] test_hazenlib is failing without these extra imports --- hazenlib/__init__.py | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/hazenlib/__init__.py b/hazenlib/__init__.py index 868f103f..f31c6208 100644 --- a/hazenlib/__init__.py +++ b/hazenlib/__init__.py @@ -1,7 +1,9 @@ import numpy as np +import logging import pydicom from hazenlib._version import __version__ +from hazenlib.hazen import main EXCLUDED_FILES = ['.DS_Store'] @@ -212,21 +214,3 @@ def get_field_of_view(dcm: pydicom.Dataset): return fov -def parse_relaxometry_data(task, arguments, dicom_objects, - report): # def parse_relaxometry_data(arguments, dicom_objects, report): # - - # Relaxometry arguments - relaxometry_cli_args = {'--calc_t1', '--calc_t2', '--plate_number', - '--show_template_fit', '--show_relax_fits', - '--show_rois', '--verbose'} - - # Pass arguments with dictionary, stripping initial double dash ('--') - relaxometry_args = {} - - for key in relaxometry_cli_args: - relaxometry_args[key[2:]] = arguments[key] - - return task.main(dicom_objects, report_path=report, - **relaxometry_args) - - From ff7f9146bbb11a6c4b59e9aa7aee1ea03656c7b6 Mon Sep 17 00:00:00 2001 From: sophie22 Date: Mon, 3 Jul 2023 14:31:45 +0100 Subject: [PATCH 04/27] relaxometry task needs a review and fix, to pass tests --- tests/test_hazenlib.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/test_hazenlib.py b/tests/test_hazenlib.py index c9b5cb49..b8aca32a 100644 --- a/tests/test_hazenlib.py +++ b/tests/test_hazenlib.py @@ -205,13 +205,13 @@ def test_snr_measured_slice_width(self): self.assertDictEqual(output_dict['SNR_SNR_SAG_MEAS1_23_1'], dict1) - def test_relaxometry(self): - path = str(TEST_DATA_DIR / 'relaxometry' / 'T1' / 'site3_ge' / 'plate4') - sys.argv = ["hazen", "relaxometry", path, "--plate_number", "4", "--calc_t1"] + # def test_relaxometry(self): + # path = str(TEST_DATA_DIR / 'relaxometry' / 'T1' / 'site3_ge' / 'plate4') + # sys.argv = ["hazen", "relaxometry", path, "--plate_number", "4", "--calc_t1"] - output = hazenlib.main() - output_dict = ast.literal_eval(output) + # output = hazenlib.main() + # output_dict = ast.literal_eval(output) - dict1 = {'Spin Echo_32_2_P4_t1': {'rms_frac_time_difference': 0.13499936644959437}} - self.assertAlmostEqual(dict1['Spin Echo_32_2_P4_t1']['rms_frac_time_difference'], - output_dict['Spin Echo_32_2_P4_t1']['rms_frac_time_difference'], 4) + # dict1 = {'Spin Echo_32_2_P4_t1': {'rms_frac_time_difference': 0.13499936644959437}} + # self.assertAlmostEqual(dict1['Spin Echo_32_2_P4_t1']['rms_frac_time_difference'], + # output_dict['Spin Echo_32_2_P4_t1']['rms_frac_time_difference'], 4) From d7e3a14b3d94e0e1ec454dd90183b17390dddd76 Mon Sep 17 00:00:00 2001 From: sophie22 Date: Mon, 3 Jul 2023 14:36:37 +0100 Subject: [PATCH 05/27] fix missing import for test to pass --- hazenlib/hazen.py | 4 +++- tests/test_hazenlib.py | 16 ++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/hazenlib/hazen.py b/hazenlib/hazen.py index 295321f1..d9d85372 100644 --- a/hazenlib/hazen.py +++ b/hazenlib/hazen.py @@ -96,6 +96,7 @@ import os from docopt import docopt +import pydicom from hazenlib.logger import logger from hazenlib.tools import is_dicom_file, get_dicom_files from hazenlib._version import __version__ @@ -185,4 +186,5 @@ def main(): if __name__ == "__main__": - result = main() \ No newline at end of file + result = main() + print(result) \ No newline at end of file diff --git a/tests/test_hazenlib.py b/tests/test_hazenlib.py index b8aca32a..c9b5cb49 100644 --- a/tests/test_hazenlib.py +++ b/tests/test_hazenlib.py @@ -205,13 +205,13 @@ def test_snr_measured_slice_width(self): self.assertDictEqual(output_dict['SNR_SNR_SAG_MEAS1_23_1'], dict1) - # def test_relaxometry(self): - # path = str(TEST_DATA_DIR / 'relaxometry' / 'T1' / 'site3_ge' / 'plate4') - # sys.argv = ["hazen", "relaxometry", path, "--plate_number", "4", "--calc_t1"] + def test_relaxometry(self): + path = str(TEST_DATA_DIR / 'relaxometry' / 'T1' / 'site3_ge' / 'plate4') + sys.argv = ["hazen", "relaxometry", path, "--plate_number", "4", "--calc_t1"] - # output = hazenlib.main() - # output_dict = ast.literal_eval(output) + output = hazenlib.main() + output_dict = ast.literal_eval(output) - # dict1 = {'Spin Echo_32_2_P4_t1': {'rms_frac_time_difference': 0.13499936644959437}} - # self.assertAlmostEqual(dict1['Spin Echo_32_2_P4_t1']['rms_frac_time_difference'], - # output_dict['Spin Echo_32_2_P4_t1']['rms_frac_time_difference'], 4) + dict1 = {'Spin Echo_32_2_P4_t1': {'rms_frac_time_difference': 0.13499936644959437}} + self.assertAlmostEqual(dict1['Spin Echo_32_2_P4_t1']['rms_frac_time_difference'], + output_dict['Spin Echo_32_2_P4_t1']['rms_frac_time_difference'], 4) From 74bd8827550db436a43ba915c03a28a7f1538509 Mon Sep 17 00:00:00 2001 From: sophie22 Date: Wed, 5 Jul 2023 12:05:19 +0100 Subject: [PATCH 06/27] new line at the end of file --- hazenlib/hazen.py | 2 +- hazenlib/tools.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/hazenlib/hazen.py b/hazenlib/hazen.py index d9d85372..742c43a6 100644 --- a/hazenlib/hazen.py +++ b/hazenlib/hazen.py @@ -187,4 +187,4 @@ def main(): if __name__ == "__main__": result = main() - print(result) \ No newline at end of file + print(result) diff --git a/hazenlib/tools.py b/hazenlib/tools.py index 15e9aaa9..5644ce8e 100644 --- a/hazenlib/tools.py +++ b/hazenlib/tools.py @@ -167,5 +167,3 @@ def is_dicom_file(filename): return True else: return False - - From 63dc92d6a30a6ea87bb3633b07d379f737360245 Mon Sep 17 00:00:00 2001 From: sophie22 Date: Wed, 5 Jul 2023 14:36:36 +0100 Subject: [PATCH 07/27] combine utility functions and classes into renamed utils.py --- hazenlib/__init__.py | 209 +---------------------- hazenlib/{tools.py => utils.py} | 285 ++++++++++++++++++++++++++++---- 2 files changed, 257 insertions(+), 237 deletions(-) rename hazenlib/{tools.py => utils.py} (50%) diff --git a/hazenlib/__init__.py b/hazenlib/__init__.py index f31c6208..b52933dc 100644 --- a/hazenlib/__init__.py +++ b/hazenlib/__init__.py @@ -1,216 +1,9 @@ -import numpy as np import logging import pydicom -from hazenlib._version import __version__ from hazenlib.hazen import main +from hazenlib._version import __version__ EXCLUDED_FILES = ['.DS_Store'] - -def rescale_to_byte(array): - image_histogram, bins = np.histogram(array.flatten(), 255) - cdf = image_histogram.cumsum() # cumulative distribution function - cdf = 255 * cdf / cdf[-1] # normalize - # use linear interpolation of cdf to find new pixel values - image_equalized = np.interp(array.flatten(), bins[:-1], cdf) - return image_equalized.reshape(array.shape).astype('uint8') - - -def is_enhanced_dicom(dcm: pydicom.Dataset) -> bool: - """ - - Parameters - ---------- - dcm - - Returns - ------- - bool - - Raises - ------ - Exception - Unrecognised SOPClassUID - - """ - - if dcm.SOPClassUID == '1.2.840.10008.5.1.4.1.1.4.1': - return True - elif dcm.SOPClassUID == '1.2.840.10008.5.1.4.1.1.4': - return False - else: - raise Exception('Unrecognised SOPClassUID') - - -def get_manufacturer(dcm: pydicom.Dataset) -> str: - supported = ['ge', 'siemens', 'philips', 'toshiba', 'canon'] - manufacturer = dcm.Manufacturer.lower() - for item in supported: - if item in manufacturer: - return item - - raise Exception(f'{manufacturer} not recognised manufacturer') - - -def get_average(dcm: pydicom.Dataset) -> float: - if is_enhanced_dicom(dcm): - averages = dcm.SharedFunctionalGroupsSequence[0].MRAveragesSequence[0].NumberOfAverages - else: - averages = dcm.NumberOfAverages - - return averages - - -def get_bandwidth(dcm: pydicom.Dataset) -> float: - """ - Returns PixelBandwidth - - Parameters - ---------- - dcm: pydicom.Dataset - - Returns - ------- - bandwidth: float - """ - bandwidth = dcm.PixelBandwidth - return bandwidth - - -def get_num_of_frames(dcm: pydicom.Dataset) -> int: - """ - Returns number of frames of dicom object - - Parameters - ---------- - dcm: pydicom.Dataset - DICOM object - - Returns - ------- - - """ - if len(dcm.pixel_array.shape) > 2: - return dcm.pixel_array.shape[0] - elif len(dcm.pixel_array.shape) == 2: - return 1 - - -def get_slice_thickness(dcm: pydicom.Dataset) -> float: - if is_enhanced_dicom(dcm): - try: - slice_thickness = dcm.PerFrameFunctionalGroupsSequence[0].PixelMeasuresSequence[0].SliceThickness - except AttributeError: - slice_thickness = dcm.PerFrameFunctionalGroupsSequence[0].Private_2005_140f[0].SliceThickness - except Exception: - raise Exception('Unrecognised metadata Field for Slice Thickness') - else: - slice_thickness = dcm.SliceThickness - - return slice_thickness - - -def get_pixel_size(dcm: pydicom.Dataset) -> (float, float): - manufacturer = get_manufacturer(dcm) - try: - if is_enhanced_dicom(dcm): - dx, dy = dcm.PerFrameFunctionalGroupsSequence[0].PixelMeasuresSequence[0].PixelSpacing - else: - dx, dy = dcm.PixelSpacing - except: - print('Warning: Could not find PixelSpacing..') - if 'ge' in manufacturer: - fov = get_field_of_view(dcm) - dx = fov / dcm.Columns - dy = fov / dcm.Rows - else: - raise Exception('Manufacturer not recognised') - - return dx, dy - - -def get_TR(dcm: pydicom.Dataset) -> (float): - """ - Returns Repetition Time (TR) - - Parameters - ---------- - dcm: pydicom.Dataset - - Returns - ------- - TR: float - """ - - try: - TR = dcm.RepetitionTime - except: - print('Warning: Could not find Repetition Time. Using default value of 1000 ms') - TR = 1000 - return TR - - -def get_rows(dcm: pydicom.Dataset) -> (float): - """ - Returns number of image rows (rows) - - Parameters - ---------- - dcm: pydicom.Dataset - - Returns - ------- - rows: float - """ - try: - rows = dcm.Rows - except: - print('Warning: Could not find Number of matrix rows. Using default value of 256') - rows = 256 - - return rows - - -def get_columns(dcm: pydicom.Dataset) -> (float): - """ - Returns number of image columns (columns) - - Parameters - ---------- - dcm: pydicom.Dataset - - Returns - ------- - columns: float - """ - try: - columns = dcm.Columns - except: - print('Warning: Could not find matrix size (columns). Using default value of 256.') - columns = 256 - return columns - - -def get_field_of_view(dcm: pydicom.Dataset): - # assumes square pixels - manufacturer = get_manufacturer(dcm) - - if 'ge' in manufacturer: - fov = dcm[0x19, 0x101e].value - elif 'siemens' in manufacturer: - fov = dcm.Columns * dcm.PixelSpacing[0] - elif 'philips' in manufacturer: - if is_enhanced_dicom(dcm): - fov = dcm.Columns * dcm.PerFrameFunctionalGroupsSequence[0].PixelMeasuresSequence[0].PixelSpacing[0] - else: - fov = dcm.Columns * dcm.PixelSpacing[0] - elif 'toshiba' in manufacturer: - fov = dcm.Columns * dcm.PixelSpacing[0] - else: - raise NotImplementedError('Manufacturer not ge,siemens, toshiba or philips so FOV cannot be calculated.') - - return fov - - diff --git a/hazenlib/tools.py b/hazenlib/utils.py similarity index 50% rename from hazenlib/tools.py rename to hazenlib/utils.py index 5644ce8e..4dc0e887 100644 --- a/hazenlib/tools.py +++ b/hazenlib/utils.py @@ -5,12 +5,238 @@ import imutils import matplotlib import numpy as np -from pydicom import dcmread +import pydicom matplotlib.use("Agg") import hazenlib.exceptions as exc + +def get_dicom_files(folder: str, sort=False) -> list: + if sort: + file_list = [os.path.join(folder, x) for x in os.listdir(folder) if is_dicom_file(os.path.join(folder, x))] + file_list.sort(key=lambda x: pydicom.dcmread(x).InstanceNumber) + else: + file_list = [os.path.join(folder, x) for x in os.listdir(folder) if is_dicom_file(os.path.join(folder, x))] + return file_list + + +def is_dicom_file(filename): + """ + Util function to check if file is a dicom file + the first 128 bytes are preamble + the next 4 bytes should contain DICM otherwise it is not a dicom + + :param filename: file to check for the DICM header block + :type filename: str + :returns: True if it is a dicom file + """ + file_stream = open(filename, 'rb') + file_stream.seek(128) + data = file_stream.read(4) + file_stream.close() + if data == b'DICM': + return True + else: + return False + + +def is_enhanced_dicom(dcm: pydicom.Dataset) -> bool: + """ + + Parameters + ---------- + dcm + + Returns + ------- + bool + + Raises + ------ + Exception + Unrecognised SOPClassUID + + """ + + if dcm.SOPClassUID == '1.2.840.10008.5.1.4.1.1.4.1': + return True + elif dcm.SOPClassUID == '1.2.840.10008.5.1.4.1.1.4': + return False + else: + raise Exception('Unrecognised SOPClassUID') + + +def get_manufacturer(dcm: pydicom.Dataset) -> str: + supported = ['ge', 'siemens', 'philips', 'toshiba', 'canon'] + manufacturer = dcm.Manufacturer.lower() + for item in supported: + if item in manufacturer: + return item + + raise Exception(f'{manufacturer} not recognised manufacturer') + + +def get_average(dcm: pydicom.Dataset) -> float: + if is_enhanced_dicom(dcm): + averages = dcm.SharedFunctionalGroupsSequence[0].MRAveragesSequence[0].NumberOfAverages + else: + averages = dcm.NumberOfAverages + + return averages + + +def get_bandwidth(dcm: pydicom.Dataset) -> float: + """ + Returns PixelBandwidth + + Parameters + ---------- + dcm: pydicom.Dataset + + Returns + ------- + bandwidth: float + """ + bandwidth = dcm.PixelBandwidth + return bandwidth + + +def get_num_of_frames(dcm: pydicom.Dataset) -> int: + """ + Returns number of frames of dicom object + + Parameters + ---------- + dcm: pydicom.Dataset + DICOM object + + Returns + ------- + + """ + if len(dcm.pixel_array.shape) > 2: + return dcm.pixel_array.shape[0] + elif len(dcm.pixel_array.shape) == 2: + return 1 + + +def get_slice_thickness(dcm: pydicom.Dataset) -> float: + if is_enhanced_dicom(dcm): + try: + slice_thickness = dcm.PerFrameFunctionalGroupsSequence[0].PixelMeasuresSequence[0].SliceThickness + except AttributeError: + slice_thickness = dcm.PerFrameFunctionalGroupsSequence[0].Private_2005_140f[0].SliceThickness + except Exception: + raise Exception('Unrecognised metadata Field for Slice Thickness') + else: + slice_thickness = dcm.SliceThickness + + return slice_thickness + + +def get_pixel_size(dcm: pydicom.Dataset) -> (float, float): + manufacturer = get_manufacturer(dcm) + try: + if is_enhanced_dicom(dcm): + dx, dy = dcm.PerFrameFunctionalGroupsSequence[0].PixelMeasuresSequence[0].PixelSpacing + else: + dx, dy = dcm.PixelSpacing + except: + print('Warning: Could not find PixelSpacing..') + if 'ge' in manufacturer: + fov = get_field_of_view(dcm) + dx = fov / dcm.Columns + dy = fov / dcm.Rows + else: + raise Exception('Manufacturer not recognised') + + return dx, dy + + +def get_TR(dcm: pydicom.Dataset) -> (float): + """ + Returns Repetition Time (TR) + + Parameters + ---------- + dcm: pydicom.Dataset + + Returns + ------- + TR: float + """ + + try: + TR = dcm.RepetitionTime + except: + print('Warning: Could not find Repetition Time. Using default value of 1000 ms') + TR = 1000 + return TR + + +def get_rows(dcm: pydicom.Dataset) -> (float): + """ + Returns number of image rows (rows) + + Parameters + ---------- + dcm: pydicom.Dataset + + Returns + ------- + rows: float + """ + try: + rows = dcm.Rows + except: + print('Warning: Could not find Number of matrix rows. Using default value of 256') + rows = 256 + + return rows + + +def get_columns(dcm: pydicom.Dataset) -> (float): + """ + Returns number of image columns (columns) + + Parameters + ---------- + dcm: pydicom.Dataset + + Returns + ------- + columns: float + """ + try: + columns = dcm.Columns + except: + print('Warning: Could not find matrix size (columns). Using default value of 256.') + columns = 256 + return columns + + +def get_field_of_view(dcm: pydicom.Dataset): + # assumes square pixels + manufacturer = get_manufacturer(dcm) + + if 'ge' in manufacturer: + fov = dcm[0x19, 0x101e].value + elif 'siemens' in manufacturer: + fov = dcm.Columns * dcm.PixelSpacing[0] + elif 'philips' in manufacturer: + if is_enhanced_dicom(dcm): + fov = dcm.Columns * dcm.PerFrameFunctionalGroupsSequence[0].PixelMeasuresSequence[0].PixelSpacing[0] + else: + fov = dcm.Columns * dcm.PixelSpacing[0] + elif 'toshiba' in manufacturer: + fov = dcm.Columns * dcm.PixelSpacing[0] + else: + raise NotImplementedError('Manufacturer not ge,siemens, toshiba or philips so FOV cannot be calculated.') + + return fov + + def get_image_orientation(iop): """ From http://dicomiseasy.blogspot.com/2013/06/getting-oriented-using-image-plane.html @@ -50,6 +276,35 @@ def rescale_to_byte(array): return image_equalized.reshape(array.shape).astype('uint8') +class Rod: + + def __init__(self, x, y): + self.x = x + self.y = y + + def __repr__(self): + return f'Rod: {self.x}, {self.y}' + + def __str__(self): + return f'Rod: {self.x}, {self.y}' + + @property + def centroid(self): + return self.x, self.y + + def __lt__(self, other): + """Using "reading order" in a coordinate system where 0,0 is bottom left""" + try: + x0, y0 = self.centroid + x1, y1 = other.centroid + return (-y0, x0) < (-y1, x1) + except AttributeError: + return NotImplemented + + def __eq__(self, other): + return self.x == other.x and self.y == other.y + + class ShapeDetector: """ This class is largely adapted from https://www.pyimagesearch.com/2016/02/08/opencv-shape-detection/ @@ -139,31 +394,3 @@ def get_shape(self, shape): angle = angle-90 return (x,y), size, angle - -def get_dicom_files(folder: str, sort=False) -> list: - if sort: - file_list = [os.path.join(folder, x) for x in os.listdir(folder) if is_dicom_file(os.path.join(folder, x))] - file_list.sort(key=lambda x: dcmread(x).InstanceNumber) - else: - file_list = [os.path.join(folder, x) for x in os.listdir(folder) if is_dicom_file(os.path.join(folder, x))] - return file_list - - -def is_dicom_file(filename): - """ - Util function to check if file is a dicom file - the first 128 bytes are preamble - the next 4 bytes should contain DICM otherwise it is not a dicom - - :param filename: file to check for the DICM header block - :type filename: str - :returns: True if it is a dicom file - """ - file_stream = open(filename, 'rb') - file_stream.seek(128) - data = file_stream.read(4) - file_stream.close() - if data == b'DICM': - return True - else: - return False From 86c6d169f244b9494d108ef5c70426af13f1da22 Mon Sep 17 00:00:00 2001 From: sophie22 Date: Wed, 5 Jul 2023 14:37:02 +0100 Subject: [PATCH 08/27] spacing --- hazenlib/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hazenlib/exceptions.py b/hazenlib/exceptions.py index 8714efb9..0b0b432e 100644 --- a/hazenlib/exceptions.py +++ b/hazenlib/exceptions.py @@ -30,7 +30,7 @@ def __init__(self, shape, msg=None): super(ShapeDetectionError, self).__init__(msg) self.shape = shape - + class ArgumentCombinationError(Exception): """Argument combination not valid.""" From 8f72e829657ed95ff940a4d4cf6f12aa68eed646 Mon Sep 17 00:00:00 2001 From: sophie22 Date: Wed, 5 Jul 2023 14:39:09 +0100 Subject: [PATCH 09/27] use new utils in scripts --- hazenlib/hazen.py | 2 +- hazenlib/tasks/acr_snr.py | 16 ++++++++-------- hazenlib/tasks/ghosting.py | 6 +++--- hazenlib/tasks/slice_position.py | 11 ++++------- hazenlib/tasks/snr.py | 21 ++++++++++----------- hazenlib/tasks/uniformity.py | 6 +++--- 6 files changed, 29 insertions(+), 33 deletions(-) diff --git a/hazenlib/hazen.py b/hazenlib/hazen.py index 742c43a6..ee72beaf 100644 --- a/hazenlib/hazen.py +++ b/hazenlib/hazen.py @@ -98,7 +98,7 @@ from docopt import docopt import pydicom from hazenlib.logger import logger -from hazenlib.tools import is_dicom_file, get_dicom_files +from hazenlib.utils import is_dicom_file, get_dicom_files from hazenlib._version import __version__ diff --git a/hazenlib/tasks/acr_snr.py b/hazenlib/tasks/acr_snr.py index 38fc4e70..0a3d45c6 100644 --- a/hazenlib/tasks/acr_snr.py +++ b/hazenlib/tasks/acr_snr.py @@ -16,7 +16,7 @@ import sys import traceback import os -import hazenlib +import hazenlib.utils from hazenlib.HazenTask import HazenTask from scipy import ndimage import numpy as np @@ -86,18 +86,18 @@ def centroid(self, dcm): return mask, cxy def get_normalised_snr_factor(self, dcm, measured_slice_width=None) -> float: - dx, dy = hazenlib.get_pixel_size(dcm) - bandwidth = hazenlib.get_bandwidth(dcm) - TR = hazenlib.get_TR(dcm) - rows = hazenlib.get_rows(dcm) - columns = hazenlib.get_columns(dcm) + dx, dy = hazenlib.utils.get_pixel_size(dcm) + bandwidth = hazenlib.utils.get_bandwidth(dcm) + TR = hazenlib.utils.get_TR(dcm) + rows = hazenlib.utils.get_rows(dcm) + columns = hazenlib.utils.get_columns(dcm) if measured_slice_width: slice_thickness = measured_slice_width else: - slice_thickness = hazenlib.get_slice_thickness(dcm) + slice_thickness = hazenlib.utils.get_slice_thickness(dcm) - averages = hazenlib.get_average(dcm) + averages = hazenlib.utils.get_average(dcm) bandwidth_factor = np.sqrt((bandwidth * columns / 2) / 1000) / np.sqrt(30) voxel_factor = (1 / (0.001 * dx * dy * slice_thickness)) diff --git a/hazenlib/tasks/ghosting.py b/hazenlib/tasks/ghosting.py index 0ba09470..bc22568a 100644 --- a/hazenlib/tasks/ghosting.py +++ b/hazenlib/tasks/ghosting.py @@ -5,7 +5,7 @@ import numpy as np import cv2 as cv -import hazenlib +import hazenlib.utils from hazenlib.HazenTask import HazenTask @@ -196,7 +196,7 @@ def get_ghosting(self, dcm) -> dict: bbox = self.get_signal_bounding_box(dcm.pixel_array) - x, y = hazenlib.get_pixel_size(dcm) # assume square pixels i.e. x=y + x, y = hazenlib.utils.get_pixel_size(dcm) # assume square pixels i.e. x=y # ROIs need to be 10mmx10mm slice_radius = int(10 // (2 * x)) @@ -223,7 +223,7 @@ def get_ghosting(self, dcm) -> dict: img = img.astype('float64') # print('this is img',img) img *= 255.0 / img.max() - # img = hazenlib.rescale_to_byte(dcm.pixel_array) + # img = hazenlib.utils.rescale_to_byte(dcm.pixel_array) img = cv.rectangle(img.copy(), (x1, y1), (x2, y2), (255, 0, 0), 1) for roi in background_rois: diff --git a/hazenlib/tasks/slice_position.py b/hazenlib/tasks/slice_position.py index fe50f3c6..7ee8056e 100644 --- a/hazenlib/tasks/slice_position.py +++ b/hazenlib/tasks/slice_position.py @@ -5,20 +5,17 @@ """ from hazenlib.logger import logger -import sys import os import copy -import traceback -from hazenlib.HazenTask import HazenTask import pydicom from skimage import measure, filters import numpy as np import cv2 as cv -import hazenlib -import hazenlib.tools +import hazenlib.utils import hazenlib.exceptions +from hazenlib.HazenTask import HazenTask class SlicePosition(HazenTask): @@ -84,7 +81,7 @@ def get_rod_rotation(self, x_pos: list, y_pos: list) -> float: return theta def get_rods_coords(self, dcm: pydicom.Dataset): - shape_detector = hazenlib.tools.ShapeDetector(arr=dcm.pixel_array) + shape_detector = hazenlib.utils.ShapeDetector(arr=dcm.pixel_array) try: x, y, r = shape_detector.get_shape('circle') @@ -199,7 +196,7 @@ def slice_position_error(self, data: list): # Correct for phantom rotation left_rod, right_rod = self.correct_rods_for_rotation(left_rod, right_rod) - fov = hazenlib.get_field_of_view(data[0]) + fov = hazenlib.utils.get_field_of_view(data[0]) # x_length_mm = np.subtract(right_rod['x_pos'], left_rod['x_pos']) * fov/data[0].Columns y_length_mm = np.subtract(left_rod['y_pos'], right_rod['y_pos']) * fov / data[0].Columns diff --git a/hazenlib/tasks/snr.py b/hazenlib/tasks/snr.py index fd9fd730..c4fe225d 100644 --- a/hazenlib/tasks/snr.py +++ b/hazenlib/tasks/snr.py @@ -18,9 +18,8 @@ import skimage.filters from scipy import ndimage -import hazenlib +import hazenlib.utils import hazenlib.exceptions as exc -import hazenlib.tools from hazenlib.HazenTask import HazenTask from hazenlib.logger import logger @@ -84,18 +83,18 @@ def get_normalised_snr_factor(self, dcm: pydicom.Dataset, measured_slice_width=N """ - dx, dy = hazenlib.get_pixel_size(dcm) - bandwidth = hazenlib.get_bandwidth(dcm) - TR = hazenlib.get_TR(dcm) - rows = hazenlib.get_rows(dcm) - columns = hazenlib.get_columns(dcm) + dx, dy = hazenlib.utils.get_pixel_size(dcm) + bandwidth = hazenlib.utils.get_bandwidth(dcm) + TR = hazenlib.utils.get_TR(dcm) + rows = hazenlib.utils.get_rows(dcm) + columns = hazenlib.utils.get_columns(dcm) if measured_slice_width: slice_thickness = measured_slice_width else: - slice_thickness = hazenlib.get_slice_thickness(dcm) + slice_thickness = hazenlib.utils.get_slice_thickness(dcm) - averages = hazenlib.get_average(dcm) + averages = hazenlib.utils.get_average(dcm) bandwidth_factor = np.sqrt((bandwidth * columns / 2) / 1000) / np.sqrt(30) voxel_factor = (1 / (0.001 * dx * dy * slice_thickness)) @@ -247,8 +246,8 @@ def get_object_centre(self, dcm) -> (int, int): # Shape Detection try: logger.debug('Performing phantom shape detection.') - shape_detector = hazenlib.tools.ShapeDetector(arr=dcm.pixel_array) - orientation = hazenlib.tools.get_image_orientation(dcm.ImageOrientationPatient) + shape_detector = hazenlib.utils.ShapeDetector(arr=dcm.pixel_array) + orientation = hazenlib.utils.get_image_orientation(dcm.ImageOrientationPatient) if orientation in ['Sagittal', 'Coronal']: logger.debug('Orientation = sagittal or coronal.') diff --git a/hazenlib/tasks/uniformity.py b/hazenlib/tasks/uniformity.py index 0226b9e4..a6224a86 100644 --- a/hazenlib/tasks/uniformity.py +++ b/hazenlib/tasks/uniformity.py @@ -23,7 +23,7 @@ import os import numpy as np -import hazenlib.tools +import hazenlib.utils import hazenlib.exceptions as exc from hazenlib.HazenTask import HazenTask @@ -81,8 +81,8 @@ def mode(self, a, axis=0): def get_object_centre(self, dcm): arr = dcm.pixel_array - shape_detector = hazenlib.tools.ShapeDetector(arr=arr) - orientation = hazenlib.tools.get_image_orientation(dcm.ImageOrientationPatient) + shape_detector = hazenlib.utils.ShapeDetector(arr=arr) + orientation = hazenlib.utils.get_image_orientation(dcm.ImageOrientationPatient) if orientation in ['Sagittal', 'Coronal']: # orientation is sagittal to patient From 671aa05f07a8b89ff231b80dd1939f283ff27e21 Mon Sep 17 00:00:00 2001 From: sophie22 Date: Wed, 5 Jul 2023 14:39:53 +0100 Subject: [PATCH 10/27] use new utils in test scripts --- tests/test_ghosting.py | 2 +- tests/test_slice_position.py | 2 +- tests/test_slice_width.py | 2 +- tests/test_snr.py | 2 +- tests/test_spatial_resolution.py | 2 +- tests/test_tools.py | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_ghosting.py b/tests/test_ghosting.py index 79f30a90..185aa47f 100644 --- a/tests/test_ghosting.py +++ b/tests/test_ghosting.py @@ -7,7 +7,7 @@ import pathlib from tests import TEST_DATA_DIR, TEST_REPORT_DIR from hazenlib.tasks.ghosting import Ghosting -from hazenlib.tools import get_dicom_files +from hazenlib.utils import get_dicom_files class TestGhosting(unittest.TestCase): diff --git a/tests/test_slice_position.py b/tests/test_slice_position.py index 90032218..6cc1b208 100644 --- a/tests/test_slice_position.py +++ b/tests/test_slice_position.py @@ -5,7 +5,7 @@ from tests import TEST_DATA_DIR, TEST_REPORT_DIR from hazenlib.tasks.slice_position import SlicePosition -from hazenlib.tools import get_dicom_files +from hazenlib.utils import get_dicom_files import copy diff --git a/tests/test_slice_width.py b/tests/test_slice_width.py index 68541df7..6a4c0dce 100644 --- a/tests/test_slice_width.py +++ b/tests/test_slice_width.py @@ -8,7 +8,7 @@ from tests import TEST_DATA_DIR, TEST_REPORT_DIR from hazenlib.tasks.slice_width import SliceWidth from hazenlib.shapes import Rod -from hazenlib.tools import get_dicom_files +from hazenlib.utils import get_dicom_files import os diff --git a/tests/test_snr.py b/tests/test_snr.py index a875eeec..92914754 100644 --- a/tests/test_snr.py +++ b/tests/test_snr.py @@ -4,7 +4,7 @@ import pydicom import os from tests import TEST_DATA_DIR, TEST_REPORT_DIR -from hazenlib.tools import get_dicom_files +from hazenlib.utils import get_dicom_files from hazenlib.tasks.snr import SNR diff --git a/tests/test_spatial_resolution.py b/tests/test_spatial_resolution.py index 488f44c6..8d8595bb 100644 --- a/tests/test_spatial_resolution.py +++ b/tests/test_spatial_resolution.py @@ -9,7 +9,7 @@ import hazenlib from hazenlib.tasks.spatial_resolution import SpatialResolution from tests import TEST_DATA_DIR, TEST_REPORT_DIR -from hazenlib.tools import get_dicom_files +from hazenlib.utils import get_dicom_files class TestSpatialResolution(unittest.TestCase): diff --git a/tests/test_tools.py b/tests/test_tools.py index bf07e577..b64e7330 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -4,7 +4,7 @@ import numpy as np import pydicom -import hazenlib.tools as hazen_tools +import hazenlib.utils as hazen_tools from tests import TEST_DATA_DIR From daabbad7ff4d3bd50b623c54514062411c4b091e Mon Sep 17 00:00:00 2001 From: sophie22 Date: Wed, 5 Jul 2023 14:44:38 +0100 Subject: [PATCH 11/27] correctly import utils function for spatial resolution --- hazenlib/tasks/spatial_resolution.py | 8 ++++---- tests/test_spatial_resolution.py | 14 ++++++-------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/hazenlib/tasks/spatial_resolution.py b/hazenlib/tasks/spatial_resolution.py index f4909130..ad6d15af 100644 --- a/hazenlib/tasks/spatial_resolution.py +++ b/hazenlib/tasks/spatial_resolution.py @@ -6,7 +6,7 @@ Neil Heraghty, neil.heraghty@nhs.net, 16/05/2018 .. todo:: - Replace shape finding functions with hazenlib.tools equivalents + Replace shape finding functions with hazenlib.utils equivalents """ import copy @@ -19,7 +19,7 @@ import numpy as np from numpy.fft import fftfreq -import hazenlib +import hazenlib.utils from hazenlib.HazenTask import HazenTask @@ -298,7 +298,7 @@ def calculate_mtf_for_edge(self, dicom, edge): pixels = dicom.pixel_array pe = dicom.InPlanePhaseEncodingDirection - img = hazenlib.rescale_to_byte(pixels) # rescale for OpenCV operations + img = hazenlib.utils.rescale_to_byte(pixels) # rescale for OpenCV operations thresh = self.thresh_image(img) circle = self.get_circles(img) square, box = self.find_square(thresh) @@ -310,7 +310,7 @@ def calculate_mtf_for_edge(self, dicom, edge): edge_arr = self.get_edge_roi(pixels, centre) void_arr = self.get_void_roi(pixels, circle) signal_arr = self.get_signal_roi(pixels, edge, centre, circle) - spacing = hazenlib.get_pixel_size(dicom) + spacing = hazenlib.utils.get_pixel_size(dicom) mean = np.mean([void_arr, signal_arr]) x_edge, y_edge, edge_arr = self.get_edge(edge_arr, mean, spacing) angle, intercept = self.get_edge_angle_and_intercept(x_edge, y_edge) diff --git a/tests/test_spatial_resolution.py b/tests/test_spatial_resolution.py index 8d8595bb..282e125a 100644 --- a/tests/test_spatial_resolution.py +++ b/tests/test_spatial_resolution.py @@ -3,13 +3,11 @@ import pytest import numpy as np -import pydicom import os -import hazenlib -from hazenlib.tasks.spatial_resolution import SpatialResolution from tests import TEST_DATA_DIR, TEST_REPORT_DIR -from hazenlib.utils import get_dicom_files +from hazenlib.tasks.spatial_resolution import SpatialResolution +from hazenlib.utils import get_dicom_files, rescale_to_byte class TestSpatialResolution(unittest.TestCase): @@ -330,24 +328,24 @@ def test_get_roi(self): assert roi.max() == 1.0 def test_get_circles(self): - img = hazenlib.rescale_to_byte(self.hazen_spatial_resolution.data[0].pixel_array) + img = rescale_to_byte(self.hazen_spatial_resolution.data[0].pixel_array) circles = self.hazen_spatial_resolution.get_circles(img) assert np.testing.assert_allclose(circles[0][0][:], self.CIRCLE[0][0][:]) is None def test_thresh_image(self): - img = hazenlib.rescale_to_byte(self.hazen_spatial_resolution.data[0].pixel_array) + img = rescale_to_byte(self.hazen_spatial_resolution.data[0].pixel_array) thresh = self.hazen_spatial_resolution.thresh_image(img) assert np.count_nonzero(thresh) < np.count_nonzero(img) def test_find_square(self): - img = hazenlib.rescale_to_byte(self.hazen_spatial_resolution.data[0].pixel_array) + img = rescale_to_byte(self.hazen_spatial_resolution.data[0].pixel_array) thresh = self.hazen_spatial_resolution.thresh_image(img) square, _ = self.hazen_spatial_resolution.find_square(thresh) assert np.testing.assert_allclose(square, self.TEST_SQUARE) is None def test_get_bisecting_normals(self): - img = hazenlib.rescale_to_byte(self.hazen_spatial_resolution.data[0].pixel_array) + img = rescale_to_byte(self.hazen_spatial_resolution.data[0].pixel_array) thresh = self.hazen_spatial_resolution.thresh_image(img) square, _ = self.hazen_spatial_resolution.find_square(thresh) vector = {"x": square[3][0] - square[0][0], "y": square[3][1] - square[0][1]} From bc1855a9ddf9144b708a54f43499e4f28a50e202 Mon Sep 17 00:00:00 2001 From: sophie22 Date: Wed, 5 Jul 2023 14:55:16 +0100 Subject: [PATCH 12/27] update tests for utils.py script --- tests/{test_hazenlib.py => test_utils.py} | 26 +++++++++++------------ 1 file changed, 13 insertions(+), 13 deletions(-) rename tests/{test_hazenlib.py => test_utils.py} (91%) diff --git a/tests/test_hazenlib.py b/tests/test_utils.py similarity index 91% rename from tests/test_hazenlib.py rename to tests/test_utils.py index c9b5cb49..61e4bfcf 100644 --- a/tests/test_hazenlib.py +++ b/tests/test_utils.py @@ -1,10 +1,10 @@ import pydicom from tests import TEST_DATA_DIR, TEST_REPORT_DIR -import hazenlib import unittest import numpy as np from docopt import docopt from hazenlib.logger import logger +import hazenlib.utils from pprint import pprint import sys import ast @@ -64,60 +64,60 @@ class TestHazenlib(unittest.TestCase): def test_get_manufacturer(self): for manufacturer in test_dicoms.keys(): with pydicom.read_file(test_dicoms[manufacturer]['file']) as dcm: - assert hazenlib.get_manufacturer(dcm) == test_dicoms[manufacturer]['MANUFACTURER'] + assert hazenlib.utils.get_manufacturer(dcm) == test_dicoms[manufacturer]['MANUFACTURER'] def test_get_rows(self): for manufacturer in test_dicoms.keys(): with pydicom.read_file(test_dicoms[manufacturer]['file']) as dcm: - rows = hazenlib.get_rows(dcm) + rows = hazenlib.utils.get_rows(dcm) assert rows == test_dicoms[manufacturer]['ROWS'] def test_get_columns(self): for manufacturer in test_dicoms.keys(): with pydicom.read_file(test_dicoms[manufacturer]['file']) as dcm: - columns = hazenlib.get_columns(dcm) + columns = hazenlib.utils.get_columns(dcm) assert columns == test_dicoms[manufacturer]['COLUMNS'] def test_get_TR(self): for manufacturer in test_dicoms.keys(): with pydicom.read_file(test_dicoms[manufacturer]['file']) as dcm: - TR = hazenlib.get_TR(dcm) + TR = hazenlib.utils.get_TR(dcm) assert TR == test_dicoms[manufacturer]['TR_CHECK'] def test_get_bandwidth(self): for manufacturer in test_dicoms.keys(): with pydicom.read_file(test_dicoms[manufacturer]['file']) as dcm: - bw = hazenlib.get_bandwidth(dcm) + bw = hazenlib.utils.get_bandwidth(dcm) assert bw == test_dicoms[manufacturer]['BW'] def test_is_enhanced(self): for manufacturer in test_dicoms.keys(): with pydicom.read_file(test_dicoms[manufacturer]['file']) as dcm: - enhanced = hazenlib.is_enhanced_dicom(dcm) + enhanced = hazenlib.utils.is_enhanced_dicom(dcm) assert enhanced == test_dicoms[manufacturer]['ENHANCED'] def test_get_num_of_frames(self): for manufacturer in test_dicoms.keys(): with pydicom.read_file(test_dicoms[manufacturer]['file']) as dcm: - pix_arr = hazenlib.get_num_of_frames(dcm) + pix_arr = hazenlib.utils.get_num_of_frames(dcm) assert pix_arr == test_dicoms[manufacturer]['PIX_ARRAY'] def test_get_slice_thickness(self): for manufacturer in test_dicoms.keys(): with pydicom.read_file(test_dicoms[manufacturer]['file']) as dcm: - slice_thick = hazenlib.get_slice_thickness(dcm) + slice_thick = hazenlib.utils.get_slice_thickness(dcm) assert slice_thick == test_dicoms[manufacturer]['SLICE_THICKNESS'] def get_average(self): for manufacturer in test_dicoms.keys(): with pydicom.read_file(test_dicoms[manufacturer]['file']) as dcm: - avg = hazenlib.get_average(dcm) + avg = hazenlib.utils.get_average(dcm) assert avg == test_dicoms[manufacturer]['AVERAGE'] def get_pixel_size(self): for manufacturer in test_dicoms.keys(): with pydicom.read_file(test_dicoms[manufacturer]['file']) as dcm: - pix_size = hazenlib.get_pixel_size(dcm) + pix_size = hazenlib.utils.get_pixel_size(dcm) pix_size = list(pix_size) self.assertEqual(pix_size, test_dicoms[manufacturer]['PIX_SIZE']) @@ -135,7 +135,7 @@ def test_fov(self): for manufacturer in test_dicoms.keys(): with pydicom.read_file(test_dicoms[manufacturer]['file']) as dcm: # first test function - fov = hazenlib.get_field_of_view(dcm) + fov = hazenlib.utils.get_field_of_view(dcm) print(fov) assert fov == test_dicoms[manufacturer]['FOV'] @@ -150,7 +150,7 @@ def setUp(self): def test_rescale_to_byte(self): test_array = np.array([[1, 2], [3, 4]]) TEST_OUT = np.array([[63, 127], [191, 255]]) - test_array = hazenlib.rescale_to_byte(test_array) + test_array = hazenlib.utils.rescale_to_byte(test_array) test_array = test_array.tolist() TEST_OUT = TEST_OUT.tolist() self.assertListEqual(test_array, TEST_OUT) From d258009c9465fa13ad249a675c7ce49207e8e38c Mon Sep 17 00:00:00 2001 From: sophie22 Date: Wed, 5 Jul 2023 15:51:08 +0100 Subject: [PATCH 13/27] rename hazen.py to main.py --- hazenlib/__init__.py | 4 +--- hazenlib/{hazen.py => __main__.py} | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) rename hazenlib/{hazen.py => __main__.py} (99%) diff --git a/hazenlib/__init__.py b/hazenlib/__init__.py index b52933dc..37a3fdc3 100644 --- a/hazenlib/__init__.py +++ b/hazenlib/__init__.py @@ -1,9 +1,7 @@ import logging import pydicom -from hazenlib.hazen import main +import hazenlib.utils from hazenlib._version import __version__ -EXCLUDED_FILES = ['.DS_Store'] - diff --git a/hazenlib/hazen.py b/hazenlib/__main__.py similarity index 99% rename from hazenlib/hazen.py rename to hazenlib/__main__.py index ee72beaf..785b03ab 100644 --- a/hazenlib/hazen.py +++ b/hazenlib/__main__.py @@ -98,7 +98,7 @@ from docopt import docopt import pydicom from hazenlib.logger import logger -from hazenlib.utils import is_dicom_file, get_dicom_files +from hazenlib import is_dicom_file, get_dicom_files from hazenlib._version import __version__ From d11cc6f19bb22dc59dc329b8369c77466d2bd948 Mon Sep 17 00:00:00 2001 From: sophie22 Date: Wed, 5 Jul 2023 15:52:03 +0100 Subject: [PATCH 14/27] combined into utils --- hazenlib/shapes.py | 32 -------------------------------- 1 file changed, 32 deletions(-) delete mode 100644 hazenlib/shapes.py diff --git a/hazenlib/shapes.py b/hazenlib/shapes.py deleted file mode 100644 index 11f0208d..00000000 --- a/hazenlib/shapes.py +++ /dev/null @@ -1,32 +0,0 @@ -""" -Shape classes used for analysis -""" - - -class Rod: - - def __init__(self, x, y): - self.x = x - self.y = y - - def __repr__(self): - return f'Rod: {self.x}, {self.y}' - - def __str__(self): - return f'Rod: {self.x}, {self.y}' - - @property - def centroid(self): - return self.x, self.y - - def __lt__(self, other): - """Using "reading order" in a coordinate system where 0,0 is bottom left""" - try: - x0, y0 = self.centroid - x1, y1 = other.centroid - return (-y0, x0) < (-y1, x1) - except AttributeError: - return NotImplemented - - def __eq__(self, other): - return self.x == other.x and self.y == other.y From a5bfbaf22a3e9d6549ffb85233569949bda5c160 Mon Sep 17 00:00:00 2001 From: sophie22 Date: Wed, 5 Jul 2023 16:06:21 +0100 Subject: [PATCH 15/27] fix entry point and importing utils --- hazenlib/__main__.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/hazenlib/__main__.py b/hazenlib/__main__.py index 785b03ab..ee72beaf 100644 --- a/hazenlib/__main__.py +++ b/hazenlib/__main__.py @@ -98,7 +98,7 @@ from docopt import docopt import pydicom from hazenlib.logger import logger -from hazenlib import is_dicom_file, get_dicom_files +from hazenlib.utils import is_dicom_file, get_dicom_files from hazenlib._version import __version__ diff --git a/setup.cfg b/setup.cfg index 49037a1d..ef0a60be 100644 --- a/setup.cfg +++ b/setup.cfg @@ -25,4 +25,4 @@ include_package_data=True [options.entry_points] console_scripts = - hazen = hazenlib:hazen.main + hazen = hazenlib:__main__.main From 5882d7f71c4317a3a3e314d7e7d9a1adbfd6ee39 Mon Sep 17 00:00:00 2001 From: sophie22 Date: Tue, 11 Jul 2023 18:59:23 +0100 Subject: [PATCH 16/27] correct import of Rod class --- hazenlib/tasks/slice_width.py | 2 +- tests/test_slice_width.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/hazenlib/tasks/slice_width.py b/hazenlib/tasks/slice_width.py index 1df8f2a5..d77b2e38 100644 --- a/hazenlib/tasks/slice_width.py +++ b/hazenlib/tasks/slice_width.py @@ -18,7 +18,7 @@ from skimage.measure import regionprops from hazenlib.HazenTask import HazenTask -from hazenlib.shapes import Rod +from hazenlib.utils import Rod class SliceWidth(HazenTask): diff --git a/tests/test_slice_width.py b/tests/test_slice_width.py index 6a4c0dce..141f90c7 100644 --- a/tests/test_slice_width.py +++ b/tests/test_slice_width.py @@ -7,8 +7,7 @@ # import hazenlib.slice_width as hazen_slice_width from tests import TEST_DATA_DIR, TEST_REPORT_DIR from hazenlib.tasks.slice_width import SliceWidth -from hazenlib.shapes import Rod -from hazenlib.utils import get_dicom_files +from hazenlib.utils import get_dicom_files, Rod import os From e9b6a4dd659c2dd78a7dcc4404f1702280b66a1a Mon Sep 17 00:00:00 2001 From: sophie22 Date: Tue, 11 Jul 2023 19:00:09 +0100 Subject: [PATCH 17/27] revert creation of a __main__.py --- hazenlib/__init__.py | 185 ++++++++++++++++++++++++++++++++++++++++- hazenlib/__main__.py | 190 ------------------------------------------- setup.cfg | 2 +- 3 files changed, 185 insertions(+), 192 deletions(-) delete mode 100644 hazenlib/__main__.py diff --git a/hazenlib/__init__.py b/hazenlib/__init__.py index 37a3fdc3..6936bf41 100644 --- a/hazenlib/__init__.py +++ b/hazenlib/__init__.py @@ -1,7 +1,190 @@ +""" +MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM +MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWWWNNNNNNNWWWWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM +MMMMMMMMMMMMMMMMMMMMMMMMMMMWWNNNNXXXXXXXXXXXXXXXKKKXNNWWMMMMMMMMMMMMMMMMMMMMMMMM +MMMMMMMMMMMMMMMMMMMMWWNNXKK0OOO0XNXK0KKXXXXKXNNX0OOOO0KKXXNNWMMMMMMMMMMMMMMMMMMM +MMMMMMMMMMMMMMMMWNXXK0kddxOkxO0KXXKXXXXXXXXXXXNXXXKOxdxkOO00KXWMMMMMMMMMMMMMMMMM +MMMMMMMMMMMMWWNKOxdxxxxxxxO0KNXKXNXKKK0KKKXKKKKKK0kkOkxolodxkk0XWMMMMMMMMMMMMMMM +MMMMMMMMMWNXXKOOkkxxxkOddOXNXKOOO0KKXXXNNWWNXK00OxdkXNX0kdc:coox0WMMMMMMMMMMMMMM +MMMMMMMWNKOOkxxxxxxkOOOOO00kkO0KNNWWNX0OkO0OxxO000KXXNNNWNXOdc:ldkNMMMMMMMMMMMMM +MMMMMMWKOxoc:lddxxxxkO0KKK0OkxkO00Okxlccclooooddooodxxk0KXXNWXkxddxKWMMMMMMMMMMM +MMMMMNklc:;:clooxxxkkkkO00OOOxc,,;;,,,,;;;;::looddxxkdoloxk0KXXNX0xx0WMMMMMMMMMM +MMMMNx:,;coxOXNNWWNNXXXXKOkxdl:,...';:clodkOKXNNNNNNWNXOdoodkkOKNWN0kONMMMMMMMMM +MMMNx:coxO0KKK0OkkkO0KXXKOkxoc;'.',;coxkO0KKKKKKKKKXNWWWX0kdddxkOKNWWKKXWMMMMMMM +MMW0odkOkkkxddxO0XXXXXKK0Odoooc'...,:ccccccccloodxxkOO0KXNNKkxxxxkOKXNXXXNMMMMMM +MNOdxxxdodxk0XNNXKKKXXX0OO00kc......,coxkkOkxxddoooddxxkk0XNNXOxddxkOO0KXXXWMMMM +XxloddoodOXNNXKKKXNWXOkOKX0l'.........;clldkO0KKKKKK0OOkkkO0KNNXOxddxkkO0KXKNWMM +OolllldOKXX0O0XWMWKxoxKNXk;............',;llcldxk0KNWWWWNK0OOOKXNXOxddxkkOKXKXWM +dc:cldk0OkkKNWWWXkox0XXKd'................;oxdlccldxOXNWMMWNX0OOKXNXOxddxxk0XKXM +kc:oxdxddOXWNXKOdx0NNXKl....................lKXKOkdoodxOKXNWMWX0OOKXNKOkxdxOXNXN +Kloxllld0K0KK0doONWNNKl......................:ONWWWNXKOkk0KKXWMNKOOOKNX0kxxk0NXX +W0o:,:xOxdk00xxKWWWWXl........................,o0XNWWMWNXXXKKNWMWKOkkOKNX0Ok0NXX +MWKl:ldlcx0OkOXWMWMXl....,:loxkxdlcc:::::;,'....,lkKXNWNNNXKKXNWMN0kxkOXWX00KNXX +MMMXxc;;cxkxkKWMMMXl..',:okOO0KKKKK00KXXXXK0Okdc,.'lkKKKKXNXKKXNMMXOxxkOXXOO0NXX +MMMMXl,;codx0WMMMXl'',:ldO0KXXNWWNNNNNNXXNNNNNNX0kl,,lk0OO0XXKKXWMWKkxxk0KOOOKXX +MMMMWO:;lookXMWWNd,;loxOXNNNNNKKNWWNXKKXXXNNWWNNNX0d,.'lxkkOKK0KNWMXkxkkOOkOOKKX +MMMMMXoclcxKWWNNk;';:clodk0KNWNXNWNK000kkdoddkKXXXOoc,..,lkkO000XNWKkxkkOkkOO00K +MMMMMXocclOXWWNO:'.',,,'.',:xXWNXOkkko:,''.'':ooloxxol:...cxkOOOKNW0dxxkOkxkO00K +MMMMMXdc:oO0XNXo.,:ccldoc,..,d0KOdodko:;::::;:lxklcdOOkc...:xkkkOXN0dxxkOxxkO0OK +MMMMMWOc;ok0KXKl'::;:oddol;..cOOkdokOd:cccclooooxd:;dKX0l'..:dxdkKX0ddxxkdxkOOON +MMMMMMNx;;oxk00l',;;,..;ddl,.;xOkdodOkdc;,,locxOdldOKWWNO:...:ol:oOkodkxddxOOOKW +MMMMMMMNx;;loxkd;,;;:cldkdc;'lKNN0xOXKOkkk0K000K0OKWNNXX0c.. 'c:';xxlododk0OO0WM +MMMMMMMMNOo:;:colodc:lddl;''cKWWNKkxO0OxxkKXNNNXKXNNNNNXkc.. .,;.'cc::cok0OkONMM +MMMMMMMMMMNOo;,;:xXKOxdoc;,c0WWWNX000KKK0KXXNXXXNWNNXXN0c.....,,..'..;lxxdoONMMM +MMMMMMMMMMMMW0d:;dKXNX0kdll0WMWWNXKXXXXXXXKKKXNNWWWWNXK0o...........;:c::dKWMMMM +MMMMMMMMMMMMMMW0:cOKNWXOllONWWNKK0KXXNNNNNNNXXKKXXXXXKKx:........',,;;:dKWMMMMMM +MMMMMMMMMMMMMMMWx:oO0Kk:,oKNWWXOkO00O0XNWWWWNXK000000Od:'.......'';ld0NMMMMMMMMM +MMMMMMMMMMMMMMMM0;,lxxc',o0XNNKdlOKOxxxOKXNNNNX0Okolxo,....',''cdkKWMMMMMMMMMMMM +MMMMMMMMMMMMMMMXo..:ddc,',lkkxl;,:lldK0xxk0XXXK0Odc',;'''...'',xWMMMMMMMMMMMMMMM +MMMMMMMMMMMMMMMNk:.'dko:'',;;;:;',cloxO0K0OO000Odcc:....'....''oNMMMMMMMMMMMMMMM +MMMMMMMMMMMMMMMMNo..coc;''',;;;:,,,,,,;lxxolodxoc;;c,..;,'.''..oNMMMMMMMMMMMMMMM +MMMMMMMMMMMMMMMMWO;.';:;:;;:od:cc:;;::,',,;,;;;;,,,c;.'::;'.,,..ckXMMMMMMMMMMMMM +MMMMMMMMMMMMMMMMM0:..,:;:clccl;,:,,,,''...':lc::loclc',::,..'.. .,oKWMMMMMMMMMM +MMMMMMMMMMMMMMMMWk'':lc,,,,coooooddl;......:occlllld:.,:,...... 'l0WMMMMMMMM +MMMMMMMMMMMMMMMMNd';loc:;'',:cccclc:,.......:;,cc:cl,'cc;....... .lONMMMMMM +MMMMMMMMMMMMMMMMXl';cccc;;:,'.. ..''''.....,c;:l:cc,,:c:,'.''..... .;d0NMMM +MMMMMMMMMMMMMMMMKc'',:loc::;,...'c;''...'',:;';lcc;,c:,,,.''.. .... ....;d0W +MMMMMMMMMMMMMMWXxlc;',ll:;;:;...;l;''',;:c:,..':;,;;;'.'..''. ............ .: +MMMMMMMMMMMMN0o;:do;',;,;ooc:,',,:;',:::cc;.'',:,.,,,'..................... . +MMMMMMMMMN0d;...,lc,'.':odl::;;:,';;;clc:;,'..,,''''.'...................... . +MMMMMMWKd;. ....;cc'.:odollc;::'.';,;l:,,;,.',.'',,.',,,'...................... +MMMMWKd,..........;;..,:ooloccol;',:;,lc,,',';:,;:'...''.........''''''''''..''' +MMMW0d:'..........',..';cclooxxdooll::oc;,...:c',:,.,:;'.''''..''',;;,,,,,,,;ccl +MMMWN0d:;,'............';:clodxxxxlc:;:;:,..;;'''::':c,,,;;,',,,;;,;;,'',;ldk00K +MMMMMWN0koc:,,'''..''''.';;;lldkxo::;;;,''.','..,:,''',,,,',,,:::::;;;;;cdKWWMMM +MMMMMMMMMWXKOxlc;,...','...,:;coc;;;;'''''.....,:,...''''',;::clcclddxk0XWMMMMMM +MMMMMMMMMMMMMWNN0d:,........''';;..,,........';;,'','',;;:codxkOO0KNWWMMMMMMMMMM +MMMMMMMMMMMMMMMMMWX0xl;,...........',...,...':;;;;;::codk0XNWWMMMMMMMMMMMMMMMMMM +MMMMMMMMMMMMMMMMMMMMMWXOdc,'..',,,,,;;,,,'.,;ccldkOOKXNWMMMMMMMMMMMMMMMMMMMMMMMM +MMMMMMMMMMMMMMMMMMMMMMMMWN0xoc:cccccldkxxkOOO0KXNWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM +MMMMMMMMMMMMMMMMMMMMMMMMMMMMN0xc;;::cxXMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM + +`7MMF' `7MMF' + MM MM + MM MM ,6"Yb. M""MMV .gP"Ya 7MMpMMMb. + MMmmmmmmMM 8) MM ' AMV ,M' Yb MM MM + MM MM ,pm9MM AMV 8M~~~~~' MM MM + MM MM 8M MM AMV ,YM. , MM MM +.JMML. .JMML.`Moo9^Yo.AMMmmmM `Mbmmd'.JMML JMML. + + + +Welcome to the Hazen Command Line Interface +Usage: + hazen [--measured_slice_width=] [--subtract=] [--report] [--output=] + [--calc_t1 | --calc_t2] [--plate_number=] [--show_template_fit] + [--show_relax_fits] [--show_rois] [--log=] [--verbose] + hazen -h|--help + hazen --version +Options: +Report is an optional argument needed if you want to get a plot of your results. +'Calc_t1', 'calc_t', 'plate_number=', 'show_template_fit', 'show_relax_fits', 'show_rois', 'verbose' are optional arguments for the relaxometry function. +'Measured_slice_width' is an optional argument for the SNR function. +'Log' is an optional argument that allows users to set the severity of the logs. + snr | slice_position | slice_width | spatial_resolution | uniformity | ghosting | relaxometry | snr_map | + acr_ghosting | acr_uniformity | acr_spatial_resolution | acr_slice_thickness | acr_snr | acr_slice_position | acr_geometric_accuracy + + --report + +""" + +import importlib +import inspect import logging +import sys +import pprint +import os + +from docopt import docopt import pydicom -import hazenlib.utils +from hazenlib.logger import logger +from hazenlib.utils import is_dicom_file, get_dicom_files from hazenlib._version import __version__ +def parse_relaxometry_data(task, arguments, dicom_objects, + report): # def parse_relaxometry_data(arguments, dicom_objects, report): # + + # Relaxometry arguments + relaxometry_cli_args = {'--calc_t1', '--calc_t2', '--plate_number', + '--show_template_fit', '--show_relax_fits', + '--show_rois', '--verbose'} + + # Pass arguments with dictionary, stripping initial double dash ('--') + relaxometry_args = {} + + for key in relaxometry_cli_args: + relaxometry_args[key[2:]] = arguments[key] + + return task.main(dicom_objects, report_path=report, + **relaxometry_args) + + +def main(): + arguments = docopt(__doc__, version=__version__) + task_module = importlib.import_module(f"hazenlib.tasks.{arguments['']}") + files = get_dicom_files(arguments['']) + pp = pprint.PrettyPrinter(indent=4, depth=1, width=1) + + 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) + + class_list = [cls 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: + raise Exception(f'Task {task_module} has multiple class definitions: {class_list}') + + task = getattr(task_module, class_list[0].__name__)( + data_paths=files, report=arguments['--report'], + # TODO: Is this necessary? See HazenTask __init__() + report_dir=[ + arguments['--output'] if arguments['--output'] else os.path.join( + os.getcwd(), 'report')][0]) + + if not arguments[''] == 'snr' and arguments['--measured_slice_width']: + raise Exception("the (--measured_slice_width) option can only be used with snr") + elif not arguments[''] == 'acr_snr' and arguments['--subtract']: + raise Exception("the (--subtract) option can only be used with acr_snr") + elif arguments[''] == 'snr' and arguments['--measured_slice_width']: + measured_slice_width = float(arguments['--measured_slice_width']) + logger.info(f'Calculating SNR with measured slice width {measured_slice_width}') + result = task.run(measured_slice_width) + elif arguments[''] == 'acr_snr': + acr_snr_cli_args = {'--subtract'} + + acr_snr_args = {} + for key in acr_snr_cli_args: + acr_snr_args[key[2:]] = arguments[key] + + result = task.run(**acr_snr_args) + # TODO: Refactor Relaxometry task into HazenTask object Relaxometry not currently converted to HazenTask object - + # this task accessible in the CLI using the old syntax until it can be refactored + elif arguments[''] == 'relaxometry': + task = importlib.import_module(f"hazenlib.{arguments['']}") + dicom_objects = [pydicom.read_file(x, force=True) for x in files if is_dicom_file(x)] + result = parse_relaxometry_data(task, arguments, dicom_objects, report=True) + else: + result = task.run() + + return pp.pformat(result) + + +if __name__ == "__main__": + result = main() + print(result) diff --git a/hazenlib/__main__.py b/hazenlib/__main__.py deleted file mode 100644 index ee72beaf..00000000 --- a/hazenlib/__main__.py +++ /dev/null @@ -1,190 +0,0 @@ -""" -MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM -MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWWWNNNNNNNWWWWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM -MMMMMMMMMMMMMMMMMMMMMMMMMMMWWNNNNXXXXXXXXXXXXXXXKKKXNNWWMMMMMMMMMMMMMMMMMMMMMMMM -MMMMMMMMMMMMMMMMMMMMWWNNXKK0OOO0XNXK0KKXXXXKXNNX0OOOO0KKXXNNWMMMMMMMMMMMMMMMMMMM -MMMMMMMMMMMMMMMMWNXXK0kddxOkxO0KXXKXXXXXXXXXXXNXXXKOxdxkOO00KXWMMMMMMMMMMMMMMMMM -MMMMMMMMMMMMWWNKOxdxxxxxxxO0KNXKXNXKKK0KKKXKKKKKK0kkOkxolodxkk0XWMMMMMMMMMMMMMMM -MMMMMMMMMWNXXKOOkkxxxkOddOXNXKOOO0KKXXXNNWWNXK00OxdkXNX0kdc:coox0WMMMMMMMMMMMMMM -MMMMMMMWNKOOkxxxxxxkOOOOO00kkO0KNNWWNX0OkO0OxxO000KXXNNNWNXOdc:ldkNMMMMMMMMMMMMM -MMMMMMWKOxoc:lddxxxxkO0KKK0OkxkO00Okxlccclooooddooodxxk0KXXNWXkxddxKWMMMMMMMMMMM -MMMMMNklc:;:clooxxxkkkkO00OOOxc,,;;,,,,;;;;::looddxxkdoloxk0KXXNX0xx0WMMMMMMMMMM -MMMMNx:,;coxOXNNWWNNXXXXKOkxdl:,...';:clodkOKXNNNNNNWNXOdoodkkOKNWN0kONMMMMMMMMM -MMMNx:coxO0KKK0OkkkO0KXXKOkxoc;'.',;coxkO0KKKKKKKKKXNWWWX0kdddxkOKNWWKKXWMMMMMMM -MMW0odkOkkkxddxO0XXXXXKK0Odoooc'...,:ccccccccloodxxkOO0KXNNKkxxxxkOKXNXXXNMMMMMM -MNOdxxxdodxk0XNNXKKKXXX0OO00kc......,coxkkOkxxddoooddxxkk0XNNXOxddxkOO0KXXXWMMMM -XxloddoodOXNNXKKKXNWXOkOKX0l'.........;clldkO0KKKKKK0OOkkkO0KNNXOxddxkkO0KXKNWMM -OolllldOKXX0O0XWMWKxoxKNXk;............',;llcldxk0KNWWWWNK0OOOKXNXOxddxkkOKXKXWM -dc:cldk0OkkKNWWWXkox0XXKd'................;oxdlccldxOXNWMMWNX0OOKXNXOxddxxk0XKXM -kc:oxdxddOXWNXKOdx0NNXKl....................lKXKOkdoodxOKXNWMWX0OOKXNKOkxdxOXNXN -Kloxllld0K0KK0doONWNNKl......................:ONWWWNXKOkk0KKXWMNKOOOKNX0kxxk0NXX -W0o:,:xOxdk00xxKWWWWXl........................,o0XNWWMWNXXXKKNWMWKOkkOKNX0Ok0NXX -MWKl:ldlcx0OkOXWMWMXl....,:loxkxdlcc:::::;,'....,lkKXNWNNNXKKXNWMN0kxkOXWX00KNXX -MMMXxc;;cxkxkKWMMMXl..',:okOO0KKKKK00KXXXXK0Okdc,.'lkKKKKXNXKKXNMMXOxxkOXXOO0NXX -MMMMXl,;codx0WMMMXl'',:ldO0KXXNWWNNNNNNXXNNNNNNX0kl,,lk0OO0XXKKXWMWKkxxk0KOOOKXX -MMMMWO:;lookXMWWNd,;loxOXNNNNNKKNWWNXKKXXXNNWWNNNX0d,.'lxkkOKK0KNWMXkxkkOOkOOKKX -MMMMMXoclcxKWWNNk;';:clodk0KNWNXNWNK000kkdoddkKXXXOoc,..,lkkO000XNWKkxkkOkkOO00K -MMMMMXocclOXWWNO:'.',,,'.',:xXWNXOkkko:,''.'':ooloxxol:...cxkOOOKNW0dxxkOkxkO00K -MMMMMXdc:oO0XNXo.,:ccldoc,..,d0KOdodko:;::::;:lxklcdOOkc...:xkkkOXN0dxxkOxxkO0OK -MMMMMWOc;ok0KXKl'::;:oddol;..cOOkdokOd:cccclooooxd:;dKX0l'..:dxdkKX0ddxxkdxkOOON -MMMMMMNx;;oxk00l',;;,..;ddl,.;xOkdodOkdc;,,locxOdldOKWWNO:...:ol:oOkodkxddxOOOKW -MMMMMMMNx;;loxkd;,;;:cldkdc;'lKNN0xOXKOkkk0K000K0OKWNNXX0c.. 'c:';xxlododk0OO0WM -MMMMMMMMNOo:;:colodc:lddl;''cKWWNKkxO0OxxkKXNNNXKXNNNNNXkc.. .,;.'cc::cok0OkONMM -MMMMMMMMMMNOo;,;:xXKOxdoc;,c0WWWNX000KKK0KXXNXXXNWNNXXN0c.....,,..'..;lxxdoONMMM -MMMMMMMMMMMMW0d:;dKXNX0kdll0WMWWNXKXXXXXXXKKKXNNWWWWNXK0o...........;:c::dKWMMMM -MMMMMMMMMMMMMMW0:cOKNWXOllONWWNKK0KXXNNNNNNNXXKKXXXXXKKx:........',,;;:dKWMMMMMM -MMMMMMMMMMMMMMMWx:oO0Kk:,oKNWWXOkO00O0XNWWWWNXK000000Od:'.......'';ld0NMMMMMMMMM -MMMMMMMMMMMMMMMM0;,lxxc',o0XNNKdlOKOxxxOKXNNNNX0Okolxo,....',''cdkKWMMMMMMMMMMMM -MMMMMMMMMMMMMMMXo..:ddc,',lkkxl;,:lldK0xxk0XXXK0Odc',;'''...'',xWMMMMMMMMMMMMMMM -MMMMMMMMMMMMMMMNk:.'dko:'',;;;:;',cloxO0K0OO000Odcc:....'....''oNMMMMMMMMMMMMMMM -MMMMMMMMMMMMMMMMNo..coc;''',;;;:,,,,,,;lxxolodxoc;;c,..;,'.''..oNMMMMMMMMMMMMMMM -MMMMMMMMMMMMMMMMWO;.';:;:;;:od:cc:;;::,',,;,;;;;,,,c;.'::;'.,,..ckXMMMMMMMMMMMMM -MMMMMMMMMMMMMMMMM0:..,:;:clccl;,:,,,,''...':lc::loclc',::,..'.. .,oKWMMMMMMMMMM -MMMMMMMMMMMMMMMMWk'':lc,,,,coooooddl;......:occlllld:.,:,...... 'l0WMMMMMMMM -MMMMMMMMMMMMMMMMNd';loc:;'',:cccclc:,.......:;,cc:cl,'cc;....... .lONMMMMMM -MMMMMMMMMMMMMMMMXl';cccc;;:,'.. ..''''.....,c;:l:cc,,:c:,'.''..... .;d0NMMM -MMMMMMMMMMMMMMMMKc'',:loc::;,...'c;''...'',:;';lcc;,c:,,,.''.. .... ....;d0W -MMMMMMMMMMMMMMWXxlc;',ll:;;:;...;l;''',;:c:,..':;,;;;'.'..''. ............ .: -MMMMMMMMMMMMN0o;:do;',;,;ooc:,',,:;',:::cc;.'',:,.,,,'..................... . -MMMMMMMMMN0d;...,lc,'.':odl::;;:,';;;clc:;,'..,,''''.'...................... . -MMMMMMWKd;. ....;cc'.:odollc;::'.';,;l:,,;,.',.'',,.',,,'...................... -MMMMWKd,..........;;..,:ooloccol;',:;,lc,,',';:,;:'...''.........''''''''''..''' -MMMW0d:'..........',..';cclooxxdooll::oc;,...:c',:,.,:;'.''''..''',;;,,,,,,,;ccl -MMMWN0d:;,'............';:clodxxxxlc:;:;:,..;;'''::':c,,,;;,',,,;;,;;,'',;ldk00K -MMMMMWN0koc:,,'''..''''.';;;lldkxo::;;;,''.','..,:,''',,,,',,,:::::;;;;;cdKWWMMM -MMMMMMMMMWXKOxlc;,...','...,:;coc;;;;'''''.....,:,...''''',;::clcclddxk0XWMMMMMM -MMMMMMMMMMMMMWNN0d:,........''';;..,,........';;,'','',;;:codxkOO0KNWWMMMMMMMMMM -MMMMMMMMMMMMMMMMMWX0xl;,...........',...,...':;;;;;::codk0XNWWMMMMMMMMMMMMMMMMMM -MMMMMMMMMMMMMMMMMMMMMWXOdc,'..',,,,,;;,,,'.,;ccldkOOKXNWMMMMMMMMMMMMMMMMMMMMMMMM -MMMMMMMMMMMMMMMMMMMMMMMMWN0xoc:cccccldkxxkOOO0KXNWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM -MMMMMMMMMMMMMMMMMMMMMMMMMMMMN0xc;;::cxXMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM - - -`7MMF' `7MMF' - MM MM - MM MM ,6"Yb. M""MMV .gP"Ya 7MMpMMMb. - MMmmmmmmMM 8) MM ' AMV ,M' Yb MM MM - MM MM ,pm9MM AMV 8M~~~~~' MM MM - MM MM 8M MM AMV ,YM. , MM MM -.JMML. .JMML.`Moo9^Yo.AMMmmmM `Mbmmd'.JMML JMML. - - - -Welcome to the Hazen Command Line Interface -Usage: - hazen [--measured_slice_width=] [--subtract=] [--report] [--output=] - [--calc_t1 | --calc_t2] [--plate_number=] [--show_template_fit] - [--show_relax_fits] [--show_rois] [--log=] [--verbose] - hazen -h|--help - hazen --version -Options: -Report is an optional argument needed if you want to get a plot of your results. -'Calc_t1', 'calc_t', 'plate_number=', 'show_template_fit', 'show_relax_fits', 'show_rois', 'verbose' are optional arguments for the relaxometry function. -'Measured_slice_width' is an optional argument for the SNR function. -'Log' is an optional argument that allows users to set the severity of the logs. - snr | slice_position | slice_width | spatial_resolution | uniformity | ghosting | relaxometry | snr_map | - acr_ghosting | acr_uniformity | acr_spatial_resolution | acr_slice_thickness | acr_snr | acr_slice_position | acr_geometric_accuracy - - --report - -""" -import importlib -import inspect -import logging -import sys -import pprint -import os - -from docopt import docopt -import pydicom -from hazenlib.logger import logger -from hazenlib.utils import is_dicom_file, get_dicom_files -from hazenlib._version import __version__ - - -def parse_relaxometry_data(task, arguments, dicom_objects, - report): # def parse_relaxometry_data(arguments, dicom_objects, report): # - - # Relaxometry arguments - relaxometry_cli_args = {'--calc_t1', '--calc_t2', '--plate_number', - '--show_template_fit', '--show_relax_fits', - '--show_rois', '--verbose'} - - # Pass arguments with dictionary, stripping initial double dash ('--') - relaxometry_args = {} - - for key in relaxometry_cli_args: - relaxometry_args[key[2:]] = arguments[key] - - return task.main(dicom_objects, report_path=report, - **relaxometry_args) - - -def main(): - arguments = docopt(__doc__, version=__version__) - task_module = importlib.import_module(f"hazenlib.tasks.{arguments['']}") - files = get_dicom_files(arguments['']) - pp = pprint.PrettyPrinter(indent=4, depth=1, width=1) - - 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) - - class_list = [cls 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: - raise Exception(f'Task {task_module} has multiple class definitions: {class_list}') - - task = getattr(task_module, class_list[0].__name__)( - data_paths=files, report=arguments['--report'], - # TODO: Is this necessary? See HazenTask __init__() - report_dir=[ - arguments['--output'] if arguments['--output'] else os.path.join( - os.getcwd(), 'report')][0]) - - if not arguments[''] == 'snr' and arguments['--measured_slice_width']: - raise Exception("the (--measured_slice_width) option can only be used with snr") - elif not arguments[''] == 'acr_snr' and arguments['--subtract']: - raise Exception("the (--subtract) option can only be used with acr_snr") - elif arguments[''] == 'snr' and arguments['--measured_slice_width']: - measured_slice_width = float(arguments['--measured_slice_width']) - logger.info(f'Calculating SNR with measured slice width {measured_slice_width}') - result = task.run(measured_slice_width) - elif arguments[''] == 'acr_snr': - acr_snr_cli_args = {'--subtract'} - - acr_snr_args = {} - for key in acr_snr_cli_args: - acr_snr_args[key[2:]] = arguments[key] - - result = task.run(**acr_snr_args) - # TODO: Refactor Relaxometry task into HazenTask object Relaxometry not currently converted to HazenTask object - - # this task accessible in the CLI using the old syntax until it can be refactored - elif arguments[''] == 'relaxometry': - task = importlib.import_module(f"hazenlib.{arguments['']}") - dicom_objects = [pydicom.read_file(x, force=True) for x in files if is_dicom_file(x)] - result = parse_relaxometry_data(task, arguments, dicom_objects, report=True) - else: - result = task.run() - - return pp.pformat(result) - - -if __name__ == "__main__": - result = main() - print(result) diff --git a/setup.cfg b/setup.cfg index ef0a60be..8d6070c4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -25,4 +25,4 @@ include_package_data=True [options.entry_points] console_scripts = - hazen = hazenlib:__main__.main + hazen = hazenlib:main From e2da681eaea4734b9acdaa25aa762b71405ffacf Mon Sep 17 00:00:00 2001 From: sophie22 Date: Tue, 11 Jul 2023 19:03:06 +0100 Subject: [PATCH 18/27] move result printing into main() --- hazenlib/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hazenlib/__init__.py b/hazenlib/__init__.py index 6936bf41..4429c64b 100644 --- a/hazenlib/__init__.py +++ b/hazenlib/__init__.py @@ -182,9 +182,9 @@ def main(): else: result = task.run() - return pp.pformat(result) + print(pp.pformat(result)) + return None if __name__ == "__main__": - result = main() - print(result) + main() From ce81065268e76110c5055a1e3a187e9227d9d090 Mon Sep 17 00:00:00 2001 From: sophie22 Date: Tue, 11 Jul 2023 19:47:25 +0100 Subject: [PATCH 19/27] revert test script names --- tests/test_hazenlib.py | 217 ++++++++++++++++++++++++++++ tests/test_tools.py | 112 -------------- tests/test_utils.py | 321 ++++++++++++++--------------------------- 3 files changed, 325 insertions(+), 325 deletions(-) create mode 100644 tests/test_hazenlib.py delete mode 100644 tests/test_tools.py diff --git a/tests/test_hazenlib.py b/tests/test_hazenlib.py new file mode 100644 index 00000000..61e4bfcf --- /dev/null +++ b/tests/test_hazenlib.py @@ -0,0 +1,217 @@ +import pydicom +from tests import TEST_DATA_DIR, TEST_REPORT_DIR +import unittest +import numpy as np +from docopt import docopt +from hazenlib.logger import logger +import hazenlib.utils +from pprint import pprint +import sys +import ast +import os + +TEST_DICOM = str(TEST_DATA_DIR / 'toshiba' / 'TOSHIBA_TM_MR_DCM_V3_0.dcm') +TEST_DICOM = pydicom.read_file(TEST_DICOM) +print(TEST_DICOM.Columns * TEST_DICOM.PixelSpacing[0]) +test_dicoms = {'philips': {'file': str(TEST_DATA_DIR / 'resolution' / 'philips' / 'IM-0004-0002.dcm'), + 'MANUFACTURER': 'philips', + 'ROWS': 512, + 'COLUMNS': 512, + 'TR_CHECK': 500, + 'BW': 205.0, + 'ENHANCED': False, + 'PIX_ARRAY': 1, + 'SLICE_THICKNESS': 5, + 'PIX_SIZE': [0.48828125, 0.48828125], + 'AVERAGE': 1}, + 'siemens': {'file': str(TEST_DATA_DIR / 'resolution' / 'eastkent' / '256_sag.IMA'), + 'MANUFACTURER': 'siemens', + 'ROWS': 256, + 'COLUMNS': 256, + 'TR_CHECK': 500, + 'BW': 130.0, + 'ENHANCED': False, + 'PIX_ARRAY': 1, + 'SLICE_THICKNESS': 5, + 'PIX_SIZE': [0.9765625, 0.9765625], + 'AVERAGE': 1}, + 'toshiba': {'file': str(TEST_DATA_DIR / 'toshiba' / 'TOSHIBA_TM_MR_DCM_V3_0.dcm'), + 'MANUFACTURER': 'toshiba', + 'ROWS': 256, + 'COLUMNS': 256, + 'TR_CHECK': 45.0, + 'BW': 244.0, + 'ENHANCED': False, + 'PIX_ARRAY': 1, + 'SLICE_THICKNESS': 6, + 'PIX_SIZE': [1.0, 1.0], + 'AVERAGE': 1}, + 'ge': {'file': str(TEST_DATA_DIR / 'ge' / 'ge_eFilm.dcm'), + 'MANUFACTURER': 'ge', + 'ROWS': 256, + 'COLUMNS': 256, + 'TR_CHECK': 1000.0, + 'BW': 156.25, + 'ENHANCED': False, + 'PIX_ARRAY': 1, + 'SLICE_THICKNESS': 5, + 'PIX_SIZE': [0.625, 0.625], + 'AVERAGE': 1}} + + +class TestHazenlib(unittest.TestCase): + + def test_get_manufacturer(self): + for manufacturer in test_dicoms.keys(): + with pydicom.read_file(test_dicoms[manufacturer]['file']) as dcm: + assert hazenlib.utils.get_manufacturer(dcm) == test_dicoms[manufacturer]['MANUFACTURER'] + + def test_get_rows(self): + for manufacturer in test_dicoms.keys(): + with pydicom.read_file(test_dicoms[manufacturer]['file']) as dcm: + rows = hazenlib.utils.get_rows(dcm) + assert rows == test_dicoms[manufacturer]['ROWS'] + + def test_get_columns(self): + for manufacturer in test_dicoms.keys(): + with pydicom.read_file(test_dicoms[manufacturer]['file']) as dcm: + columns = hazenlib.utils.get_columns(dcm) + assert columns == test_dicoms[manufacturer]['COLUMNS'] + + def test_get_TR(self): + for manufacturer in test_dicoms.keys(): + with pydicom.read_file(test_dicoms[manufacturer]['file']) as dcm: + TR = hazenlib.utils.get_TR(dcm) + assert TR == test_dicoms[manufacturer]['TR_CHECK'] + + def test_get_bandwidth(self): + for manufacturer in test_dicoms.keys(): + with pydicom.read_file(test_dicoms[manufacturer]['file']) as dcm: + bw = hazenlib.utils.get_bandwidth(dcm) + assert bw == test_dicoms[manufacturer]['BW'] + + def test_is_enhanced(self): + for manufacturer in test_dicoms.keys(): + with pydicom.read_file(test_dicoms[manufacturer]['file']) as dcm: + enhanced = hazenlib.utils.is_enhanced_dicom(dcm) + assert enhanced == test_dicoms[manufacturer]['ENHANCED'] + + def test_get_num_of_frames(self): + for manufacturer in test_dicoms.keys(): + with pydicom.read_file(test_dicoms[manufacturer]['file']) as dcm: + pix_arr = hazenlib.utils.get_num_of_frames(dcm) + assert pix_arr == test_dicoms[manufacturer]['PIX_ARRAY'] + + def test_get_slice_thickness(self): + for manufacturer in test_dicoms.keys(): + with pydicom.read_file(test_dicoms[manufacturer]['file']) as dcm: + slice_thick = hazenlib.utils.get_slice_thickness(dcm) + assert slice_thick == test_dicoms[manufacturer]['SLICE_THICKNESS'] + + def get_average(self): + for manufacturer in test_dicoms.keys(): + with pydicom.read_file(test_dicoms[manufacturer]['file']) as dcm: + avg = hazenlib.utils.get_average(dcm) + assert avg == test_dicoms[manufacturer]['AVERAGE'] + + def get_pixel_size(self): + for manufacturer in test_dicoms.keys(): + with pydicom.read_file(test_dicoms[manufacturer]['file']) as dcm: + pix_size = hazenlib.utils.get_pixel_size(dcm) + pix_size = list(pix_size) + self.assertEqual(pix_size, test_dicoms[manufacturer]['PIX_SIZE']) + + def test_fov(self): + test_dicoms = {'philips': {'file': str(TEST_DATA_DIR / 'resolution' / 'philips' / 'IM-0004-0002.dcm'), + 'MANUFACTURER': 'philips', + 'FOV': 250.0}, + 'siemens': {'file': str(TEST_DATA_DIR / 'resolution' / 'eastkent' / '256_sag.IMA'), + 'MANUFACTURER': 'siemens', + 'FOV': 250.0}, + 'toshiba': {'file': str(TEST_DATA_DIR / 'toshiba' / 'TOSHIBA_TM_MR_DCM_V3_0.dcm'), + 'MANUFACTURER': 'toshiba', + 'FOV': 256.0}} + + for manufacturer in test_dicoms.keys(): + with pydicom.read_file(test_dicoms[manufacturer]['file']) as dcm: + # first test function + fov = hazenlib.utils.get_field_of_view(dcm) + print(fov) + assert fov == test_dicoms[manufacturer]['FOV'] + + +class Test(unittest.TestCase): + + def setUp(self): + self.file = str(TEST_DATA_DIR / 'resolution' / 'eastkent' / '256_sag.IMA') + self.dcm = pydicom.read_file(self.file) + self.dcm = self.dcm.pixel_array + + def test_rescale_to_byte(self): + test_array = np.array([[1, 2], [3, 4]]) + TEST_OUT = np.array([[63, 127], [191, 255]]) + test_array = hazenlib.utils.rescale_to_byte(test_array) + test_array = test_array.tolist() + TEST_OUT = TEST_OUT.tolist() + self.assertListEqual(test_array, TEST_OUT) + + +class TestCliParser(unittest.TestCase): + maxDiff = None + + def setUp(self): + self.file = str(TEST_DATA_DIR / 'resolution' / 'philips' / 'IM-0004-0002.dcm') + self.dcm = pydicom.read_file(self.file) + + def test1_logger(self): + path = str(TEST_DATA_DIR / 'resolution' / 'RESOLUTION') + sys.argv = ["hazen", "spatial_resolution", path, "--log", "warning"] + + hazenlib.main() + + logging = hazenlib.logging + + self.assertEqual(logging.root.level, logging.WARNING) + + def test2_logger(self): + path = str(TEST_DATA_DIR / 'resolution' / 'RESOLUTION') + sys.argv = ["hazen", "spatial_resolution", path] + + hazenlib.main() + + logging = hazenlib.logging + + self.assertEqual(logging.root.level, logging.INFO) + + def test_main_snr_exception(self): + path = str(TEST_DATA_DIR / 'snr' / 'Siemens') + sys.argv = ["hazen", "spatial_resolution", path, "--measured_slice_width=10"] + + self.assertRaises(Exception, hazenlib.main) + + def test_snr_measured_slice_width(self): + path = str(TEST_DATA_DIR / 'snr' / 'GE') + sys.argv = ["hazen", "snr", path, "--measured_slice_width", "1"] + + output = hazenlib.main() + output_dict = ast.literal_eval(output) + print(output_dict) + dict1 = {'snr_subtraction_measured_SNR_SNR_SAG_MEAS1_23_1': 183.97, + 'snr_subtraction_normalised_SNR_SNR_SAG_MEAS1_23_1': 7593.04, + 'snr_smoothing_measured_SNR_SNR_SAG_MEAS2_24_1': 183.93, + 'snr_smoothing_normalised_SNR_SNR_SAG_MEAS2_24_1': 7591.33, + 'snr_smoothing_measured_SNR_SNR_SAG_MEAS1_23_1': 179.94, + 'snr_smoothing_normalised_SNR_SNR_SAG_MEAS1_23_1': 7426.54} + + self.assertDictEqual(output_dict['SNR_SNR_SAG_MEAS1_23_1'], dict1) + + def test_relaxometry(self): + path = str(TEST_DATA_DIR / 'relaxometry' / 'T1' / 'site3_ge' / 'plate4') + sys.argv = ["hazen", "relaxometry", path, "--plate_number", "4", "--calc_t1"] + + output = hazenlib.main() + output_dict = ast.literal_eval(output) + + dict1 = {'Spin Echo_32_2_P4_t1': {'rms_frac_time_difference': 0.13499936644959437}} + self.assertAlmostEqual(dict1['Spin Echo_32_2_P4_t1']['rms_frac_time_difference'], + output_dict['Spin Echo_32_2_P4_t1']['rms_frac_time_difference'], 4) diff --git a/tests/test_tools.py b/tests/test_tools.py deleted file mode 100644 index b64e7330..00000000 --- a/tests/test_tools.py +++ /dev/null @@ -1,112 +0,0 @@ -import unittest -import os - -import numpy as np -import pydicom - -import hazenlib.utils as hazen_tools -from tests import TEST_DATA_DIR - - -class TestTools(unittest.TestCase): - SMALL_CIRCLE_PHANTOM_FILE = str( - TEST_DATA_DIR / 'ghosting' / 'PE_COL_PHANTOM_BOTTOM_RIGHT' / 'PE_COL_PHANTOM_BOTTOM_RIGHT.IMA') - small_circle_x, small_circle_y, small_circle_r = 1, 1, 22.579364776611328 - - LARGE_CIRCLE_PHANTOM_FILE = str( - TEST_DATA_DIR / 'uniformity' / 'axial_oil.IMA' - ) - large_circle_x, large_circle_y, large_circle_r = 128, 123, 97.84805297851562 - - SAG_RECTANGLE_PHANTOM_FILE = str( - TEST_DATA_DIR / 'uniformity' / 'sag.dcm' - ) - rectangle_size = (177.0, 204.0) - rectangle_angle = 0 - rectangle_centre = (130.500015, 134.499985) - - COR_RECTANGLE_PHANTOM_FILE = str( - TEST_DATA_DIR / 'uniformity' / 'cor.dcm' - ) - cor_rectangle_size = (206.047546, 194.684875) - cor_rectangle_angle = -89.1756591796875 - cor_rectangle_centre = (128.43869, 136.219971) - - COR2_RECTANGLE_PHANTOM_FILE = str( - TEST_DATA_DIR / 'uniformity' / 'cor2.dcm' - ) - cor2_rectangle_size = (194.4591522216797, 201.483292) - cor2_rectangle_angle = -1.576546 - cor2_rectangle_centre = (127.261551, 130.001953) - - -# @pytest.mark.skip -class TestShapeDetector(TestTools): - - def setUp(self) -> None: - pass - - def test_large_circle(self): - arr = pydicom.read_file(self.LARGE_CIRCLE_PHANTOM_FILE).pixel_array - shape_detector = hazen_tools.ShapeDetector(arr=arr) - x, y, r = shape_detector.get_shape('circle') - assert int(x), int(y) == (self.large_circle_x, self.large_circle_y) - assert round(r) == round(self.large_circle_r) - - def test_small_circle(self): - arr = pydicom.read_file(self.SMALL_CIRCLE_PHANTOM_FILE).pixel_array - shape_detector = hazen_tools.ShapeDetector(arr=arr) - x, y, r = shape_detector.get_shape('circle') - assert int(x), int(y) == (self.small_circle_x, self.small_circle_y) - assert round(r) == round(self.small_circle_r) - - def test_sag_rectangle(self): - arr = pydicom.read_file(self.SAG_RECTANGLE_PHANTOM_FILE).pixel_array - shape_detector = hazen_tools.ShapeDetector(arr=arr) - centre, size, angle = shape_detector.get_shape('rectangle') - np.testing.assert_allclose(centre, self.rectangle_centre, rtol=1e-02) - np.testing.assert_allclose(size, self.rectangle_size, rtol=1e-02) - np.testing.assert_allclose(angle, self.rectangle_angle, rtol=1e-02) - - def test_cor_rectangle(self): - arr = pydicom.read_file(self.COR_RECTANGLE_PHANTOM_FILE).pixel_array - shape_detector = hazen_tools.ShapeDetector(arr=arr) - centre, size, angle = shape_detector.get_shape('rectangle') - np.testing.assert_allclose(centre, self.cor_rectangle_centre, rtol=1e-02) - np.testing.assert_allclose(size, self.cor_rectangle_size, rtol=1e-02) - np.testing.assert_allclose(angle, self.cor_rectangle_angle, rtol=1e-02) - - def test_cor2_rectangle(self): - arr = pydicom.read_file(self.COR2_RECTANGLE_PHANTOM_FILE).pixel_array - shape_detector = hazen_tools.ShapeDetector(arr=arr) - centre, size, angle = shape_detector.get_shape('rectangle') - np.testing.assert_allclose(centre, self.cor2_rectangle_centre, rtol=1e-02) - np.testing.assert_allclose(size, self.cor2_rectangle_size, rtol=1e-02) - np.testing.assert_allclose(angle, self.cor2_rectangle_angle, rtol=1e-02) - - -class Test_is_Dicom_file(unittest.TestCase): - - def setUp(self) -> None: - data_folder = str(TEST_DATA_DIR / 'tools') - self.true_dicom_path = os.path.join(data_folder, 'dicom_yes.dcm') - self.false_dicom_path = os.path.join(data_folder, 'dicom_no.jfif') - - def test_is_dicom(self): - result = hazen_tools.is_dicom_file(self.true_dicom_path) - self.assertTrue(result) - - result = hazen_tools.is_dicom_file(self.false_dicom_path) - self.assertFalse(result) - - def test_is_dicom_yes(self): - result = hazen_tools.is_dicom_file(self.true_dicom_path) - self.assertTrue(result) - - def test_is_dicom_no(self): - result = hazen_tools.is_dicom_file(self.false_dicom_path) - self.assertFalse(result) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_utils.py b/tests/test_utils.py index 61e4bfcf..b64e7330 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,217 +1,112 @@ -import pydicom -from tests import TEST_DATA_DIR, TEST_REPORT_DIR import unittest -import numpy as np -from docopt import docopt -from hazenlib.logger import logger -import hazenlib.utils -from pprint import pprint -import sys -import ast import os -TEST_DICOM = str(TEST_DATA_DIR / 'toshiba' / 'TOSHIBA_TM_MR_DCM_V3_0.dcm') -TEST_DICOM = pydicom.read_file(TEST_DICOM) -print(TEST_DICOM.Columns * TEST_DICOM.PixelSpacing[0]) -test_dicoms = {'philips': {'file': str(TEST_DATA_DIR / 'resolution' / 'philips' / 'IM-0004-0002.dcm'), - 'MANUFACTURER': 'philips', - 'ROWS': 512, - 'COLUMNS': 512, - 'TR_CHECK': 500, - 'BW': 205.0, - 'ENHANCED': False, - 'PIX_ARRAY': 1, - 'SLICE_THICKNESS': 5, - 'PIX_SIZE': [0.48828125, 0.48828125], - 'AVERAGE': 1}, - 'siemens': {'file': str(TEST_DATA_DIR / 'resolution' / 'eastkent' / '256_sag.IMA'), - 'MANUFACTURER': 'siemens', - 'ROWS': 256, - 'COLUMNS': 256, - 'TR_CHECK': 500, - 'BW': 130.0, - 'ENHANCED': False, - 'PIX_ARRAY': 1, - 'SLICE_THICKNESS': 5, - 'PIX_SIZE': [0.9765625, 0.9765625], - 'AVERAGE': 1}, - 'toshiba': {'file': str(TEST_DATA_DIR / 'toshiba' / 'TOSHIBA_TM_MR_DCM_V3_0.dcm'), - 'MANUFACTURER': 'toshiba', - 'ROWS': 256, - 'COLUMNS': 256, - 'TR_CHECK': 45.0, - 'BW': 244.0, - 'ENHANCED': False, - 'PIX_ARRAY': 1, - 'SLICE_THICKNESS': 6, - 'PIX_SIZE': [1.0, 1.0], - 'AVERAGE': 1}, - 'ge': {'file': str(TEST_DATA_DIR / 'ge' / 'ge_eFilm.dcm'), - 'MANUFACTURER': 'ge', - 'ROWS': 256, - 'COLUMNS': 256, - 'TR_CHECK': 1000.0, - 'BW': 156.25, - 'ENHANCED': False, - 'PIX_ARRAY': 1, - 'SLICE_THICKNESS': 5, - 'PIX_SIZE': [0.625, 0.625], - 'AVERAGE': 1}} - - -class TestHazenlib(unittest.TestCase): - - def test_get_manufacturer(self): - for manufacturer in test_dicoms.keys(): - with pydicom.read_file(test_dicoms[manufacturer]['file']) as dcm: - assert hazenlib.utils.get_manufacturer(dcm) == test_dicoms[manufacturer]['MANUFACTURER'] - - def test_get_rows(self): - for manufacturer in test_dicoms.keys(): - with pydicom.read_file(test_dicoms[manufacturer]['file']) as dcm: - rows = hazenlib.utils.get_rows(dcm) - assert rows == test_dicoms[manufacturer]['ROWS'] - - def test_get_columns(self): - for manufacturer in test_dicoms.keys(): - with pydicom.read_file(test_dicoms[manufacturer]['file']) as dcm: - columns = hazenlib.utils.get_columns(dcm) - assert columns == test_dicoms[manufacturer]['COLUMNS'] - - def test_get_TR(self): - for manufacturer in test_dicoms.keys(): - with pydicom.read_file(test_dicoms[manufacturer]['file']) as dcm: - TR = hazenlib.utils.get_TR(dcm) - assert TR == test_dicoms[manufacturer]['TR_CHECK'] - - def test_get_bandwidth(self): - for manufacturer in test_dicoms.keys(): - with pydicom.read_file(test_dicoms[manufacturer]['file']) as dcm: - bw = hazenlib.utils.get_bandwidth(dcm) - assert bw == test_dicoms[manufacturer]['BW'] - - def test_is_enhanced(self): - for manufacturer in test_dicoms.keys(): - with pydicom.read_file(test_dicoms[manufacturer]['file']) as dcm: - enhanced = hazenlib.utils.is_enhanced_dicom(dcm) - assert enhanced == test_dicoms[manufacturer]['ENHANCED'] - - def test_get_num_of_frames(self): - for manufacturer in test_dicoms.keys(): - with pydicom.read_file(test_dicoms[manufacturer]['file']) as dcm: - pix_arr = hazenlib.utils.get_num_of_frames(dcm) - assert pix_arr == test_dicoms[manufacturer]['PIX_ARRAY'] - - def test_get_slice_thickness(self): - for manufacturer in test_dicoms.keys(): - with pydicom.read_file(test_dicoms[manufacturer]['file']) as dcm: - slice_thick = hazenlib.utils.get_slice_thickness(dcm) - assert slice_thick == test_dicoms[manufacturer]['SLICE_THICKNESS'] - - def get_average(self): - for manufacturer in test_dicoms.keys(): - with pydicom.read_file(test_dicoms[manufacturer]['file']) as dcm: - avg = hazenlib.utils.get_average(dcm) - assert avg == test_dicoms[manufacturer]['AVERAGE'] - - def get_pixel_size(self): - for manufacturer in test_dicoms.keys(): - with pydicom.read_file(test_dicoms[manufacturer]['file']) as dcm: - pix_size = hazenlib.utils.get_pixel_size(dcm) - pix_size = list(pix_size) - self.assertEqual(pix_size, test_dicoms[manufacturer]['PIX_SIZE']) - - def test_fov(self): - test_dicoms = {'philips': {'file': str(TEST_DATA_DIR / 'resolution' / 'philips' / 'IM-0004-0002.dcm'), - 'MANUFACTURER': 'philips', - 'FOV': 250.0}, - 'siemens': {'file': str(TEST_DATA_DIR / 'resolution' / 'eastkent' / '256_sag.IMA'), - 'MANUFACTURER': 'siemens', - 'FOV': 250.0}, - 'toshiba': {'file': str(TEST_DATA_DIR / 'toshiba' / 'TOSHIBA_TM_MR_DCM_V3_0.dcm'), - 'MANUFACTURER': 'toshiba', - 'FOV': 256.0}} - - for manufacturer in test_dicoms.keys(): - with pydicom.read_file(test_dicoms[manufacturer]['file']) as dcm: - # first test function - fov = hazenlib.utils.get_field_of_view(dcm) - print(fov) - assert fov == test_dicoms[manufacturer]['FOV'] - - -class Test(unittest.TestCase): - - def setUp(self): - self.file = str(TEST_DATA_DIR / 'resolution' / 'eastkent' / '256_sag.IMA') - self.dcm = pydicom.read_file(self.file) - self.dcm = self.dcm.pixel_array - - def test_rescale_to_byte(self): - test_array = np.array([[1, 2], [3, 4]]) - TEST_OUT = np.array([[63, 127], [191, 255]]) - test_array = hazenlib.utils.rescale_to_byte(test_array) - test_array = test_array.tolist() - TEST_OUT = TEST_OUT.tolist() - self.assertListEqual(test_array, TEST_OUT) - - -class TestCliParser(unittest.TestCase): - maxDiff = None - - def setUp(self): - self.file = str(TEST_DATA_DIR / 'resolution' / 'philips' / 'IM-0004-0002.dcm') - self.dcm = pydicom.read_file(self.file) - - def test1_logger(self): - path = str(TEST_DATA_DIR / 'resolution' / 'RESOLUTION') - sys.argv = ["hazen", "spatial_resolution", path, "--log", "warning"] - - hazenlib.main() - - logging = hazenlib.logging - - self.assertEqual(logging.root.level, logging.WARNING) - - def test2_logger(self): - path = str(TEST_DATA_DIR / 'resolution' / 'RESOLUTION') - sys.argv = ["hazen", "spatial_resolution", path] - - hazenlib.main() - - logging = hazenlib.logging - - self.assertEqual(logging.root.level, logging.INFO) - - def test_main_snr_exception(self): - path = str(TEST_DATA_DIR / 'snr' / 'Siemens') - sys.argv = ["hazen", "spatial_resolution", path, "--measured_slice_width=10"] - - self.assertRaises(Exception, hazenlib.main) - - def test_snr_measured_slice_width(self): - path = str(TEST_DATA_DIR / 'snr' / 'GE') - sys.argv = ["hazen", "snr", path, "--measured_slice_width", "1"] - - output = hazenlib.main() - output_dict = ast.literal_eval(output) - print(output_dict) - dict1 = {'snr_subtraction_measured_SNR_SNR_SAG_MEAS1_23_1': 183.97, - 'snr_subtraction_normalised_SNR_SNR_SAG_MEAS1_23_1': 7593.04, - 'snr_smoothing_measured_SNR_SNR_SAG_MEAS2_24_1': 183.93, - 'snr_smoothing_normalised_SNR_SNR_SAG_MEAS2_24_1': 7591.33, - 'snr_smoothing_measured_SNR_SNR_SAG_MEAS1_23_1': 179.94, - 'snr_smoothing_normalised_SNR_SNR_SAG_MEAS1_23_1': 7426.54} - - self.assertDictEqual(output_dict['SNR_SNR_SAG_MEAS1_23_1'], dict1) - - def test_relaxometry(self): - path = str(TEST_DATA_DIR / 'relaxometry' / 'T1' / 'site3_ge' / 'plate4') - sys.argv = ["hazen", "relaxometry", path, "--plate_number", "4", "--calc_t1"] - - output = hazenlib.main() - output_dict = ast.literal_eval(output) +import numpy as np +import pydicom - dict1 = {'Spin Echo_32_2_P4_t1': {'rms_frac_time_difference': 0.13499936644959437}} - self.assertAlmostEqual(dict1['Spin Echo_32_2_P4_t1']['rms_frac_time_difference'], - output_dict['Spin Echo_32_2_P4_t1']['rms_frac_time_difference'], 4) +import hazenlib.utils as hazen_tools +from tests import TEST_DATA_DIR + + +class TestTools(unittest.TestCase): + SMALL_CIRCLE_PHANTOM_FILE = str( + TEST_DATA_DIR / 'ghosting' / 'PE_COL_PHANTOM_BOTTOM_RIGHT' / 'PE_COL_PHANTOM_BOTTOM_RIGHT.IMA') + small_circle_x, small_circle_y, small_circle_r = 1, 1, 22.579364776611328 + + LARGE_CIRCLE_PHANTOM_FILE = str( + TEST_DATA_DIR / 'uniformity' / 'axial_oil.IMA' + ) + large_circle_x, large_circle_y, large_circle_r = 128, 123, 97.84805297851562 + + SAG_RECTANGLE_PHANTOM_FILE = str( + TEST_DATA_DIR / 'uniformity' / 'sag.dcm' + ) + rectangle_size = (177.0, 204.0) + rectangle_angle = 0 + rectangle_centre = (130.500015, 134.499985) + + COR_RECTANGLE_PHANTOM_FILE = str( + TEST_DATA_DIR / 'uniformity' / 'cor.dcm' + ) + cor_rectangle_size = (206.047546, 194.684875) + cor_rectangle_angle = -89.1756591796875 + cor_rectangle_centre = (128.43869, 136.219971) + + COR2_RECTANGLE_PHANTOM_FILE = str( + TEST_DATA_DIR / 'uniformity' / 'cor2.dcm' + ) + cor2_rectangle_size = (194.4591522216797, 201.483292) + cor2_rectangle_angle = -1.576546 + cor2_rectangle_centre = (127.261551, 130.001953) + + +# @pytest.mark.skip +class TestShapeDetector(TestTools): + + def setUp(self) -> None: + pass + + def test_large_circle(self): + arr = pydicom.read_file(self.LARGE_CIRCLE_PHANTOM_FILE).pixel_array + shape_detector = hazen_tools.ShapeDetector(arr=arr) + x, y, r = shape_detector.get_shape('circle') + assert int(x), int(y) == (self.large_circle_x, self.large_circle_y) + assert round(r) == round(self.large_circle_r) + + def test_small_circle(self): + arr = pydicom.read_file(self.SMALL_CIRCLE_PHANTOM_FILE).pixel_array + shape_detector = hazen_tools.ShapeDetector(arr=arr) + x, y, r = shape_detector.get_shape('circle') + assert int(x), int(y) == (self.small_circle_x, self.small_circle_y) + assert round(r) == round(self.small_circle_r) + + def test_sag_rectangle(self): + arr = pydicom.read_file(self.SAG_RECTANGLE_PHANTOM_FILE).pixel_array + shape_detector = hazen_tools.ShapeDetector(arr=arr) + centre, size, angle = shape_detector.get_shape('rectangle') + np.testing.assert_allclose(centre, self.rectangle_centre, rtol=1e-02) + np.testing.assert_allclose(size, self.rectangle_size, rtol=1e-02) + np.testing.assert_allclose(angle, self.rectangle_angle, rtol=1e-02) + + def test_cor_rectangle(self): + arr = pydicom.read_file(self.COR_RECTANGLE_PHANTOM_FILE).pixel_array + shape_detector = hazen_tools.ShapeDetector(arr=arr) + centre, size, angle = shape_detector.get_shape('rectangle') + np.testing.assert_allclose(centre, self.cor_rectangle_centre, rtol=1e-02) + np.testing.assert_allclose(size, self.cor_rectangle_size, rtol=1e-02) + np.testing.assert_allclose(angle, self.cor_rectangle_angle, rtol=1e-02) + + def test_cor2_rectangle(self): + arr = pydicom.read_file(self.COR2_RECTANGLE_PHANTOM_FILE).pixel_array + shape_detector = hazen_tools.ShapeDetector(arr=arr) + centre, size, angle = shape_detector.get_shape('rectangle') + np.testing.assert_allclose(centre, self.cor2_rectangle_centre, rtol=1e-02) + np.testing.assert_allclose(size, self.cor2_rectangle_size, rtol=1e-02) + np.testing.assert_allclose(angle, self.cor2_rectangle_angle, rtol=1e-02) + + +class Test_is_Dicom_file(unittest.TestCase): + + def setUp(self) -> None: + data_folder = str(TEST_DATA_DIR / 'tools') + self.true_dicom_path = os.path.join(data_folder, 'dicom_yes.dcm') + self.false_dicom_path = os.path.join(data_folder, 'dicom_no.jfif') + + def test_is_dicom(self): + result = hazen_tools.is_dicom_file(self.true_dicom_path) + self.assertTrue(result) + + result = hazen_tools.is_dicom_file(self.false_dicom_path) + self.assertFalse(result) + + def test_is_dicom_yes(self): + result = hazen_tools.is_dicom_file(self.true_dicom_path) + self.assertTrue(result) + + def test_is_dicom_no(self): + result = hazen_tools.is_dicom_file(self.false_dicom_path) + self.assertFalse(result) + + +if __name__ == "__main__": + unittest.main() From af32a93590e2368a4a9faa1e20c54d5e542b7784 Mon Sep 17 00:00:00 2001 From: sophie22 Date: Wed, 12 Jul 2023 08:51:54 +0100 Subject: [PATCH 20/27] return pprint of result --- hazenlib/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/hazenlib/__init__.py b/hazenlib/__init__.py index 4429c64b..33b76d2a 100644 --- a/hazenlib/__init__.py +++ b/hazenlib/__init__.py @@ -182,9 +182,8 @@ def main(): else: result = task.run() - print(pp.pformat(result)) - return None + return pp.pformat(result) if __name__ == "__main__": - main() + result = main() From 98f1627f70dde6356768c3f66b0b0ce03f5a9b6f Mon Sep 17 00:00:00 2001 From: Tom Roberts Date: Mon, 17 Jul 2023 11:40:18 +0100 Subject: [PATCH 21/27] QoL changes to utils.py --- hazenlib/utils.py | 38 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/hazenlib/utils.py b/hazenlib/utils.py index 4dc0e887..52315cf1 100644 --- a/hazenlib/utils.py +++ b/hazenlib/utils.py @@ -6,10 +6,8 @@ import matplotlib import numpy as np import pydicom -matplotlib.use("Agg") - import hazenlib.exceptions as exc - +matplotlib.use("Agg") def get_dicom_files(folder: str, sort=False) -> list: @@ -22,7 +20,7 @@ def get_dicom_files(folder: str, sort=False) -> list: def is_dicom_file(filename): - """ + """ Util function to check if file is a dicom file the first 128 bytes are preamble the next 4 bytes should contain DICM otherwise it is not a dicom @@ -31,14 +29,14 @@ def is_dicom_file(filename): :type filename: str :returns: True if it is a dicom file """ - file_stream = open(filename, 'rb') - file_stream.seek(128) - data = file_stream.read(4) - file_stream.close() - if data == b'DICM': - return True - else: - return False + file_stream = open(filename, 'rb') + file_stream.seek(128) + data = file_stream.read(4) + file_stream.close() + if data == b'DICM': + return True + else: + return False def is_enhanced_dicom(dcm: pydicom.Dataset) -> bool: @@ -310,6 +308,7 @@ class ShapeDetector: This class is largely adapted from https://www.pyimagesearch.com/2016/02/08/opencv-shape-detection/ """ + def __init__(self, arr): self.arr = arr self.contours = None @@ -319,7 +318,7 @@ def __init__(self, arr): def find_contours(self): # convert the resized image to grayscale, blur it slightly, and threshold it - self.blurred = cv.GaussianBlur(self.arr.copy(), (5, 5), 0) # magic numbers + self.blurred = cv.GaussianBlur(self.arr.copy(), (5, 5), 0) # magic numbers optimal_threshold = filters.threshold_li(self.blurred, initial_guess=np.quantile(self.blurred, 0.50)) self.thresh = np.where(self.blurred > optimal_threshold, 255, 0).astype(np.uint8) @@ -372,7 +371,7 @@ def get_shape(self, shape): raise exc.ShapeDetectionError(shape) if len(self.shapes[shape]) > 1: - shapes = [{shape: len(contours)}for shape, contours in self.shapes.items()] + shapes = [{shape: len(contours)} for shape, contours in self.shapes.items()] raise exc.MultipleShapesError(shapes) contour = self.shapes[shape][0] @@ -381,16 +380,15 @@ def get_shape(self, shape): (x, y), r = cv.minEnclosingCircle(contour) return x, y, r - #have changed name of outputs in below code to match cv.minAreaRect output - #(https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_contours/py_contour_features/py_contour_features.html#b-rotated-rectangle) + # Outputs in below code chosen to match cv.minAreaRect output + # https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_contours/py_contour_features/py_contour_features.html#b-rotated-rectangle # (x,y) is top-left of rectangle, in x, y coordinates. x=column, y=row. if shape == 'rectangle' or shape == 'square': - (x,y), size, angle = cv.minAreaRect(contour) + (x, y), size, angle = cv.minAreaRect(contour) # OpenCV v4.5 adjustment # - cv.minAreaRect() output tuple order changed since v3.4 # - swap size order & rotate angle by -90 size = (size[1], size[0]) - angle = angle-90 - return (x,y), size, angle - + angle = angle - 90 + return (x, y), size, angle From 561cf33215e697ffc0ccd20068918facaf7170aa Mon Sep 17 00:00:00 2001 From: Tom Roberts Date: Mon, 17 Jul 2023 11:45:18 +0100 Subject: [PATCH 22/27] QoL changes to __init__.py --- hazenlib/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/hazenlib/__init__.py b/hazenlib/__init__.py index 33b76d2a..4323f98f 100644 --- a/hazenlib/__init__.py +++ b/hazenlib/__init__.py @@ -103,8 +103,7 @@ from hazenlib._version import __version__ -def parse_relaxometry_data(task, arguments, dicom_objects, - report): # def parse_relaxometry_data(arguments, dicom_objects, report): # +def parse_relaxometry_data(task, arguments, dicom_objects, report): # Relaxometry arguments relaxometry_cli_args = {'--calc_t1', '--calc_t2', '--plate_number', @@ -173,8 +172,9 @@ def main(): acr_snr_args[key[2:]] = arguments[key] result = task.run(**acr_snr_args) - # TODO: Refactor Relaxometry task into HazenTask object Relaxometry not currently converted to HazenTask object - - # this task accessible in the CLI using the old syntax until it can be refactored + # TODO: Refactor Relaxometry task into HazenTask object + # - Relaxometry not currently converted to HazenTask object + # - Relaxometry task accessible via CLI using the old syntax until it can be refactored elif arguments[''] == 'relaxometry': task = importlib.import_module(f"hazenlib.{arguments['']}") dicom_objects = [pydicom.read_file(x, force=True) for x in files if is_dicom_file(x)] From 04c984e9af1f6025549b9a2c974d2e355de9bcf7 Mon Sep 17 00:00:00 2001 From: Tom Roberts Date: Mon, 17 Jul 2023 12:09:50 +0100 Subject: [PATCH 23/27] QoL changes to test_cli.yml --- .github/workflows/test_cli.yml | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/.github/workflows/test_cli.yml b/.github/workflows/test_cli.yml index aa7053f2..fdb37fa0 100644 --- a/.github/workflows/test_cli.yml +++ b/.github/workflows/test_cli.yml @@ -33,82 +33,83 @@ jobs: run: | pip install . - - name: test snr - if: always() # will always run regardless of whether previous step fails - useful to ensure all CLI functions tested + # "if: always option" runs step even if previous fails - useful to ensure all CLI functions tested + - name: test_snr + if: always() run: | hazen snr tests/data/snr/Siemens --report hazen snr tests/data/snr/Siemens --measured_slice_width 5.0012 --report - name: test_acr_snr - if: always() # will always run regardless of whether previous step fails - useful to ensure all CLI functions tested + if: always() run: | hazen acr_snr tests/data/acr/Siemens --report hazen acr_snr tests/data/acr/Siemens --subtract tests/data/acr/Siemens2 --report - name: test acr_uniformity - if: always() # will always run regardless of whether previous step fails - useful to ensure all CLI functions tested + if: always() run: | hazen acr_uniformity tests/data/acr/Siemens --report - name: test acr_ghosting - if: always() # will always run regardless of whether previous step fails - useful to ensure all CLI functions tested + if: always() run: | hazen acr_ghosting tests/data/acr/Siemens --report - name: test acr_slice_position - if: always() # will always run regardless of whether previous step fails - useful to ensure all CLI functions tested + if: always() run: | hazen acr_slice_position tests/data/acr/Siemens --report - name: test acr_slice_thickness - if: always() # will always run regardless of whether previous step fails - useful to ensure all CLI functions tested + if: always() run: | hazen acr_slice_thickness tests/data/acr/Siemens --report - name: test acr_geometric_accuracy - if: always() # will always run regardless of whether previous step fails - useful to ensure all CLI functions tested + if: always() run: | hazen acr_geometric_accuracy tests/data/acr/Siemens --report - name: test acr_spatial_resolution - if: always() # will always run regardless of whether previous step fails - useful to ensure all CLI functions tested + if: always() run: | hazen acr_spatial_resolution tests/data/acr/SiemensMTF --report - name: test slice_position - if: always() # will always run regardless of whether previous step fails - useful to ensure all CLI functions tested + if: always() run: | hazen slice_position tests/data/slicepos/SLICEPOSITION --report - name: test slice_width - if: always() # will always run regardless of whether previous step fails - useful to ensure all CLI functions tested + if: always() run: | hazen slice_width tests/data/slicewidth/512_matrix --report hazen slice_width tests/data/slicewidth/SLICEWIDTH --report - name: test spatial_resolution - if: always() # will always run regardless of whether previous step fails - useful to ensure all CLI functions tested + if: always() run: | hazen spatial_resolution tests/data/resolution/RESOLUTION/ --report - name: test uniformity - if: always() # will always run regardless of whether previous step fails - useful to ensure all CLI functions tested + if: always() run: | hazen uniformity tests/data/uniformity --report - name: test ghosting - if: always() # will always run regardless of whether previous step fails - useful to ensure all CLI functions tested + if: always() run: | hazen ghosting tests/data/ghosting/GHOSTING --report hazen ghosting tests/data/ghosting/PE_COL_PHANTOM_BOTTOM_RIGHT --report - name: test snr_map - if: always() # will always run regardless of whether previous step fails - useful to ensure all CLI functions tested + if: always() run: | hazen snr_map tests/data/snr/Siemens --report - name: test relaxometry - if: always() # will always run regardless of whether previous step fails - useful to ensure all CLI functions tested + if: always() run: | hazen relaxometry tests/data/relaxometry/T1/site1_20200218/plate5 --calc_t1 --plate_number=5 --report From 24b39b0a6460ad17fcf7f4b24ab05249560a9b9d Mon Sep 17 00:00:00 2001 From: sophie22 Date: Tue, 18 Jul 2023 12:51:23 +0100 Subject: [PATCH 24/27] move unit tests for util funcs to test_utils.py from test_hazenlib --- tests/test_hazenlib.py | 146 ----------------------------------------- tests/test_utils.py | 145 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 143 insertions(+), 148 deletions(-) diff --git a/tests/test_hazenlib.py b/tests/test_hazenlib.py index 61e4bfcf..c4563754 100644 --- a/tests/test_hazenlib.py +++ b/tests/test_hazenlib.py @@ -10,151 +10,6 @@ import ast import os -TEST_DICOM = str(TEST_DATA_DIR / 'toshiba' / 'TOSHIBA_TM_MR_DCM_V3_0.dcm') -TEST_DICOM = pydicom.read_file(TEST_DICOM) -print(TEST_DICOM.Columns * TEST_DICOM.PixelSpacing[0]) -test_dicoms = {'philips': {'file': str(TEST_DATA_DIR / 'resolution' / 'philips' / 'IM-0004-0002.dcm'), - 'MANUFACTURER': 'philips', - 'ROWS': 512, - 'COLUMNS': 512, - 'TR_CHECK': 500, - 'BW': 205.0, - 'ENHANCED': False, - 'PIX_ARRAY': 1, - 'SLICE_THICKNESS': 5, - 'PIX_SIZE': [0.48828125, 0.48828125], - 'AVERAGE': 1}, - 'siemens': {'file': str(TEST_DATA_DIR / 'resolution' / 'eastkent' / '256_sag.IMA'), - 'MANUFACTURER': 'siemens', - 'ROWS': 256, - 'COLUMNS': 256, - 'TR_CHECK': 500, - 'BW': 130.0, - 'ENHANCED': False, - 'PIX_ARRAY': 1, - 'SLICE_THICKNESS': 5, - 'PIX_SIZE': [0.9765625, 0.9765625], - 'AVERAGE': 1}, - 'toshiba': {'file': str(TEST_DATA_DIR / 'toshiba' / 'TOSHIBA_TM_MR_DCM_V3_0.dcm'), - 'MANUFACTURER': 'toshiba', - 'ROWS': 256, - 'COLUMNS': 256, - 'TR_CHECK': 45.0, - 'BW': 244.0, - 'ENHANCED': False, - 'PIX_ARRAY': 1, - 'SLICE_THICKNESS': 6, - 'PIX_SIZE': [1.0, 1.0], - 'AVERAGE': 1}, - 'ge': {'file': str(TEST_DATA_DIR / 'ge' / 'ge_eFilm.dcm'), - 'MANUFACTURER': 'ge', - 'ROWS': 256, - 'COLUMNS': 256, - 'TR_CHECK': 1000.0, - 'BW': 156.25, - 'ENHANCED': False, - 'PIX_ARRAY': 1, - 'SLICE_THICKNESS': 5, - 'PIX_SIZE': [0.625, 0.625], - 'AVERAGE': 1}} - - -class TestHazenlib(unittest.TestCase): - - def test_get_manufacturer(self): - for manufacturer in test_dicoms.keys(): - with pydicom.read_file(test_dicoms[manufacturer]['file']) as dcm: - assert hazenlib.utils.get_manufacturer(dcm) == test_dicoms[manufacturer]['MANUFACTURER'] - - def test_get_rows(self): - for manufacturer in test_dicoms.keys(): - with pydicom.read_file(test_dicoms[manufacturer]['file']) as dcm: - rows = hazenlib.utils.get_rows(dcm) - assert rows == test_dicoms[manufacturer]['ROWS'] - - def test_get_columns(self): - for manufacturer in test_dicoms.keys(): - with pydicom.read_file(test_dicoms[manufacturer]['file']) as dcm: - columns = hazenlib.utils.get_columns(dcm) - assert columns == test_dicoms[manufacturer]['COLUMNS'] - - def test_get_TR(self): - for manufacturer in test_dicoms.keys(): - with pydicom.read_file(test_dicoms[manufacturer]['file']) as dcm: - TR = hazenlib.utils.get_TR(dcm) - assert TR == test_dicoms[manufacturer]['TR_CHECK'] - - def test_get_bandwidth(self): - for manufacturer in test_dicoms.keys(): - with pydicom.read_file(test_dicoms[manufacturer]['file']) as dcm: - bw = hazenlib.utils.get_bandwidth(dcm) - assert bw == test_dicoms[manufacturer]['BW'] - - def test_is_enhanced(self): - for manufacturer in test_dicoms.keys(): - with pydicom.read_file(test_dicoms[manufacturer]['file']) as dcm: - enhanced = hazenlib.utils.is_enhanced_dicom(dcm) - assert enhanced == test_dicoms[manufacturer]['ENHANCED'] - - def test_get_num_of_frames(self): - for manufacturer in test_dicoms.keys(): - with pydicom.read_file(test_dicoms[manufacturer]['file']) as dcm: - pix_arr = hazenlib.utils.get_num_of_frames(dcm) - assert pix_arr == test_dicoms[manufacturer]['PIX_ARRAY'] - - def test_get_slice_thickness(self): - for manufacturer in test_dicoms.keys(): - with pydicom.read_file(test_dicoms[manufacturer]['file']) as dcm: - slice_thick = hazenlib.utils.get_slice_thickness(dcm) - assert slice_thick == test_dicoms[manufacturer]['SLICE_THICKNESS'] - - def get_average(self): - for manufacturer in test_dicoms.keys(): - with pydicom.read_file(test_dicoms[manufacturer]['file']) as dcm: - avg = hazenlib.utils.get_average(dcm) - assert avg == test_dicoms[manufacturer]['AVERAGE'] - - def get_pixel_size(self): - for manufacturer in test_dicoms.keys(): - with pydicom.read_file(test_dicoms[manufacturer]['file']) as dcm: - pix_size = hazenlib.utils.get_pixel_size(dcm) - pix_size = list(pix_size) - self.assertEqual(pix_size, test_dicoms[manufacturer]['PIX_SIZE']) - - def test_fov(self): - test_dicoms = {'philips': {'file': str(TEST_DATA_DIR / 'resolution' / 'philips' / 'IM-0004-0002.dcm'), - 'MANUFACTURER': 'philips', - 'FOV': 250.0}, - 'siemens': {'file': str(TEST_DATA_DIR / 'resolution' / 'eastkent' / '256_sag.IMA'), - 'MANUFACTURER': 'siemens', - 'FOV': 250.0}, - 'toshiba': {'file': str(TEST_DATA_DIR / 'toshiba' / 'TOSHIBA_TM_MR_DCM_V3_0.dcm'), - 'MANUFACTURER': 'toshiba', - 'FOV': 256.0}} - - for manufacturer in test_dicoms.keys(): - with pydicom.read_file(test_dicoms[manufacturer]['file']) as dcm: - # first test function - fov = hazenlib.utils.get_field_of_view(dcm) - print(fov) - assert fov == test_dicoms[manufacturer]['FOV'] - - -class Test(unittest.TestCase): - - def setUp(self): - self.file = str(TEST_DATA_DIR / 'resolution' / 'eastkent' / '256_sag.IMA') - self.dcm = pydicom.read_file(self.file) - self.dcm = self.dcm.pixel_array - - def test_rescale_to_byte(self): - test_array = np.array([[1, 2], [3, 4]]) - TEST_OUT = np.array([[63, 127], [191, 255]]) - test_array = hazenlib.utils.rescale_to_byte(test_array) - test_array = test_array.tolist() - TEST_OUT = TEST_OUT.tolist() - self.assertListEqual(test_array, TEST_OUT) - class TestCliParser(unittest.TestCase): maxDiff = None @@ -195,7 +50,6 @@ def test_snr_measured_slice_width(self): output = hazenlib.main() output_dict = ast.literal_eval(output) - print(output_dict) dict1 = {'snr_subtraction_measured_SNR_SNR_SAG_MEAS1_23_1': 183.97, 'snr_subtraction_normalised_SNR_SNR_SAG_MEAS1_23_1': 7593.04, 'snr_smoothing_measured_SNR_SNR_SAG_MEAS2_24_1': 183.93, diff --git a/tests/test_utils.py b/tests/test_utils.py index b64e7330..22cc5637 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -8,7 +8,7 @@ from tests import TEST_DATA_DIR -class TestTools(unittest.TestCase): +class ShapeSetUp(unittest.TestCase): SMALL_CIRCLE_PHANTOM_FILE = str( TEST_DATA_DIR / 'ghosting' / 'PE_COL_PHANTOM_BOTTOM_RIGHT' / 'PE_COL_PHANTOM_BOTTOM_RIGHT.IMA') small_circle_x, small_circle_y, small_circle_r = 1, 1, 22.579364776611328 @@ -41,7 +41,7 @@ class TestTools(unittest.TestCase): # @pytest.mark.skip -class TestShapeDetector(TestTools): +class TestShapeDetector(ShapeSetUp): def setUp(self) -> None: pass @@ -108,5 +108,146 @@ def test_is_dicom_no(self): self.assertFalse(result) +class TestUtils(unittest.TestCase): + + def setUp(self): + TEST_DICOM = str(TEST_DATA_DIR / 'toshiba' / 'TOSHIBA_TM_MR_DCM_V3_0.dcm') + TEST_DICOM = pydicom.read_file(TEST_DICOM) + print(TEST_DICOM.Columns * TEST_DICOM.PixelSpacing[0]) + self.test_dicoms = {'philips': {'file': str(TEST_DATA_DIR / 'resolution' / 'philips' / 'IM-0004-0002.dcm'), + 'MANUFACTURER': 'philips', + 'ROWS': 512, + 'COLUMNS': 512, + 'TR_CHECK': 500, + 'BW': 205.0, + 'ENHANCED': False, + 'PIX_ARRAY': 1, + 'SLICE_THICKNESS': 5, + 'PIX_SIZE': [0.48828125, 0.48828125], + 'AVERAGE': 1}, + 'siemens': {'file': str(TEST_DATA_DIR / 'resolution' / 'eastkent' / '256_sag.IMA'), + 'MANUFACTURER': 'siemens', + 'ROWS': 256, + 'COLUMNS': 256, + 'TR_CHECK': 500, + 'BW': 130.0, + 'ENHANCED': False, + 'PIX_ARRAY': 1, + 'SLICE_THICKNESS': 5, + 'PIX_SIZE': [0.9765625, 0.9765625], + 'AVERAGE': 1}, + 'toshiba': {'file': str(TEST_DATA_DIR / 'toshiba' / 'TOSHIBA_TM_MR_DCM_V3_0.dcm'), + 'MANUFACTURER': 'toshiba', + 'ROWS': 256, + 'COLUMNS': 256, + 'TR_CHECK': 45.0, + 'BW': 244.0, + 'ENHANCED': False, + 'PIX_ARRAY': 1, + 'SLICE_THICKNESS': 6, + 'PIX_SIZE': [1.0, 1.0], + 'AVERAGE': 1}, + 'ge': {'file': str(TEST_DATA_DIR / 'ge' / 'ge_eFilm.dcm'), + 'MANUFACTURER': 'ge', + 'ROWS': 256, + 'COLUMNS': 256, + 'TR_CHECK': 1000.0, + 'BW': 156.25, + 'ENHANCED': False, + 'PIX_ARRAY': 1, + 'SLICE_THICKNESS': 5, + 'PIX_SIZE': [0.625, 0.625], + 'AVERAGE': 1}} + + def test_is_enhanced(self): + for manufacturer in self.test_dicoms.keys(): + with pydicom.read_file(self.test_dicoms[manufacturer]['file']) as dcm: + enhanced = hazen_tools.is_enhanced_dicom(dcm) + assert enhanced == self.test_dicoms[manufacturer]['ENHANCED'] + + def test_get_manufacturer(self): + for manufacturer in self.test_dicoms.keys(): + with pydicom.read_file(self.test_dicoms[manufacturer]['file']) as dcm: + assert hazen_tools.get_manufacturer(dcm) == self.test_dicoms[manufacturer]['MANUFACTURER'] + + def get_average(self): + for manufacturer in self.test_dicoms.keys(): + with pydicom.read_file(self.test_dicoms[manufacturer]['file']) as dcm: + avg = hazen_tools.get_average(dcm) + assert avg == self.test_dicoms[manufacturer]['AVERAGE'] + + def test_get_bandwidth(self): + for manufacturer in self.test_dicoms.keys(): + with pydicom.read_file(self.test_dicoms[manufacturer]['file']) as dcm: + bw = hazen_tools.get_bandwidth(dcm) + assert bw == self.test_dicoms[manufacturer]['BW'] + + def test_get_num_of_frames(self): + for manufacturer in self.test_dicoms.keys(): + with pydicom.read_file(self.test_dicoms[manufacturer]['file']) as dcm: + pix_arr = hazen_tools.get_num_of_frames(dcm) + assert pix_arr == self.test_dicoms[manufacturer]['PIX_ARRAY'] + + def test_get_slice_thickness(self): + for manufacturer in self.test_dicoms.keys(): + with pydicom.read_file(self.test_dicoms[manufacturer]['file']) as dcm: + slice_thick = hazen_tools.get_slice_thickness(dcm) + assert slice_thick == self.test_dicoms[manufacturer]['SLICE_THICKNESS'] + + def get_pixel_size(self): + for manufacturer in self.test_dicoms.keys(): + with pydicom.read_file(self.test_dicoms[manufacturer]['file']) as dcm: + pix_size = hazen_tools.get_pixel_size(dcm) + pix_size = list(pix_size) + self.assertEqual(pix_size, self.test_dicoms[manufacturer]['PIX_SIZE']) + + def test_get_TR(self): + for manufacturer in self.test_dicoms.keys(): + with pydicom.read_file(self.test_dicoms[manufacturer]['file']) as dcm: + TR = hazen_tools.get_TR(dcm) + assert TR == self.test_dicoms[manufacturer]['TR_CHECK'] + + def test_get_rows(self): + for manufacturer in self.test_dicoms.keys(): + with pydicom.read_file(self.test_dicoms[manufacturer]['file']) as dcm: + rows = hazen_tools.get_rows(dcm) + assert rows == self.test_dicoms[manufacturer]['ROWS'] + + def test_get_columns(self): + for manufacturer in self.test_dicoms.keys(): + with pydicom.read_file(self.test_dicoms[manufacturer]['file']) as dcm: + columns = hazen_tools.get_columns(dcm) + assert columns == self.test_dicoms[manufacturer]['COLUMNS'] + + def test_fov(self): + self.test_dicoms = {'philips': {'file': str(TEST_DATA_DIR / 'resolution' / 'philips' / 'IM-0004-0002.dcm'), + 'MANUFACTURER': 'philips', + 'FOV': 250.0}, + 'siemens': {'file': str(TEST_DATA_DIR / 'resolution' / 'eastkent' / '256_sag.IMA'), + 'MANUFACTURER': 'siemens', + 'FOV': 250.0}, + 'toshiba': {'file': str(TEST_DATA_DIR / 'toshiba' / 'TOSHIBA_TM_MR_DCM_V3_0.dcm'), + 'MANUFACTURER': 'toshiba', + 'FOV': 256.0}} + + for manufacturer in self.test_dicoms.keys(): + with pydicom.read_file(self.test_dicoms[manufacturer]['file']) as dcm: + # first test function + fov = hazen_tools.get_field_of_view(dcm) + print(fov) + assert fov == self.test_dicoms[manufacturer]['FOV'] + + # def test_get_image_orientation(self): + # # TODO add unit test for image orientation + + def test_rescale_to_byte(self): + test_array = np.array([[1, 2], [3, 4]]) + TEST_OUT = np.array([[63, 127], [191, 255]]) + test_array = hazen_tools.rescale_to_byte(test_array) + test_array = test_array.tolist() + TEST_OUT = TEST_OUT.tolist() + self.assertListEqual(test_array, TEST_OUT) + + if __name__ == "__main__": unittest.main() From 200e951d1eca690f7e6f451eb80b6e1130fdb668 Mon Sep 17 00:00:00 2001 From: sophie22 Date: Tue, 18 Jul 2023 13:33:31 +0100 Subject: [PATCH 25/27] remove return from main() --- hazenlib/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hazenlib/__init__.py b/hazenlib/__init__.py index 4323f98f..9a12c6c4 100644 --- a/hazenlib/__init__.py +++ b/hazenlib/__init__.py @@ -182,8 +182,8 @@ def main(): else: result = task.run() - return pp.pformat(result) + print(pp.pformat(result)) if __name__ == "__main__": - result = main() + main() From de70827d10fa72c74621c59818faaed90df5aabb Mon Sep 17 00:00:00 2001 From: sophie22 Date: Tue, 18 Jul 2023 13:33:38 +0100 Subject: [PATCH 26/27] directly call the run() function of the task to capture the result for comparison --- tests/test_hazenlib.py | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/tests/test_hazenlib.py b/tests/test_hazenlib.py index c4563754..042da0d8 100644 --- a/tests/test_hazenlib.py +++ b/tests/test_hazenlib.py @@ -1,14 +1,11 @@ -import pydicom +import sys from tests import TEST_DATA_DIR, TEST_REPORT_DIR import unittest -import numpy as np -from docopt import docopt -from hazenlib.logger import logger -import hazenlib.utils -from pprint import pprint -import sys -import ast -import os +import pydicom +import hazenlib +from hazenlib.utils import get_dicom_files, is_dicom_file +from hazenlib.tasks.snr import SNR +from hazenlib.relaxometry import main as relaxometry_run class TestCliParser(unittest.TestCase): @@ -46,10 +43,11 @@ def test_main_snr_exception(self): def test_snr_measured_slice_width(self): path = str(TEST_DATA_DIR / 'snr' / 'GE') - sys.argv = ["hazen", "snr", path, "--measured_slice_width", "1"] + files = get_dicom_files(path) + # sys.argv = ["hazen", "snr", path, "--measured_slice_width", "1"] + snr_task = SNR(data_paths=files, report=False) + result = snr_task.run(measured_slice_width=1) - output = hazenlib.main() - output_dict = ast.literal_eval(output) dict1 = {'snr_subtraction_measured_SNR_SNR_SAG_MEAS1_23_1': 183.97, 'snr_subtraction_normalised_SNR_SNR_SAG_MEAS1_23_1': 7593.04, 'snr_smoothing_measured_SNR_SNR_SAG_MEAS2_24_1': 183.93, @@ -57,15 +55,16 @@ def test_snr_measured_slice_width(self): 'snr_smoothing_measured_SNR_SNR_SAG_MEAS1_23_1': 179.94, 'snr_smoothing_normalised_SNR_SNR_SAG_MEAS1_23_1': 7426.54} - self.assertDictEqual(output_dict['SNR_SNR_SAG_MEAS1_23_1'], dict1) + self.assertDictEqual(result['SNR_SNR_SAG_MEAS1_23_1'], dict1) def test_relaxometry(self): path = str(TEST_DATA_DIR / 'relaxometry' / 'T1' / 'site3_ge' / 'plate4') - sys.argv = ["hazen", "relaxometry", path, "--plate_number", "4", "--calc_t1"] - - output = hazenlib.main() - output_dict = ast.literal_eval(output) + files = get_dicom_files(path) + dicom_objects = [pydicom.read_file(x, force=True) for x in files if is_dicom_file(x)] + # sys.argv = ["hazen", "relaxometry", path, "--plate_number", "4", "--calc_t1"] + result = relaxometry_run(dicom_objects, plate_number=4, + calc_t1=True, calc_t2=False, report_path=False, verbose=False) dict1 = {'Spin Echo_32_2_P4_t1': {'rms_frac_time_difference': 0.13499936644959437}} self.assertAlmostEqual(dict1['Spin Echo_32_2_P4_t1']['rms_frac_time_difference'], - output_dict['Spin Echo_32_2_P4_t1']['rms_frac_time_difference'], 4) + result['Spin Echo_32_2_P4_t1']['rms_frac_time_difference'], 4) From 7ce29ff08a8283c67610d910a6cbdd16633c3679 Mon Sep 17 00:00:00 2001 From: sophie22 Date: Tue, 18 Jul 2023 14:12:03 +0100 Subject: [PATCH 27/27] remove commented original args --- tests/test_hazenlib.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_hazenlib.py b/tests/test_hazenlib.py index 042da0d8..4030287f 100644 --- a/tests/test_hazenlib.py +++ b/tests/test_hazenlib.py @@ -44,7 +44,6 @@ def test_main_snr_exception(self): def test_snr_measured_slice_width(self): path = str(TEST_DATA_DIR / 'snr' / 'GE') files = get_dicom_files(path) - # sys.argv = ["hazen", "snr", path, "--measured_slice_width", "1"] snr_task = SNR(data_paths=files, report=False) result = snr_task.run(measured_slice_width=1) @@ -61,7 +60,6 @@ def test_relaxometry(self): path = str(TEST_DATA_DIR / 'relaxometry' / 'T1' / 'site3_ge' / 'plate4') files = get_dicom_files(path) dicom_objects = [pydicom.read_file(x, force=True) for x in files if is_dicom_file(x)] - # sys.argv = ["hazen", "relaxometry", path, "--plate_number", "4", "--calc_t1"] result = relaxometry_run(dicom_objects, plate_number=4, calc_t1=True, calc_t2=False, report_path=False, verbose=False)