diff --git a/.github/workflows/cli-test.yml b/.github/workflows/cli-test.yml index 8ba26a93..3cc13222 100644 --- a/.github/workflows/cli-test.yml +++ b/.github/workflows/cli-test.yml @@ -36,4 +36,5 @@ jobs: - name: test snr if: always() # will always run regardless of whether previous step fails - useful to ensure all CLI functions tested run: | + set -Eeuxo pipefail hazen snr tests/data/snr/Siemens --report diff --git a/.gitignore b/.gitignore index eed13ec3..ab61ff44 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ coverage.xml **/__pycache__ .idea logs +*.log uploads tests/data/slicepos/results tests/data/uniformity/results diff --git a/Hazen_logger.log b/Hazen_logger.log index e69de29b..8b137891 100644 --- a/Hazen_logger.log +++ b/Hazen_logger.log @@ -0,0 +1 @@ + diff --git a/hazenlib/__init__.py b/hazenlib/__init__.py index a92d12bd..ff2a5256 100644 --- a/hazenlib/__init__.py +++ b/hazenlib/__init__.py @@ -93,10 +93,14 @@ import pydicom from docopt import docopt import numpy as np +import cv2 from hazenlib.tools import is_dicom_file + __version__ = '0.5.1' + + import hazenlib.exceptions EXCLUDED_FILES = ['.DS_Store'] @@ -106,12 +110,10 @@ 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: """ @@ -309,9 +311,25 @@ 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) + + def main(): arguments = docopt(__doc__, version=__version__) - task = importlib.import_module(f"hazenlib.{arguments['']}") folder = arguments[''] files = [os.path.join(folder, x) for x in os.listdir(folder) if x not in EXCLUDED_FILES] @@ -342,22 +360,15 @@ def main(): raise Exception("the (--measured_slice_width) option can only be used with snr") elif arguments[''] == 'snr' and arguments['--measured_slice_width']: measured_slice_width = float(arguments['--measured_slice_width']) - return pp.pprint(task.main(dicom_objects, measured_slice_width, report_path=report)) - - - if arguments[''] == 'relaxometry': - # 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 = {} + result = task.main(dicom_objects, measured_slice_width, report_path=report) + elif arguments[''] == 'relaxometry': + result = parse_relaxometry_data(task, arguments, dicom_objects, report) + else: + result = task.main(dicom_objects, report_path=report) - for key in relaxometry_cli_args: - relaxometry_args[key[2:]] = arguments[key] + return pp.pformat(result) - return pp.pprint(task.main(dicom_objects, report_path=report, - **relaxometry_args)) - return pp.pprint(task.main(dicom_objects, report_path=report)) +def entry_point(): + result = main() + print(result) diff --git a/hazenlib/spatial_resolution.py b/hazenlib/spatial_resolution.py index 72d075d4..6b6e9ec7 100644 --- a/hazenlib/spatial_resolution.py +++ b/hazenlib/spatial_resolution.py @@ -375,6 +375,8 @@ def get_esf(edge_arr, y): return u, esf + + def calculate_mtf_for_edge(dicom, edge, report_path=False): pixels = dicom.pixel_array pe = dicom.InPlanePhaseEncodingDirection diff --git a/setup.py b/setup.py index afb42174..81fd1dc0 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ ], entry_points={ 'console_scripts': [ - 'hazen = hazenlib:main', + 'hazen = hazenlib:entry_point', ], }, ) diff --git a/tests/data/resolution/philips/non_dicom_test.jfif b/tests/data/resolution/philips/non_dicom_test.jfif new file mode 100644 index 00000000..9c6ef300 Binary files /dev/null and b/tests/data/resolution/philips/non_dicom_test.jfif differ diff --git a/tests/test_hazenlib.py b/tests/test_hazenlib.py index a07fc27d..c94024bf 100644 --- a/tests/test_hazenlib.py +++ b/tests/test_hazenlib.py @@ -1,89 +1,232 @@ -""" -Tests functions in the hazenlib.__init__.py file -""" -import unittest import pydicom +from tests import TEST_DATA_DIR import hazenlib +import unittest +import numpy as np +from docopt import docopt +from hazenlib.logger import logger +from pprint import pprint +import sys +import ast import os -import hazenlib.tools as hazen_tools -from tests import TEST_DATA_DIR +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): - # Data by ImplementationVersionName - # all test values are taken from DICOM headers - ROWS = 512 - COLUMNS = 512 - TR_CHECK = 500 - BW = 205.0 - def setUp(self): - self.file = str(TEST_DATA_DIR / 'resolution' / 'philips' / 'IM-0004-0002.dcm') - self.dcm = pydicom.read_file(self.file) - def test_get_bandwidth(self): - bw = hazenlib.get_bandwidth(self.dcm) - assert bw == self.BW + 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'] + def test_get_rows(self): - rows = hazenlib.get_rows(self.dcm) - assert rows == self.ROWS + for manufacturer in test_dicoms.keys(): + with pydicom.read_file(test_dicoms[manufacturer]['file']) as dcm: + rows = hazenlib.get_rows(dcm) + assert rows == test_dicoms[manufacturer]['ROWS'] def test_get_columns(self): - columns = hazenlib.get_columns(self.dcm) - assert columns == self.COLUMNS + for manufacturer in test_dicoms.keys(): + with pydicom.read_file(test_dicoms[manufacturer]['file']) as dcm: + columns = hazenlib.get_columns(dcm) + assert columns == test_dicoms[manufacturer]['COLUMNS'] def test_get_TR(self): - TR = hazenlib.get_TR(self.dcm) - assert TR == self.TR_CHECK + for manufacturer in test_dicoms.keys(): + with pydicom.read_file(test_dicoms[manufacturer]['file']) as dcm: + TR = hazenlib.get_TR(dcm) + assert TR == test_dicoms[manufacturer]['TR_CHECK'] -class TestFactorsPhilipsMR531(TestHazenlib): - #PHILIPS_MR_53_1 - - ROWS = 512 - COLUMNS = 512 - TR_CHECK = 500 - BW = 205.0 + 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) + 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) + 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) + 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) + 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) + 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 = 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.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' / 'philips' / 'IM-0004-0002.dcm') + self.file = str(TEST_DATA_DIR / 'resolution' / 'eastkent' / '256_sag.IMA') self.dcm = pydicom.read_file(self.file) + self.dcm = self.dcm.pixel_array -class TestFactorsSiemensMRVE11C(TestHazenlib): - #SIEMENS_MR_VE11C - ROWS = 256 - COLUMNS = 256 - TR_CHECK = 500 - BW = 130.0 + 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 = test_array.tolist() + TEST_OUT = TEST_OUT.tolist() + self.assertListEqual(test_array, TEST_OUT) - def setUp(self): - self.file = str(TEST_DATA_DIR / 'resolution' / 'eastkent' / '256_sag.IMA') - self.dcm = pydicom.read_file(self.file) -class TestFactorsToshibaTMMRDCMV30(TestHazenlib): - # TOSHIBA_TM_MR_DCM_V3_0 - ROWS = 256 - COLUMNS = 256 - TR_CHECK = 45.0 - BW = 244.0 +class TestCliParser(unittest.TestCase): + maxDiff = None def setUp(self): - self.file = str(TEST_DATA_DIR / 'toshiba' / 'TOSHIBA_TM_MR_DCM_V3_0.dcm') + self.file = str(TEST_DATA_DIR / 'resolution' / 'philips' / 'IM-0004-0002.dcm') self.dcm = pydicom.read_file(self.file) -class TestFactorsGEeFilm(TestHazenlib): - # GE_eFILM - ROWS = 256 - COLUMNS = 256 - TR_CHECK = 1000.0 - BW = 156.25 + def test1_logger(self): + sys.argv = ["hazen", "spatial_resolution", ".\\tests\\data\\resolution\\RESOLUTION\\", "--log", "warning"] + + sys.argv = [item.replace("\\","/") for item in sys.argv] + + hazenlib.main() + + logging = hazenlib.logging + + self.assertEqual(logging.root.level, logging.WARNING) + + def test2_logger(self): + sys.argv = ["hazen", "spatial_resolution", ".\\tests\\data\\resolution\\RESOLUTION\\"] + + sys.argv = [item.replace("\\", "/") for item in sys.argv] + + hazenlib.main() + + logging = hazenlib.logging + + self.assertEqual(logging.root.level, logging.INFO) + + def test_main_snr_exception(self): + sys.argv = ["hazen", "spatial_resolution", ".\\tests\\data\\snr\\Siemens\\", "--measured_slice_width=10"] + + sys.argv = [item.replace("\\", "/") for item in sys.argv] + + self.assertRaises(Exception, hazenlib.main) + + def test_snr_measured_slice_width(self): + sys.argv = ["hazen", "snr", ".\\tests\\data\\snr\\GE", "--measured_slice_width", "1"] + + sys.argv = [item.replace("\\", "/") for item in sys.argv] + + output=hazenlib.main() + output_dict = ast.literal_eval(output) + + dict1={'snr_subtraction_measured_SNR SAG MEAS2_24_1': 182.87, + 'snr_subtraction_normalised_SNR SAG MEAS2_24_1': 7547.37, + 'snr_smoothing_measured_SNR SAG MEAS2_24_1': 189.38, + 'snr_smoothing_normalised_SNR SAG MEAS2_24_1': 7816.0, + 'snr_smoothing_measured_SNR SAG MEAS1_23_1': 184.41, + 'snr_smoothing_normalised_SNR SAG MEAS1_23_1': 7610.83} + + maxDiff = None + self.assertDictEqual(output_dict, dict1) + + def test_relaxometry(self): + sys.argv = ["hazen", "relaxometry", ".\\tests\\data\\relaxometry\\T1\\site3_ge\\plate4\\", "--plate_number", "4", "--calc_t1"] + + sys.argv = [item.replace("\\", "/") for item in sys.argv] + + 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) + + + - def setUp(self): - self.file = str(TEST_DATA_DIR / 'ge' / 'ge_eFilm.dcm') - self.dcm = pydicom.read_file(self.file) -if __name__ == "__main__": - unittest.main()