From 640b6f7b9d924a561f65405decc237d6cf61a9e9 Mon Sep 17 00:00:00 2001 From: Susannah Trevino Date: Wed, 16 Aug 2023 10:29:38 -0500 Subject: [PATCH 01/25] FDA binary struct additions --- .../readers/binary_structs/fda_binary.py | 268 ++++++++++++++---- 1 file changed, 210 insertions(+), 58 deletions(-) diff --git a/oct_converter/readers/binary_structs/fda_binary.py b/oct_converter/readers/binary_structs/fda_binary.py index 601241a..ae84a57 100644 --- a/oct_converter/readers/binary_structs/fda_binary.py +++ b/oct_converter/readers/binary_structs/fda_binary.py @@ -1,4 +1,4 @@ -from construct import Float32n, Float64n, Int8un, Int16un, Int32un, PaddedString, Struct +from construct import Float32n, Float64n, Int8un, Int16un, Int32un, PaddedString, Struct, Array, this """ Notes: @@ -6,36 +6,49 @@ https://bitbucket.org/uocte/uocte/wiki/Topcon%20File%20Format header (obj:Struct): Defines structure of volume's header. - oct_header (obj:Struct): Defines structure of OCT header. - fundus_header (obj:Struct): Defines structure of fundus header. - chunk_dict (dict): Name of data chunks present in the file, and their start locations. - hw_info_03_header (obj:Struct) : Defines structure of hw info header - patient_info_02_header (obj:Struct) : Defines patient info header - file_info_header (obj:Struct) : Defines fda file info header - capture_info_02_header (obj:Struct) : Defines capture info header - param_scan_04_header (obj:Struct) : Defines param scan header - img_trc_02_header (obj:Struct) : Defines img trc header - param_obs_02_header (obj:Struct) : Defines param obs header - img_mot_comp_03_header (obj:Struct) : Defines img mot comp header - effective_scan_range_header (obj:Struct) : Defines effective scan range header - regist_info_header (obj:Struct) : Defines regist info header - result_cornea_curve_header (obj:Struct) : Defines result cornea curve header - result_cornea_thickness_header (obj:Struct) : Defines result cornea thickness header - contour_info_header (obj:Struct) : Defines contour info header - align_info_header (obj:Struct) : Defines align info header - fast_q2_info_header (obj:Struct) : Defines fast q2 info header - gla_littmann_01_header (obj : Struct) : Defines gla littmann 01 header + oct_header (obj:Struct): Defines structure of OCT header (IMG_JPEG). + oct_header_2 (obj:Struct): Defines structure of OCT header (IMG_MOT_COMP_03). + img_mot_comp_02_header (obj:Struct): Defines IMG_MOT_COMP_02 header. + img_mot_comp_header (obj:Struct): Defines IMG_MOT_COMP header. + fundus_header (obj:Struct): Defines structure of fundus header (IMG_FUNDUS). + hw_info_03_header (obj:Struct) : Defines structure of HW_INFO_03 header. + hw_info_02_header (obj:Struct) : Defines structure of HW_INFO_02 header. + hw_info_01_header (obj:Struct) : Defines structure of HW_INFO_01 header. + patient_info_02_header (obj:Struct) : Defines PATIENT_INFO_02 header. + file_info_header (obj:Struct) : Defines fda FILE_INFO header. + fda_file_info_header (obj:Struct) : Defines FDA_FILE_INFO header. + capture_info_02_header (obj:Struct) : Defines CAPTURE_INFO_02 header. + capture_info_header (obj:Struct) : Defines CAPTURE_INFO header. + param_scan_04_header (obj:Struct) : Defines PARAM_SCAN_04 header. + param_scan_02_header (obj:Struct) : Defines PARAM_SCAN_02 header. + img_trc_02_header (obj:Struct) : Defines IMG_TRC_02 header (Fundus grayscale). + img_trc_header (obj:Struct) : Defines IMG_TRC header. + param_obs_02_header (obj:Struct) : Defines PARAM_OBS_02 header. + img_projection_header (obj:Struct) : Defines IMG_PROJECTION header. + img_mot_comp_03_header (obj:Struct) : Defines IMG_MOT_COMP_03 header (Duplicate of oct_header_2) + effective_scan_range_header (obj:Struct) : Defines EFFECTIVE_SCAN_RANGE header. + regist_info_header (obj:Struct) : Defines REGIST_INFO header. + result_cornea_curve_header (obj:Struct) : Defines RESULT_CORNEA_CURVE header. + result_cornea_thickness_header (obj:Struct) : Defines RESULT_CORNEA_THICKNESS header. + contour_info_header (obj:Struct) : Defines CONTOUR_INFO header. + align_info_header (obj:Struct) : Defines ALIGN_INFO header. + main_module_info_header (obj:Struct) : Defines MAIN_MODULE_INFO header. + fast_q2_info_header (obj:Struct) : Defines FAST_Q2_INFO header. + gla_littmann_01_header (obj:Struct) : Defines GLA_LITTMANN_01 header. + thumbnail_header (obj:Struct) : Defines THUMBNAIL header. + patientext_info_header (obj:Struct) : Defines PATIENTEXT_INFO header. """ header = Struct( - "FOCT" / PaddedString(4, "ascii"), - "FDA" / PaddedString(3, "ascii"), - "version_info_1" / Int32un, - "version_info_2" / Int32un, + "file_code" / PaddedString(4, "ascii"), # Always "FOCT" + "file_type" / PaddedString(3, "ascii"), # "FDA" or "FAA", denoting "macula" or "external" fixation + "major_ver" / Int32un, + "minor_ver" / Int32un, ) +# IMG_JPEG oct_header = Struct( - "type" / PaddedString(1, "ascii"), + "scan_mode" / Int8un, "unknown1" / Int32un, "unknown2" / Int32un, "width" / Int32un, @@ -44,22 +57,47 @@ "unknown3" / Int32un, ) +# IMG_MOT_COMP_03 oct_header_2 = Struct( - "unknown" / PaddedString(1, "ascii"), + "scan_mode" / Int8un, "width" / Int32un, "height" / Int32un, "bits_per_pixel" / Int32un, "number_slices" / Int32un, - "unknown" / PaddedString(1, "ascii"), + "format" / Int8un, "size" / Int32un, ) +# There may be earlier versions of IMG_MOT_COMP +# that could also be used here, but needs testing. +img_mot_comp_02_header = Struct( + "temp" / Int8un, + "motion_width" / Int32un, + "motion_height" / Int32un, + "motion_depth" / Int32un, + "motion_number" / Int32un, + "motion_format" / Int8un, + "motion_start_x_pos" / Int32un, + "motion_start_y_pos" / Int32un, + "motion_end_x_pos" / Int32un, + "motion_end_y_pos" / Int32un, + "size" / Int32un, +) + +img_mot_comp_header = Struct( + "motion_width" / Int32un, + "motion_height" / Int32un, + "motion_depth" / Int32un, + "size" / Int32un, +) + +# IMG_FUNDUS fundus_header = Struct( "width" / Int32un, "height" / Int32un, "bits_per_pixel" / Int32un, "number_slices" / Int32un, - "unknown" / PaddedString(4, "ascii"), + "format" / PaddedString(4, "ascii"), "size" / Int32un, # 'img' / Int8un, ) @@ -67,26 +105,68 @@ hw_info_03_header = Struct( "model_name" / PaddedString(16, "ascii"), "serial_number" / PaddedString(16, "ascii"), - "zeros" / PaddedString(32, "ascii"), - "version" / PaddedString(16, "ascii"), - "build_year" / Int16un, - "build_month" / Int16un, - "build_day" / Int16un, - "build_hour" / Int16un, - "build_minute" / Int16un, - "build_second" / Int16un, - "zeros" / PaddedString(8, "ascii"), - "version_numbers" / PaddedString(8, "ascii"), + "spect_sn" / PaddedString(16, "ascii"), + "rom_ver" / PaddedString(16, "ascii"), + "unknown" / PaddedString(16, "ascii"), + "eq_calib_year" / Int16un, + "eq_calib_month" / Int16un, + "eq_calib_day" / Int16un, + "eq_calib_hour" / Int16un, + "eq_calib_minute" / Int16un, + "spect_calib_year" / Int16un, + "spect_calib_month" / Int16un, + "spect_calib_day" / Int16un, + "spect_calib_hour" / Int16un, + "spect_calib_minute" / Int16un, +) + +hw_info_02_header = Struct( + "model_name" / PaddedString(16, "ascii"), + "serial_number" / PaddedString(16, "ascii"), + "spect_sn" / PaddedString(16, "ascii"), + "rom_ver" / PaddedString(16, "ascii"), + "eq_calib_year" / Int16un, + "eq_calib_month" / Int16un, + "eq_calib_day" / Int16un, + "spect_calib_year" / Int16un, + "spect_calib_month" / Int16un, + "spect_calib_day" / Int16un, +) + +hw_info_01_header = Struct( + "model_name" / PaddedString(16, "ascii"), + "serial_number" / PaddedString(16, "ascii"), + "spect_sn" / PaddedString(16, "ascii"), + "rom_ver" / PaddedString(16, "ascii"), + "eq_calib_year" / Int16un, + "eq_calib_month" / Int16un, + "eq_calib_day" / Int16un, + "spect_calib_year" / Int16un, + "spect_calib_month" / Int16un, + "spect_calib_day" / Int16un, ) patient_info_02_header = Struct( - "patient_id" / PaddedString(8, "ascii"), - "patient_given_name" / PaddedString(8, "ascii"), - "patient_surname" / PaddedString(8, "ascii"), - "birth_date_type" / Int8un, - "birth_year" / Int16un, - "birth_month" / Int16un, - "birth_day" / Int16un, + "patient_id" / PaddedString(32, "ascii"), + "first_name" / PaddedString(32, "ascii"), + "last_name" / PaddedString(32, "ascii"), + "mid_name" / PaddedString(8, "ascii"), + "sex" / Int8un, # 1: "M", 2: "F", 3: "O" + "birth_date" / Int16un[3], + "occup_reg" / Int8un[20][2], + "r_date" / Int16un[3], + "record_id" / Int8un[16], + "lv_date" / Int16un[3], + # I've not found files that have the below information, + # so it's difficult to confirm the remaining. + "physician" / Int8un[64][2], # [64 2] ??? + "zip_code" / Int8un[12], #how does this make sense. + "addr" / Int8un[48][2], # [48 2] + "phones" / Int8un[16][2], # [16 2] + "nx_date" / Int16un[6], # [1 6] + "multipurpose_field" / Int8un[20][3], # [20 3] + "descp" / Int8un[64], + "reserved" / Int8un[32], ) file_info_header = Struct( @@ -95,30 +175,62 @@ "8.0.1.20198" / PaddedString(32, "ascii"), ) +fda_file_info_header = Struct( + "0x2" / Int32un, + "0x3e8" / Int32un, + "8.0.1.20198" / Int8un[32], +) + capture_info_02_header = Struct( - "x" / Int16un, - "zeros" / PaddedString(52, "ascii"), - "aquisition_year" / Int16un, - "aquisition_month" / Int16un, - "aquisition_day" / Int16un, - "aquisition_hour" / Int16un, - "aquisition_minute" / Int16un, - "aquisition_second" / Int16un, + "eye" / Int8un, # 0: R, 1: L + "scan_mode" / Int8un, + "session_id" / Int32un, + "label" / PaddedString(100, "ascii"), + "cap_date" / Int16un[6], +) + +capture_info_header = Struct( + "eye" / Int8un, # 0: R, 1: L + "cap_date" / Int16un[6], ) param_scan_04_header = Struct( - "unknown" / Int16un[6], + "fixation" / Int32un, + "mirror_pos" / Int32un, + "polar" / Int32un, "x_dimension_mm" / Float64n, "y_dimension_mm" / Float64n, "z_resolution_um" / Float64n, + "comp_eff_2" / Float64n, + "comp_eff_3" / Float64n, + "base_pos" / Int8un, + "used_calib_data" / Int8un, ) +param_scan_02_header = Struct( + "scan_mode" / Int8un, + "light_level" / Int32un, + "fixation" / Int32un, + "mirror_pos" / Int32un, + "nd" / Int32un, + "polar" / Int32un, + "x_dimension_mm" / Float64n, + "y_dimension_mm" / Float64n, + "z_resolution_um" / Float64n, + "comp_eff_2" / Float64n, + "comp_eff_3" / Float64n, + "noise_thresh" / Float64n, + "range_adj" / Float64n, + "base_pos" / Int8un, +) + +# Fundus Grayscale img_trc_02_header = Struct( "width" / Int32un, "height" / Int32un, "bits_per_pixel" / Int32un, "num_slices_0x2" / Int32un, - "0x1" / Int8un, + "format" / Int8un, "size" / Int32un, ) @@ -127,13 +239,23 @@ "jpeg_quality" / Int8un, "color_temparature" / Int8un, ) +# The above might instead be... +# param_obs_02_header = Struct( +# "ph_mode" / Int8un, +# "ph_angle" / Int8un, +# "ph_light_level" / Int16un, +# ) +# This is the same as oct_header_02, just called +# by its actual chunk name img_mot_comp_03_header = Struct( - "0x0" / Int8un, + "scan_mode" / Int8un, "width" / Int32un, "height" / Int32un, "bits_per_pixel" / Int32un, - "num_slices" / Int32un, + "number_slices" / Int32un, + "format" / Int8un, + "size" / Int32un, ) img_projection_header = Struct( @@ -173,7 +295,8 @@ contour_info_header = Struct( "id" / PaddedString(20, "ascii"), - "type" / Int16un, + "method" / Int8un, + "format" / Int8un, "width" / Int32un, "height" / Int32un, "size" / Int32un, @@ -182,3 +305,32 @@ fast_q2_info_header = Struct("various_quality_statistics" / Float32n[6]) gla_littmann_01_header = Struct("0xffff" / Int32un, "0x1" / Int32un) + +align_info_header = Struct( + "unlabeled_1" / Int8un, + "unlabeled_2" / Int8un, + "w" / Int32un, + "n_size" / Int32un, + "aligndata" / Array(this.w * 2, Int16un), # if n_size > 0 + # if nblockbytes - (10+n_size) >= 16 + "keyframe_1" / Int32un, + "keyframe_2" / Int32un, + "unlabeled_3" / Int32un, + "unlabeled_4" / Int32un, +) + +main_module_info_header = Struct( + "software_name" / PaddedString(128, "ascii"), + "file_version_1" / Int16un, + "file_version_2" / Int16un, + "file_version_3" / Int16un, + "file_version_4" / Int16un, + "string" / PaddedString(128, "ascii") +) + +thumbnail_header = Struct( + "size" / Int32un, + # "img" / Int8un[this.size] +) + +patientext_info_header = Struct("unknown" / Int8un[128]) From bd186c52cc182c33d6073519305f95aacaca4003 Mon Sep 17 00:00:00 2001 From: Susannah Trevino Date: Wed, 16 Aug 2023 10:48:45 -0500 Subject: [PATCH 02/25] FDS binary struct additions --- .../readers/binary_structs/fds_binary.py | 269 ++++++++++++++---- 1 file changed, 213 insertions(+), 56 deletions(-) diff --git a/oct_converter/readers/binary_structs/fds_binary.py b/oct_converter/readers/binary_structs/fds_binary.py index 9ce7c50..b5e98bf 100644 --- a/oct_converter/readers/binary_structs/fds_binary.py +++ b/oct_converter/readers/binary_structs/fds_binary.py @@ -1,88 +1,180 @@ -from construct import Float32n, Float64n, Int8un, Int16un, Int32un, PaddedString, Struct +from construct import Float32n, Float64n, Int8un, Int16un, Int32un, PaddedString, Struct, Array, this """ Notes: Mostly based on description of .fds file format here: https://bitbucket.org/uocte/uocte/wiki/Topcon%20File%20Format - header (obj:Struct): Defines structure of volume's header. - oct_header (obj:Struct): Defines structure of OCT header. - fundus_header (obj:Struct): Defines structure of fundus header. - chunk_dict (dict): Name of data chunks present in the file, and their start locations. - hw_info_03_header (obj:Struct) : Defines structure of hw info header - patient_info_02_header (obj:Struct) : Defines patient info header - file_info_header (obj:Struct) : Defines fda file info header - capture_info_02_header (obj:Struct) : Defines capture info header - param_scan_04_header (obj:Struct) : Defines param scan header - img_trc_02_header (obj:Struct) : Defines img trc header - param_obs_02_header (obj:Struct) : Defines param obs header - img_mot_comp_03_header (obj:Struct) : Defines img mot comp header - effective_scan_range_header (obj:Struct) : Defines effective scan range header - regist_info_header (obj:Struct) : Defines regist info header - result_cornea_curve_header (obj:Struct) : Defines result cornea curve header - result_cornea_thickness_header (obj:Struct) : Defines result cornea thickness header - contour_info_header (obj:Struct) : Defines contour info header - align_info_header (obj:Struct) : Defines align info header - fast_q2_info_header (obj:Struct) : Defines fast q2 info header - gla_littmann_01_header (obj : Struct) : Defines gla littmann 01 header + header (obj:Struct) : Defines structure of volume's header. + oct_header (obj:Struct) : Defines structure of OCT header (IMG_SCAN_03). + oct_header_2 (obj:Struct) : Defines structure of OCT header (IMG_SCAN_02). + fundus_header (obj:Struct): Defines structure of fundus header (IMG_OBS). + param_scan_04_header (obj:Struct) : Defines PARAM_SCAN_04 header. + param_scan_02_header (obj:Struct) : Defines PARAM_SCAN_02 header. + hw_info_03_header (obj:Struct) : Defines HW_INFO_03 header. + hw_info_02_header (obj:Struct) : Defines HW_INFO_02 header. + hw_info_01_header (obj:Struct) : Defines HW_INFO_01 header. + patient_info_02_header (obj:Struct) : Defines PATIENT_INFO_02 header. + file_info_header (obj:Struct) : Defines FILE_INFO header. + capture_info_02_header (obj:Struct) : Defines CAPTURE_INFO_02 header. + capture_info_header (obj:Struct) : Defines CAPTURE_INFO header. + img_trc_02_header (obj:Struct) : Defines IMG_TRC_02 header. + img_trc_header (obj:Struct) : Defines IMG_TRC header. + param_obs_02_header (obj:Struct) : Defines PARAM_OBS_02 header. + param_trc_02_header (obj:Struct) : Defines PARAM_TRC_02 header. + img_mot_comp_03_header (obj:Struct) : Defines IMG_MOT_COMP_03 header. + img_mot_comp_02_header (obj:Struct) : Defines IMG_MOT_COMP_02 header. + img_mot_comp_header (obj:Struct) : Defines IMG_MOT_COMP header. + img_projection_header (obj:Struct) : Defines IMG_PROJECTION header. + effective_scan_range_header (obj:Struct) : Defines EFFECTIVE_SCAN_RANGE header. + regist_info_header (obj:Struct) : Defines REGIST_INFO header. + regist_scan_02_header (obj:Struct) : Defines REGIST_SCAN_02 header. + result_cornea_curve_header (obj:Struct) : Defines RESULT_CORNEA_CURVE header. + result_cornea_thickness_header (obj:Struct) : Defines RESULT_CORNEA_THICKNESS header. + contour_info_header (obj:Struct) : Defines CONTOUR_INFO header. + align_info_header (obj:Struct) : Defines ALIGN_INFO header. + main_module_info_header (obj:Struct) : Defines MAIN_MODULE_INFO header. + fast_q2_info_header (obj:Struct) : Defines FAST_Q2_INFO header. + gla_littmann_01_header (obj:Struct) : Defines GLA_LITTMANN_01 header. + thumbnail_header (obj:Struct) : Defines THUMBNAIL header. + patientext_info_header (obj:Struct) : Defines PATIENTEXT_INFO header. """ header = Struct( - "FOCT" / PaddedString(4, "ascii"), - "FDA" / PaddedString(3, "ascii"), - "version_info_1" / Int32un, - "version_info_2" / Int32un, + "file_code" / PaddedString(4, "ascii"), # Always "FOCT" + "file_type" / PaddedString(3, "ascii"), # "FDA" or "FAA", denoting "macula" or "external" fixation + "major_ver" / Int32un, + "minor_ver" / Int32un, ) + +# IMG_SCAN_03 oct_header = Struct( - "unknown" / PaddedString(1, "ascii"), + "scan_mode" / Int8un, # 2 = 3D, 3 = Radial, 4 = Cross "width" / Int32un, "height" / Int32un, "bits_per_pixel" / Int32un, "number_slices" / Int32un, - "unknown" / PaddedString(1, "ascii"), + "format" / Int8un, "size" / Int32un, ) + +# IMG_SCAN_02 +oct_header_2 = Struct( + "scan_mode" / Int8un, # 2 = 3D, 3 = Radial, 4 = Cross + "width" / Int32un, + "height" / Int32un, + "bits_per_pixel" / Int32un, + "number_slices" / Int32un, + "fast_q" / Float64n, + "format" / Int8un, + "size" / Int32un, +) + +# IMG_OBS fundus_header = Struct( "width" / Int32un, "height" / Int32un, "bits_per_pixel" / Int32un, "number_slices" / Int32un, - "unknown" / PaddedString(1, "ascii"), + "format" / Int8un, "size" / Int32un, ) -# ref: https://github.com/neurodial/LibOctData/blob/master/octdata/import/topcon/topconread.cpp#L318 param_scan_04_header = Struct( - "unknown" / Int16un[6], + "fixation" / Int32un, + "mirror_pos" / Int32un, + "polar" / Int32un, "x_dimension_mm" / Float64n, "y_dimension_mm" / Float64n, "z_resolution_um" / Float64n, + "comp_eff_2" / Float64n, + "comp_eff_3" / Float64n, + "base_pos" / Int8un, + "used_calib_data" / Int8un, ) +param_scan_02_header = Struct( + "scan_mode" / Int8un, + "light_level" / Int32un, + "fixation" / Int32un, + "mirror_pos" / Int32un, + "nd" / Int32un, + "polar" / Int32un, + "x_dimension_mm" / Float64n, + "y_dimension_mm" / Float64n, + "z_resolution_um" / Float64n, + "comp_eff_2" / Float64n, + "comp_eff_3" / Float64n, + "noise_thresh" / Float64n, + "range_adj" / Float64n, + "base_pos" / Int8un, +) hw_info_03_header = Struct( "model_name" / PaddedString(16, "ascii"), "serial_number" / PaddedString(16, "ascii"), - "zeros" / PaddedString(32, "ascii"), - "version" / PaddedString(16, "ascii"), - "build_year" / Int16un, - "build_month" / Int16un, - "build_day" / Int16un, - "build_hour" / Int16un, - "build_minute" / Int16un, - "build_second" / Int16un, - "zeros" / PaddedString(8, "ascii"), - "version_numbers" / PaddedString(8, "ascii"), + "spect_sn" / PaddedString(16, "ascii"), + "rom_ver" / PaddedString(16, "ascii"), + "unknown" / PaddedString(16, "ascii"), + "eq_calib_year" / Int16un, + "eq_calib_month" / Int16un, + "eq_calib_day" / Int16un, + "eq_calib_hour" / Int16un, + "eq_calib_minute" / Int16un, + "spect_calib_year" / Int16un, + "spect_calib_month" / Int16un, + "spect_calib_day" / Int16un, + "spect_calib_hour" / Int16un, + "spect_calib_minute" / Int16un, +) + +hw_info_02_header = Struct( + "model_name" / PaddedString(16, "ascii"), + "serial_number" / PaddedString(16, "ascii"), + "spect_sn" / PaddedString(16, "ascii"), + "rom_ver" / PaddedString(16, "ascii"), + "eq_calib_year" / Int16un, + "eq_calib_month" / Int16un, + "eq_calib_day" / Int16un, + "spect_calib_year" / Int16un, + "spect_calib_month" / Int16un, + "spect_calib_day" / Int16un, +) + +hw_info_01_header = Struct( + "model_name" / PaddedString(16, "ascii"), + "serial_number" / PaddedString(16, "ascii"), + "spect_sn" / PaddedString(16, "ascii"), + "rom_ver" / PaddedString(16, "ascii"), + "eq_calib_year" / Int16un, + "eq_calib_month" / Int16un, + "eq_calib_day" / Int16un, + "spect_calib_year" / Int16un, + "spect_calib_month" / Int16un, + "spect_calib_day" / Int16un, ) patient_info_02_header = Struct( - "patient_id" / PaddedString(8, "ascii"), - "patient_given_name" / PaddedString(8, "ascii"), - "patient_surname" / PaddedString(8, "ascii"), - "birth_date_type" / Int8un, - "birth_year" / Int16un, - "birth_month" / Int16un, - "birth_day" / Int16un, + "patient_id" / PaddedString(32, "ascii"), + "first_name" / PaddedString(32, "ascii"), + "last_name" / PaddedString(32, "ascii"), + "mid_name" / PaddedString(8, "ascii"), + "sex" / Int8un, + "birth_date" / Int16un[3], + "occup_reg" / Int8un[20][2], + "r_date" / Int16un[3], + "record_id" / Int8un[16], + "lv_date" / Int16un[3], + # I've not found files that have the below information, + # so it's difficult to confirm the remaining. + "physician" / Int8un[64][2], # [64 2] ??? + "zip_code" / Int8un[12], #how does this make sense. + "addr" / Int8un[48][2], # [48 2] + "phones" / Int8un[16][2], # [16 2] + "nx_date" / Int16un[6], # [1 6] + "multipurpose_field" / Int8un[20][3], # [20 3] + "descp" / Int8un[64], + "reserved" / Int8un[32], ) file_info_header = Struct( @@ -92,26 +184,36 @@ ) capture_info_02_header = Struct( - "x" / Int16un, - "zeros" / PaddedString(52, "ascii"), - "aquisition_year" / Int16un, - "aquisition_month" / Int16un, - "aquisition_day" / Int16un, - "aquisition_hour" / Int16un, - "aquisition_minute" / Int16un, - "aquisition_second" / Int16un, + "eye" / Int8un, + "scan_mode" / Int8un, + "session_id" / Int32un, + "label" / PaddedString(100, "ascii"), + "cap_date" / Int16un[6], ) +capture_info_header = Struct( + "eye" / Int8un, + "cap_date" / Int16un[6], +) img_trc_02_header = Struct( "width" / Int32un, "height" / Int32un, "bits_per_pixel" / Int32un, "num_slices_0x2" / Int32un, - "0x1" / Int8un, + "format" / Int8un, "size" / Int32un, ) +img_trc_header = Struct( + "width" / Int32un, + "height" / Int32un, + "bits_per_pixel" / Int32un, + "size" / Int32un, +) + +# TODO This chunk still needs work. +# Total [3] Int16un param_obs_02_header = Struct( "camera_model" / PaddedString(12, "utf16"), "jpeg_quality" / Int8un, @@ -126,6 +228,27 @@ "num_slices" / Int32un, ) +img_mot_comp_02_header = Struct( + "temp" / Int8un, + "motion_width" / Int32un, + "motion_height" / Int32un, + "motion_depth" / Int32un, + "motion_number" / Int32un, + "motion_format" / Int8un, + "motion_start_x_pos" / Int32un, + "motion_start_y_pos" / Int32un, + "motion_end_x_pos" / Int32un, + "motion_end_y_pos" / Int32un, + "size" / Int32un, +) + +img_mot_comp_header = Struct( + "motion_width" / Int32un, + "motion_height" / Int32un, + "motion_depth" / Int32un, + "size" / Int32un, +) + img_projection_header = Struct( "width" / Int32un, "height" / Int32un, @@ -147,6 +270,11 @@ "bounding_box_in_trc_pixels" / Int32un[4], ) +regist_scan_02_header = Struct( + "clr_scan_region" / Int32un[4], + "obs_scan_region" / Int32un[4], +) + result_cornea_curve_header = Struct( "id" / Int8un[20], "width" / Int32un, @@ -169,6 +297,35 @@ "size" / Int32un, ) +align_info_header = Struct( + "unlabeled_1" / Int8un, + "unlabeled_2" / Int8un, + "w" / Int32un, + "n_size" / Int32un, + "aligndata" / Array(this.w * 2, Int16un), # if n_size > 0 + # if nblockbytes - (10+n_size) >= 16 + "keyframe_1" / Int32un, + "keyframe_2" / Int32un, + "unlabeled_3" / Int32un, + "unlabeled_4" / Int32un, +) + +main_module_info_header = Struct( + "software_name" / PaddedString(128, "ascii"), + "file_version_1" / Int16un, + "file_version_2" / Int16un, + "file_version_3" / Int16un, + "file_version_4" / Int16un, + "string" / PaddedString(128, "ascii") +) + fast_q2_info_header = Struct("various_quality_statistics" / Float32n[6]) gla_littmann_01_header = Struct("0xffff" / Int32un, "0x1" / Int32un) + +thumbnail_header = Struct( + "size" / Int32un, + # "img" / Int8un[this.size] +) + +patientext_info_header = Struct("unknown" / Int8un[128]) From 5214eb021dea9a200b3f91d01455de0333b30d69 Mon Sep 17 00:00:00 2001 From: Susannah Trevino Date: Thu, 17 Aug 2023 15:10:48 -0500 Subject: [PATCH 03/25] Add Nate's DICOM code --- oct_converter/dicom/__init__.py | 8 ++ oct_converter/dicom/dicom.py | 174 ++++++++++++++++++++++++++ oct_converter/dicom/metadata.py | 121 ++++++++++++++++++ oct_converter/image_types/__init__.py | 9 ++ 4 files changed, 312 insertions(+) create mode 100644 oct_converter/dicom/__init__.py create mode 100644 oct_converter/dicom/dicom.py create mode 100644 oct_converter/dicom/metadata.py diff --git a/oct_converter/dicom/__init__.py b/oct_converter/dicom/__init__.py new file mode 100644 index 0000000..c19a5ad --- /dev/null +++ b/oct_converter/dicom/__init__.py @@ -0,0 +1,8 @@ +"""Init module.""" +from pydicom.uid import generate_uid +from importlib import metadata + +version = metadata.version('oct_converter') + +# Deterministic implentation UID based on package name and version +implementation_uid = generate_uid(entropy_srcs=["oct_converter", version]) \ No newline at end of file diff --git a/oct_converter/dicom/dicom.py b/oct_converter/dicom/dicom.py new file mode 100644 index 0000000..cf21732 --- /dev/null +++ b/oct_converter/dicom/dicom.py @@ -0,0 +1,174 @@ +from pydicom.dataset import FileDataset, FileMetaDataset, Dataset +from pydicom.uid import generate_uid, OphthalmicTomographyImageStorage, ExplicitVRLittleEndian +from pydicom.encaps import encapsulate +from oct_converter.dicom import implementation_uid +import numpy as np +from oct_converter.dicom.metadata import DicomMetadata +from pathlib import Path +import typing as t +import tempfile +from datetime import datetime +import uuid + + +def opt_base_dicom() -> t.Tuple[Path, Dataset]: + suffix = '.dcm' + file_ = Path(tempfile.NamedTemporaryFile(suffix=suffix).name) + + # Populate required values for file meta information + file_meta = FileMetaDataset() + file_meta.MediaStorageSOPClassUID = OphthalmicTomographyImageStorage + file_meta.MediaStorageSOPInstanceUID = generate_uid() + file_meta.ImplementationClassUID = implementation_uid + file_meta.TransferSyntaxUID = ExplicitVRLittleEndian + + # Create the FileDataset instance with file meta, preamble and empty DS + ds = FileDataset(str(file_), {}, file_meta=file_meta, preamble=b"\0" * 128) + ds.is_little_endian = True + ds.is_implicit_VR = False # Explicit VR + return file_, ds + + +def populate_patient_info(ds: Dataset, meta: DicomMetadata) -> Dataset: + # Patient Module PS3.3 C.7.1.1 + ds.PatientName = f"{meta.patient_info.last_name}^{meta.patient_info.first_name}" + ds.PatientID = meta.patient_info.patient_id + ds.PatientSex = meta.patient_info.patient_sex + ds.PatientBirthDate = ( + meta.patient_info.patient_dob.strftime('%Y%m%d') if meta.patient_info.patient_dob else "" + ) + return ds + + +def populate_manufacturer_info(ds: Dataset, meta: DicomMetadata) -> Dataset: + # General and enhanced equipment module PS3.3 C.7.5.1, PS3.3 C.7.5.2 + ds.Manufacturer = meta.manufacturer_info.manufacturer + ds.ManufacturerModelName = meta.manufacturer_info.manufacturer_model + ds.DeviceSerialNumber = meta.manufacturer_info.device_serial + ds.SoftwareVersions = meta.manufacturer_info.software_version + + # OPT parameter module PS3.3 C.8.17.9 + cd, cv, cm = meta.oct_image_params.opt_acquisition_device.value + ds.AcquisitionDeviceTypeCodeSequence = [Dataset()] + ds.AcquisitionDeviceTypeCodeSequence[0].CodeValue = cv + ds.AcquisitionDeviceTypeCodeSequence[0].CodingSchemeDesignator = cd + ds.AcquisitionDeviceTypeCodeSequence[0].CodeMeaning = cm + ds.DetectorType = meta.oct_image_params.DetectorType.value + return ds + + +def populate_opt_series(ds: Dataset, meta: DicomMetadata) -> Dataset: + # General study module PS3.3 C.7.2.1 + # Deterministic StudyInstanceUID based on study ID + ds.StudyInstanceUID = generate_uid(entropy_srcs=[ + uuid.uuid4(), + meta.series_info.study_id + ]) + + # General series module PS3.3 C.7.3.1 + ds.SeriesInstanceUID = generate_uid(entropy_srcs=[ + uuid.uuid4(), + meta.series_info.series_id + ]) + ds.Laterality = meta.series_info.laterality + # Ophthalmic Tomography Series PS3.3 C.8.17.6 + ds.Modality = 'OPT' + ds.SeriesNumber = int(meta.series_info.series_id) + + # SOP Common module PS3.3 C.12.1 + ds.SOPClassUID = OphthalmicTomographyImageStorage + ds.SOPInstanceUID = generate_uid() + return ds + + +def populate_ocular_region(ds: Dataset, meta: DicomMetadata) -> Dataset: + # Ocular region imaged module PS3.3 C.8.17.5 + cd, cv, cm = meta.series_info.opt_anatomy.value + ds.ImageLaterality = meta.series_info.laterality + ds.AnatomicRegionSequence = [Dataset()] + ds.AnatomicRegionSequence[0].CodeValue = cv + ds.AnatomicRegionSequence[0].CodingSchemeDesignator = cd + ds.AnatomicRegionSequence[0].CodeMeaning = cm + return ds + + +def opt_shared_functional_groups(ds: Dataset, meta: DicomMetadata) -> Dataset: + # ---- Shared + shared_ds = [Dataset()] + # Frame anatomy PS3.3 C.7.6.16.2.8 + shared_ds[0].FrameAnatomySequence = [Dataset()] + shared_ds[0].FrameAnatomySequence[0] = ds.AnatomicRegionSequence[0].copy() + shared_ds[0].FrameAnatomySequence[0].FrameLaterality = meta.series_info.laterality + # Pixel Measures PS3.3 C.7.6.16.2.1 + shared_ds[0].PixelMeasuresSequence = [Dataset()] + shared_ds[0].PixelMeasuresSequence[0].PixelSpacing = meta.image_geometry.pixel_spacing + shared_ds[0].PixelMeasuresSequence[0].SliceThickness = meta.image_geometry.slice_thickness + # Plane Orientation PS3.3 C.7.6.16.2.4 + shared_ds[0].PlaneOrientationSequence = [Dataset()] + shared_ds[0].PlaneOrientationSequence[0].ImageOrientationPatient = meta.image_geometry.image_orientation + ds.SharedFunctionalGroupsSequence = shared_ds + return ds + + +def write_opt_dicom( + meta: DicomMetadata, + frames: t.List[np.ndarray] +): + file_, ds = opt_base_dicom() + ds = populate_patient_info(ds, meta) + ds = populate_manufacturer_info(ds, meta) + ds = populate_opt_series(ds, meta) + ds = populate_ocular_region(ds, meta) + ds = opt_shared_functional_groups(ds, meta) + + # TODO: Frame of reference if fundus image present + + # OPT Image Module PS3.3 C.8.17.7 + ds.ImageType = ['DERIVED', 'SECONDARY'] + ds.SamplesPerPixel = 1 + ds.AcquisitionDateTime = ( +meta.series_info.acquisition_date.strftime('%Y%m%d%H%M%S.%f') if meta.series_info.acquisition_date else "" + ) + ds.AcquisitionNumber = 1 + ds.PhotometricInterpretation = 'MONOCHROME2' + # Unsigned integer + ds.PixelRepresentation = 0 + # Use 16 bit pixel + ds.BitsAllocated = 16 + ds.BitsStored = ds.BitsAllocated + ds.HighBit = ds.BitsAllocated - 1 + ds.SamplesPerPixel = 1 + ds.NumberOfFrames = len(frames) + + + # Multi-frame Functional Groups Module PS3.3 C.7.6.16 + dt = datetime.now() + ds.ContentDate = dt.strftime('%Y%m%d') + timeStr = dt.strftime('%H%M%S.%f') # long format with micro seconds + ds.ContentTime = timeStr + ds.InstanceNumber = 1 + + per_frame = [] + pixel_data_bytes = list() + # Convert to a 3d volume + pixel_data = np.array(frames).astype(np.uint16) + ds.Rows = pixel_data.shape[1] + ds.Columns = pixel_data.shape[2] + for i in range(pixel_data.shape[0]): + # Per Frame Functional Groups + frame_fgs = Dataset() + frame_fgs.PlanePositionSequence = [Dataset()] + ipp = [0, 0, i*meta.image_geometry.slice_thickness] + frame_fgs.PlanePositionSequence[0].ImagePositionPatient = ipp + frame_fgs.FrameContentSequence = [Dataset()] + frame_fgs.FrameContentSequence[0].InStackPositionNumber = i + 1 + frame_fgs.FrameContentSequence[0].StackID = '1' + + # Pixel data + frame_dat = pixel_data[i, :, :] + pixel_data_bytes.append(frame_dat.tobytes()) + per_frame.append(frame_fgs) + ds.PerFrameFunctionalGroupsSequence = per_frame + ds.PixelData = pixel_data.tobytes() + ds.save_as(file_) + return file_ \ No newline at end of file diff --git a/oct_converter/dicom/metadata.py b/oct_converter/dicom/metadata.py new file mode 100644 index 0000000..94bae20 --- /dev/null +++ b/oct_converter/dicom/metadata.py @@ -0,0 +1,121 @@ +import dataclasses +import enum +import datetime +import typing as t + +class OPTAcquisitionDevice(enum.Enum): + """OPT Acquisition Device enumeration. + + Contains code designator, code value and code meaning for each entry. + """ + OCTScanner = ("SRT", "A-00FBE", "Optical Coherence Tomography Scanner") + RetinalThicknessAnalyzer = ("SRT", "R-FAB5A", "Retinal Thickness Analyzer") + ConfocalScanningLaserOphthalmoscope = ("SRT", "A-00E8B", "Confocal Scanning Laser Ophthalmoscope") + ScheimpflugCamera = ("DCM", "111626", "Scheimpflug Camera") + ScanningLaserPolarimeter = ("SRT", "A-00E8C", "Scanning Laser Polarimeter") + ElevationBasedCornealTomographer = ("DCM", "111945", "Elevation-based corneal tomographer") + ReflectionBasedCornealTopographer = ("DCM", "111946", "Reflection-based corneal topographer") + InterferometryBasedCornealTomographer = ("DCM", "111947", "Interferometry-based corneal tomographer") + Unspecified = ("OCT-converter", "D-0001", "Unspecified scanner") + + +class OPTAnatomyStructure(enum.Enum): + """OPT Anatomy enumeration. + + Contains code designator, code value and code meaning for each entry. + """ + AnteriorChamberOfEye = ('SRT', 'T-AA050', 'Anterior chamber of eye'), + BothEyes = ('SRT', 'T-AA180', 'Both eyes'), + ChoroidOfEye = ('SRT', 'T-AA310', 'Choroid of eye'), + CiliaryBody = ('SRT', 'T-AA400', 'Ciliary body'), + Conjunctiva = ('SRT', 'T-AA860', 'Conjunctiva'), + Cornea = ('SRT', 'T-AA200', 'Cornea'), + Eye = ('SRT', 'T-AA000', 'Eye'), + Eyelid = ('SRT', 'T-AA810', 'Eyelid'), + FoveaCentralis = ('SRT', 'T-AA621', 'Fovea centralis'), + Iris = ('SRT', 'T-AA500', 'Iris'), + LacrimalCaruncle = ('SRT', 'T-AA862', 'Lacrimal caruncle'), + LacrimalGland = ('SRT', 'T-AA910', 'Lacrimal gland'), + LacrimalSac = ('SRT', 'T-AA940', 'Lacrimal sac'), + Lens = ('SRT', 'T-AA700', 'Lens'), + LowerEyeLid = ('SRT', 'T-AA830', 'Lower Eyelid'), + OphthalmicArtery = ('SRT', 'T-45400', 'Ophthalmic artery'), + OpticNerveHead = ('SRT', 'T-AA630', 'Optic nerve head'), + Retina = ('SRT', 'T-AA610', 'Retina'), + Sclera = ('SRT', 'T-AA110', 'Sclera'), + UpperEyeLid = ('SRT', 'T-AA820', 'Upper Eyelid') + Unspecified = ('OCT-converter', 'A-0001', 'Unspecified anatomy') + + +class OCTDetectorType(enum.Enum): + CCD = "CCD" + CMOS = "CMOS" + PHOTO = "PHOTO" + INT = "INT" + Unknown = "UNKNOWN" + + +@dataclasses.dataclass +class PatientMeta(): + # Patient Info + first_name: str = '' + last_name: str = '' + patient_id: str = '' + patient_sex: str = '' + patient_dob: t.Optional[datetime.datetime] = None + + +@dataclasses.dataclass +class SeriesMeta(): + # Study and Series + study_id: str = '' + series_id: str = '' + laterality: str = '' + acquisition_date: t.Optional[datetime.datetime] = None + # Anatomy + opt_anatomy: OPTAnatomyStructure = OPTAnatomyStructure.Unspecified + + +@dataclasses.dataclass +class ManufacturerMeta(): + # Manufacturer info + manufacturer: str = '' + manufacturer_model: str = 'unknown' + device_serial: str = 'unknown' + software_version: str = 'unknown' + + +@dataclasses.dataclass +class ImageGeometry(): + # Image geometry info + pixel_spacing: t.List[float] = [1.0, 1.0] + slice_thickness: float = 1.0 + image_orientation: t.List[float] = [1, 0, 0, 0 , 1, 0] + + +@dataclasses.dataclass +class OCTImageParams(): + # PS3.3 C.8.17.9 + opt_acquisition_device: OPTAcquisitionDevice = OPTAcquisitionDevice.Unspecified + DetectorType: OCTDetectorType = OCTDetectorType.Unknown + IlluminationWaveLength: t.Optional[float] = None + IlluminationPower: t.Optional[float] = None + IlluminationBandwidth: t.Optional[float] = None + DepthSpatialResolution: t.Optional[float] = None + MaximumDepthDistortion: t.Optional[float] = None + AlongscanSpatialResolution: t.Optional[float] = None + MaximumAlongscanDistortion: t.Optional[float] = None + AcrossscanSpatialResolution: t.Optional[float] = None + MaximumAcrossscanDistortion: t.Optional[float] = None + + # NOTE: Could eventually include C.8.17.8 Acquisition Params. + + +@dataclasses.dataclass +class DicomMetadata(): + patient_info: PatientMeta + series_info: SeriesMeta + manufacturer_info: ManufacturerMeta + + image_geometry: ImageGeometry + oct_image_params: OCTImageParams diff --git a/oct_converter/image_types/__init__.py b/oct_converter/image_types/__init__.py index ed52a6f..da8331c 100644 --- a/oct_converter/image_types/__init__.py +++ b/oct_converter/image_types/__init__.py @@ -1,2 +1,11 @@ +"""Init module.""" + from .fundus import FundusImageWithMetaData from .oct import OCTVolumeWithMetaData + +__all__ = [ + "version", + "implementaation_uid", + "FundusImageWithMetaData", + "OCTVolumeWithMetaData" +] \ No newline at end of file From 62c87c53db1917241424e15b44eb537f0d490e9d Mon Sep 17 00:00:00 2001 From: Susannah Trevino Date: Fri, 18 Aug 2023 11:33:20 -0500 Subject: [PATCH 04/25] Add print arg, fill out metadata --- oct_converter/readers/fds.py | 42 +++++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/oct_converter/readers/fds.py b/oct_converter/readers/fds.py index 77c5654..4f22916 100644 --- a/oct_converter/readers/fds.py +++ b/oct_converter/readers/fds.py @@ -1,7 +1,7 @@ from __future__ import annotations from pathlib import Path - +from datetime import datetime import numpy as np from construct import ListContainer @@ -26,9 +26,9 @@ def __init__(self, filepath: str | Path) -> None: if not self.filepath.exists(): raise FileNotFoundError(self.filepath) - self.chunk_dict = self.get_list_of_file_chunks() + self.chunk_dict, self.header = self.get_list_of_file_chunks() - def get_list_of_file_chunks(self) -> dict: + def get_list_of_file_chunks(self, printing: bool = True) -> dict: """Find all data chunks present in the file. Returns: @@ -51,10 +51,12 @@ def get_list_of_file_chunks(self) -> dict: chunk_location = f.tell() f.seek(chunk_size, 1) chunk_dict[chunk_name] = [chunk_location, chunk_size] - print("File {} contains the following chunks:".format(self.filepath)) - for key in chunk_dict.keys(): - print(key) - return chunk_dict + if printing: + print("File {} contains the following chunks:".format(self.filepath)) + for key in chunk_dict.keys(): + print(key) + print("") + return chunk_dict, header def read_oct_volume(self) -> OCTVolumeWithMetaData: """Reads OCT data. @@ -95,9 +97,33 @@ def read_oct_volume(self) -> OCTVolumeWithMetaData: scan_params.y_dimension_mm / oct_header.width, # Depth ] + # Other code uses the following, listed as + # WidthPixelS, FramePixelS, and zHeightPixelS + pixel_spacing_2 = [ + scan_params.get("x_dimension_mm") / oct_header.width, # WidthPixelS, PixelSpacing[1] + scan_params.get("y_dimension_mm") / oct_header.number_slices, # FramePixelS / SliceThickness + scan_params.get("z_resolution_um") / 1000, # zHeightPixelS, PixelSpacing[0] + ] + # read all other metadata + metadata = self.read_all_metadata() + patient_info = metadata.get("patient_info_02") or metadata.get("patient_info", {}) + capture_info = metadata.get("capture_info_02") or metadata.get("capture_info", {}) + sex_map = {1: "M", 2: "F", 3: "O", None: ""} + lat_map = {0: "R", 1: "L", None: ""} + oct_volume = OCTVolumeWithMetaData( [volume[:, :, i] for i in range(volume.shape[2])], - pixel_spacing=pixel_spacing, + patient_id=patient_info.get("patient_id"), + first_name=patient_info.get("first_name"), + surname=patient_info.get("last_name"), + sex=sex_map[patient_info.get("sex", None)], + patient_dob=datetime(*patient_info.get("birth_date")) if patient_info.get("birth_date")[0] != 0 else None, + acquisition_date=datetime(*capture_info.get("cap_date")), + laterality=lat_map[capture_info.get("eye", None)], + pixel_spacing=pixel_spacing_2, + metadata=metadata, + header=self.header, + oct_header=dict(oct_header), ) return oct_volume From 8a2ef0c55995192db8795ce3e765456fd6dc7d95 Mon Sep 17 00:00:00 2001 From: Susannah Trevino Date: Fri, 18 Aug 2023 11:33:56 -0500 Subject: [PATCH 05/25] Add pixel spacing, fill out metadata --- oct_converter/readers/fda.py | 50 +++++++++++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/oct_converter/readers/fda.py b/oct_converter/readers/fda.py index 5f70e96..a497634 100644 --- a/oct_converter/readers/fda.py +++ b/oct_converter/readers/fda.py @@ -6,6 +6,7 @@ import numpy as np from construct import ListContainer +from datetime import datetime from PIL import Image from oct_converter.image_types import FundusImageWithMetaData, OCTVolumeWithMetaData @@ -25,7 +26,7 @@ def __init__(self, filepath: str | Path, printing: bool = True) -> None: if not self.filepath.exists(): raise FileNotFoundError(self.filepath) - self.chunk_dict = self.get_list_of_file_chunks(printing=printing) + self.chunk_dict, self.header = self.get_list_of_file_chunks(printing=printing) def get_list_of_file_chunks(self, printing: bool = True) -> dict: """Find all data chunks present in the file. @@ -64,7 +65,7 @@ def get_list_of_file_chunks(self, printing: bool = True) -> dict: for key in chunk_dict.keys(): print(key) print("") - return chunk_dict + return chunk_dict, header def read_oct_volume(self) -> OCTVolumeWithMetaData: """Reads OCT data. @@ -92,6 +93,31 @@ def read_oct_volume(self) -> OCTVolumeWithMetaData: raw_slice = f.read(size) image = Image.open(io.BytesIO(raw_slice)) volume.append(np.asarray(image)) + + chunk_loc, chunk_size = self.chunk_dict.get(b"@PARAM_SCAN_04", (None, None)) + pixel_spacing = None + if chunk_loc: + f.seek(chunk_loc) + scan_params = fda_binary.param_scan_04_header.parse(f.read(chunk_size)) + # NOTE: this will need reordering for dicom pixel spacing and + # image orientation/position patient as well as possibly for nifti + # depending on what x,y,z means here. + + # In either nifti/dicom coordinate systems, the x-y plan in raw space + # corresponds to the x-z plane, just depends which direction. + pixel_spacing = [ + scan_params.x_dimension_mm / oct_header.height, # Left/Right + scan_params.z_resolution_um / 1000, # Up/Down + scan_params.y_dimension_mm / oct_header.width, # Depth + ] + + # Other code uses the following, listed as + # WidthPixelS, FramePixelS, and zHeightPixelS + pixel_spacing_2 = [ + scan_params.get("x_dimension_mm") / oct_header.width, # WidthPixelS, PixelSpacing[1] + scan_params.get("y_dimension_mm") / oct_header.number_slices, # FramePixelS / SliceThickness + scan_params.get("z_resolution_um") / 1000, # zHeightPixelS, PixelSpacing[0] + ] # read segmentation contours if possible and store them as distance # from top of scan to be compatible with plotting in OCTVolume @@ -101,8 +127,26 @@ def read_oct_volume(self) -> OCTVolumeWithMetaData: # read all other metadata metadata = self.read_all_metadata() + patient_info = metadata.get("patient_info_02") or metadata.get("patient_info", {}) + capture_info = metadata.get("capture_info_02") or metadata.get("capture_info", {}) + sex_map = {1: "M", 2: "F", 3: "O", None: ""} + lat_map = {0: "R", 1: "L", None: ""} - oct_volume = OCTVolumeWithMetaData(volume, contours=contours, metadata=metadata) + oct_volume = OCTVolumeWithMetaData( + volume, + patient_id=patient_info.get("patient_id"), + first_name=patient_info.get("first_name"), + surname=patient_info.get("last_name"), + sex=sex_map[patient_info.get("sex", None)], + patient_dob=datetime(*patient_info.get("birth_date")) if patient_info.get("birth_date")[0] != 0 else None, + acquisition_date=datetime(*capture_info.get("cap_date")), + laterality=lat_map[capture_info.get("eye", None)], + contours=contours, + pixel_spacing=pixel_spacing_2, + metadata=metadata, + header=self.header, + oct_header=dict(oct_header), + ) return oct_volume def read_oct_volume_2(self) -> OCTVolumeWithMetaData: From 5bdcc5a0883bb6ee1ad452933fe1537fb6952214 Mon Sep 17 00:00:00 2001 From: Susannah Trevino Date: Fri, 18 Aug 2023 11:34:37 -0500 Subject: [PATCH 06/25] Comment out entropy_srcs for now --- oct_converter/dicom/dicom.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/oct_converter/dicom/dicom.py b/oct_converter/dicom/dicom.py index cf21732..5c49833 100644 --- a/oct_converter/dicom/dicom.py +++ b/oct_converter/dicom/dicom.py @@ -60,16 +60,18 @@ def populate_manufacturer_info(ds: Dataset, meta: DicomMetadata) -> Dataset: def populate_opt_series(ds: Dataset, meta: DicomMetadata) -> Dataset: # General study module PS3.3 C.7.2.1 # Deterministic StudyInstanceUID based on study ID - ds.StudyInstanceUID = generate_uid(entropy_srcs=[ - uuid.uuid4(), - meta.series_info.study_id - ]) - - # General series module PS3.3 C.7.3.1 - ds.SeriesInstanceUID = generate_uid(entropy_srcs=[ - uuid.uuid4(), - meta.series_info.series_id - ]) + # ds.StudyInstanceUID = generate_uid(entropy_srcs=[ + # # str(uuid.uuid4()), + # str(meta.series_info.study_id) + # ]) + + # # General series module PS3.3 C.7.3.1 + # ds.SeriesInstanceUID = generate_uid(entropy_srcs=[ + # # str(uuid.uuid4()), + # str(meta.series_info.series_id) + # ]) + ds.StudyInstanceUID = generate_uid() + ds.SeriesInstanceUID = generate_uid() ds.Laterality = meta.series_info.laterality # Ophthalmic Tomography Series PS3.3 C.8.17.6 ds.Modality = 'OPT' From f3483c0b1048817f1e183f0ea51edc07e1c95efa Mon Sep 17 00:00:00 2001 From: Susannah Trevino Date: Fri, 18 Aug 2023 11:35:13 -0500 Subject: [PATCH 07/25] Add header and oct_header as attributes --- oct_converter/image_types/oct.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/oct_converter/image_types/oct.py b/oct_converter/image_types/oct.py index 5af3a4b..773f2fb 100644 --- a/oct_converter/image_types/oct.py +++ b/oct_converter/image_types/oct.py @@ -53,6 +53,8 @@ def __init__( contours: dict | None = None, pixel_spacing: list[float] | None = None, metadata: dict | None = None, + header: dict | None = None, + oct_header: dict | None = None, ) -> None: # image self.volume = volume @@ -76,6 +78,8 @@ def __init__( # metadata self.metadata = metadata + self.header = header + self.oct_header = oct_header def peek( self, From 0245fb9333a438d69e00eb6d5aaa4508a3a505ff Mon Sep 17 00:00:00 2001 From: Susannah Trevino Date: Fri, 18 Aug 2023 11:36:24 -0500 Subject: [PATCH 08/25] Make dataclass happy --- oct_converter/dicom/metadata.py | 43 +++++++++++++++++---------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/oct_converter/dicom/metadata.py b/oct_converter/dicom/metadata.py index 94bae20..aabe81e 100644 --- a/oct_converter/dicom/metadata.py +++ b/oct_converter/dicom/metadata.py @@ -2,6 +2,7 @@ import enum import datetime import typing as t +from dataclasses import field class OPTAcquisitionDevice(enum.Enum): """OPT Acquisition Device enumeration. @@ -24,25 +25,25 @@ class OPTAnatomyStructure(enum.Enum): Contains code designator, code value and code meaning for each entry. """ - AnteriorChamberOfEye = ('SRT', 'T-AA050', 'Anterior chamber of eye'), - BothEyes = ('SRT', 'T-AA180', 'Both eyes'), - ChoroidOfEye = ('SRT', 'T-AA310', 'Choroid of eye'), - CiliaryBody = ('SRT', 'T-AA400', 'Ciliary body'), - Conjunctiva = ('SRT', 'T-AA860', 'Conjunctiva'), - Cornea = ('SRT', 'T-AA200', 'Cornea'), - Eye = ('SRT', 'T-AA000', 'Eye'), - Eyelid = ('SRT', 'T-AA810', 'Eyelid'), - FoveaCentralis = ('SRT', 'T-AA621', 'Fovea centralis'), - Iris = ('SRT', 'T-AA500', 'Iris'), - LacrimalCaruncle = ('SRT', 'T-AA862', 'Lacrimal caruncle'), - LacrimalGland = ('SRT', 'T-AA910', 'Lacrimal gland'), - LacrimalSac = ('SRT', 'T-AA940', 'Lacrimal sac'), - Lens = ('SRT', 'T-AA700', 'Lens'), - LowerEyeLid = ('SRT', 'T-AA830', 'Lower Eyelid'), - OphthalmicArtery = ('SRT', 'T-45400', 'Ophthalmic artery'), - OpticNerveHead = ('SRT', 'T-AA630', 'Optic nerve head'), - Retina = ('SRT', 'T-AA610', 'Retina'), - Sclera = ('SRT', 'T-AA110', 'Sclera'), + AnteriorChamberOfEye = ('SRT', 'T-AA050', 'Anterior chamber of eye') + BothEyes = ('SRT', 'T-AA180', 'Both eyes') + ChoroidOfEye = ('SRT', 'T-AA310', 'Choroid of eye') + CiliaryBody = ('SRT', 'T-AA400', 'Ciliary body') + Conjunctiva = ('SRT', 'T-AA860', 'Conjunctiva') + Cornea = ('SRT', 'T-AA200', 'Cornea') + Eye = ('SRT', 'T-AA000', 'Eye') + Eyelid = ('SRT', 'T-AA810', 'Eyelid') + FoveaCentralis = ('SRT', 'T-AA621', 'Fovea centralis') + Iris = ('SRT', 'T-AA500', 'Iris') + LacrimalCaruncle = ('SRT', 'T-AA862', 'Lacrimal caruncle') + LacrimalGland = ('SRT', 'T-AA910', 'Lacrimal gland') + LacrimalSac = ('SRT', 'T-AA940', 'Lacrimal sac') + Lens = ('SRT', 'T-AA700', 'Lens') + LowerEyeLid = ('SRT', 'T-AA830', 'Lower Eyelid') + OphthalmicArtery = ('SRT', 'T-45400', 'Ophthalmic artery') + OpticNerveHead = ('SRT', 'T-AA630', 'Optic nerve head') + Retina = ('SRT', 'T-AA610', 'Retina') + Sclera = ('SRT', 'T-AA110', 'Sclera') UpperEyeLid = ('SRT', 'T-AA820', 'Upper Eyelid') Unspecified = ('OCT-converter', 'A-0001', 'Unspecified anatomy') @@ -88,9 +89,9 @@ class ManufacturerMeta(): @dataclasses.dataclass class ImageGeometry(): # Image geometry info - pixel_spacing: t.List[float] = [1.0, 1.0] + pixel_spacing: list[float] = field(default_factory=list) slice_thickness: float = 1.0 - image_orientation: t.List[float] = [1, 0, 0, 0 , 1, 0] + image_orientation: list[float] = field(default_factory=list) @dataclasses.dataclass From 7da0e2bc40910710d958f822a1cd9aa9121a5e98 Mon Sep 17 00:00:00 2001 From: Susannah Trevino Date: Fri, 18 Aug 2023 11:37:15 -0500 Subject: [PATCH 09/25] Add Topcon metadata parsers --- oct_converter/dicom/fda_meta.py | 80 +++++++++++++++++++++++++++++++++ oct_converter/dicom/fds_meta.py | 80 +++++++++++++++++++++++++++++++++ 2 files changed, 160 insertions(+) create mode 100644 oct_converter/dicom/fda_meta.py create mode 100644 oct_converter/dicom/fds_meta.py diff --git a/oct_converter/dicom/fda_meta.py b/oct_converter/dicom/fda_meta.py new file mode 100644 index 0000000..fb3a4bb --- /dev/null +++ b/oct_converter/dicom/fda_meta.py @@ -0,0 +1,80 @@ +from oct_converter.dicom.metadata import * +from datetime import datetime + +from oct_converter.image_types import OCTVolumeWithMetaData + +def fda_patient_meta(fds_metadata: dict) -> PatientMeta: + patient_info = fds_metadata.get("patient_info_02") or fds_metadata.get("patient_info", {}) + sex_map = {1: "M", 2: "F", 3: "O", None: ""} + patient = PatientMeta() + + patient.first_name = patient_info.get("first_name") + patient.last_name = patient_info.get("last_name") + patient.patient_id = patient_info.get("patient_id") + patient.patient_sex = sex_map[patient_info.get("sex", None)] + patient.patient_dob = datetime(*patient_info.get("birth_date")) if patient_info.get('birth_date')[0] != 0 else None + + return patient + + +def fda_series_meta(fds_metadata: dict) -> SeriesMeta: + capture_info = fds_metadata.get("capture_info_02") or fds_metadata.get("capture_info", {}) + lat_map = {0: "R", 1: "L", None: ""} + series = SeriesMeta() + + series.study_id = "" + series.series_id = capture_info.get("session_id", "") + series.laterality = lat_map[capture_info.get("eye", None)] + series.acquisition_date = datetime(*capture_info.get('cap_date')) + series.opt_anatomy = OPTAnatomyStructure.Retina + + return series + + +def fda_manu_meta(fds_metadata: dict, fds_header: dict) -> ManufacturerMeta: + hw_info = fds_metadata.get("hw_info_03") or fds_metadata.get("hw_info_02") or fds_metadata.get("hw_info_01", {}) + manufacture = ManufacturerMeta() + + manufacture.manufacturer = "Topcon" + manufacture.manufacturer_model = hw_info.get("model_name") + manufacture.device_serial = hw_info.get("serial_number") + manufacture.software_version = f"{fds_header.get('major_ver')}.{fds_header.get('minor_ver')}" + + return manufacture + + +def fda_image_geom(pixel_spacing: list) -> ImageGeometry: + image_geom = ImageGeometry() + image_geom.pixel_spacing = [pixel_spacing[2], pixel_spacing[0]] + image_geom.slice_thickness = pixel_spacing[1] + image_geom.image_orientation = [1, 0, 0, 0, 1, 0] + + return image_geom + + +def fda_image_params() -> OCTImageParams: + image_params = OCTImageParams() + image_params.opt_acquisition_device = OPTAcquisitionDevice.OCTScanner + image_params.DetectorType = OCTDetectorType.CCD + image_params.IlluminationWaveLength = 830 + image_params.IlluminationPower = 800 + image_params.IlluminationBandwidth = 50 + image_params.DepthSpatialResolution = 6 + image_params.MaximumDepthDistortion = 0.5 + image_params.AlongscanSpatialResolution = 20 + image_params.MaximumAlongscanDistortion = 0.5 + image_params.AcrossscanSpatialResolution = 20 + image_params.MaximumAcrossscanDistortion = 0.5 + + return image_params + + +def fda_dicom_metadata(oct: OCTVolumeWithMetaData) -> DicomMetadata: + meta = DicomMetadata + meta.patient_info = fda_patient_meta(oct.metadata) + meta.series_info = fda_series_meta(oct.metadata) + meta.manufacturer_info = fda_manu_meta(oct.metadata, oct.header) + meta.image_geometry = fda_image_geom(oct.pixel_spacing) + meta.oct_image_params = fda_image_params() + + return meta \ No newline at end of file diff --git a/oct_converter/dicom/fds_meta.py b/oct_converter/dicom/fds_meta.py new file mode 100644 index 0000000..79f600c --- /dev/null +++ b/oct_converter/dicom/fds_meta.py @@ -0,0 +1,80 @@ +from oct_converter.dicom.metadata import * +from datetime import datetime + +from oct_converter.image_types import OCTVolumeWithMetaData + +def fds_patient_meta(fds_metadata: dict) -> PatientMeta: + patient_info = fds_metadata.get("patient_info_02") or fds_metadata.get("patient_info", {}) + sex_map = {1: "M", 2: "F", 3: "O", None: ""} + patient = PatientMeta() + + patient.first_name = patient_info.get("first_name") + patient.last_name = patient_info.get("last_name") + patient.patient_id = patient_info.get("patient_id") + patient.patient_sex = sex_map[patient_info.get("sex", None)] + patient.patient_dob = datetime(*patient_info.get("birth_date")) if patient_info.get('birth_date')[0] != 0 else None + + return patient + + +def fds_series_meta(fds_metadata: dict) -> SeriesMeta: + capture_info = fds_metadata.get("capture_info_02") or fds_metadata.get("capture_info", {}) + lat_map = {0: "R", 1: "L", None: ""} + series = SeriesMeta() + + series.study_id = "" + series.series_id = capture_info.get("session_id", "") + series.laterality = lat_map[capture_info.get("eye", None)] + series.acquisition_date = datetime(*capture_info.get('cap_date')) + series.opt_anatomy = OPTAnatomyStructure.Retina + + return series + + +def fds_manu_meta(fds_metadata: dict, fds_header: dict) -> ManufacturerMeta: + hw_info = fds_metadata.get("hw_info_03") or fds_metadata.get("hw_info_02") or fds_metadata.get("hw_info_01", {}) + manufacture = ManufacturerMeta() + + manufacture.manufacturer = "Topcon" + manufacture.manufacturer_model = hw_info.get("model_name") + manufacture.device_serial = hw_info.get("serial_number") + manufacture.software_version = f"{fds_header.get('major_ver')}.{fds_header.get('minor_ver')}" + + return manufacture + + +def fds_image_geom(pixel_spacing: list) -> ImageGeometry: + image_geom = ImageGeometry() + image_geom.pixel_spacing = [pixel_spacing[2], pixel_spacing[0]] + image_geom.slice_thickness = pixel_spacing[1] + image_geom.image_orientation = [1, 0, 0, 0, 1, 0] + + return image_geom + + +def fds_image_params() -> OCTImageParams: + image_params = OCTImageParams() + image_params.opt_acquisition_device = OPTAcquisitionDevice.OCTScanner + image_params.DetectorType = OCTDetectorType.CCD + image_params.IlluminationWaveLength = 830 + image_params.IlluminationPower = 800 + image_params.IlluminationBandwidth = 50 + image_params.DepthSpatialResolution = 6 + image_params.MaximumDepthDistortion = 0.5 + image_params.AlongscanSpatialResolution = 20 + image_params.MaximumAlongscanDistortion = 0.5 + image_params.AcrossscanSpatialResolution = 20 + image_params.MaximumAcrossscanDistortion = 0.5 + + return image_params + + +def fds_dicom_metadata(oct: OCTVolumeWithMetaData) -> DicomMetadata: + meta = DicomMetadata + meta.patient_info = fds_patient_meta(oct.metadata) + meta.series_info = fds_series_meta(oct.metadata) + meta.manufacturer_info = fds_manu_meta(oct.metadata, oct.header) + meta.image_geometry = fds_image_geom(oct.pixel_spacing) + meta.oct_image_params = fds_image_params() + + return meta \ No newline at end of file From 5c13dc5270212e3e7801d020a20019d065629daf Mon Sep 17 00:00:00 2001 From: Susannah Trevino Date: Fri, 18 Aug 2023 15:24:48 -0500 Subject: [PATCH 10/25] Add create_dicom_from_oct, linting --- oct_converter/dicom/dicom.py | 406 ++++++++++++++++++++++------------- 1 file changed, 252 insertions(+), 154 deletions(-) diff --git a/oct_converter/dicom/dicom.py b/oct_converter/dicom/dicom.py index 5c49833..0922707 100644 --- a/oct_converter/dicom/dicom.py +++ b/oct_converter/dicom/dicom.py @@ -1,176 +1,274 @@ -from pydicom.dataset import FileDataset, FileMetaDataset, Dataset -from pydicom.uid import generate_uid, OphthalmicTomographyImageStorage, ExplicitVRLittleEndian -from pydicom.encaps import encapsulate -from oct_converter.dicom import implementation_uid -import numpy as np -from oct_converter.dicom.metadata import DicomMetadata -from pathlib import Path -import typing as t +import shutil import tempfile -from datetime import datetime +import typing as t import uuid +from datetime import datetime +from pathlib import Path + +import numpy as np +from pydicom.dataset import Dataset, FileDataset, FileMetaDataset +from pydicom.uid import ( + ExplicitVRLittleEndian, + OphthalmicTomographyImageStorage, + generate_uid, +) + +from oct_converter.dicom import implementation_uid +from oct_converter.dicom.fda_meta import fda_dicom_metadata +from oct_converter.dicom.fds_meta import fds_dicom_metadata +from oct_converter.dicom.metadata import DicomMetadata +from oct_converter.readers import FDA, FDS def opt_base_dicom() -> t.Tuple[Path, Dataset]: - suffix = '.dcm' - file_ = Path(tempfile.NamedTemporaryFile(suffix=suffix).name) + """Creates the base dicom to be populated. + + Args: + None + Returns: + file_: Path to created .dcm, + ds: FileDataset with file meta, preamble, and empty dataset + """ + suffix = ".dcm" + file_ = Path(tempfile.NamedTemporaryFile(suffix=suffix).name) - # Populate required values for file meta information - file_meta = FileMetaDataset() - file_meta.MediaStorageSOPClassUID = OphthalmicTomographyImageStorage - file_meta.MediaStorageSOPInstanceUID = generate_uid() - file_meta.ImplementationClassUID = implementation_uid - file_meta.TransferSyntaxUID = ExplicitVRLittleEndian + # Populate required values for file meta information + file_meta = FileMetaDataset() + file_meta.MediaStorageSOPClassUID = OphthalmicTomographyImageStorage + file_meta.MediaStorageSOPInstanceUID = generate_uid() + file_meta.ImplementationClassUID = implementation_uid + file_meta.TransferSyntaxUID = ExplicitVRLittleEndian - # Create the FileDataset instance with file meta, preamble and empty DS - ds = FileDataset(str(file_), {}, file_meta=file_meta, preamble=b"\0" * 128) - ds.is_little_endian = True - ds.is_implicit_VR = False # Explicit VR - return file_, ds + # Create the FileDataset instance with file meta, preamble and empty DS + ds = FileDataset(str(file_), {}, file_meta=file_meta, preamble=b"\0" * 128) + ds.is_little_endian = True + ds.is_implicit_VR = False # Explicit VR + return file_, ds def populate_patient_info(ds: Dataset, meta: DicomMetadata) -> Dataset: - # Patient Module PS3.3 C.7.1.1 - ds.PatientName = f"{meta.patient_info.last_name}^{meta.patient_info.first_name}" - ds.PatientID = meta.patient_info.patient_id - ds.PatientSex = meta.patient_info.patient_sex - ds.PatientBirthDate = ( - meta.patient_info.patient_dob.strftime('%Y%m%d') if meta.patient_info.patient_dob else "" - ) - return ds + """Populates Patient Module PS3.3 C.7.1.1 + + Args: + ds: current dataset + meta: DICOM metadata information + Returns: + ds: Dataset, updated with patient information + """ + # Patient Module PS3.3 C.7.1.1 + ds.PatientName = f"{meta.patient_info.last_name}^{meta.patient_info.first_name}" + ds.PatientID = meta.patient_info.patient_id + ds.PatientSex = meta.patient_info.patient_sex + ds.PatientBirthDate = ( + meta.patient_info.patient_dob.strftime("%Y%m%d") + if meta.patient_info.patient_dob + else "" + ) + return ds def populate_manufacturer_info(ds: Dataset, meta: DicomMetadata) -> Dataset: - # General and enhanced equipment module PS3.3 C.7.5.1, PS3.3 C.7.5.2 - ds.Manufacturer = meta.manufacturer_info.manufacturer - ds.ManufacturerModelName = meta.manufacturer_info.manufacturer_model - ds.DeviceSerialNumber = meta.manufacturer_info.device_serial - ds.SoftwareVersions = meta.manufacturer_info.software_version - - # OPT parameter module PS3.3 C.8.17.9 - cd, cv, cm = meta.oct_image_params.opt_acquisition_device.value - ds.AcquisitionDeviceTypeCodeSequence = [Dataset()] - ds.AcquisitionDeviceTypeCodeSequence[0].CodeValue = cv - ds.AcquisitionDeviceTypeCodeSequence[0].CodingSchemeDesignator = cd - ds.AcquisitionDeviceTypeCodeSequence[0].CodeMeaning = cm - ds.DetectorType = meta.oct_image_params.DetectorType.value - return ds + """Populates equipment modules PS3.3 C.7.5.1, PS3.3 C.7.5.2 + + Args: + ds: current dataset + meta: DICOM metadata information + Returns: + ds: Dataset, updated with equipment information + """ + # General and enhanced equipment module PS3.3 C.7.5.1, PS3.3 C.7.5.2 + ds.Manufacturer = meta.manufacturer_info.manufacturer + ds.ManufacturerModelName = meta.manufacturer_info.manufacturer_model + ds.DeviceSerialNumber = meta.manufacturer_info.device_serial + ds.SoftwareVersions = meta.manufacturer_info.software_version + + # OPT parameter module PS3.3 C.8.17.9 + cd, cv, cm = meta.oct_image_params.opt_acquisition_device.value + ds.AcquisitionDeviceTypeCodeSequence = [Dataset()] + ds.AcquisitionDeviceTypeCodeSequence[0].CodeValue = cv + ds.AcquisitionDeviceTypeCodeSequence[0].CodingSchemeDesignator = cd + ds.AcquisitionDeviceTypeCodeSequence[0].CodeMeaning = cm + ds.DetectorType = meta.oct_image_params.DetectorType.value + return ds def populate_opt_series(ds: Dataset, meta: DicomMetadata) -> Dataset: - # General study module PS3.3 C.7.2.1 - # Deterministic StudyInstanceUID based on study ID - # ds.StudyInstanceUID = generate_uid(entropy_srcs=[ - # # str(uuid.uuid4()), - # str(meta.series_info.study_id) - # ]) - - # # General series module PS3.3 C.7.3.1 - # ds.SeriesInstanceUID = generate_uid(entropy_srcs=[ - # # str(uuid.uuid4()), - # str(meta.series_info.series_id) - # ]) - ds.StudyInstanceUID = generate_uid() - ds.SeriesInstanceUID = generate_uid() - ds.Laterality = meta.series_info.laterality - # Ophthalmic Tomography Series PS3.3 C.8.17.6 - ds.Modality = 'OPT' - ds.SeriesNumber = int(meta.series_info.series_id) - - # SOP Common module PS3.3 C.12.1 - ds.SOPClassUID = OphthalmicTomographyImageStorage - ds.SOPInstanceUID = generate_uid() - return ds + """Populates study and series modules, PS3.3 C.7.2.1, PS3.3 C.7.3.1, + PS3.3 C.8.17.6, and PS3.3 C.12.1 + + Args: + ds: current dataset + meta: DICOM metadata information + Returns: + ds: Dataset, updated with study and series information + """ + # General study module PS3.3 C.7.2.1 + # Deterministic StudyInstanceUID based on study ID + # ds.StudyInstanceUID = generate_uid(entropy_srcs=[ + # # str(uuid.uuid4()), + # str(meta.series_info.study_id) + # ]) + + # # General series module PS3.3 C.7.3.1 + # ds.SeriesInstanceUID = generate_uid(entropy_srcs=[ + # # str(uuid.uuid4()), + # str(meta.series_info.series_id) + # ]) + ds.StudyInstanceUID = generate_uid() + ds.SeriesInstanceUID = generate_uid() + ds.Laterality = meta.series_info.laterality + # Ophthalmic Tomography Series PS3.3 C.8.17.6 + ds.Modality = "OPT" + ds.SeriesNumber = int(meta.series_info.series_id) + + # SOP Common module PS3.3 C.12.1 + ds.SOPClassUID = OphthalmicTomographyImageStorage + ds.SOPInstanceUID = generate_uid() + return ds def populate_ocular_region(ds: Dataset, meta: DicomMetadata) -> Dataset: - # Ocular region imaged module PS3.3 C.8.17.5 - cd, cv, cm = meta.series_info.opt_anatomy.value - ds.ImageLaterality = meta.series_info.laterality - ds.AnatomicRegionSequence = [Dataset()] - ds.AnatomicRegionSequence[0].CodeValue = cv - ds.AnatomicRegionSequence[0].CodingSchemeDesignator = cd - ds.AnatomicRegionSequence[0].CodeMeaning = cm - return ds + """Populates ocular region modules, PS3.3 C.8.17.5, PS3.3 C.7.6.16.2.8, + and PS3.3 C.7.6.16.2.1 + + Args: + ds: current dataset + meta: DICOM metadata information + Returns: + ds: Dataset, updated with ocular region information + """ + # Ocular region imaged module PS3.3 C.8.17.5 + cd, cv, cm = meta.series_info.opt_anatomy.value + ds.ImageLaterality = meta.series_info.laterality + ds.AnatomicRegionSequence = [Dataset()] + ds.AnatomicRegionSequence[0].CodeValue = cv + ds.AnatomicRegionSequence[0].CodingSchemeDesignator = cd + ds.AnatomicRegionSequence[0].CodeMeaning = cm + return ds def opt_shared_functional_groups(ds: Dataset, meta: DicomMetadata) -> Dataset: - # ---- Shared - shared_ds = [Dataset()] - # Frame anatomy PS3.3 C.7.6.16.2.8 - shared_ds[0].FrameAnatomySequence = [Dataset()] - shared_ds[0].FrameAnatomySequence[0] = ds.AnatomicRegionSequence[0].copy() - shared_ds[0].FrameAnatomySequence[0].FrameLaterality = meta.series_info.laterality - # Pixel Measures PS3.3 C.7.6.16.2.1 - shared_ds[0].PixelMeasuresSequence = [Dataset()] - shared_ds[0].PixelMeasuresSequence[0].PixelSpacing = meta.image_geometry.pixel_spacing - shared_ds[0].PixelMeasuresSequence[0].SliceThickness = meta.image_geometry.slice_thickness - # Plane Orientation PS3.3 C.7.6.16.2.4 - shared_ds[0].PlaneOrientationSequence = [Dataset()] - shared_ds[0].PlaneOrientationSequence[0].ImageOrientationPatient = meta.image_geometry.image_orientation - ds.SharedFunctionalGroupsSequence = shared_ds - return ds - - -def write_opt_dicom( - meta: DicomMetadata, - frames: t.List[np.ndarray] -): - file_, ds = opt_base_dicom() - ds = populate_patient_info(ds, meta) - ds = populate_manufacturer_info(ds, meta) - ds = populate_opt_series(ds, meta) - ds = populate_ocular_region(ds, meta) - ds = opt_shared_functional_groups(ds, meta) - - # TODO: Frame of reference if fundus image present - - # OPT Image Module PS3.3 C.8.17.7 - ds.ImageType = ['DERIVED', 'SECONDARY'] - ds.SamplesPerPixel = 1 - ds.AcquisitionDateTime = ( -meta.series_info.acquisition_date.strftime('%Y%m%d%H%M%S.%f') if meta.series_info.acquisition_date else "" - ) - ds.AcquisitionNumber = 1 - ds.PhotometricInterpretation = 'MONOCHROME2' - # Unsigned integer - ds.PixelRepresentation = 0 - # Use 16 bit pixel - ds.BitsAllocated = 16 - ds.BitsStored = ds.BitsAllocated - ds.HighBit = ds.BitsAllocated - 1 - ds.SamplesPerPixel = 1 - ds.NumberOfFrames = len(frames) - - - # Multi-frame Functional Groups Module PS3.3 C.7.6.16 - dt = datetime.now() - ds.ContentDate = dt.strftime('%Y%m%d') - timeStr = dt.strftime('%H%M%S.%f') # long format with micro seconds - ds.ContentTime = timeStr - ds.InstanceNumber = 1 - - per_frame = [] - pixel_data_bytes = list() - # Convert to a 3d volume - pixel_data = np.array(frames).astype(np.uint16) - ds.Rows = pixel_data.shape[1] - ds.Columns = pixel_data.shape[2] - for i in range(pixel_data.shape[0]): - # Per Frame Functional Groups - frame_fgs = Dataset() - frame_fgs.PlanePositionSequence = [Dataset()] - ipp = [0, 0, i*meta.image_geometry.slice_thickness] - frame_fgs.PlanePositionSequence[0].ImagePositionPatient = ipp - frame_fgs.FrameContentSequence = [Dataset()] - frame_fgs.FrameContentSequence[0].InStackPositionNumber = i + 1 - frame_fgs.FrameContentSequence[0].StackID = '1' - - # Pixel data - frame_dat = pixel_data[i, :, :] - pixel_data_bytes.append(frame_dat.tobytes()) - per_frame.append(frame_fgs) - ds.PerFrameFunctionalGroupsSequence = per_frame - ds.PixelData = pixel_data.tobytes() - ds.save_as(file_) - return file_ \ No newline at end of file + # ---- Shared + shared_ds = [Dataset()] + # Frame anatomy PS3.3 C.7.6.16.2.8 + shared_ds[0].FrameAnatomySequence = [Dataset()] + shared_ds[0].FrameAnatomySequence[0] = ds.AnatomicRegionSequence[0].copy() + shared_ds[0].FrameAnatomySequence[0].FrameLaterality = meta.series_info.laterality + # Pixel Measures PS3.3 C.7.6.16.2.1 + shared_ds[0].PixelMeasuresSequence = [Dataset()] + shared_ds[0].PixelMeasuresSequence[ + 0 + ].PixelSpacing = meta.image_geometry.pixel_spacing + shared_ds[0].PixelMeasuresSequence[ + 0 + ].SliceThickness = meta.image_geometry.slice_thickness + # Plane Orientation PS3.3 C.7.6.16.2.4 + shared_ds[0].PlaneOrientationSequence = [Dataset()] + shared_ds[0].PlaneOrientationSequence[ + 0 + ].ImageOrientationPatient = meta.image_geometry.image_orientation + ds.SharedFunctionalGroupsSequence = shared_ds + return ds + + +def write_opt_dicom(meta: DicomMetadata, frames: t.List[np.ndarray]) -> Path: + """Writes required DICOM metadata and pixel data to .dcm file. + + Args: + meta: DICOM metadata information + frames: list of frames of pixel data + Returns: + Path to created DICOM file + """ + file_, ds = opt_base_dicom() + ds = populate_patient_info(ds, meta) + ds = populate_manufacturer_info(ds, meta) + ds = populate_opt_series(ds, meta) + ds = populate_ocular_region(ds, meta) + ds = opt_shared_functional_groups(ds, meta) + + # TODO: Frame of reference if fundus image present + + # OPT Image Module PS3.3 C.8.17.7 + ds.ImageType = ["DERIVED", "SECONDARY"] + ds.SamplesPerPixel = 1 + ds.AcquisitionDateTime = ( + meta.series_info.acquisition_date.strftime("%Y%m%d%H%M%S.%f") + if meta.series_info.acquisition_date + else "" + ) + ds.AcquisitionNumber = 1 + ds.PhotometricInterpretation = "MONOCHROME2" + # Unsigned integer + ds.PixelRepresentation = 0 + # Use 16 bit pixel + ds.BitsAllocated = 16 + ds.BitsStored = ds.BitsAllocated + ds.HighBit = ds.BitsAllocated - 1 + ds.SamplesPerPixel = 1 + ds.NumberOfFrames = len(frames) + + # Multi-frame Functional Groups Module PS3.3 C.7.6.16 + dt = datetime.now() + ds.ContentDate = dt.strftime("%Y%m%d") + timeStr = dt.strftime("%H%M%S.%f") # long format with micro seconds + ds.ContentTime = timeStr + ds.InstanceNumber = 1 + + per_frame = [] + pixel_data_bytes = list() + # Convert to a 3d volume + pixel_data = np.array(frames).astype(np.uint16) + ds.Rows = pixel_data.shape[1] + ds.Columns = pixel_data.shape[2] + for i in range(pixel_data.shape[0]): + # Per Frame Functional Groups + frame_fgs = Dataset() + frame_fgs.PlanePositionSequence = [Dataset()] + ipp = [0, 0, i * meta.image_geometry.slice_thickness] + frame_fgs.PlanePositionSequence[0].ImagePositionPatient = ipp + frame_fgs.FrameContentSequence = [Dataset()] + frame_fgs.FrameContentSequence[0].InStackPositionNumber = i + 1 + frame_fgs.FrameContentSequence[0].StackID = "1" + + # Pixel data + frame_dat = pixel_data[i, :, :] + pixel_data_bytes.append(frame_dat.tobytes()) + per_frame.append(frame_fgs) + ds.PerFrameFunctionalGroupsSequence = per_frame + ds.PixelData = pixel_data.tobytes() + ds.save_as(file_) + return file_ + + +def create_dicom_from_oct(input_file: str, output_dest: str = None) -> Path: + """Creates a DICOM file with the data parsed from + the input file. + + Args: + input_file: File with OCT data + (Currently only Topcon files supported) + output_dest: Output directory OR output path + i.e. `"/tmp"` or `"/tmp/filename.dcm"` + + Returns: + Path to DICOM file + """ + file_suffix = input_file.split(".")[-1] + if file_suffix == "fds": + fds = FDS(input_file) + oct = fds.read_oct_volume() + meta = fds_dicom_metadata(oct) + elif file_suffix == "fda": + fda = FDA(input_file) + oct = fda.read_oct_volume() + meta = fda_dicom_metadata(oct) + elif file_suffix in ["e2e", "img", "oct", "OCT"]: + raise NotImplementedError("Filetype not yet supported.") + else: + raise TypeError("Invalid input_file type.") + file = write_opt_dicom(meta, oct.volume) + if output_dest: + dst = shutil.move(file, output_dest) + return dst + return file From e01f9f1bea1b9764beee51c12a34e6b39050f43f Mon Sep 17 00:00:00 2001 From: Susannah Trevino Date: Fri, 18 Aug 2023 15:25:53 -0500 Subject: [PATCH 11/25] Minor linting --- oct_converter/dicom/__init__.py | 7 ++++--- oct_converter/image_types/__init__.py | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/oct_converter/dicom/__init__.py b/oct_converter/dicom/__init__.py index c19a5ad..e7df8de 100644 --- a/oct_converter/dicom/__init__.py +++ b/oct_converter/dicom/__init__.py @@ -1,8 +1,9 @@ """Init module.""" -from pydicom.uid import generate_uid from importlib import metadata -version = metadata.version('oct_converter') +from pydicom.uid import generate_uid + +version = metadata.version("oct_converter") # Deterministic implentation UID based on package name and version -implementation_uid = generate_uid(entropy_srcs=["oct_converter", version]) \ No newline at end of file +implementation_uid = generate_uid(entropy_srcs=["oct_converter", version]) diff --git a/oct_converter/image_types/__init__.py b/oct_converter/image_types/__init__.py index da8331c..4899fe7 100644 --- a/oct_converter/image_types/__init__.py +++ b/oct_converter/image_types/__init__.py @@ -5,7 +5,7 @@ __all__ = [ "version", - "implementaation_uid", + "implementation_uid", "FundusImageWithMetaData", - "OCTVolumeWithMetaData" -] \ No newline at end of file + "OCTVolumeWithMetaData", +] From db95c5a0fb6155b51f005cb4967d3ba1eee3e88e Mon Sep 17 00:00:00 2001 From: Susannah Trevino Date: Fri, 18 Aug 2023 15:28:21 -0500 Subject: [PATCH 12/25] More linting --- oct_converter/dicom/metadata.py | 201 ++++++++++-------- .../readers/binary_structs/fda_binary.py | 41 ++-- .../readers/binary_structs/fds_binary.py | 41 ++-- 3 files changed, 164 insertions(+), 119 deletions(-) diff --git a/oct_converter/dicom/metadata.py b/oct_converter/dicom/metadata.py index aabe81e..b1e3724 100644 --- a/oct_converter/dicom/metadata.py +++ b/oct_converter/dicom/metadata.py @@ -1,122 +1,141 @@ import dataclasses -import enum import datetime +import enum import typing as t from dataclasses import field -class OPTAcquisitionDevice(enum.Enum): - """OPT Acquisition Device enumeration. - Contains code designator, code value and code meaning for each entry. - """ - OCTScanner = ("SRT", "A-00FBE", "Optical Coherence Tomography Scanner") - RetinalThicknessAnalyzer = ("SRT", "R-FAB5A", "Retinal Thickness Analyzer") - ConfocalScanningLaserOphthalmoscope = ("SRT", "A-00E8B", "Confocal Scanning Laser Ophthalmoscope") - ScheimpflugCamera = ("DCM", "111626", "Scheimpflug Camera") - ScanningLaserPolarimeter = ("SRT", "A-00E8C", "Scanning Laser Polarimeter") - ElevationBasedCornealTomographer = ("DCM", "111945", "Elevation-based corneal tomographer") - ReflectionBasedCornealTopographer = ("DCM", "111946", "Reflection-based corneal topographer") - InterferometryBasedCornealTomographer = ("DCM", "111947", "Interferometry-based corneal tomographer") - Unspecified = ("OCT-converter", "D-0001", "Unspecified scanner") +class OPTAcquisitionDevice(enum.Enum): + """OPT Acquisition Device enumeration. + + Contains code designator, code value and code meaning for each entry. + """ + + OCTScanner = ("SRT", "A-00FBE", "Optical Coherence Tomography Scanner") + RetinalThicknessAnalyzer = ("SRT", "R-FAB5A", "Retinal Thickness Analyzer") + ConfocalScanningLaserOphthalmoscope = ( + "SRT", + "A-00E8B", + "Confocal Scanning Laser Ophthalmoscope", + ) + ScheimpflugCamera = ("DCM", "111626", "Scheimpflug Camera") + ScanningLaserPolarimeter = ("SRT", "A-00E8C", "Scanning Laser Polarimeter") + ElevationBasedCornealTomographer = ( + "DCM", + "111945", + "Elevation-based corneal tomographer", + ) + ReflectionBasedCornealTopographer = ( + "DCM", + "111946", + "Reflection-based corneal topographer", + ) + InterferometryBasedCornealTomographer = ( + "DCM", + "111947", + "Interferometry-based corneal tomographer", + ) + Unspecified = ("OCT-converter", "D-0001", "Unspecified scanner") class OPTAnatomyStructure(enum.Enum): - """OPT Anatomy enumeration. - - Contains code designator, code value and code meaning for each entry. - """ - AnteriorChamberOfEye = ('SRT', 'T-AA050', 'Anterior chamber of eye') - BothEyes = ('SRT', 'T-AA180', 'Both eyes') - ChoroidOfEye = ('SRT', 'T-AA310', 'Choroid of eye') - CiliaryBody = ('SRT', 'T-AA400', 'Ciliary body') - Conjunctiva = ('SRT', 'T-AA860', 'Conjunctiva') - Cornea = ('SRT', 'T-AA200', 'Cornea') - Eye = ('SRT', 'T-AA000', 'Eye') - Eyelid = ('SRT', 'T-AA810', 'Eyelid') - FoveaCentralis = ('SRT', 'T-AA621', 'Fovea centralis') - Iris = ('SRT', 'T-AA500', 'Iris') - LacrimalCaruncle = ('SRT', 'T-AA862', 'Lacrimal caruncle') - LacrimalGland = ('SRT', 'T-AA910', 'Lacrimal gland') - LacrimalSac = ('SRT', 'T-AA940', 'Lacrimal sac') - Lens = ('SRT', 'T-AA700', 'Lens') - LowerEyeLid = ('SRT', 'T-AA830', 'Lower Eyelid') - OphthalmicArtery = ('SRT', 'T-45400', 'Ophthalmic artery') - OpticNerveHead = ('SRT', 'T-AA630', 'Optic nerve head') - Retina = ('SRT', 'T-AA610', 'Retina') - Sclera = ('SRT', 'T-AA110', 'Sclera') - UpperEyeLid = ('SRT', 'T-AA820', 'Upper Eyelid') - Unspecified = ('OCT-converter', 'A-0001', 'Unspecified anatomy') + """OPT Anatomy enumeration. + + Contains code designator, code value and code meaning for each entry. + """ + + AnteriorChamberOfEye = ("SRT", "T-AA050", "Anterior chamber of eye") + BothEyes = ("SRT", "T-AA180", "Both eyes") + ChoroidOfEye = ("SRT", "T-AA310", "Choroid of eye") + CiliaryBody = ("SRT", "T-AA400", "Ciliary body") + Conjunctiva = ("SRT", "T-AA860", "Conjunctiva") + Cornea = ("SRT", "T-AA200", "Cornea") + Eye = ("SRT", "T-AA000", "Eye") + Eyelid = ("SRT", "T-AA810", "Eyelid") + FoveaCentralis = ("SRT", "T-AA621", "Fovea centralis") + Iris = ("SRT", "T-AA500", "Iris") + LacrimalCaruncle = ("SRT", "T-AA862", "Lacrimal caruncle") + LacrimalGland = ("SRT", "T-AA910", "Lacrimal gland") + LacrimalSac = ("SRT", "T-AA940", "Lacrimal sac") + Lens = ("SRT", "T-AA700", "Lens") + LowerEyeLid = ("SRT", "T-AA830", "Lower Eyelid") + OphthalmicArtery = ("SRT", "T-45400", "Ophthalmic artery") + OpticNerveHead = ("SRT", "T-AA630", "Optic nerve head") + Retina = ("SRT", "T-AA610", "Retina") + Sclera = ("SRT", "T-AA110", "Sclera") + UpperEyeLid = ("SRT", "T-AA820", "Upper Eyelid") + Unspecified = ("OCT-converter", "A-0001", "Unspecified anatomy") class OCTDetectorType(enum.Enum): - CCD = "CCD" - CMOS = "CMOS" - PHOTO = "PHOTO" - INT = "INT" - Unknown = "UNKNOWN" + CCD = "CCD" + CMOS = "CMOS" + PHOTO = "PHOTO" + INT = "INT" + Unknown = "UNKNOWN" @dataclasses.dataclass -class PatientMeta(): - # Patient Info - first_name: str = '' - last_name: str = '' - patient_id: str = '' - patient_sex: str = '' - patient_dob: t.Optional[datetime.datetime] = None +class PatientMeta: + # Patient Info + first_name: str = "" + last_name: str = "" + patient_id: str = "" + patient_sex: str = "" + patient_dob: t.Optional[datetime.datetime] = None @dataclasses.dataclass -class SeriesMeta(): - # Study and Series - study_id: str = '' - series_id: str = '' - laterality: str = '' - acquisition_date: t.Optional[datetime.datetime] = None - # Anatomy - opt_anatomy: OPTAnatomyStructure = OPTAnatomyStructure.Unspecified +class SeriesMeta: + # Study and Series + study_id: str = "" + series_id: str = "" + laterality: str = "" + acquisition_date: t.Optional[datetime.datetime] = None + # Anatomy + opt_anatomy: OPTAnatomyStructure = OPTAnatomyStructure.Unspecified @dataclasses.dataclass -class ManufacturerMeta(): - # Manufacturer info - manufacturer: str = '' - manufacturer_model: str = 'unknown' - device_serial: str = 'unknown' - software_version: str = 'unknown' +class ManufacturerMeta: + # Manufacturer info + manufacturer: str = "" + manufacturer_model: str = "unknown" + device_serial: str = "unknown" + software_version: str = "unknown" @dataclasses.dataclass -class ImageGeometry(): - # Image geometry info - pixel_spacing: list[float] = field(default_factory=list) - slice_thickness: float = 1.0 - image_orientation: list[float] = field(default_factory=list) +class ImageGeometry: + # Image geometry info + pixel_spacing: list[float] = field(default_factory=list) + slice_thickness: float = 1.0 + image_orientation: list[float] = field(default_factory=list) @dataclasses.dataclass -class OCTImageParams(): - # PS3.3 C.8.17.9 - opt_acquisition_device: OPTAcquisitionDevice = OPTAcquisitionDevice.Unspecified - DetectorType: OCTDetectorType = OCTDetectorType.Unknown - IlluminationWaveLength: t.Optional[float] = None - IlluminationPower: t.Optional[float] = None - IlluminationBandwidth: t.Optional[float] = None - DepthSpatialResolution: t.Optional[float] = None - MaximumDepthDistortion: t.Optional[float] = None - AlongscanSpatialResolution: t.Optional[float] = None - MaximumAlongscanDistortion: t.Optional[float] = None - AcrossscanSpatialResolution: t.Optional[float] = None - MaximumAcrossscanDistortion: t.Optional[float] = None - - # NOTE: Could eventually include C.8.17.8 Acquisition Params. +class OCTImageParams: + # PS3.3 C.8.17.9 + opt_acquisition_device: OPTAcquisitionDevice = OPTAcquisitionDevice.Unspecified + DetectorType: OCTDetectorType = OCTDetectorType.Unknown + IlluminationWaveLength: t.Optional[float] = None + IlluminationPower: t.Optional[float] = None + IlluminationBandwidth: t.Optional[float] = None + DepthSpatialResolution: t.Optional[float] = None + MaximumDepthDistortion: t.Optional[float] = None + AlongscanSpatialResolution: t.Optional[float] = None + MaximumAlongscanDistortion: t.Optional[float] = None + AcrossscanSpatialResolution: t.Optional[float] = None + MaximumAcrossscanDistortion: t.Optional[float] = None + + # NOTE: Could eventually include C.8.17.8 Acquisition Params. @dataclasses.dataclass -class DicomMetadata(): - patient_info: PatientMeta - series_info: SeriesMeta - manufacturer_info: ManufacturerMeta +class DicomMetadata: + patient_info: PatientMeta + series_info: SeriesMeta + manufacturer_info: ManufacturerMeta - image_geometry: ImageGeometry - oct_image_params: OCTImageParams + image_geometry: ImageGeometry + oct_image_params: OCTImageParams diff --git a/oct_converter/readers/binary_structs/fda_binary.py b/oct_converter/readers/binary_structs/fda_binary.py index ae84a57..2b309f0 100644 --- a/oct_converter/readers/binary_structs/fda_binary.py +++ b/oct_converter/readers/binary_structs/fda_binary.py @@ -1,4 +1,14 @@ -from construct import Float32n, Float64n, Int8un, Int16un, Int32un, PaddedString, Struct, Array, this +from construct import ( + Array, + Float32n, + Float64n, + Int8un, + Int16un, + Int32un, + PaddedString, + Struct, + this, +) """ Notes: @@ -40,8 +50,11 @@ """ header = Struct( - "file_code" / PaddedString(4, "ascii"), # Always "FOCT" - "file_type" / PaddedString(3, "ascii"), # "FDA" or "FAA", denoting "macula" or "external" fixation + "file_code" / PaddedString(4, "ascii"), # Always "FOCT" + "file_type" + / PaddedString( + 3, "ascii" + ), # "FDA" or "FAA", denoting "macula" or "external" fixation "major_ver" / Int32un, "minor_ver" / Int32un, ) @@ -151,7 +164,7 @@ "first_name" / PaddedString(32, "ascii"), "last_name" / PaddedString(32, "ascii"), "mid_name" / PaddedString(8, "ascii"), - "sex" / Int8un, # 1: "M", 2: "F", 3: "O" + "sex" / Int8un, # 1: "M", 2: "F", 3: "O" "birth_date" / Int16un[3], "occup_reg" / Int8un[20][2], "r_date" / Int16un[3], @@ -159,12 +172,12 @@ "lv_date" / Int16un[3], # I've not found files that have the below information, # so it's difficult to confirm the remaining. - "physician" / Int8un[64][2], # [64 2] ??? - "zip_code" / Int8un[12], #how does this make sense. - "addr" / Int8un[48][2], # [48 2] - "phones" / Int8un[16][2], # [16 2] - "nx_date" / Int16un[6], # [1 6] - "multipurpose_field" / Int8un[20][3], # [20 3] + "physician" / Int8un[64][2], # [64 2] ??? + "zip_code" / Int8un[12], # how does this make sense. + "addr" / Int8un[48][2], # [48 2] + "phones" / Int8un[16][2], # [16 2] + "nx_date" / Int16un[6], # [1 6] + "multipurpose_field" / Int8un[20][3], # [20 3] "descp" / Int8un[64], "reserved" / Int8un[32], ) @@ -182,7 +195,7 @@ ) capture_info_02_header = Struct( - "eye" / Int8un, # 0: R, 1: L + "eye" / Int8un, # 0: R, 1: L "scan_mode" / Int8un, "session_id" / Int32un, "label" / PaddedString(100, "ascii"), @@ -190,7 +203,7 @@ ) capture_info_header = Struct( - "eye" / Int8un, # 0: R, 1: L + "eye" / Int8un, # 0: R, 1: L "cap_date" / Int16un[6], ) @@ -311,7 +324,7 @@ "unlabeled_2" / Int8un, "w" / Int32un, "n_size" / Int32un, - "aligndata" / Array(this.w * 2, Int16un), # if n_size > 0 + "aligndata" / Array(this.w * 2, Int16un), # if n_size > 0 # if nblockbytes - (10+n_size) >= 16 "keyframe_1" / Int32un, "keyframe_2" / Int32un, @@ -325,7 +338,7 @@ "file_version_2" / Int16un, "file_version_3" / Int16un, "file_version_4" / Int16un, - "string" / PaddedString(128, "ascii") + "string" / PaddedString(128, "ascii"), ) thumbnail_header = Struct( diff --git a/oct_converter/readers/binary_structs/fds_binary.py b/oct_converter/readers/binary_structs/fds_binary.py index b5e98bf..7e34ce8 100644 --- a/oct_converter/readers/binary_structs/fds_binary.py +++ b/oct_converter/readers/binary_structs/fds_binary.py @@ -1,4 +1,14 @@ -from construct import Float32n, Float64n, Int8un, Int16un, Int32un, PaddedString, Struct, Array, this +from construct import ( + Array, + Float32n, + Float64n, + Int8un, + Int16un, + Int32un, + PaddedString, + Struct, + this, +) """ Notes: @@ -41,15 +51,18 @@ """ header = Struct( - "file_code" / PaddedString(4, "ascii"), # Always "FOCT" - "file_type" / PaddedString(3, "ascii"), # "FDA" or "FAA", denoting "macula" or "external" fixation + "file_code" / PaddedString(4, "ascii"), # Always "FOCT" + "file_type" + / PaddedString( + 3, "ascii" + ), # "FDA" or "FAA", denoting "macula" or "external" fixation "major_ver" / Int32un, "minor_ver" / Int32un, ) # IMG_SCAN_03 oct_header = Struct( - "scan_mode" / Int8un, # 2 = 3D, 3 = Radial, 4 = Cross + "scan_mode" / Int8un, # 2 = 3D, 3 = Radial, 4 = Cross "width" / Int32un, "height" / Int32un, "bits_per_pixel" / Int32un, @@ -60,7 +73,7 @@ # IMG_SCAN_02 oct_header_2 = Struct( - "scan_mode" / Int8un, # 2 = 3D, 3 = Radial, 4 = Cross + "scan_mode" / Int8un, # 2 = 3D, 3 = Radial, 4 = Cross "width" / Int32un, "height" / Int32un, "bits_per_pixel" / Int32un, @@ -167,12 +180,12 @@ "lv_date" / Int16un[3], # I've not found files that have the below information, # so it's difficult to confirm the remaining. - "physician" / Int8un[64][2], # [64 2] ??? - "zip_code" / Int8un[12], #how does this make sense. - "addr" / Int8un[48][2], # [48 2] - "phones" / Int8un[16][2], # [16 2] - "nx_date" / Int16un[6], # [1 6] - "multipurpose_field" / Int8un[20][3], # [20 3] + "physician" / Int8un[64][2], # [64 2] ??? + "zip_code" / Int8un[12], # how does this make sense. + "addr" / Int8un[48][2], # [48 2] + "phones" / Int8un[16][2], # [16 2] + "nx_date" / Int16un[6], # [1 6] + "multipurpose_field" / Int8un[20][3], # [20 3] "descp" / Int8un[64], "reserved" / Int8un[32], ) @@ -212,7 +225,7 @@ "size" / Int32un, ) -# TODO This chunk still needs work. +# TODO This chunk still needs work. # Total [3] Int16un param_obs_02_header = Struct( "camera_model" / PaddedString(12, "utf16"), @@ -302,7 +315,7 @@ "unlabeled_2" / Int8un, "w" / Int32un, "n_size" / Int32un, - "aligndata" / Array(this.w * 2, Int16un), # if n_size > 0 + "aligndata" / Array(this.w * 2, Int16un), # if n_size > 0 # if nblockbytes - (10+n_size) >= 16 "keyframe_1" / Int32un, "keyframe_2" / Int32un, @@ -316,7 +329,7 @@ "file_version_2" / Int16un, "file_version_3" / Int16un, "file_version_4" / Int16un, - "string" / PaddedString(128, "ascii") + "string" / PaddedString(128, "ascii"), ) fast_q2_info_header = Struct("various_quality_statistics" / Float32n[6]) From 34ac1c8d1183f81ba1f0d4c094dc755342e0fe30 Mon Sep 17 00:00:00 2001 From: Susannah Trevino Date: Fri, 18 Aug 2023 15:55:12 -0500 Subject: [PATCH 13/25] Docstrings and linting --- oct_converter/dicom/fda_meta.py | 89 +++++++++++++++++++++++++++++---- oct_converter/dicom/fds_meta.py | 83 +++++++++++++++++++++++++++--- 2 files changed, 153 insertions(+), 19 deletions(-) diff --git a/oct_converter/dicom/fda_meta.py b/oct_converter/dicom/fda_meta.py index fb3a4bb..b80e354 100644 --- a/oct_converter/dicom/fda_meta.py +++ b/oct_converter/dicom/fda_meta.py @@ -1,10 +1,30 @@ -from oct_converter.dicom.metadata import * from datetime import datetime +from oct_converter.dicom.metadata import ( + DicomMetadata, + ImageGeometry, + ManufacturerMeta, + OCTDetectorType, + OCTImageParams, + OPTAcquisitionDevice, + OPTAnatomyStructure, + PatientMeta, + SeriesMeta, +) from oct_converter.image_types import OCTVolumeWithMetaData -def fda_patient_meta(fds_metadata: dict) -> PatientMeta: - patient_info = fds_metadata.get("patient_info_02") or fds_metadata.get("patient_info", {}) + +def fda_patient_meta(fda_metadata: dict) -> PatientMeta: + """Creates PatientMeta from FDA metadata + + Args: + fda_metadata: Nested dictionary of metadata collected by the fda reader + Returns: + PatientMeta: Patient metadata populated by fda_metadata + """ + patient_info = fda_metadata.get("patient_info_02") or fda_metadata.get( + "patient_info", {} + ) sex_map = {1: "M", 2: "F", 3: "O", None: ""} patient = PatientMeta() @@ -12,38 +32,71 @@ def fda_patient_meta(fds_metadata: dict) -> PatientMeta: patient.last_name = patient_info.get("last_name") patient.patient_id = patient_info.get("patient_id") patient.patient_sex = sex_map[patient_info.get("sex", None)] - patient.patient_dob = datetime(*patient_info.get("birth_date")) if patient_info.get('birth_date')[0] != 0 else None + patient.patient_dob = ( + datetime(*patient_info.get("birth_date")) + if patient_info.get("birth_date")[0] != 0 + else None + ) return patient -def fda_series_meta(fds_metadata: dict) -> SeriesMeta: - capture_info = fds_metadata.get("capture_info_02") or fds_metadata.get("capture_info", {}) +def fda_series_meta(fda_metadata: dict) -> SeriesMeta: + """Creates SeriesMeta from FDA metadata + + Args: + fda_metadata: Nested dictionary of metadata collected by the fda reader + Returns: + SeriesMeta: Series metadata populated by fda_metadata + """ + capture_info = fda_metadata.get("capture_info_02") or fda_metadata.get( + "capture_info", {} + ) lat_map = {0: "R", 1: "L", None: ""} series = SeriesMeta() series.study_id = "" series.series_id = capture_info.get("session_id", "") series.laterality = lat_map[capture_info.get("eye", None)] - series.acquisition_date = datetime(*capture_info.get('cap_date')) + series.acquisition_date = datetime(*capture_info.get("cap_date")) series.opt_anatomy = OPTAnatomyStructure.Retina return series -def fda_manu_meta(fds_metadata: dict, fds_header: dict) -> ManufacturerMeta: - hw_info = fds_metadata.get("hw_info_03") or fds_metadata.get("hw_info_02") or fds_metadata.get("hw_info_01", {}) +def fda_manu_meta(fda_metadata: dict, fda_header: dict) -> ManufacturerMeta: + """Creates ManufacturerMeta from FDA metadata + + Args: + fda_metadata: Nested dictionary of metadata collected by the fda reader + Returns: + ManufacturerMeta: Manufacture metadata populated by fda_metadata + """ + hw_info = ( + fda_metadata.get("hw_info_03") + or fda_metadata.get("hw_info_02") + or fda_metadata.get("hw_info_01", {}) + ) manufacture = ManufacturerMeta() manufacture.manufacturer = "Topcon" manufacture.manufacturer_model = hw_info.get("model_name") manufacture.device_serial = hw_info.get("serial_number") - manufacture.software_version = f"{fds_header.get('major_ver')}.{fds_header.get('minor_ver')}" + manufacture.software_version = ( + f"{fda_header.get('major_ver')}.{fda_header.get('minor_ver')}" + ) return manufacture def fda_image_geom(pixel_spacing: list) -> ImageGeometry: + """Creates ImageGeometry from FDA metadata + + Args: + pixel_spacing: Pixel spacing calculated in the fda reader + Returns: + ImageGeometry: Geometry data populated by pixel_spacing + """ image_geom = ImageGeometry() image_geom.pixel_spacing = [pixel_spacing[2], pixel_spacing[0]] image_geom.slice_thickness = pixel_spacing[1] @@ -53,6 +106,13 @@ def fda_image_geom(pixel_spacing: list) -> ImageGeometry: def fda_image_params() -> OCTImageParams: + """Creates OCTImageParams specific to Topcon + + Args: + None + Returns: + OCTImageParams: Image params populated with Topcon defaults + """ image_params = OCTImageParams() image_params.opt_acquisition_device = OPTAcquisitionDevice.OCTScanner image_params.DetectorType = OCTDetectorType.CCD @@ -70,6 +130,13 @@ def fda_image_params() -> OCTImageParams: def fda_dicom_metadata(oct: OCTVolumeWithMetaData) -> DicomMetadata: + """Creates DicomMetadata and populates each module + + Args: + oct: OCTVolumeWithMetaData created by the fda reader + Returns: + DicomMetadata: Populated DicomMetadata created with OCT metadata + """ meta = DicomMetadata meta.patient_info = fda_patient_meta(oct.metadata) meta.series_info = fda_series_meta(oct.metadata) @@ -77,4 +144,4 @@ def fda_dicom_metadata(oct: OCTVolumeWithMetaData) -> DicomMetadata: meta.image_geometry = fda_image_geom(oct.pixel_spacing) meta.oct_image_params = fda_image_params() - return meta \ No newline at end of file + return meta diff --git a/oct_converter/dicom/fds_meta.py b/oct_converter/dicom/fds_meta.py index 79f600c..e4222db 100644 --- a/oct_converter/dicom/fds_meta.py +++ b/oct_converter/dicom/fds_meta.py @@ -1,10 +1,30 @@ -from oct_converter.dicom.metadata import * from datetime import datetime +from oct_converter.dicom.metadata import ( + DicomMetadata, + ImageGeometry, + ManufacturerMeta, + OCTDetectorType, + OCTImageParams, + OPTAcquisitionDevice, + OPTAnatomyStructure, + PatientMeta, + SeriesMeta, +) from oct_converter.image_types import OCTVolumeWithMetaData + def fds_patient_meta(fds_metadata: dict) -> PatientMeta: - patient_info = fds_metadata.get("patient_info_02") or fds_metadata.get("patient_info", {}) + """Creates PatientMeta from FDS metadata + + Args: + fds_metadata: Nested dictionary of metadata collected by the fds reader + Returns: + PatientMeta: Patient metadata populated by fds_metadata + """ + patient_info = fds_metadata.get("patient_info_02") or fds_metadata.get( + "patient_info", {} + ) sex_map = {1: "M", 2: "F", 3: "O", None: ""} patient = PatientMeta() @@ -12,38 +32,71 @@ def fds_patient_meta(fds_metadata: dict) -> PatientMeta: patient.last_name = patient_info.get("last_name") patient.patient_id = patient_info.get("patient_id") patient.patient_sex = sex_map[patient_info.get("sex", None)] - patient.patient_dob = datetime(*patient_info.get("birth_date")) if patient_info.get('birth_date')[0] != 0 else None + patient.patient_dob = ( + datetime(*patient_info.get("birth_date")) + if patient_info.get("birth_date")[0] != 0 + else None + ) return patient def fds_series_meta(fds_metadata: dict) -> SeriesMeta: - capture_info = fds_metadata.get("capture_info_02") or fds_metadata.get("capture_info", {}) + """Creates SeriesMeta from FDS metadata + + Args: + fds_metadata: Nested dictionary of metadata collected by the fds reader + Returns: + SeriesMeta: Series metadata populated by fds_metadata + """ + capture_info = fds_metadata.get("capture_info_02") or fds_metadata.get( + "capture_info", {} + ) lat_map = {0: "R", 1: "L", None: ""} series = SeriesMeta() series.study_id = "" series.series_id = capture_info.get("session_id", "") series.laterality = lat_map[capture_info.get("eye", None)] - series.acquisition_date = datetime(*capture_info.get('cap_date')) + series.acquisition_date = datetime(*capture_info.get("cap_date")) series.opt_anatomy = OPTAnatomyStructure.Retina return series def fds_manu_meta(fds_metadata: dict, fds_header: dict) -> ManufacturerMeta: - hw_info = fds_metadata.get("hw_info_03") or fds_metadata.get("hw_info_02") or fds_metadata.get("hw_info_01", {}) + """Creates ManufacturerMeta from FDS metadata + + Args: + fds_metadata: Nested dictionary of metadata collected by the fds reader + Returns: + ManufacturerMeta: Manufacture metadata populated by fds_metadata + """ + hw_info = ( + fds_metadata.get("hw_info_03") + or fds_metadata.get("hw_info_02") + or fds_metadata.get("hw_info_01", {}) + ) manufacture = ManufacturerMeta() manufacture.manufacturer = "Topcon" manufacture.manufacturer_model = hw_info.get("model_name") manufacture.device_serial = hw_info.get("serial_number") - manufacture.software_version = f"{fds_header.get('major_ver')}.{fds_header.get('minor_ver')}" + manufacture.software_version = ( + f"{fds_header.get('major_ver')}.{fds_header.get('minor_ver')}" + ) return manufacture def fds_image_geom(pixel_spacing: list) -> ImageGeometry: + """Creates ImageGeometry from FDS metadata + + Args: + pixel_spacing: Pixel spacing calculated in the fds reader + Returns: + ImageGeometry: Geometry data populated by pixel_spacing + """ image_geom = ImageGeometry() image_geom.pixel_spacing = [pixel_spacing[2], pixel_spacing[0]] image_geom.slice_thickness = pixel_spacing[1] @@ -53,6 +106,13 @@ def fds_image_geom(pixel_spacing: list) -> ImageGeometry: def fds_image_params() -> OCTImageParams: + """Creates OCTImageParams specific to Topcon + + Args: + None + Returns: + OCTImageParams: Image params populated with Topcon defaults + """ image_params = OCTImageParams() image_params.opt_acquisition_device = OPTAcquisitionDevice.OCTScanner image_params.DetectorType = OCTDetectorType.CCD @@ -70,6 +130,13 @@ def fds_image_params() -> OCTImageParams: def fds_dicom_metadata(oct: OCTVolumeWithMetaData) -> DicomMetadata: + """Creates DicomMetadata and populates each module + + Args: + oct: OCTVolumeWithMetaData created by the fds reader + Returns: + DicomMetadata: Populated DicomMetadata created with OCT metadata + """ meta = DicomMetadata meta.patient_info = fds_patient_meta(oct.metadata) meta.series_info = fds_series_meta(oct.metadata) @@ -77,4 +144,4 @@ def fds_dicom_metadata(oct: OCTVolumeWithMetaData) -> DicomMetadata: meta.image_geometry = fds_image_geom(oct.pixel_spacing) meta.oct_image_params = fds_image_params() - return meta \ No newline at end of file + return meta From 2b0977f085f8d288969d382a1ae428202396e5b3 Mon Sep 17 00:00:00 2001 From: Susannah Trevino Date: Mon, 21 Aug 2023 08:37:33 -0500 Subject: [PATCH 14/25] Fixing bug created by linting --- oct_converter/readers/binary_structs/fda_binary.py | 7 +++---- oct_converter/readers/binary_structs/fds_binary.py | 7 +++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/oct_converter/readers/binary_structs/fda_binary.py b/oct_converter/readers/binary_structs/fda_binary.py index 2b309f0..de3c4d7 100644 --- a/oct_converter/readers/binary_structs/fda_binary.py +++ b/oct_converter/readers/binary_structs/fda_binary.py @@ -51,10 +51,9 @@ header = Struct( "file_code" / PaddedString(4, "ascii"), # Always "FOCT" - "file_type" - / PaddedString( - 3, "ascii" - ), # "FDA" or "FAA", denoting "macula" or "external" fixation + "file_type" / PaddedString(3, "ascii"), + # file_type is "FDA" or "FAA", + # denoting "macula" or "external" fixation "major_ver" / Int32un, "minor_ver" / Int32un, ) diff --git a/oct_converter/readers/binary_structs/fds_binary.py b/oct_converter/readers/binary_structs/fds_binary.py index 7e34ce8..d3f317a 100644 --- a/oct_converter/readers/binary_structs/fds_binary.py +++ b/oct_converter/readers/binary_structs/fds_binary.py @@ -52,10 +52,9 @@ header = Struct( "file_code" / PaddedString(4, "ascii"), # Always "FOCT" - "file_type" - / PaddedString( - 3, "ascii" - ), # "FDA" or "FAA", denoting "macula" or "external" fixation + "file_type" / PaddedString(3, "ascii"), + # file_type is "FDA" or "FAA", + # denoting "macula" or "external" fixation "major_ver" / Int32un, "minor_ver" / Int32un, ) From ed04df36644c7503f00efef1afcfdcbbc7025de0 Mon Sep 17 00:00:00 2001 From: Susannah Trevino Date: Mon, 21 Aug 2023 10:59:02 -0500 Subject: [PATCH 15/25] Add args for output file handling --- oct_converter/dicom/dicom.py | 38 +++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/oct_converter/dicom/dicom.py b/oct_converter/dicom/dicom.py index 0922707..ae1678d 100644 --- a/oct_converter/dicom/dicom.py +++ b/oct_converter/dicom/dicom.py @@ -1,3 +1,4 @@ +import os import shutil import tempfile import typing as t @@ -241,15 +242,24 @@ def write_opt_dicom(meta: DicomMetadata, frames: t.List[np.ndarray]) -> Path: return file_ -def create_dicom_from_oct(input_file: str, output_dest: str = None) -> Path: +def create_dicom_from_oct( + input_file: str, + output_dir: str = None, + output_filename: str = None +) -> Path: """Creates a DICOM file with the data parsed from the input file. Args: - input_file: File with OCT data - (Currently only Topcon files supported) - output_dest: Output directory OR output path - i.e. `"/tmp"` or `"/tmp/filename.dcm"` + input_file: File with OCT data (Currently only Topcon + files supported) + output_dir: Output directory, will be created if + not currently exists. Default None places file in + current working directory. + output_filename: Name to save the file under, i.e. + `filename.dcm`. Default None saves the file under + the input filename (if input_file = `test.fds`, + output_filename = `test.dcm`) Returns: Path to DICOM file @@ -268,7 +278,17 @@ def create_dicom_from_oct(input_file: str, output_dest: str = None) -> Path: else: raise TypeError("Invalid input_file type.") file = write_opt_dicom(meta, oct.volume) - if output_dest: - dst = shutil.move(file, output_dest) - return dst - return file + + # This could be broken into a separate function + if output_dir: + if not os.path.exists(output_dir): + os.makedirs(output_dir, exist_ok=True) + else: + output_dir = os.getcwd() + if output_filename: + dst = shutil.move(file, f"{output_dir}/{output_filename}") + else: + base = os.path.basename(input_file) + base = base.removesuffix(f".{file_suffix}") + dst = shutil.move(file, f"{output_dir}/{base}.dcm") + return dst From cb1a7dd8f3bf8a48e6a108ba4776620c61038880 Mon Sep 17 00:00:00 2001 From: Susannah Trevino Date: Mon, 21 Aug 2023 10:59:43 -0500 Subject: [PATCH 16/25] Add example for topcon to dicom conversion --- examples/demo_fda_extraction.py | 8 ++++++++ examples/demo_fds_extraction.py | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/examples/demo_fda_extraction.py b/examples/demo_fda_extraction.py index ca75317..ecd3d50 100644 --- a/examples/demo_fda_extraction.py +++ b/examples/demo_fda_extraction.py @@ -1,6 +1,7 @@ import json from oct_converter.readers import FDA +from oct_converter.dicom.dicom import create_dicom_from_oct # a sample .fda file can be downloaded from the Biobank resource here: # https://biobank.ndph.ox.ac.uk/showcase/refer.cgi?id=31 @@ -32,3 +33,10 @@ metadata = fda.read_all_metadata() with open("metadata.json", "w") as outfile: outfile.write(json.dumps(metadata, indent=4)) + +# create a DICOM from FDA +dcm = create_dicom_from_oct(filepath) +# Output dir can be specified, otherwise will +# default to current working directory. +# Output filename can be specified, otherwise +# will default to the input filename. diff --git a/examples/demo_fds_extraction.py b/examples/demo_fds_extraction.py index 7f89872..90e8849 100644 --- a/examples/demo_fds_extraction.py +++ b/examples/demo_fds_extraction.py @@ -1,6 +1,7 @@ import json from oct_converter.readers import FDS +from oct_converter.dicom.dicom import create_dicom_from_oct # An example .fds file can be downloaded from the Biobank website: # https://biobank.ndph.ox.ac.uk/showcase/refer.cgi?id=30 @@ -26,3 +27,10 @@ metadata = fds.read_all_metadata(verbose=True) with open("fds_metadata.json", "w") as outfile: outfile.write(json.dumps(metadata, indent=4)) + +# create a DICOM from FDS +dcm = create_dicom_from_oct(filepath) +# Output dir can be specified, otherwise will +# default to current working directory. +# Output filename can be specified, otherwise +# will default to the input filename. From 2af7e4f355f26d93551d73e1d13ba8b4954fcff6 Mon Sep 17 00:00:00 2001 From: Susannah Trevino Date: Mon, 21 Aug 2023 11:04:13 -0500 Subject: [PATCH 17/25] Linting --- examples/demo_fda_extraction.py | 2 +- examples/demo_fds_extraction.py | 2 +- oct_converter/dicom/dicom.py | 6 ++-- .../readers/binary_structs/fda_binary.py | 2 +- .../readers/binary_structs/fds_binary.py | 2 +- oct_converter/readers/fda.py | 29 ++++++++++++------- oct_converter/readers/fds.py | 26 ++++++++++++----- 7 files changed, 43 insertions(+), 26 deletions(-) diff --git a/examples/demo_fda_extraction.py b/examples/demo_fda_extraction.py index ecd3d50..8b785c2 100644 --- a/examples/demo_fda_extraction.py +++ b/examples/demo_fda_extraction.py @@ -1,7 +1,7 @@ import json -from oct_converter.readers import FDA from oct_converter.dicom.dicom import create_dicom_from_oct +from oct_converter.readers import FDA # a sample .fda file can be downloaded from the Biobank resource here: # https://biobank.ndph.ox.ac.uk/showcase/refer.cgi?id=31 diff --git a/examples/demo_fds_extraction.py b/examples/demo_fds_extraction.py index 90e8849..c2022c9 100644 --- a/examples/demo_fds_extraction.py +++ b/examples/demo_fds_extraction.py @@ -1,7 +1,7 @@ import json -from oct_converter.readers import FDS from oct_converter.dicom.dicom import create_dicom_from_oct +from oct_converter.readers import FDS # An example .fds file can be downloaded from the Biobank website: # https://biobank.ndph.ox.ac.uk/showcase/refer.cgi?id=30 diff --git a/oct_converter/dicom/dicom.py b/oct_converter/dicom/dicom.py index ae1678d..2353ea9 100644 --- a/oct_converter/dicom/dicom.py +++ b/oct_converter/dicom/dicom.py @@ -243,9 +243,7 @@ def write_opt_dicom(meta: DicomMetadata, frames: t.List[np.ndarray]) -> Path: def create_dicom_from_oct( - input_file: str, - output_dir: str = None, - output_filename: str = None + input_file: str, output_dir: str = None, output_filename: str = None ) -> Path: """Creates a DICOM file with the data parsed from the input file. @@ -256,7 +254,7 @@ def create_dicom_from_oct( output_dir: Output directory, will be created if not currently exists. Default None places file in current working directory. - output_filename: Name to save the file under, i.e. + output_filename: Name to save the file under, i.e. `filename.dcm`. Default None saves the file under the input filename (if input_file = `test.fds`, output_filename = `test.dcm`) diff --git a/oct_converter/readers/binary_structs/fda_binary.py b/oct_converter/readers/binary_structs/fda_binary.py index de3c4d7..9c0aeab 100644 --- a/oct_converter/readers/binary_structs/fda_binary.py +++ b/oct_converter/readers/binary_structs/fda_binary.py @@ -51,7 +51,7 @@ header = Struct( "file_code" / PaddedString(4, "ascii"), # Always "FOCT" - "file_type" / PaddedString(3, "ascii"), + "file_type" / PaddedString(3, "ascii"), # file_type is "FDA" or "FAA", # denoting "macula" or "external" fixation "major_ver" / Int32un, diff --git a/oct_converter/readers/binary_structs/fds_binary.py b/oct_converter/readers/binary_structs/fds_binary.py index d3f317a..6c2aaae 100644 --- a/oct_converter/readers/binary_structs/fds_binary.py +++ b/oct_converter/readers/binary_structs/fds_binary.py @@ -52,7 +52,7 @@ header = Struct( "file_code" / PaddedString(4, "ascii"), # Always "FOCT" - "file_type" / PaddedString(3, "ascii"), + "file_type" / PaddedString(3, "ascii"), # file_type is "FDA" or "FAA", # denoting "macula" or "external" fixation "major_ver" / Int32un, diff --git a/oct_converter/readers/fda.py b/oct_converter/readers/fda.py index a497634..80b843f 100644 --- a/oct_converter/readers/fda.py +++ b/oct_converter/readers/fda.py @@ -2,11 +2,11 @@ import io import struct +from datetime import datetime from pathlib import Path import numpy as np from construct import ListContainer -from datetime import datetime from PIL import Image from oct_converter.image_types import FundusImageWithMetaData, OCTVolumeWithMetaData @@ -93,7 +93,7 @@ def read_oct_volume(self) -> OCTVolumeWithMetaData: raw_slice = f.read(size) image = Image.open(io.BytesIO(raw_slice)) volume.append(np.asarray(image)) - + chunk_loc, chunk_size = self.chunk_dict.get(b"@PARAM_SCAN_04", (None, None)) pixel_spacing = None if chunk_loc: @@ -111,12 +111,15 @@ def read_oct_volume(self) -> OCTVolumeWithMetaData: scan_params.y_dimension_mm / oct_header.width, # Depth ] - # Other code uses the following, listed as + # Other code uses the following, listed as # WidthPixelS, FramePixelS, and zHeightPixelS pixel_spacing_2 = [ - scan_params.get("x_dimension_mm") / oct_header.width, # WidthPixelS, PixelSpacing[1] - scan_params.get("y_dimension_mm") / oct_header.number_slices, # FramePixelS / SliceThickness - scan_params.get("z_resolution_um") / 1000, # zHeightPixelS, PixelSpacing[0] + scan_params.get("x_dimension_mm") + / oct_header.width, # WidthPixelS, PixelSpacing[1] + scan_params.get("y_dimension_mm") + / oct_header.number_slices, # FramePixelS / SliceThickness + scan_params.get("z_resolution_um") + / 1000, # zHeightPixelS, PixelSpacing[0] ] # read segmentation contours if possible and store them as distance @@ -127,8 +130,12 @@ def read_oct_volume(self) -> OCTVolumeWithMetaData: # read all other metadata metadata = self.read_all_metadata() - patient_info = metadata.get("patient_info_02") or metadata.get("patient_info", {}) - capture_info = metadata.get("capture_info_02") or metadata.get("capture_info", {}) + patient_info = metadata.get("patient_info_02") or metadata.get( + "patient_info", {} + ) + capture_info = metadata.get("capture_info_02") or metadata.get( + "capture_info", {} + ) sex_map = {1: "M", 2: "F", 3: "O", None: ""} lat_map = {0: "R", 1: "L", None: ""} @@ -138,7 +145,9 @@ def read_oct_volume(self) -> OCTVolumeWithMetaData: first_name=patient_info.get("first_name"), surname=patient_info.get("last_name"), sex=sex_map[patient_info.get("sex", None)], - patient_dob=datetime(*patient_info.get("birth_date")) if patient_info.get("birth_date")[0] != 0 else None, + patient_dob=datetime(*patient_info.get("birth_date")) + if patient_info.get("birth_date")[0] != 0 + else None, acquisition_date=datetime(*capture_info.get("cap_date")), laterality=lat_map[capture_info.get("eye", None)], contours=contours, @@ -146,7 +155,7 @@ def read_oct_volume(self) -> OCTVolumeWithMetaData: metadata=metadata, header=self.header, oct_header=dict(oct_header), - ) + ) return oct_volume def read_oct_volume_2(self) -> OCTVolumeWithMetaData: diff --git a/oct_converter/readers/fds.py b/oct_converter/readers/fds.py index 4f22916..98359bd 100644 --- a/oct_converter/readers/fds.py +++ b/oct_converter/readers/fds.py @@ -1,7 +1,8 @@ from __future__ import annotations -from pathlib import Path from datetime import datetime +from pathlib import Path + import numpy as np from construct import ListContainer @@ -97,17 +98,24 @@ def read_oct_volume(self) -> OCTVolumeWithMetaData: scan_params.y_dimension_mm / oct_header.width, # Depth ] - # Other code uses the following, listed as + # Other code uses the following, listed as # WidthPixelS, FramePixelS, and zHeightPixelS pixel_spacing_2 = [ - scan_params.get("x_dimension_mm") / oct_header.width, # WidthPixelS, PixelSpacing[1] - scan_params.get("y_dimension_mm") / oct_header.number_slices, # FramePixelS / SliceThickness - scan_params.get("z_resolution_um") / 1000, # zHeightPixelS, PixelSpacing[0] + scan_params.get("x_dimension_mm") + / oct_header.width, # WidthPixelS, PixelSpacing[1] + scan_params.get("y_dimension_mm") + / oct_header.number_slices, # FramePixelS / SliceThickness + scan_params.get("z_resolution_um") + / 1000, # zHeightPixelS, PixelSpacing[0] ] # read all other metadata metadata = self.read_all_metadata() - patient_info = metadata.get("patient_info_02") or metadata.get("patient_info", {}) - capture_info = metadata.get("capture_info_02") or metadata.get("capture_info", {}) + patient_info = metadata.get("patient_info_02") or metadata.get( + "patient_info", {} + ) + capture_info = metadata.get("capture_info_02") or metadata.get( + "capture_info", {} + ) sex_map = {1: "M", 2: "F", 3: "O", None: ""} lat_map = {0: "R", 1: "L", None: ""} @@ -117,7 +125,9 @@ def read_oct_volume(self) -> OCTVolumeWithMetaData: first_name=patient_info.get("first_name"), surname=patient_info.get("last_name"), sex=sex_map[patient_info.get("sex", None)], - patient_dob=datetime(*patient_info.get("birth_date")) if patient_info.get("birth_date")[0] != 0 else None, + patient_dob=datetime(*patient_info.get("birth_date")) + if patient_info.get("birth_date")[0] != 0 + else None, acquisition_date=datetime(*capture_info.get("cap_date")), laterality=lat_map[capture_info.get("eye", None)], pixel_spacing=pixel_spacing_2, From 5f2f7d5b0ebaf884dbb907e758be09c8bd32de53 Mon Sep 17 00:00:00 2001 From: Susannah Trevino Date: Mon, 21 Aug 2023 13:53:14 -0500 Subject: [PATCH 18/25] Update oct_header and param_scan parsing --- oct_converter/readers/fda.py | 174 ++++++++++++++++++++--------------- 1 file changed, 101 insertions(+), 73 deletions(-) diff --git a/oct_converter/readers/fda.py b/oct_converter/readers/fda.py index 80b843f..64dd216 100644 --- a/oct_converter/readers/fda.py +++ b/oct_converter/readers/fda.py @@ -2,6 +2,7 @@ import io import struct +import typing as t from datetime import datetime from pathlib import Path @@ -21,24 +22,25 @@ class FDA(object): chunk_dict: names of data chunks present in the file, and their start locations. """ - def __init__(self, filepath: str | Path, printing: bool = True) -> None: + def __init__(self, filepath: str | Path, printing: bool = False) -> None: self.filepath = Path(filepath) if not self.filepath.exists(): raise FileNotFoundError(self.filepath) self.chunk_dict, self.header = self.get_list_of_file_chunks(printing=printing) - def get_list_of_file_chunks(self, printing: bool = True) -> dict: + def get_list_of_file_chunks(self, printing: bool = False) -> t.Tuple[dict, dict]: """Find all data chunks present in the file. Returns: - dictionary of chunk names, containing their locations in the file and size. + chunk_dict: dictionary of chunk names, containing their locations + in the file and size. + header: dictionary of file header information """ chunk_dict = {} with open(self.filepath, "rb") as f: - # skip header raw = f.read(15) - header = fda_binary.header.parse(raw) + header = dict(fda_binary.header.parse(raw)) eof = False while not eof: @@ -77,56 +79,20 @@ def read_oct_volume(self) -> OCTVolumeWithMetaData: Returns: OCTVolumeWithMetaData """ - - if b"@IMG_JPEG" not in self.chunk_dict: - print("@IMG_JPEG is not in chunk list, skipping.") - return None - with open(self.filepath, "rb") as f: - chunk_location, chunk_size = self.chunk_dict[b"@IMG_JPEG"] - f.seek(chunk_location) # Set the chunk’s current position. - raw = f.read(25) - oct_header = fda_binary.oct_header.parse(raw) - - volume = [] - for i in range(oct_header.number_slices): - size = np.fromstring(f.read(4), dtype=np.int32)[0] - raw_slice = f.read(size) - image = Image.open(io.BytesIO(raw_slice)) - volume.append(np.asarray(image)) - - chunk_loc, chunk_size = self.chunk_dict.get(b"@PARAM_SCAN_04", (None, None)) + # read oct data chunk, whether that's IMG_JPEG or IMG_MOT_COMP_03 + # TODO: could support the other IMG_MOT_COMP variants as well. + volume, oct_header = self.read_oct_data_chunk() + # if oct data is found, calculate pixel spacing. + if oct_header: + pixel_spacing = self.read_scan_params(oct_header) + else: pixel_spacing = None - if chunk_loc: - f.seek(chunk_loc) - scan_params = fda_binary.param_scan_04_header.parse(f.read(chunk_size)) - # NOTE: this will need reordering for dicom pixel spacing and - # image orientation/position patient as well as possibly for nifti - # depending on what x,y,z means here. - - # In either nifti/dicom coordinate systems, the x-y plan in raw space - # corresponds to the x-z plane, just depends which direction. - pixel_spacing = [ - scan_params.x_dimension_mm / oct_header.height, # Left/Right - scan_params.z_resolution_um / 1000, # Up/Down - scan_params.y_dimension_mm / oct_header.width, # Depth - ] - - # Other code uses the following, listed as - # WidthPixelS, FramePixelS, and zHeightPixelS - pixel_spacing_2 = [ - scan_params.get("x_dimension_mm") - / oct_header.width, # WidthPixelS, PixelSpacing[1] - scan_params.get("y_dimension_mm") - / oct_header.number_slices, # FramePixelS / SliceThickness - scan_params.get("z_resolution_um") - / 1000, # zHeightPixelS, PixelSpacing[0] - ] # read segmentation contours if possible and store them as distance # from top of scan to be compatible with plotting in OCTVolume contours = self.read_segmentation() if contours: - contours = {k: oct_header.height - v for k, v in contours.items()} + contours = {k: oct_header.get("height") - v for k, v in contours.items()} # read all other metadata metadata = self.read_all_metadata() @@ -151,41 +117,103 @@ def read_oct_volume(self) -> OCTVolumeWithMetaData: acquisition_date=datetime(*capture_info.get("cap_date")), laterality=lat_map[capture_info.get("eye", None)], contours=contours, - pixel_spacing=pixel_spacing_2, + pixel_spacing=pixel_spacing, metadata=metadata, header=self.header, - oct_header=dict(oct_header), + oct_header=oct_header, ) return oct_volume - def read_oct_volume_2(self) -> OCTVolumeWithMetaData: - """Reads OCT data. Worth trying if read_oct_volume fails. + def read_oct_data_chunk(self) -> t.Tuple[np.ndarray, dict]: + """Given available chunks, identifies which chunk to utilize + as the primary OCT data and returns the volume. Returns: - OCTVolumeWithMetaData + volume: OCT volume as array + oct_header: OCT header as dict """ + if b"@IMG_JPEG" in self.chunk_dict: + with open(self.filepath, "rb") as f: + chunk_location, chunk_size = self.chunk_dict[b"@IMG_JPEG"] + f.seek(chunk_location) # Set the chunk’s current position. + raw = f.read(25) + oct_header = fda_binary.oct_header.parse(raw) + + volume = [] + for i in range(oct_header.number_slices): + size = np.fromstring(f.read(4), dtype=np.int32)[0] + raw_slice = f.read(size) + image = Image.open(io.BytesIO(raw_slice)) + volume.append(np.asarray(image)) + return volume, dict(oct_header) + + elif b"@IMG_MOT_COMP_03" in self.chunk_dict: + with open(self.filepath, "rb") as f: + chunk_location, chunk_size = self.chunk_dict[b"@IMG_MOT_COMP_03"] + f.seek(chunk_location) # Set the chunk’s current position. + raw = f.read(22) + oct_header = fda_binary.oct_header_2.parse(raw) + number_pixels = ( + oct_header.width * oct_header.height * oct_header.number_slices + ) + raw_volume = np.fromstring(f.read(number_pixels * 2), dtype=np.uint16) + volume = np.array(raw_volume) + volume = volume.reshape( + oct_header.width, + oct_header.height, + oct_header.number_slices, + order="F", + ) + volume = np.transpose(volume, [1, 0, 2]) + adjusted_volume = [volume[:, :, i] for i in range(volume.shape[2])] + return adjusted_volume, dict(oct_header) + + else: + print( + "Neither @IMG_JPEG nor @IMG_MOT_COMP_03 found. OCT volume not identified." + ) + return None, None + + def read_scan_params(self, oct_header: dict) -> list: + """Given available chunks, identifies available PARAM_SCAN chunk + and calculates pixel spacing. + + Args: + oct_header: OCT header information as dict - if b"@IMG_MOT_COMP_03" not in self.chunk_dict: - print("@IMG_MOT_COMP_03 is not in chunk list, skipping.") + Returns: + pixel_spacing: list of pixel spacing, ordered by width, slice thickness, height. + """ + if ( + b"@PARAM_SCAN_04" not in self.chunk_dict + and b"@PARAM_SCAN_02" not in self.chunk_dict + ): + print( + "Neither @PARAM_SCAN_04 nor @PARAM_SCAN_02 found. Pixel spacing not calculated." + ) return None with open(self.filepath, "rb") as f: - chunk_location, chunk_size = self.chunk_dict[b"@IMG_MOT_COMP_03"] - f.seek(chunk_location) # Set the chunk’s current position. - raw = f.read(22) - oct_header = fda_binary.oct_header_2.parse(raw) - number_pixels = ( - oct_header.width * oct_header.height * oct_header.number_slices - ) - raw_volume = np.fromstring(f.read(number_pixels * 2), dtype=np.uint16) - volume = np.array(raw_volume) - volume = volume.reshape( - oct_header.width, oct_header.height, oct_header.number_slices, order="F" - ) - volume = np.transpose(volume, [1, 0, 2]) - oct_volume = OCTVolumeWithMetaData( - [volume[:, :, i] for i in range(volume.shape[2])] - ) - return oct_volume + if b"@PARAM_SCAN_04" in self.chunk_dict: + chunk_loc, chunk_size = self.chunk_dict.get( + b"@PARAM_SCAN_04", (None, None) + ) + f.seek(chunk_loc) + scan_params = fda_binary.param_scan_04_header.parse(f.read(chunk_size)) + elif b"@PARAM_SCAN_02" in self.chunk_dict: + chunk_loc, chunk_size = self.chunk_dict.get( + b"@PARAM_SCAN_02", (None, None) + ) + f.seek(chunk_loc) + scan_params = fda_binary.param_scan_02_header.parse(f.read(chunk_size)) + pixel_spacing = [ + scan_params.get("x_dimension_mm") + / oct_header.get("width"), # WidthPixelS, PixelSpacing[1] + scan_params.get("y_dimension_mm") + / oct_header.get("number_slices"), # FramePixelS / SliceThickness + scan_params.get("z_resolution_um") + / 1000, # zHeightPixelS, PixelSpacing[0] + ] + return pixel_spacing def read_fundus_image(self) -> FundusImageWithMetaData: """Reads fundus image. From 4f235288bddcdada40291eff8ca677104c93b71f Mon Sep 17 00:00:00 2001 From: Susannah Trevino Date: Mon, 21 Aug 2023 13:53:42 -0500 Subject: [PATCH 19/25] Update param_scan parsing --- oct_converter/readers/fds.py | 86 ++++++++++++++++++++++-------------- 1 file changed, 54 insertions(+), 32 deletions(-) diff --git a/oct_converter/readers/fds.py b/oct_converter/readers/fds.py index 98359bd..04a760b 100644 --- a/oct_converter/readers/fds.py +++ b/oct_converter/readers/fds.py @@ -1,5 +1,6 @@ from __future__ import annotations +import typing as t from datetime import datetime from pathlib import Path @@ -29,17 +30,19 @@ def __init__(self, filepath: str | Path) -> None: self.chunk_dict, self.header = self.get_list_of_file_chunks() - def get_list_of_file_chunks(self, printing: bool = True) -> dict: + def get_list_of_file_chunks(self, printing: bool = False) -> t.Tuple[dict, dict]: """Find all data chunks present in the file. Returns: - dictionary of chunk names, containing their locations in the file and size. + chunk_dict: dictionary of chunk names, containing their locations + in the file and size. + header: dictionary of file header information + """ chunk_dict = {} with open(self.filepath, "rb") as f: - # skip header raw = f.read(15) - header = fds_binary.header.parse(raw) + header = dict(fds_binary.header.parse(raw)) eof = False while not eof: @@ -65,6 +68,7 @@ def read_oct_volume(self) -> OCTVolumeWithMetaData: Returns: OCTVolumeWithMetaData """ + # TODO: could support the other IMG_SCAN variants as well. if b"@IMG_SCAN_03" not in self.chunk_dict: raise ValueError("Could not find OCT header @IMG_SCAN_03 in chunk list") with open(self.filepath, "rb") as f: @@ -81,33 +85,10 @@ def read_oct_volume(self) -> OCTVolumeWithMetaData: oct_header.width, oct_header.height, oct_header.number_slices, order="F" ) volume = np.transpose(volume, [1, 0, 2]) - chunk_loc, chunk_size = self.chunk_dict.get(b"@PARAM_SCAN_04", (None, None)) - pixel_spacing = None - if chunk_loc: - f.seek(chunk_loc) - scan_params = fds_binary.param_scan_04_header.parse(f.read(chunk_size)) - # NOTE: this will need reordering for dicom pixel spacing and - # image orientation/position patient as well as possibly for nifti - # depending on what x,y,z means here. - - # In either nifti/dicom coordinate systems, the x-y plan in raw space - # corresponds to the x-z plane, just depends which direction. - pixel_spacing = [ - scan_params.x_dimension_mm / oct_header.height, # Left/Right - scan_params.z_resolution_um / 1000, # Up/Down - scan_params.y_dimension_mm / oct_header.width, # Depth - ] - - # Other code uses the following, listed as - # WidthPixelS, FramePixelS, and zHeightPixelS - pixel_spacing_2 = [ - scan_params.get("x_dimension_mm") - / oct_header.width, # WidthPixelS, PixelSpacing[1] - scan_params.get("y_dimension_mm") - / oct_header.number_slices, # FramePixelS / SliceThickness - scan_params.get("z_resolution_um") - / 1000, # zHeightPixelS, PixelSpacing[0] - ] + + # calculate pixel spacing + pixel_spacing = self.read_scan_params(oct_header) + # read all other metadata metadata = self.read_all_metadata() patient_info = metadata.get("patient_info_02") or metadata.get( @@ -130,7 +111,7 @@ def read_oct_volume(self) -> OCTVolumeWithMetaData: else None, acquisition_date=datetime(*capture_info.get("cap_date")), laterality=lat_map[capture_info.get("eye", None)], - pixel_spacing=pixel_spacing_2, + pixel_spacing=pixel_spacing, metadata=metadata, header=self.header, oct_header=dict(oct_header), @@ -163,6 +144,47 @@ def read_fundus_image(self) -> FundusImageWithMetaData: image = np.flip(image, 2) fundus_image = FundusImageWithMetaData(image) return fundus_image + + def read_scan_params(self, oct_header: dict) -> list: + """Given available chunks, identifies available PARAM_SCAN chunk + and calculates pixel spacing. + + Args: + oct_header: OCT header information as dict + + Returns: + pixel_spacing: list of pixel spacing, ordered by width, slice thickness, height. + """ + if ( + b"@PARAM_SCAN_04" not in self.chunk_dict + and b"@PARAM_SCAN_02" not in self.chunk_dict + ): + print( + "Neither @PARAM_SCAN_04 nor @PARAM_SCAN_02 found. Pixel spacing not calculated." + ) + return None + with open(self.filepath, "rb") as f: + if b"@PARAM_SCAN_04" in self.chunk_dict: + chunk_loc, chunk_size = self.chunk_dict.get( + b"@PARAM_SCAN_04", (None, None) + ) + f.seek(chunk_loc) + scan_params = fds_binary.param_scan_04_header.parse(f.read(chunk_size)) + elif b"@PARAM_SCAN_02" in self.chunk_dict: + chunk_loc, chunk_size = self.chunk_dict.get( + b"@PARAM_SCAN_02", (None, None) + ) + f.seek(chunk_loc) + scan_params = fds_binary.param_scan_02_header.parse(f.read(chunk_size)) + pixel_spacing = [ + scan_params.get("x_dimension_mm") + / oct_header.get("width"), # WidthPixelS, PixelSpacing[1] + scan_params.get("y_dimension_mm") + / oct_header.get("number_slices"), # FramePixelS / SliceThickness + scan_params.get("z_resolution_um") + / 1000, # zHeightPixelS, PixelSpacing[0] + ] + return pixel_spacing def read_all_metadata(self, verbose: bool = False): """ From 146c17975344270ac8c33e75168eb2b7deea38ab Mon Sep 17 00:00:00 2001 From: Susannah Trevino Date: Mon, 21 Aug 2023 13:56:12 -0500 Subject: [PATCH 20/25] yet more linting --- oct_converter/dicom/dicom.py | 2 +- oct_converter/readers/fds.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/oct_converter/dicom/dicom.py b/oct_converter/dicom/dicom.py index 2353ea9..d6e086b 100644 --- a/oct_converter/dicom/dicom.py +++ b/oct_converter/dicom/dicom.py @@ -2,7 +2,7 @@ import shutil import tempfile import typing as t -import uuid +# import uuid from datetime import datetime from pathlib import Path diff --git a/oct_converter/readers/fds.py b/oct_converter/readers/fds.py index 04a760b..874b5de 100644 --- a/oct_converter/readers/fds.py +++ b/oct_converter/readers/fds.py @@ -85,7 +85,7 @@ def read_oct_volume(self) -> OCTVolumeWithMetaData: oct_header.width, oct_header.height, oct_header.number_slices, order="F" ) volume = np.transpose(volume, [1, 0, 2]) - + # calculate pixel spacing pixel_spacing = self.read_scan_params(oct_header) @@ -144,7 +144,7 @@ def read_fundus_image(self) -> FundusImageWithMetaData: image = np.flip(image, 2) fundus_image = FundusImageWithMetaData(image) return fundus_image - + def read_scan_params(self, oct_header: dict) -> list: """Given available chunks, identifies available PARAM_SCAN chunk and calculates pixel spacing. From 57d3afeb35ff86c7318d20476648d9a061ba683e Mon Sep 17 00:00:00 2001 From: Susannah Trevino Date: Thu, 7 Sep 2023 10:31:47 -0500 Subject: [PATCH 21/25] Minor PR update chunk --- examples/demo_fda_extraction.py | 2 +- examples/demo_fds_extraction.py | 2 +- oct_converter/dicom/__init__.py | 2 ++ oct_converter/dicom/dicom.py | 13 ++++++++----- oct_converter/dicom/fda_meta.py | 1 + oct_converter/dicom/fds_meta.py | 1 + 6 files changed, 14 insertions(+), 7 deletions(-) diff --git a/examples/demo_fda_extraction.py b/examples/demo_fda_extraction.py index 8b785c2..73ae9fd 100644 --- a/examples/demo_fda_extraction.py +++ b/examples/demo_fda_extraction.py @@ -1,6 +1,6 @@ import json -from oct_converter.dicom.dicom import create_dicom_from_oct +from oct_converter.dicom import create_dicom_from_oct from oct_converter.readers import FDA # a sample .fda file can be downloaded from the Biobank resource here: diff --git a/examples/demo_fds_extraction.py b/examples/demo_fds_extraction.py index c2022c9..1d57243 100644 --- a/examples/demo_fds_extraction.py +++ b/examples/demo_fds_extraction.py @@ -1,6 +1,6 @@ import json -from oct_converter.dicom.dicom import create_dicom_from_oct +from oct_converter.dicom import create_dicom_from_oct from oct_converter.readers import FDS # An example .fds file can be downloaded from the Biobank website: diff --git a/oct_converter/dicom/__init__.py b/oct_converter/dicom/__init__.py index e7df8de..55c00f6 100644 --- a/oct_converter/dicom/__init__.py +++ b/oct_converter/dicom/__init__.py @@ -3,6 +3,8 @@ from pydicom.uid import generate_uid +from .dicom import create_dicom_from_oct + version = metadata.version("oct_converter") # Deterministic implentation UID based on package name and version diff --git a/oct_converter/dicom/dicom.py b/oct_converter/dicom/dicom.py index d6e086b..9f0b128 100644 --- a/oct_converter/dicom/dicom.py +++ b/oct_converter/dicom/dicom.py @@ -2,7 +2,6 @@ import shutil import tempfile import typing as t -# import uuid from datetime import datetime from pathlib import Path @@ -262,7 +261,7 @@ def create_dicom_from_oct( Returns: Path to DICOM file """ - file_suffix = input_file.split(".")[-1] + file_suffix = input_file.split(".")[-1].lower() if file_suffix == "fds": fds = FDS(input_file) oct = fds.read_oct_volume() @@ -271,10 +270,14 @@ def create_dicom_from_oct( fda = FDA(input_file) oct = fda.read_oct_volume() meta = fda_dicom_metadata(oct) - elif file_suffix in ["e2e", "img", "oct", "OCT"]: - raise NotImplementedError("Filetype not yet supported.") + elif file_suffix in ["e2e", "img", "oct"]: + raise NotImplementedError( + f"DICOM conversion for {file_suffix} is not yet supported. Currently supported filetypes are .fds, .fda." + ) else: - raise TypeError("Invalid input_file type.") + raise TypeError( + f"DICOM conversion for {file_suffix} is not supported. Currently supported filetypes are .fds, .fda." + ) file = write_opt_dicom(meta, oct.volume) # This could be broken into a separate function diff --git a/oct_converter/dicom/fda_meta.py b/oct_converter/dicom/fda_meta.py index b80e354..6c3179c 100644 --- a/oct_converter/dicom/fda_meta.py +++ b/oct_converter/dicom/fda_meta.py @@ -69,6 +69,7 @@ def fda_manu_meta(fda_metadata: dict, fda_header: dict) -> ManufacturerMeta: Args: fda_metadata: Nested dictionary of metadata collected by the fda reader + fda_header: Dictionary of file header information parsed by the fda reader Returns: ManufacturerMeta: Manufacture metadata populated by fda_metadata """ diff --git a/oct_converter/dicom/fds_meta.py b/oct_converter/dicom/fds_meta.py index e4222db..af47f41 100644 --- a/oct_converter/dicom/fds_meta.py +++ b/oct_converter/dicom/fds_meta.py @@ -69,6 +69,7 @@ def fds_manu_meta(fds_metadata: dict, fds_header: dict) -> ManufacturerMeta: Args: fds_metadata: Nested dictionary of metadata collected by the fds reader + fds_header: Dictionary of file header information parsed by the fda reader Returns: ManufacturerMeta: Manufacture metadata populated by fds_metadata """ From be213fd9d2698226c6f7f7f40c117474cdbd8113 Mon Sep 17 00:00:00 2001 From: Susannah Trevino Date: Thu, 7 Sep 2023 11:16:21 -0500 Subject: [PATCH 22/25] RM contour info header from fds --- oct_converter/readers/binary_structs/fds_binary.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/oct_converter/readers/binary_structs/fds_binary.py b/oct_converter/readers/binary_structs/fds_binary.py index 6c2aaae..6c7dbda 100644 --- a/oct_converter/readers/binary_structs/fds_binary.py +++ b/oct_converter/readers/binary_structs/fds_binary.py @@ -41,7 +41,6 @@ regist_scan_02_header (obj:Struct) : Defines REGIST_SCAN_02 header. result_cornea_curve_header (obj:Struct) : Defines RESULT_CORNEA_CURVE header. result_cornea_thickness_header (obj:Struct) : Defines RESULT_CORNEA_THICKNESS header. - contour_info_header (obj:Struct) : Defines CONTOUR_INFO header. align_info_header (obj:Struct) : Defines ALIGN_INFO header. main_module_info_header (obj:Struct) : Defines MAIN_MODULE_INFO header. fast_q2_info_header (obj:Struct) : Defines FAST_Q2_INFO header. @@ -301,14 +300,6 @@ "height" / Int32un, ) -contour_info_header = Struct( - "id" / PaddedString(20, "ascii"), - "type" / Int16un, - "width" / Int32un, - "height" / Int32un, - "size" / Int32un, -) - align_info_header = Struct( "unlabeled_1" / Int8un, "unlabeled_2" / Int8un, From 989408f6deed7c2f31688a96145ec37149c5a02b Mon Sep 17 00:00:00 2001 From: Susannah Trevino Date: Thu, 7 Sep 2023 14:30:33 -0500 Subject: [PATCH 23/25] Updating how dicom is saved --- oct_converter/dicom/__init__.py | 8 ----- oct_converter/dicom/dicom.py | 56 ++++++++++++++++----------------- 2 files changed, 28 insertions(+), 36 deletions(-) diff --git a/oct_converter/dicom/__init__.py b/oct_converter/dicom/__init__.py index 55c00f6..ced45fb 100644 --- a/oct_converter/dicom/__init__.py +++ b/oct_converter/dicom/__init__.py @@ -1,11 +1,3 @@ """Init module.""" -from importlib import metadata - -from pydicom.uid import generate_uid from .dicom import create_dicom_from_oct - -version = metadata.version("oct_converter") - -# Deterministic implentation UID based on package name and version -implementation_uid = generate_uid(entropy_srcs=["oct_converter", version]) diff --git a/oct_converter/dicom/dicom.py b/oct_converter/dicom/dicom.py index 9f0b128..87c895d 100644 --- a/oct_converter/dicom/dicom.py +++ b/oct_converter/dicom/dicom.py @@ -1,8 +1,6 @@ -import os -import shutil -import tempfile import typing as t from datetime import datetime +from importlib import metadata from pathlib import Path import numpy as np @@ -13,25 +11,24 @@ generate_uid, ) -from oct_converter.dicom import implementation_uid from oct_converter.dicom.fda_meta import fda_dicom_metadata from oct_converter.dicom.fds_meta import fds_dicom_metadata from oct_converter.dicom.metadata import DicomMetadata from oct_converter.readers import FDA, FDS +# Deterministic implentation UID based on package name and version +version = metadata.version("oct_converter") +implementation_uid = generate_uid(entropy_srcs=["oct_converter", version]) -def opt_base_dicom() -> t.Tuple[Path, Dataset]: + +def opt_base_dicom(filepath: Path) -> Dataset: """Creates the base dicom to be populated. Args: - None + filepath: Path to where output file is to be saved Returns: - file_: Path to created .dcm, ds: FileDataset with file meta, preamble, and empty dataset """ - suffix = ".dcm" - file_ = Path(tempfile.NamedTemporaryFile(suffix=suffix).name) - # Populate required values for file meta information file_meta = FileMetaDataset() file_meta.MediaStorageSOPClassUID = OphthalmicTomographyImageStorage @@ -40,10 +37,10 @@ def opt_base_dicom() -> t.Tuple[Path, Dataset]: file_meta.TransferSyntaxUID = ExplicitVRLittleEndian # Create the FileDataset instance with file meta, preamble and empty DS - ds = FileDataset(str(file_), {}, file_meta=file_meta, preamble=b"\0" * 128) + ds = FileDataset(str(filepath), {}, file_meta=file_meta, preamble=b"\0" * 128) ds.is_little_endian = True ds.is_implicit_VR = False # Explicit VR - return file_, ds + return ds def populate_patient_info(ds: Dataset, meta: DicomMetadata) -> Dataset: @@ -171,16 +168,19 @@ def opt_shared_functional_groups(ds: Dataset, meta: DicomMetadata) -> Dataset: return ds -def write_opt_dicom(meta: DicomMetadata, frames: t.List[np.ndarray]) -> Path: +def write_opt_dicom( + meta: DicomMetadata, frames: t.List[np.ndarray], filepath: Path +) -> Path: """Writes required DICOM metadata and pixel data to .dcm file. Args: meta: DICOM metadata information frames: list of frames of pixel data + filepath: Path to where output file is being saved Returns: Path to created DICOM file """ - file_, ds = opt_base_dicom() + ds = opt_base_dicom(filepath) ds = populate_patient_info(ds, meta) ds = populate_manufacturer_info(ds, meta) ds = populate_opt_series(ds, meta) @@ -237,8 +237,8 @@ def write_opt_dicom(meta: DicomMetadata, frames: t.List[np.ndarray]) -> Path: per_frame.append(frame_fgs) ds.PerFrameFunctionalGroupsSequence = per_frame ds.PixelData = pixel_data.tobytes() - ds.save_as(file_) - return file_ + ds.save_as(filepath) + return filepath def create_dicom_from_oct( @@ -278,18 +278,18 @@ def create_dicom_from_oct( raise TypeError( f"DICOM conversion for {file_suffix} is not supported. Currently supported filetypes are .fds, .fda." ) - file = write_opt_dicom(meta, oct.volume) - # This could be broken into a separate function if output_dir: - if not os.path.exists(output_dir): - os.makedirs(output_dir, exist_ok=True) - else: - output_dir = os.getcwd() - if output_filename: - dst = shutil.move(file, f"{output_dir}/{output_filename}") + output_dir = Path(output_dir) + output_dir.mkdir(parents=True) else: - base = os.path.basename(input_file) - base = base.removesuffix(f".{file_suffix}") - dst = shutil.move(file, f"{output_dir}/{base}.dcm") - return dst + output_dir = Path.cwd() + + if not output_filename: + output_filename = Path(input_file).stem + ".dcm" + + filepath = Path(output_dir, output_filename) + + file = write_opt_dicom(meta, oct.volume, filepath) + + return file From c0ed0032287c3d164061159cfc2208c39acf7ec1 Mon Sep 17 00:00:00 2001 From: Susannah Trevino Date: Fri, 8 Sep 2023 08:15:30 -0500 Subject: [PATCH 24/25] Add exist_ok=True to mkdir --- oct_converter/dicom/dicom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oct_converter/dicom/dicom.py b/oct_converter/dicom/dicom.py index 87c895d..58965bb 100644 --- a/oct_converter/dicom/dicom.py +++ b/oct_converter/dicom/dicom.py @@ -281,7 +281,7 @@ def create_dicom_from_oct( if output_dir: output_dir = Path(output_dir) - output_dir.mkdir(parents=True) + output_dir.mkdir(parents=True, exist_ok=True) else: output_dir = Path.cwd() From 9e011dff56f308c2a95232696d7853a45f6b79a2 Mon Sep 17 00:00:00 2001 From: Susannah Trevino Date: Fri, 8 Sep 2023 11:40:46 -0500 Subject: [PATCH 25/25] param_obs_02 handling, fix dob handling --- oct_converter/dicom/fda_meta.py | 9 ++--- oct_converter/dicom/fds_meta.py | 9 ++--- .../readers/binary_structs/fda_binary.py | 31 +++++++++----- .../readers/binary_structs/fds_binary.py | 30 +++++++++++--- oct_converter/readers/fda.py | 40 +++++++++++++++++-- oct_converter/readers/fds.py | 40 +++++++++++++++++-- 6 files changed, 127 insertions(+), 32 deletions(-) diff --git a/oct_converter/dicom/fda_meta.py b/oct_converter/dicom/fda_meta.py index 6c3179c..f47a31a 100644 --- a/oct_converter/dicom/fda_meta.py +++ b/oct_converter/dicom/fda_meta.py @@ -32,11 +32,10 @@ def fda_patient_meta(fda_metadata: dict) -> PatientMeta: patient.last_name = patient_info.get("last_name") patient.patient_id = patient_info.get("patient_id") patient.patient_sex = sex_map[patient_info.get("sex", None)] - patient.patient_dob = ( - datetime(*patient_info.get("birth_date")) - if patient_info.get("birth_date")[0] != 0 - else None - ) + try: + patient.patient_dob = datetime(*patient_info.get("birth_date")) + except (TypeError, ValueError): + pass return patient diff --git a/oct_converter/dicom/fds_meta.py b/oct_converter/dicom/fds_meta.py index af47f41..7fa7524 100644 --- a/oct_converter/dicom/fds_meta.py +++ b/oct_converter/dicom/fds_meta.py @@ -32,11 +32,10 @@ def fds_patient_meta(fds_metadata: dict) -> PatientMeta: patient.last_name = patient_info.get("last_name") patient.patient_id = patient_info.get("patient_id") patient.patient_sex = sex_map[patient_info.get("sex", None)] - patient.patient_dob = ( - datetime(*patient_info.get("birth_date")) - if patient_info.get("birth_date")[0] != 0 - else None - ) + try: + patient.patient_dob = datetime(*patient_info.get("birth_date")) + except (TypeError, ValueError): + pass return patient diff --git a/oct_converter/readers/binary_structs/fda_binary.py b/oct_converter/readers/binary_structs/fda_binary.py index 9c0aeab..7cf8592 100644 --- a/oct_converter/readers/binary_structs/fda_binary.py +++ b/oct_converter/readers/binary_structs/fda_binary.py @@ -33,7 +33,8 @@ param_scan_02_header (obj:Struct) : Defines PARAM_SCAN_02 header. img_trc_02_header (obj:Struct) : Defines IMG_TRC_02 header (Fundus grayscale). img_trc_header (obj:Struct) : Defines IMG_TRC header. - param_obs_02_header (obj:Struct) : Defines PARAM_OBS_02 header. + param_obs_02_header (obj:Struct) : Defines PARAM_OBS_02 when size is 90. + param_obs_02_short_header (obj:Struct) : Defines PARAM_OBS_02 when size is 6. img_projection_header (obj:Struct) : Defines IMG_PROJECTION header. img_mot_comp_03_header (obj:Struct) : Defines IMG_MOT_COMP_03 header (Duplicate of oct_header_2) effective_scan_range_header (obj:Struct) : Defines EFFECTIVE_SCAN_RANGE header. @@ -246,17 +247,27 @@ "size" / Int32un, ) +# param_obs_02 has been found to be 90 or 6. +# This first struct handles 90, next handles 6. +# The first "0x1" seems to indicate which type +# of header to expect (0: long, 1: short) param_obs_02_header = Struct( - "camera_model" / PaddedString(12, "utf16"), - "jpeg_quality" / Int8un, - "color_temparature" / Int8un, + "0x1" / Int16un, + "0xffff" / Int16un[2], + "camera_model" / PaddedString(12, "ascii"), + "image_quality" / PaddedString(24, "ascii"), + "0x300" / Int16un, + "0x1" / Int16un, + "0x0" / Int16un, + "color_temp" / PaddedString(24, "ascii"), + "0x2014" / Int16un, + "zeros" / Int8un[12], +) + +param_obs_02_short_header = Struct( + "0x1" / Int16un, + "0xffff" / Int16un[2], ) -# The above might instead be... -# param_obs_02_header = Struct( -# "ph_mode" / Int8un, -# "ph_angle" / Int8un, -# "ph_light_level" / Int16un, -# ) # This is the same as oct_header_02, just called # by its actual chunk name diff --git a/oct_converter/readers/binary_structs/fds_binary.py b/oct_converter/readers/binary_structs/fds_binary.py index 6c7dbda..ffe253d 100644 --- a/oct_converter/readers/binary_structs/fds_binary.py +++ b/oct_converter/readers/binary_structs/fds_binary.py @@ -30,7 +30,8 @@ capture_info_header (obj:Struct) : Defines CAPTURE_INFO header. img_trc_02_header (obj:Struct) : Defines IMG_TRC_02 header. img_trc_header (obj:Struct) : Defines IMG_TRC header. - param_obs_02_header (obj:Struct) : Defines PARAM_OBS_02 header. + param_obs_02_header (obj:Struct) : Defines PARAM_OBS_02 when size is 90. + param_obs_02_short_header (obj:Struct) : Defines PARAM_OBS_02 when size is 6. param_trc_02_header (obj:Struct) : Defines PARAM_TRC_02 header. img_mot_comp_03_header (obj:Struct) : Defines IMG_MOT_COMP_03 header. img_mot_comp_02_header (obj:Struct) : Defines IMG_MOT_COMP_02 header. @@ -223,14 +224,31 @@ "size" / Int32un, ) -# TODO This chunk still needs work. -# Total [3] Int16un +# param_obs_02 has been found to be 90 or 6. +# This first struct handles 90, next handles 6. +# The first "0x1" seems to indicate which type +# of header to expect (0: long, 1: short) param_obs_02_header = Struct( - "camera_model" / PaddedString(12, "utf16"), - "jpeg_quality" / Int8un, - "color_temparature" / Int8un, + "0x1" / Int16un, + "0xffff" / Int16un[2], + "camera_model" / PaddedString(12, "ascii"), + "image_quality" / PaddedString(24, "ascii"), + "0x300" / Int16un, + "0x1" / Int16un, + "0x0" / Int16un, + "color_temp" / PaddedString(24, "ascii"), + "0x2014" / Int16un, + "zeros" / Int8un[12], ) +param_obs_02_short_header = Struct( + "0x1" / Int16un, + "0xffff" / Int16un[2], +) +# phMode = fread(fid, 1, 'uint8'); +# phAngle = fread(fid, 1, 'uint8'); +# phLightLevel = fread(fid, 1, 'uint16'); + img_mot_comp_03_header = Struct( "0x0" / Int8un, "width" / Int32un, diff --git a/oct_converter/readers/fda.py b/oct_converter/readers/fda.py index 64dd216..99cbfdd 100644 --- a/oct_converter/readers/fda.py +++ b/oct_converter/readers/fda.py @@ -105,15 +105,18 @@ def read_oct_volume(self) -> OCTVolumeWithMetaData: sex_map = {1: "M", 2: "F", 3: "O", None: ""} lat_map = {0: "R", 1: "L", None: ""} + try: + patient_dob = datetime(*patient_info.get("birth_date")) + except (TypeError, ValueError): + patient_dob = None + oct_volume = OCTVolumeWithMetaData( volume, patient_id=patient_info.get("patient_id"), first_name=patient_info.get("first_name"), surname=patient_info.get("last_name"), sex=sex_map[patient_info.get("sex", None)], - patient_dob=datetime(*patient_info.get("birth_date")) - if patient_info.get("birth_date")[0] != 0 - else None, + patient_dob=patient_dob, acquisition_date=datetime(*capture_info.get("cap_date")), laterality=lat_map[capture_info.get("eye", None)], contours=contours, @@ -318,6 +321,9 @@ def read_all_metadata(self, verbose: bool = False): if key in [b"@IMG_JPEG", b"@IMG_FUNDUS", b"@IMG_TRC_02", b"@CONTOUR_INFO"]: # these chunks have their own dedicated methods for extraction continue + elif key == b"@PARAM_OBS_02": + metadata["param_obs_02"] = self.read_param_obs() + continue json_key = key.decode().split("@")[-1].lower() try: metadata[json_key] = self.read_any_info_and_make_dict(key) @@ -353,3 +359,31 @@ def read_any_info_and_make_dict(self, chunk_name: str) -> dict: else: chunks_info[key] = chunk_info_header[key] return chunks_info + + def read_param_obs(self) -> dict: + """Reads PARAM_OBS_02 while accounting for varied chunk sizes. + + Returns: + Chunk info data for PARAM_OBS_02 + """ + with open(self.filepath, "rb") as f: + chunk_location, chunk_size = self.chunk_dict[b"@PARAM_OBS_02"] + f.seek(chunk_location) # Set the chunk’s current position. + raw = f.read() + # PARAM_OBS_02 is either of size 90 or size 6. + if chunk_size == 90: + chunk_info_header = dict(fda_binary.param_obs_02_header.parse(raw)) + else: # chunk_size == 6 + chunk_info_header = dict( + fda_binary.param_obs_02_short_header.parse(raw) + ) + + chunks_info = dict() + for idx, key in enumerate(chunk_info_header.keys()): + if idx == 0: + continue + if type(chunk_info_header[key]) is ListContainer: + chunks_info[key] = list(chunk_info_header[key]) + else: + chunks_info[key] = chunk_info_header[key] + return chunks_info diff --git a/oct_converter/readers/fds.py b/oct_converter/readers/fds.py index 874b5de..efe2fcd 100644 --- a/oct_converter/readers/fds.py +++ b/oct_converter/readers/fds.py @@ -100,15 +100,18 @@ def read_oct_volume(self) -> OCTVolumeWithMetaData: sex_map = {1: "M", 2: "F", 3: "O", None: ""} lat_map = {0: "R", 1: "L", None: ""} + try: + patient_dob = datetime(*patient_info.get("birth_date")) + except (TypeError, ValueError): + patient_dob = None + oct_volume = OCTVolumeWithMetaData( [volume[:, :, i] for i in range(volume.shape[2])], patient_id=patient_info.get("patient_id"), first_name=patient_info.get("first_name"), surname=patient_info.get("last_name"), sex=sex_map[patient_info.get("sex", None)], - patient_dob=datetime(*patient_info.get("birth_date")) - if patient_info.get("birth_date")[0] != 0 - else None, + patient_dob=patient_dob, acquisition_date=datetime(*capture_info.get("cap_date")), laterality=lat_map[capture_info.get("eye", None)], pixel_spacing=pixel_spacing, @@ -201,6 +204,9 @@ def read_all_metadata(self, verbose: bool = False): if key in [b"IMG_SCAN_03", b"@IMG_OBS"]: # these chunks have their own dedicated methods for extraction continue + elif key == b"@PARAM_OBS_02": + metadata["param_obs_02"] = self.read_param_obs() + continue json_key = key.decode().split("@")[-1].lower() try: metadata[json_key] = self.read_any_info_and_make_dict(key) @@ -236,3 +242,31 @@ def read_any_info_and_make_dict(self, chunk_name: str) -> dict: else: chunks_info[key] = chunk_info_header[key] return chunks_info + + def read_param_obs(self) -> dict: + """Reads PARAM_OBS_02 while accounting for varied chunk sizes. + + Returns: + Chunk info data for PARAM_OBS_02 + """ + with open(self.filepath, "rb") as f: + chunk_location, chunk_size = self.chunk_dict[b"@PARAM_OBS_02"] + f.seek(chunk_location) # Set the chunk’s current position. + raw = f.read() + # PARAM_OBS_02 is either of size 90 or size 6. + if chunk_size == 90: + chunk_info_header = dict(fds_binary.param_obs_02_header.parse(raw)) + else: # chunk_size == 6 + chunk_info_header = dict( + fds_binary.param_obs_02_short_header.parse(raw) + ) + + chunks_info = dict() + for idx, key in enumerate(chunk_info_header.keys()): + if idx == 0: + continue + if type(chunk_info_header[key]) is ListContainer: + chunks_info[key] = list(chunk_info_header[key]) + else: + chunks_info[key] = chunk_info_header[key] + return chunks_info