mediml 0.9.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- MEDiml/MEDscan.py +1696 -0
- MEDiml/__init__.py +21 -0
- MEDiml/biomarkers/BatchExtractor.py +806 -0
- MEDiml/biomarkers/BatchExtractorTexturalFilters.py +840 -0
- MEDiml/biomarkers/__init__.py +16 -0
- MEDiml/biomarkers/diagnostics.py +125 -0
- MEDiml/biomarkers/get_oriented_bound_box.py +158 -0
- MEDiml/biomarkers/glcm.py +1602 -0
- MEDiml/biomarkers/gldzm.py +523 -0
- MEDiml/biomarkers/glrlm.py +1315 -0
- MEDiml/biomarkers/glszm.py +555 -0
- MEDiml/biomarkers/int_vol_hist.py +527 -0
- MEDiml/biomarkers/intensity_histogram.py +615 -0
- MEDiml/biomarkers/local_intensity.py +89 -0
- MEDiml/biomarkers/morph.py +1756 -0
- MEDiml/biomarkers/ngldm.py +780 -0
- MEDiml/biomarkers/ngtdm.py +414 -0
- MEDiml/biomarkers/stats.py +373 -0
- MEDiml/biomarkers/utils.py +389 -0
- MEDiml/filters/TexturalFilter.py +299 -0
- MEDiml/filters/__init__.py +9 -0
- MEDiml/filters/apply_filter.py +134 -0
- MEDiml/filters/gabor.py +215 -0
- MEDiml/filters/laws.py +283 -0
- MEDiml/filters/log.py +147 -0
- MEDiml/filters/mean.py +121 -0
- MEDiml/filters/textural_filters_kernels.py +1738 -0
- MEDiml/filters/utils.py +107 -0
- MEDiml/filters/wavelet.py +237 -0
- MEDiml/learning/DataCleaner.py +198 -0
- MEDiml/learning/DesignExperiment.py +480 -0
- MEDiml/learning/FSR.py +667 -0
- MEDiml/learning/Normalization.py +112 -0
- MEDiml/learning/RadiomicsLearner.py +714 -0
- MEDiml/learning/Results.py +2237 -0
- MEDiml/learning/Stats.py +694 -0
- MEDiml/learning/__init__.py +10 -0
- MEDiml/learning/cleaning_utils.py +107 -0
- MEDiml/learning/ml_utils.py +1015 -0
- MEDiml/processing/__init__.py +6 -0
- MEDiml/processing/compute_suv_map.py +121 -0
- MEDiml/processing/discretisation.py +149 -0
- MEDiml/processing/interpolation.py +275 -0
- MEDiml/processing/resegmentation.py +66 -0
- MEDiml/processing/segmentation.py +912 -0
- MEDiml/utils/__init__.py +25 -0
- MEDiml/utils/batch_patients.py +45 -0
- MEDiml/utils/create_radiomics_table.py +131 -0
- MEDiml/utils/data_frame_export.py +42 -0
- MEDiml/utils/find_process_names.py +16 -0
- MEDiml/utils/get_file_paths.py +34 -0
- MEDiml/utils/get_full_rad_names.py +21 -0
- MEDiml/utils/get_institutions_from_ids.py +16 -0
- MEDiml/utils/get_patient_id_from_scan_name.py +22 -0
- MEDiml/utils/get_patient_names.py +26 -0
- MEDiml/utils/get_radiomic_names.py +27 -0
- MEDiml/utils/get_scan_name_from_rad_name.py +22 -0
- MEDiml/utils/image_reader_SITK.py +37 -0
- MEDiml/utils/image_volume_obj.py +22 -0
- MEDiml/utils/imref.py +340 -0
- MEDiml/utils/initialize_features_names.py +62 -0
- MEDiml/utils/inpolygon.py +159 -0
- MEDiml/utils/interp3.py +43 -0
- MEDiml/utils/json_utils.py +78 -0
- MEDiml/utils/mode.py +31 -0
- MEDiml/utils/parse_contour_string.py +58 -0
- MEDiml/utils/save_MEDscan.py +30 -0
- MEDiml/utils/strfind.py +32 -0
- MEDiml/utils/textureTools.py +188 -0
- MEDiml/utils/texture_features_names.py +115 -0
- MEDiml/utils/write_radiomics_csv.py +47 -0
- MEDiml/wrangling/DataManager.py +1724 -0
- MEDiml/wrangling/ProcessDICOM.py +512 -0
- MEDiml/wrangling/__init__.py +3 -0
- mediml-0.9.9.dist-info/LICENSE.md +674 -0
- mediml-0.9.9.dist-info/METADATA +232 -0
- mediml-0.9.9.dist-info/RECORD +78 -0
- mediml-0.9.9.dist-info/WHEEL +4 -0
MEDiml/MEDscan.py
ADDED
|
@@ -0,0 +1,1696 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from json import dump
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Dict, List, Union
|
|
6
|
+
|
|
7
|
+
import matplotlib.pyplot as plt
|
|
8
|
+
import nibabel as nib
|
|
9
|
+
import numpy as np
|
|
10
|
+
from numpyencoder import NumpyEncoder
|
|
11
|
+
from PIL import Image
|
|
12
|
+
|
|
13
|
+
from .utils.image_volume_obj import image_volume_obj
|
|
14
|
+
from .utils.imref import imref3d
|
|
15
|
+
from .utils.json_utils import load_json
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MEDscan(object):
|
|
19
|
+
"""Organizes all scan data (patientID, imaging data, scan type...).
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
patientID (str): Patient ID.
|
|
23
|
+
type (str): Scan type (MRscan, CTscan...).
|
|
24
|
+
format (str): Scan file format. Either 'npy' or 'nifti'.
|
|
25
|
+
dicomH (pydicom.dataset.FileDataset): DICOM header.
|
|
26
|
+
data (MEDscan.data): Instance of MEDscan.data inner class.
|
|
27
|
+
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, medscan=None) -> None:
|
|
31
|
+
"""Constructor of the MEDscan class
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
medscan(MEDscan): A MEDscan class instance.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
None
|
|
38
|
+
"""
|
|
39
|
+
try:
|
|
40
|
+
self.patientID = medscan.patientID
|
|
41
|
+
except:
|
|
42
|
+
self.patientID = ""
|
|
43
|
+
try:
|
|
44
|
+
self.type = medscan.type
|
|
45
|
+
except:
|
|
46
|
+
self.type = ""
|
|
47
|
+
try:
|
|
48
|
+
self.series_description = medscan.series_description
|
|
49
|
+
except:
|
|
50
|
+
self.series_description = ""
|
|
51
|
+
try:
|
|
52
|
+
self.format = medscan.format
|
|
53
|
+
except:
|
|
54
|
+
self.format = ""
|
|
55
|
+
try:
|
|
56
|
+
self.dicomH = medscan.dicomH
|
|
57
|
+
except:
|
|
58
|
+
self.dicomH = []
|
|
59
|
+
try:
|
|
60
|
+
self.data = medscan.data
|
|
61
|
+
except:
|
|
62
|
+
self.data = self.data()
|
|
63
|
+
|
|
64
|
+
self.params = self.Params()
|
|
65
|
+
self.radiomics = self.Radiomics()
|
|
66
|
+
self.skip = False
|
|
67
|
+
|
|
68
|
+
def __init_process_params(self, im_params: Dict) -> None:
|
|
69
|
+
"""Initializes the processing params from a given Dict.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
im_params(Dict): Dictionary of different processing params.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
None.
|
|
76
|
+
"""
|
|
77
|
+
if self.type == 'CTscan' and 'imParamCT' in im_params:
|
|
78
|
+
im_params = im_params['imParamCT']
|
|
79
|
+
elif self.type == 'MRscan' and 'imParamMR' in im_params:
|
|
80
|
+
im_params = im_params['imParamMR']
|
|
81
|
+
elif self.type == 'PTscan' and 'imParamPET' in im_params:
|
|
82
|
+
im_params = im_params['imParamPET']
|
|
83
|
+
else:
|
|
84
|
+
raise ValueError(f"The given parameters dict is not valid, no params found for {self.type} modality")
|
|
85
|
+
|
|
86
|
+
# re-segmentation range processing
|
|
87
|
+
if(im_params['reSeg']['range'] and (im_params['reSeg']['range'][0] == "inf" or im_params['reSeg']['range'][0] == "-inf")):
|
|
88
|
+
im_params['reSeg']['range'][0] = -np.inf
|
|
89
|
+
if(im_params['reSeg']['range'] and im_params['reSeg']['range'][1] == "inf"):
|
|
90
|
+
im_params['reSeg']['range'][1] = np.inf
|
|
91
|
+
|
|
92
|
+
if 'box_string' in im_params:
|
|
93
|
+
box_string = im_params['box_string']
|
|
94
|
+
else:
|
|
95
|
+
# By default, we add 10 voxels in all three dimensions are added to the smallest
|
|
96
|
+
# bounding box. This setting is used to speed up interpolation
|
|
97
|
+
# processes (mostly) prior to the computation of radiomics
|
|
98
|
+
# features. Optional argument in the function computeRadiomics.
|
|
99
|
+
box_string = 'box10'
|
|
100
|
+
if 'compute_diag_features' in im_params:
|
|
101
|
+
compute_diag_features = im_params['compute_diag_features']
|
|
102
|
+
else:
|
|
103
|
+
compute_diag_features = False
|
|
104
|
+
if compute_diag_features: # If compute_diag_features is true.
|
|
105
|
+
box_string = 'full' # This is required for proper comparison.
|
|
106
|
+
|
|
107
|
+
self.params.process.box_string = box_string
|
|
108
|
+
|
|
109
|
+
# get default scan parameters from im_param_scan
|
|
110
|
+
self.params.process.scale_non_text = im_params['interp']['scale_non_text']
|
|
111
|
+
self.params.process.vol_interp = im_params['interp']['vol_interp']
|
|
112
|
+
self.params.process.roi_interp = im_params['interp']['roi_interp']
|
|
113
|
+
self.params.process.gl_round = im_params['interp']['gl_round']
|
|
114
|
+
self.params.process.roi_pv = im_params['interp']['roi_pv']
|
|
115
|
+
self.params.process.im_range = im_params['reSeg']['range'] if 'range' in im_params['reSeg'] else None
|
|
116
|
+
self.params.process.outliers = im_params['reSeg']['outliers']
|
|
117
|
+
self.params.process.ih = im_params['discretisation']['IH']
|
|
118
|
+
self.params.process.ivh = im_params['discretisation']['IVH']
|
|
119
|
+
self.params.process.scale_text = im_params['interp']['scale_text']
|
|
120
|
+
self.params.process.algo = im_params['discretisation']['texture']['type'] if 'type' in im_params['discretisation']['texture'] else []
|
|
121
|
+
self.params.process.gray_levels = im_params['discretisation']['texture']['val'] if 'val' in im_params['discretisation']['texture'] else [[]]
|
|
122
|
+
self.params.process.im_type = self.type
|
|
123
|
+
|
|
124
|
+
# Voxels dimension
|
|
125
|
+
self.params.process.n_scale = len(self.params.process.scale_text)
|
|
126
|
+
# Setting up discretisation params
|
|
127
|
+
self.params.process.n_algo = len(self.params.process.algo)
|
|
128
|
+
self.params.process.n_gl = len(self.params.process.gray_levels[0])
|
|
129
|
+
self.params.process.n_exp = self.params.process.n_scale * self.params.process.n_algo * self.params.process.n_gl
|
|
130
|
+
|
|
131
|
+
# Setting up user_set_min_value
|
|
132
|
+
if self.params.process.im_range is not None and type(self.params.process.im_range) is list and self.params.process.im_range:
|
|
133
|
+
user_set_min_value = self.params.process.im_range[0]
|
|
134
|
+
if user_set_min_value == -np.inf:
|
|
135
|
+
# In case no re-seg im_range is defined for the FBS algorithm,
|
|
136
|
+
# the minimum value of ROI will be used (not recommended).
|
|
137
|
+
user_set_min_value = []
|
|
138
|
+
else:
|
|
139
|
+
# In case no re-seg im_range is defined for the FBS algorithm,
|
|
140
|
+
# the minimum value of ROI will be used (not recommended).
|
|
141
|
+
user_set_min_value = []
|
|
142
|
+
self.params.process.user_set_min_value = user_set_min_value
|
|
143
|
+
|
|
144
|
+
# box_string argument is optional. If not present, we use the full box.
|
|
145
|
+
if self.params.process.box_string is None:
|
|
146
|
+
self.params.process.box_string = 'full'
|
|
147
|
+
|
|
148
|
+
# set filter type for the modality
|
|
149
|
+
if 'filter_type' in im_params:
|
|
150
|
+
self.params.filter.filter_type = im_params['filter_type']
|
|
151
|
+
|
|
152
|
+
# Set intensity type
|
|
153
|
+
if 'intensity_type' in im_params and im_params['intensity_type'] != "":
|
|
154
|
+
self.params.process.intensity_type = im_params['intensity_type']
|
|
155
|
+
elif self.params.filter.filter_type != "":
|
|
156
|
+
self.params.process.intensity_type = 'filtered'
|
|
157
|
+
elif self.type == 'MRscan':
|
|
158
|
+
self.params.process.intensity_type = 'arbitrary'
|
|
159
|
+
else:
|
|
160
|
+
self.params.process.intensity_type = 'definite'
|
|
161
|
+
|
|
162
|
+
def __init_extraction_params(self, im_params: Dict):
|
|
163
|
+
"""Initializes the extraction params from a given Dict.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
im_params(Dict): Dictionary of different extraction params.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
None.
|
|
170
|
+
"""
|
|
171
|
+
if self.type == 'CTscan' and 'imParamCT' in im_params:
|
|
172
|
+
im_params = im_params['imParamCT']
|
|
173
|
+
elif self.type == 'MRscan' and 'imParamMR' in im_params:
|
|
174
|
+
im_params = im_params['imParamMR']
|
|
175
|
+
elif self.type == 'PTscan' and 'imParamPET' in im_params:
|
|
176
|
+
im_params = im_params['imParamPET']
|
|
177
|
+
else:
|
|
178
|
+
raise ValueError(f"The given parameters dict is not valid, no params found for {self.type} modality")
|
|
179
|
+
|
|
180
|
+
# glcm features extraction params
|
|
181
|
+
if 'glcm' in im_params:
|
|
182
|
+
if 'dist_correction' in im_params['glcm']:
|
|
183
|
+
self.params.radiomics.glcm.dist_correction = im_params['glcm']['dist_correction']
|
|
184
|
+
else:
|
|
185
|
+
self.params.radiomics.glcm.dist_correction = False
|
|
186
|
+
if 'merge_method' in im_params['glcm']:
|
|
187
|
+
self.params.radiomics.glcm.merge_method = im_params['glcm']['merge_method']
|
|
188
|
+
else:
|
|
189
|
+
self.params.radiomics.glcm.merge_method = "vol_merge"
|
|
190
|
+
else:
|
|
191
|
+
self.params.radiomics.glcm.dist_correction = False
|
|
192
|
+
self.params.radiomics.glcm.merge_method = "vol_merge"
|
|
193
|
+
|
|
194
|
+
# glrlm features extraction params
|
|
195
|
+
if 'glrlm' in im_params:
|
|
196
|
+
if 'dist_correction' in im_params['glrlm']:
|
|
197
|
+
self.params.radiomics.glrlm.dist_correction = im_params['glrlm']['dist_correction']
|
|
198
|
+
else:
|
|
199
|
+
self.params.radiomics.glrlm.dist_correction = False
|
|
200
|
+
if 'merge_method' in im_params['glrlm']:
|
|
201
|
+
self.params.radiomics.glrlm.merge_method = im_params['glrlm']['merge_method']
|
|
202
|
+
else:
|
|
203
|
+
self.params.radiomics.glrlm.merge_method = "vol_merge"
|
|
204
|
+
else:
|
|
205
|
+
self.params.radiomics.glrlm.dist_correction = False
|
|
206
|
+
self.params.radiomics.glrlm.merge_method = "vol_merge"
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
# ngtdm features extraction params
|
|
210
|
+
if 'ngtdm' in im_params:
|
|
211
|
+
if 'dist_correction' in im_params['ngtdm']:
|
|
212
|
+
self.params.radiomics.ngtdm.dist_correction = im_params['ngtdm']['dist_correction']
|
|
213
|
+
else:
|
|
214
|
+
self.params.radiomics.ngtdm.dist_correction = False
|
|
215
|
+
else:
|
|
216
|
+
self.params.radiomics.ngtdm.dist_correction = False
|
|
217
|
+
|
|
218
|
+
# Features to extract
|
|
219
|
+
features = [
|
|
220
|
+
"Morph", "LocalIntensity", "Stats", "IntensityHistogram", "IntensityVolumeHistogram",
|
|
221
|
+
"GLCM", "GLRLM", "GLSZM", "GLDZM", "NGTDM", "NGLDM"
|
|
222
|
+
]
|
|
223
|
+
if "extract" in im_params.keys():
|
|
224
|
+
self.params.radiomics.extract = im_params['extract']
|
|
225
|
+
for key in self.params.radiomics.extract:
|
|
226
|
+
if key not in features:
|
|
227
|
+
raise ValueError(f"Invalid key in 'extract' parameter: {key} (Modality {self.type}).")
|
|
228
|
+
|
|
229
|
+
# Ensure each feature is in the extract dictionary with a default value of True
|
|
230
|
+
for feature in features:
|
|
231
|
+
if feature not in self.params.radiomics.extract:
|
|
232
|
+
self.params.radiomics.extract[feature] = True
|
|
233
|
+
|
|
234
|
+
def __init_filter_params(self, filter_params: Dict) -> None:
|
|
235
|
+
"""Initializes the filtering params from a given Dict.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
filter_params(Dict): Dictionary of the filtering parameters.
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
None.
|
|
242
|
+
"""
|
|
243
|
+
if 'imParamFilter' in filter_params:
|
|
244
|
+
filter_params = filter_params['imParamFilter']
|
|
245
|
+
|
|
246
|
+
# Initializae filter attribute
|
|
247
|
+
self.params.filter = self.params.Filter()
|
|
248
|
+
|
|
249
|
+
# mean filter params
|
|
250
|
+
if 'mean' in filter_params:
|
|
251
|
+
self.params.filter.mean.init_from_json(filter_params['mean'])
|
|
252
|
+
|
|
253
|
+
# log filter params
|
|
254
|
+
if 'log' in filter_params:
|
|
255
|
+
self.params.filter.log.init_from_json(filter_params['log'])
|
|
256
|
+
|
|
257
|
+
# laws filter params
|
|
258
|
+
if 'laws' in filter_params:
|
|
259
|
+
self.params.filter.laws.init_from_json(filter_params['laws'])
|
|
260
|
+
|
|
261
|
+
# gabor filter params
|
|
262
|
+
if 'gabor' in filter_params:
|
|
263
|
+
self.params.filter.gabor.init_from_json(filter_params['gabor'])
|
|
264
|
+
|
|
265
|
+
# wavelet filter params
|
|
266
|
+
if 'wavelet' in filter_params:
|
|
267
|
+
self.params.filter.wavelet.init_from_json(filter_params['wavelet'])
|
|
268
|
+
|
|
269
|
+
# Textural filter params
|
|
270
|
+
if 'textural' in filter_params:
|
|
271
|
+
self.params.filter.textural.init_from_json(filter_params['textural'])
|
|
272
|
+
|
|
273
|
+
def init_params(self, im_param_scan: Dict) -> None:
|
|
274
|
+
"""Initializes the Params class from a dictionary.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
im_param_scan(Dict): Dictionary of different processing, extraction and filtering params.
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
None.
|
|
281
|
+
"""
|
|
282
|
+
try:
|
|
283
|
+
# get default scan parameters from im_param_scan
|
|
284
|
+
self.__init_filter_params(im_param_scan['imParamFilter'])
|
|
285
|
+
self.__init_process_params(im_param_scan)
|
|
286
|
+
self.__init_extraction_params(im_param_scan)
|
|
287
|
+
|
|
288
|
+
# compute suv map for PT scans
|
|
289
|
+
if self.type == 'PTscan':
|
|
290
|
+
_compute_suv_map = im_param_scan['imParamPET']['compute_suv_map']
|
|
291
|
+
else :
|
|
292
|
+
_compute_suv_map = False
|
|
293
|
+
|
|
294
|
+
if self.type == 'PTscan' and _compute_suv_map and self.format != 'nifti':
|
|
295
|
+
try:
|
|
296
|
+
from .processing.compute_suv_map import compute_suv_map
|
|
297
|
+
self.data.volume.array = compute_suv_map(self.data.volume.array, self.dicomH[0])
|
|
298
|
+
except Exception as e :
|
|
299
|
+
message = f"\n ERROR COMPUTING SUV MAP - SOME FEATURES WILL BE INVALID: \n {e}"
|
|
300
|
+
logging.error(message)
|
|
301
|
+
print(message)
|
|
302
|
+
self.skip = True
|
|
303
|
+
|
|
304
|
+
# initialize radiomics structure
|
|
305
|
+
self.radiomics.image = {}
|
|
306
|
+
self.radiomics.params = im_param_scan
|
|
307
|
+
self.params.radiomics.scale_name = ''
|
|
308
|
+
self.params.radiomics.ih_name = ''
|
|
309
|
+
self.params.radiomics.ivh_name = ''
|
|
310
|
+
|
|
311
|
+
except Exception as e:
|
|
312
|
+
message = f"\n ERROR IN INITIALIZATION OF RADIOMICS FEATURE COMPUTATION\n {e}"
|
|
313
|
+
logging.error(message)
|
|
314
|
+
print(message)
|
|
315
|
+
self.skip = True
|
|
316
|
+
|
|
317
|
+
def init_ntf_calculation(self, vol_obj: image_volume_obj) -> None:
|
|
318
|
+
"""
|
|
319
|
+
Initializes all the computation parameters for non-texture features as well as the results dict.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
vol_obj(image_volume_obj): Imaging volume.
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
None.
|
|
326
|
+
"""
|
|
327
|
+
try:
|
|
328
|
+
if sum(self.params.process.scale_non_text) == 0: # In case the user chose to not interpolate
|
|
329
|
+
self.params.process.scale_non_text = [
|
|
330
|
+
vol_obj.spatialRef.PixelExtentInWorldX,
|
|
331
|
+
vol_obj.spatialRef.PixelExtentInWorldY,
|
|
332
|
+
vol_obj.spatialRef.PixelExtentInWorldZ]
|
|
333
|
+
else:
|
|
334
|
+
if len(self.params.process.scale_non_text) == 2:
|
|
335
|
+
# In case not interpolation is performed in
|
|
336
|
+
# the slice direction (e.g. 2D case)
|
|
337
|
+
self.params.process.scale_non_text = self.params.process.scale_non_text + \
|
|
338
|
+
[vol_obj.spatialRef.PixelExtentInWorldZ]
|
|
339
|
+
|
|
340
|
+
# Scale name
|
|
341
|
+
# Always isotropic resampling, so the first entry is ok.
|
|
342
|
+
self.params.radiomics.scale_name = 'scale' + (str(self.params.process.scale_non_text[0])).replace('.', 'dot')
|
|
343
|
+
|
|
344
|
+
# IH name
|
|
345
|
+
if 'val' in self.params.process.ih:
|
|
346
|
+
ih_val_name = 'bin' + (str(self.params.process.ih['val'])).replace('.', 'dot')
|
|
347
|
+
else:
|
|
348
|
+
ih_val_name = 'binNone'
|
|
349
|
+
|
|
350
|
+
# The minimum value defines the computation.
|
|
351
|
+
if self.params.process.ih['type'].find('FBS')>=0:
|
|
352
|
+
if type(self.params.process.user_set_min_value) is list and self.params.process.user_set_min_value:
|
|
353
|
+
min_val_name = '_min' + \
|
|
354
|
+
((str(self.params.process.user_set_min_value)).replace('.', 'dot')).replace('-', 'M')
|
|
355
|
+
else:
|
|
356
|
+
# Otherwise, minimum value of ROI will be used (not recommended),
|
|
357
|
+
# so no need to report it.
|
|
358
|
+
min_val_name = ''
|
|
359
|
+
else:
|
|
360
|
+
min_val_name = ''
|
|
361
|
+
self.params.radiomics.ih_name = self.params.radiomics.scale_name + \
|
|
362
|
+
'_algo' + self.params.process.ih['type'] + \
|
|
363
|
+
'_' + ih_val_name + min_val_name
|
|
364
|
+
|
|
365
|
+
# IVH name
|
|
366
|
+
if self.params.process.im_range: # The im_range defines the computation.
|
|
367
|
+
min_val_name = ((str(self.params.process.im_range[0])).replace('.', 'dot')).replace('-', 'M')
|
|
368
|
+
max_val_name = ((str(self.params.process.im_range[1])).replace('.', 'dot')).replace('-', 'M')
|
|
369
|
+
if max_val_name == 'inf':
|
|
370
|
+
# In this case, the maximum value of the ROI is used,
|
|
371
|
+
# so no need to report it.
|
|
372
|
+
range_name = '_min' + min_val_name
|
|
373
|
+
elif min_val_name == '-inf' or min_val_name == 'inf':
|
|
374
|
+
# In this case, the minimum value of the ROI is used,
|
|
375
|
+
# so no need to report it.
|
|
376
|
+
range_name = '_max' + max_val_name
|
|
377
|
+
else:
|
|
378
|
+
range_name = '_min' + min_val_name + '_max' + max_val_name
|
|
379
|
+
else:
|
|
380
|
+
# min-max of ROI will be used, no need to report it.
|
|
381
|
+
range_name = ''
|
|
382
|
+
if not self.params.process.ivh: # CT case for example
|
|
383
|
+
ivh_algo_name = 'algoNone'
|
|
384
|
+
ivh_val_name = 'bin1'
|
|
385
|
+
else:
|
|
386
|
+
ivh_algo_name = 'algo' + self.params.process.ivh['type'] if 'type' in self.params.process.ivh else 'algoNone'
|
|
387
|
+
if 'val' in self.params.process.ivh and self.params.process.ivh['val']:
|
|
388
|
+
ivh_val_name = 'bin' + (str(self.params.process.ivh['val'])).replace('.', 'dot')
|
|
389
|
+
else:
|
|
390
|
+
ivh_val_name = 'binNone'
|
|
391
|
+
self.params.radiomics.ivh_name = self.params.radiomics.scale_name + '_' + ivh_algo_name + '_' + ivh_val_name + range_name
|
|
392
|
+
|
|
393
|
+
# Now initialize the attribute that will hold the computation results
|
|
394
|
+
self.radiomics.image.update({
|
|
395
|
+
'morph_3D': {self.params.radiomics.scale_name: {}},
|
|
396
|
+
'locInt_3D': {self.params.radiomics.scale_name: {}},
|
|
397
|
+
'stats_3D': {self.params.radiomics.scale_name: {}},
|
|
398
|
+
'intHist_3D': {self.params.radiomics.ih_name: {}},
|
|
399
|
+
'intVolHist_3D': {self.params.radiomics.ivh_name: {}}
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
except Exception as e:
|
|
403
|
+
message = f"\n PROBLEM WITH PRE-PROCESSING OF FEATURES IN init_ntf_calculation(): \n {e}"
|
|
404
|
+
logging.error(message)
|
|
405
|
+
print(message)
|
|
406
|
+
self.radiomics.image.update(
|
|
407
|
+
{('scale' + (str(self.params.process.scale_non_text[0])).replace('.', 'dot')): 'ERROR_PROCESSING'})
|
|
408
|
+
|
|
409
|
+
def init_tf_calculation(self, algo:int, gl:int, scale:int) -> None:
|
|
410
|
+
"""
|
|
411
|
+
Initializes all the computation parameters for the texture-features as well as the results dict.
|
|
412
|
+
|
|
413
|
+
Args:
|
|
414
|
+
algo(int): Discretisation algorithms index.
|
|
415
|
+
gl(int): gray-level index.
|
|
416
|
+
scale(int): scale-text index.
|
|
417
|
+
|
|
418
|
+
Returns:
|
|
419
|
+
None.
|
|
420
|
+
"""
|
|
421
|
+
# check glcm merge method
|
|
422
|
+
glcm_merge_method = self.params.radiomics.glcm.merge_method
|
|
423
|
+
if glcm_merge_method:
|
|
424
|
+
if glcm_merge_method == 'average':
|
|
425
|
+
glcm_merge_method = '_avg'
|
|
426
|
+
elif glcm_merge_method == 'vol_merge':
|
|
427
|
+
glcm_merge_method = '_comb'
|
|
428
|
+
else:
|
|
429
|
+
error_msg = f"{glcm_merge_method} Method not supported in glcm computation, \
|
|
430
|
+
only 'average' or 'vol_merge' are supported. \
|
|
431
|
+
Radiomics will be saved without any specific merge method."
|
|
432
|
+
logging.warning(error_msg)
|
|
433
|
+
print(error_msg)
|
|
434
|
+
|
|
435
|
+
# check glrlm merge method
|
|
436
|
+
glrlm_merge_method = self.params.radiomics.glrlm.merge_method
|
|
437
|
+
if glrlm_merge_method:
|
|
438
|
+
if glrlm_merge_method == 'average':
|
|
439
|
+
glrlm_merge_method = '_avg'
|
|
440
|
+
elif glrlm_merge_method == 'vol_merge':
|
|
441
|
+
glrlm_merge_method = '_comb'
|
|
442
|
+
else:
|
|
443
|
+
error_msg = f"{glcm_merge_method} Method not supported in glrlm computation, \
|
|
444
|
+
only 'average' or 'vol_merge' are supported. \
|
|
445
|
+
Radiomics will be saved without any specific merge method"
|
|
446
|
+
logging.warning(error_msg)
|
|
447
|
+
print(error_msg)
|
|
448
|
+
# set texture features names and updates radiomics dict
|
|
449
|
+
self.params.radiomics.name_text_types = [
|
|
450
|
+
'glcm_3D' + glcm_merge_method,
|
|
451
|
+
'glrlm_3D' + glrlm_merge_method,
|
|
452
|
+
'glszm_3D',
|
|
453
|
+
'gldzm_3D',
|
|
454
|
+
'ngtdm_3D',
|
|
455
|
+
'ngldm_3D']
|
|
456
|
+
n_text_types = len(self.params.radiomics.name_text_types)
|
|
457
|
+
if not ('texture' in self.radiomics.image):
|
|
458
|
+
self.radiomics.image.update({'texture': {}})
|
|
459
|
+
for t in range(n_text_types):
|
|
460
|
+
self.radiomics.image['texture'].update({self.params.radiomics.name_text_types[t]: {}})
|
|
461
|
+
|
|
462
|
+
# scale name
|
|
463
|
+
# Always isotropic resampling, so the first entry is ok.
|
|
464
|
+
scale_name = 'scale' + (str(self.params.process.scale_text[scale][0])).replace('.', 'dot')
|
|
465
|
+
if hasattr(self.params.radiomics, "scale_name"):
|
|
466
|
+
setattr(self.params.radiomics, 'scale_name', scale_name)
|
|
467
|
+
else:
|
|
468
|
+
self.params.radiomics.scale_name = scale_name
|
|
469
|
+
|
|
470
|
+
# Discretisation name
|
|
471
|
+
gray_levels_name = (str(self.params.process.gray_levels[algo][gl])).replace('.', 'dot')
|
|
472
|
+
|
|
473
|
+
if 'FBS' in self.params.process.algo[algo]: # The minimum value defines the computation.
|
|
474
|
+
if type(self.params.process.user_set_min_value) is list and self.params.process.user_set_min_value:
|
|
475
|
+
min_val_name = '_min' + ((str(self.params.process.user_set_min_value)).replace('.', 'dot')).replace('-', 'M')
|
|
476
|
+
else:
|
|
477
|
+
# Otherwise, minimum value of ROI will be used (not recommended),
|
|
478
|
+
# so no need to report it.
|
|
479
|
+
min_val_name = ''
|
|
480
|
+
else:
|
|
481
|
+
min_val_name = ''
|
|
482
|
+
|
|
483
|
+
if 'equal'in self.params.process.algo[algo]:
|
|
484
|
+
# The number of gray-levels used for equalization is currently
|
|
485
|
+
# hard-coded to 64 in equalization.m
|
|
486
|
+
discretisation_name = 'algo' + self.params.process.algo[algo] + '256_bin' + gray_levels_name + min_val_name
|
|
487
|
+
else:
|
|
488
|
+
discretisation_name = 'algo' + self.params.process.algo[algo] + '_bin' + gray_levels_name + min_val_name
|
|
489
|
+
|
|
490
|
+
# Processing full name
|
|
491
|
+
processing_name = scale_name + '_' + discretisation_name
|
|
492
|
+
if hasattr(self.params.radiomics, "processing_name"):
|
|
493
|
+
setattr(self.params.radiomics, 'processing_name', processing_name)
|
|
494
|
+
else:
|
|
495
|
+
self.params.radiomics.processing_name = processing_name
|
|
496
|
+
|
|
497
|
+
def init_from_nifti(self, nifti_image_path: Path) -> None:
|
|
498
|
+
"""Initializes the MEDscan class using a NIfTI file.
|
|
499
|
+
|
|
500
|
+
Args:
|
|
501
|
+
nifti_image_path (Path): NIfTI file path.
|
|
502
|
+
|
|
503
|
+
Returns:
|
|
504
|
+
None.
|
|
505
|
+
|
|
506
|
+
"""
|
|
507
|
+
self.patientID = os.path.basename(nifti_image_path).split("_")[0]
|
|
508
|
+
self.type = os.path.basename(nifti_image_path).split(".")[-3]
|
|
509
|
+
self.format = "nifti"
|
|
510
|
+
self.data.set_orientation(orientation="Axial")
|
|
511
|
+
self.data.set_patient_position(patient_position="HFS")
|
|
512
|
+
self.data.ROI.get_roi_from_path(roi_path=os.path.dirname(nifti_image_path),
|
|
513
|
+
id=Path(nifti_image_path).name.split("(")[0])
|
|
514
|
+
self.data.volume.array = nib.load(nifti_image_path).get_fdata()
|
|
515
|
+
# RAS to LPS
|
|
516
|
+
self.data.volume.convert_to_LPS()
|
|
517
|
+
self.data.volume.scan_rot = None
|
|
518
|
+
|
|
519
|
+
def update_radiomics(
|
|
520
|
+
self, int_vol_hist_features: Dict = {},
|
|
521
|
+
morph_features: Dict = {}, loc_int_features: Dict = {},
|
|
522
|
+
stats_features: Dict = {}, int_hist_features: Dict = {},
|
|
523
|
+
glcm_features: Dict = {}, glrlm_features: Dict = {},
|
|
524
|
+
glszm_features: Dict = {}, gldzm_features: Dict = {},
|
|
525
|
+
ngtdm_features: Dict = {}, ngldm_features: Dict = {}) -> None:
|
|
526
|
+
"""Updates the results attribute with the extracted features.
|
|
527
|
+
|
|
528
|
+
Args:
|
|
529
|
+
int_vol_hist_features(Dict, optional): Dictionary of the intensity volume histogram features.
|
|
530
|
+
morph_features(Dict, optional): Dictionary of the morphological features.
|
|
531
|
+
loc_int_features(Dict, optional): Dictionary of the intensity local intensity features.
|
|
532
|
+
stats_features(Dict, optional): Dictionary of the statistical features.
|
|
533
|
+
int_hist_features(Dict, optional): Dictionary of the intensity histogram features.
|
|
534
|
+
glcm_features(Dict, optional): Dictionary of the GLCM features.
|
|
535
|
+
glrlm_features(Dict, optional): Dictionary of the GLRLM features.
|
|
536
|
+
glszm_features(Dict, optional): Dictionary of the GLSZM features.
|
|
537
|
+
gldzm_features(Dict, optional): Dictionary of the GLDZM features.
|
|
538
|
+
ngtdm_features(Dict, optional): Dictionary of the NGTDM features.
|
|
539
|
+
ngldm_features(Dict, optional): Dictionary of the NGLDM features.
|
|
540
|
+
Returns:
|
|
541
|
+
None.
|
|
542
|
+
"""
|
|
543
|
+
# check glcm merge method
|
|
544
|
+
glcm_merge_method = self.params.radiomics.glcm.merge_method
|
|
545
|
+
if glcm_merge_method:
|
|
546
|
+
if glcm_merge_method == 'average':
|
|
547
|
+
glcm_merge_method = '_avg'
|
|
548
|
+
elif glcm_merge_method == 'vol_merge':
|
|
549
|
+
glcm_merge_method = '_comb'
|
|
550
|
+
|
|
551
|
+
# check glrlm merge method
|
|
552
|
+
glrlm_merge_method = self.params.radiomics.glrlm.merge_method
|
|
553
|
+
if glrlm_merge_method:
|
|
554
|
+
if glrlm_merge_method == 'average':
|
|
555
|
+
glrlm_merge_method = '_avg'
|
|
556
|
+
elif glrlm_merge_method == 'vol_merge':
|
|
557
|
+
glrlm_merge_method = '_comb'
|
|
558
|
+
|
|
559
|
+
# Non-texture Features
|
|
560
|
+
if int_vol_hist_features:
|
|
561
|
+
self.radiomics.image['intVolHist_3D'][self.params.radiomics.ivh_name] = int_vol_hist_features
|
|
562
|
+
if morph_features:
|
|
563
|
+
self.radiomics.image['morph_3D'][self.params.radiomics.scale_name] = morph_features
|
|
564
|
+
if loc_int_features:
|
|
565
|
+
self.radiomics.image['locInt_3D'][self.params.radiomics.scale_name] = loc_int_features
|
|
566
|
+
if stats_features:
|
|
567
|
+
self.radiomics.image['stats_3D'][self.params.radiomics.scale_name] = stats_features
|
|
568
|
+
if int_hist_features:
|
|
569
|
+
self.radiomics.image['intHist_3D'][self.params.radiomics.ih_name] = int_hist_features
|
|
570
|
+
|
|
571
|
+
# Texture Features
|
|
572
|
+
if glcm_features:
|
|
573
|
+
self.radiomics.image['texture'][
|
|
574
|
+
'glcm_3D' + glcm_merge_method][self.params.radiomics.processing_name] = glcm_features
|
|
575
|
+
if glrlm_features:
|
|
576
|
+
self.radiomics.image['texture'][
|
|
577
|
+
'glrlm_3D' + glrlm_merge_method][self.params.radiomics.processing_name] = glrlm_features
|
|
578
|
+
if glszm_features:
|
|
579
|
+
self.radiomics.image['texture']['glszm_3D'][self.params.radiomics.processing_name] = glszm_features
|
|
580
|
+
if gldzm_features:
|
|
581
|
+
self.radiomics.image['texture']['gldzm_3D'][self.params.radiomics.processing_name] = gldzm_features
|
|
582
|
+
if ngtdm_features:
|
|
583
|
+
self.radiomics.image['texture']['ngtdm_3D'][self.params.radiomics.processing_name] = ngtdm_features
|
|
584
|
+
if ngldm_features:
|
|
585
|
+
self.radiomics.image['texture']['ngldm_3D'][self.params.radiomics.processing_name] = ngldm_features
|
|
586
|
+
|
|
587
|
+
def save_radiomics(
|
|
588
|
+
self, scan_file_name: List,
|
|
589
|
+
path_save: Path, roi_type: str,
|
|
590
|
+
roi_type_label: str, patient_num: int = None) -> None:
|
|
591
|
+
"""
|
|
592
|
+
Saves extracted radiomics features in a JSON file.
|
|
593
|
+
|
|
594
|
+
Args:
|
|
595
|
+
scan_file_name(List): List of scan files.
|
|
596
|
+
path_save(Path): Saving path.
|
|
597
|
+
roi_type(str): Type of the ROI.
|
|
598
|
+
roi_type_label(str): Label of the ROI type.
|
|
599
|
+
patient_num(int): Index of scan.
|
|
600
|
+
|
|
601
|
+
Returns:
|
|
602
|
+
None.
|
|
603
|
+
"""
|
|
604
|
+
if path_save.name != f'features({roi_type})':
|
|
605
|
+
if not (path_save / f'features({roi_type})').exists():
|
|
606
|
+
(path_save / f'features({roi_type})').mkdir()
|
|
607
|
+
path_save = Path(path_save / f'features({roi_type})')
|
|
608
|
+
else:
|
|
609
|
+
path_save = Path(path_save) / f'features({roi_type})'
|
|
610
|
+
else:
|
|
611
|
+
path_save = Path(path_save)
|
|
612
|
+
params = {}
|
|
613
|
+
params['roi_type'] = roi_type_label
|
|
614
|
+
params['patientID'] = self.patientID
|
|
615
|
+
params['vox_dim'] = list([
|
|
616
|
+
self.data.volume.spatialRef.PixelExtentInWorldX,
|
|
617
|
+
self.data.volume.spatialRef.PixelExtentInWorldY,
|
|
618
|
+
self.data.volume.spatialRef.PixelExtentInWorldZ
|
|
619
|
+
])
|
|
620
|
+
self.radiomics.update_params(params)
|
|
621
|
+
if type(scan_file_name) is str:
|
|
622
|
+
index_dot = scan_file_name.find('.')
|
|
623
|
+
ext = scan_file_name.find('.npy')
|
|
624
|
+
name_save = scan_file_name[:index_dot] + \
|
|
625
|
+
'(' + roi_type_label + ')' + \
|
|
626
|
+
scan_file_name[index_dot : ext]
|
|
627
|
+
elif patient_num is not None:
|
|
628
|
+
index_dot = scan_file_name[patient_num].find('.')
|
|
629
|
+
ext = scan_file_name[patient_num].find('.npy')
|
|
630
|
+
name_save = scan_file_name[patient_num][:index_dot] + \
|
|
631
|
+
'(' + roi_type_label + ')' + \
|
|
632
|
+
scan_file_name[patient_num][index_dot : ext]
|
|
633
|
+
else:
|
|
634
|
+
raise ValueError("`patient_num` must be specified or `scan_file_name` must be str")
|
|
635
|
+
|
|
636
|
+
with open(path_save / f"{name_save}.json", "w") as fp:
|
|
637
|
+
dump(self.radiomics.to_json(), fp, indent=4, cls=NumpyEncoder)
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
class Params:
|
|
641
|
+
"""Organizes all processing, filtering and features extraction parameters"""
|
|
642
|
+
|
|
643
|
+
def __init__(self) -> None:
|
|
644
|
+
"""
|
|
645
|
+
Organizes all processing, filtering and features extraction
|
|
646
|
+
"""
|
|
647
|
+
self.process = self.Process()
|
|
648
|
+
self.filter = self.Filter()
|
|
649
|
+
self.radiomics = self.Radiomics()
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
class Process:
|
|
653
|
+
"""Organizes all processing parameters."""
|
|
654
|
+
def __init__(self, **kwargs) -> None:
|
|
655
|
+
"""
|
|
656
|
+
Constructor of the `Process` class.
|
|
657
|
+
"""
|
|
658
|
+
self.algo = kwargs['algo'] if 'algo' in kwargs else None
|
|
659
|
+
self.box_string = kwargs['box_string'] if 'box_string' in kwargs else None
|
|
660
|
+
self.gl_round = kwargs['gl_round'] if 'gl_round' in kwargs else None
|
|
661
|
+
self.gray_levels = kwargs['gray_levels'] if 'gray_levels' in kwargs else None
|
|
662
|
+
self.ih = kwargs['ih'] if 'ih' in kwargs else None
|
|
663
|
+
self.im_range = kwargs['im_range'] if 'im_range' in kwargs else None
|
|
664
|
+
self.im_type = kwargs['im_type'] if 'im_type' in kwargs else None
|
|
665
|
+
self.intensity_type = kwargs['intensity_type'] if 'intensity_type' in kwargs else None
|
|
666
|
+
self.ivh = kwargs['ivh'] if 'ivh' in kwargs else None
|
|
667
|
+
self.n_algo = kwargs['n_algo'] if 'n_algo' in kwargs else None
|
|
668
|
+
self.n_exp = kwargs['n_exp'] if 'n_exp' in kwargs else None
|
|
669
|
+
self.n_gl = kwargs['n_gl'] if 'n_gl' in kwargs else None
|
|
670
|
+
self.n_scale = kwargs['n_scale'] if 'n_scale' in kwargs else None
|
|
671
|
+
self.outliers = kwargs['outliers'] if 'outliers' in kwargs else None
|
|
672
|
+
self.scale_non_text = kwargs['scale_non_text'] if 'scale_non_text' in kwargs else None
|
|
673
|
+
self.scale_text = kwargs['scale_text'] if 'scale_text' in kwargs else None
|
|
674
|
+
self.roi_interp = kwargs['roi_interp'] if 'roi_interp' in kwargs else None
|
|
675
|
+
self.roi_pv = kwargs['roi_pv'] if 'roi_pv' in kwargs else None
|
|
676
|
+
self.user_set_min_value = kwargs['user_set_min_value'] if 'user_set_min_value' in kwargs else None
|
|
677
|
+
self.vol_interp = kwargs['vol_interp'] if 'vol_interp' in kwargs else None
|
|
678
|
+
|
|
679
|
+
def init_from_json(self, path_to_json: Union[Path, str]) -> None:
|
|
680
|
+
"""
|
|
681
|
+
Updates class attributes from json file.
|
|
682
|
+
|
|
683
|
+
Args:
|
|
684
|
+
path_to_json(Union[Path, str]): Path to the JSON file with processing parameters.
|
|
685
|
+
|
|
686
|
+
Returns:
|
|
687
|
+
None.
|
|
688
|
+
"""
|
|
689
|
+
__params = load_json(Path(path_to_json))
|
|
690
|
+
|
|
691
|
+
self.algo = __params['algo'] if 'algo' in __params else self.algo
|
|
692
|
+
self.box_string = __params['box_string'] if 'box_string' in __params else self.box_string
|
|
693
|
+
self.gl_round = __params['gl_round'] if 'gl_round' in __params else self.gl_round
|
|
694
|
+
self.gray_levels = __params['gray_levels'] if 'gray_levels' in __params else self.gray_levels
|
|
695
|
+
self.ih = __params['ih'] if 'ih' in __params else self.ih
|
|
696
|
+
self.im_range = __params['im_range'] if 'im_range' in __params else self.im_range
|
|
697
|
+
self.im_type = __params['im_type'] if 'im_type' in __params else self.im_type
|
|
698
|
+
self.ivh = __params['ivh'] if 'ivh' in __params else self.ivh
|
|
699
|
+
self.n_algo = __params['n_algo'] if 'n_algo' in __params else self.n_algo
|
|
700
|
+
self.n_exp = __params['n_exp'] if 'n_exp' in __params else self.n_exp
|
|
701
|
+
self.n_gl = __params['n_gl'] if 'n_gl' in __params else self.n_gl
|
|
702
|
+
self.n_scale = __params['n_scale'] if 'n_scale' in __params else self.n_scale
|
|
703
|
+
self.outliers = __params['outliers'] if 'outliers' in __params else self.outliers
|
|
704
|
+
self.scale_non_text = __params['scale_non_text'] if 'scale_non_text' in __params else self.scale_non_text
|
|
705
|
+
self.scale_text = __params['scale_text'] if 'scale_text' in __params else self.scale_text
|
|
706
|
+
self.roi_interp = __params['roi_interp'] if 'roi_interp' in __params else self.roi_interp
|
|
707
|
+
self.roi_pv = __params['roi_pv'] if 'roi_pv' in __params else self.roi_pv
|
|
708
|
+
self.user_set_min_value = __params['user_set_min_value'] if 'user_set_min_value' in __params else self.user_set_min_value
|
|
709
|
+
self.vol_interp = __params['vol_interp'] if 'vol_interp' in __params else self.vol_interp
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
class Filter:
|
|
713
|
+
"""Organizes all filtering parameters"""
|
|
714
|
+
def __init__(self, filter_type: str = "") -> None:
|
|
715
|
+
"""
|
|
716
|
+
Constructor of the Filter class.
|
|
717
|
+
|
|
718
|
+
Args:
|
|
719
|
+
filter_type(str): Type of the filter that will be used (Must be 'mean', 'log', 'laws',
|
|
720
|
+
'gabor' or 'wavelet').
|
|
721
|
+
|
|
722
|
+
Returns:
|
|
723
|
+
None.
|
|
724
|
+
"""
|
|
725
|
+
self.filter_type = filter_type
|
|
726
|
+
self.mean = self.Mean()
|
|
727
|
+
self.log = self.Log()
|
|
728
|
+
self.gabor = self.Gabor()
|
|
729
|
+
self.laws = self.Laws()
|
|
730
|
+
self.wavelet = self.Wavelet()
|
|
731
|
+
self.textural = self.Textural()
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
class Mean:
|
|
735
|
+
"""Organizes the Mean filter parameters"""
|
|
736
|
+
def __init__(
|
|
737
|
+
self, ndims: int = 0, name_save: str = '',
|
|
738
|
+
padding: str = '', size: int = 0, orthogonal_rot: bool = False
|
|
739
|
+
) -> None:
|
|
740
|
+
"""
|
|
741
|
+
Constructor of the Mean class.
|
|
742
|
+
|
|
743
|
+
Args:
|
|
744
|
+
ndims(int): Filter dimension.
|
|
745
|
+
name_save(str): Specific name added to final extraction results file.
|
|
746
|
+
padding(str): padding mode.
|
|
747
|
+
size(int): Filter size.
|
|
748
|
+
|
|
749
|
+
Returns:
|
|
750
|
+
None.
|
|
751
|
+
"""
|
|
752
|
+
self.name_save = name_save
|
|
753
|
+
self.ndims = ndims
|
|
754
|
+
self.orthogonal_rot = orthogonal_rot
|
|
755
|
+
self.padding = padding
|
|
756
|
+
self.size = size
|
|
757
|
+
|
|
758
|
+
def init_from_json(self, params: Dict) -> None:
|
|
759
|
+
"""
|
|
760
|
+
Updates class attributes from json file.
|
|
761
|
+
|
|
762
|
+
Args:
|
|
763
|
+
params(Dict): Dictionary of the Mean filter parameters.
|
|
764
|
+
|
|
765
|
+
Returns:
|
|
766
|
+
None.
|
|
767
|
+
"""
|
|
768
|
+
self.name_save = params['name_save']
|
|
769
|
+
self.ndims = params['ndims']
|
|
770
|
+
self.padding = params['padding']
|
|
771
|
+
self.size = params['size']
|
|
772
|
+
self.orthogonal_rot = params['orthogonal_rot']
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
class Log:
|
|
776
|
+
"""Organizes the Log filter parameters"""
|
|
777
|
+
def __init__(
|
|
778
|
+
self, ndims: int = 0, sigma: float = 0.0,
|
|
779
|
+
padding: str = '', orthogonal_rot: bool = False,
|
|
780
|
+
name_save: str = ''
|
|
781
|
+
) -> None:
|
|
782
|
+
"""
|
|
783
|
+
Constructor of the Log class.
|
|
784
|
+
|
|
785
|
+
Args:
|
|
786
|
+
ndims(int): Filter dimension.
|
|
787
|
+
sigma(float): Float of the sigma value.
|
|
788
|
+
padding(str): padding mode.
|
|
789
|
+
orthogonal_rot(bool): If True will compute average response over orthogonal planes.
|
|
790
|
+
name_save(str): Specific name added to final extraction results file.
|
|
791
|
+
|
|
792
|
+
Returns:
|
|
793
|
+
None.
|
|
794
|
+
"""
|
|
795
|
+
self.name_save = name_save
|
|
796
|
+
self.ndims = ndims
|
|
797
|
+
self.orthogonal_rot = orthogonal_rot
|
|
798
|
+
self.padding = padding
|
|
799
|
+
self.sigma = sigma
|
|
800
|
+
|
|
801
|
+
def init_from_json(self, params: Dict) -> None:
|
|
802
|
+
"""
|
|
803
|
+
Updates class attributes from json file.
|
|
804
|
+
|
|
805
|
+
Args:
|
|
806
|
+
params(Dict): Dictionary of the Log filter parameters.
|
|
807
|
+
|
|
808
|
+
Returns:
|
|
809
|
+
None.
|
|
810
|
+
"""
|
|
811
|
+
self.name_save = params['name_save']
|
|
812
|
+
self.ndims = params['ndims']
|
|
813
|
+
self.orthogonal_rot = params['orthogonal_rot']
|
|
814
|
+
self.padding = params['padding']
|
|
815
|
+
self.sigma = params['sigma']
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
class Gabor:
|
|
819
|
+
"""Organizes the gabor filter parameters"""
|
|
820
|
+
def __init__(
|
|
821
|
+
self, sigma: float = 0.0, _lambda: float = 0.0,
|
|
822
|
+
gamma: float = 0.0, theta: str = '', rot_invariance: bool = False,
|
|
823
|
+
orthogonal_rot: bool= False, name_save: str = '',
|
|
824
|
+
padding: str = ''
|
|
825
|
+
) -> None:
|
|
826
|
+
"""
|
|
827
|
+
Constructor of the Gabor class.
|
|
828
|
+
|
|
829
|
+
Args:
|
|
830
|
+
sigma(float): Float of the sigma value.
|
|
831
|
+
_lambda(float): Float of the lambda value.
|
|
832
|
+
gamma(float): Float of the gamma value.
|
|
833
|
+
theta(str): String of the theta angle value.
|
|
834
|
+
rot_invariance(bool): If True the filter will be rotation invariant.
|
|
835
|
+
orthogonal_rot(bool): If True will compute average response over orthogonal planes.
|
|
836
|
+
name_save(str): Specific name added to final extraction results file.
|
|
837
|
+
padding(str): padding mode.
|
|
838
|
+
|
|
839
|
+
Returns:
|
|
840
|
+
None.
|
|
841
|
+
"""
|
|
842
|
+
self._lambda = _lambda
|
|
843
|
+
self.gamma = gamma
|
|
844
|
+
self.name_save = name_save
|
|
845
|
+
self.orthogonal_rot = orthogonal_rot
|
|
846
|
+
self.padding = padding
|
|
847
|
+
self.rot_invariance = rot_invariance
|
|
848
|
+
self.sigma = sigma
|
|
849
|
+
self.theta = theta
|
|
850
|
+
|
|
851
|
+
def init_from_json(self, params: Dict) -> None:
|
|
852
|
+
"""
|
|
853
|
+
Updates class attributes from json file.
|
|
854
|
+
|
|
855
|
+
Args:
|
|
856
|
+
params(Dict): Dictionary of the gabor filter parameters.
|
|
857
|
+
|
|
858
|
+
Returns:
|
|
859
|
+
None.
|
|
860
|
+
"""
|
|
861
|
+
self._lambda = params['lambda']
|
|
862
|
+
self.gamma = params['gamma']
|
|
863
|
+
self.name_save = params['name_save']
|
|
864
|
+
self.orthogonal_rot = params['orthogonal_rot']
|
|
865
|
+
self.padding = params['padding']
|
|
866
|
+
self.rot_invariance = params['rot_invariance']
|
|
867
|
+
self.sigma = params['sigma']
|
|
868
|
+
if type(params["theta"]) is str:
|
|
869
|
+
if params["theta"].lower().startswith('pi/'):
|
|
870
|
+
self.theta = np.pi / int(params["theta"].split('/')[1])
|
|
871
|
+
elif params["theta"].lower().startswith('-'):
|
|
872
|
+
if params["theta"].lower().startswith('-pi/'):
|
|
873
|
+
self.theta = -np.pi / int(params["theta"].split('/')[1])
|
|
874
|
+
else:
|
|
875
|
+
nom, denom = params["theta"].replace('-', '').replace('Pi', '').split('/')
|
|
876
|
+
self.theta = -np.pi*int(nom) / int(denom)
|
|
877
|
+
else:
|
|
878
|
+
self.theta = float(params["theta"])
|
|
879
|
+
|
|
880
|
+
|
|
881
|
+
class Laws:
|
|
882
|
+
"""Organizes the laws filter parameters"""
|
|
883
|
+
def __init__(
|
|
884
|
+
self, config: List = [], energy_distance: int = 0,
|
|
885
|
+
energy_image: bool = False, rot_invariance: bool = False,
|
|
886
|
+
orthogonal_rot: bool = False, name_save: str = '', padding: str = ''
|
|
887
|
+
) -> None:
|
|
888
|
+
"""
|
|
889
|
+
Constructor of the Laws class.
|
|
890
|
+
|
|
891
|
+
Args:
|
|
892
|
+
config(List): Configuration of the Laws filter, for ex: ['E5', 'L5', 'E5'].
|
|
893
|
+
energy_distance(int): Chebyshev distance.
|
|
894
|
+
energy_image(bool): If True will compute the Laws texture energy image.
|
|
895
|
+
rot_invariance(bool): If True the filter will be rotation invariant.
|
|
896
|
+
orthogonal_rot(bool): If True will compute average response over orthogonal planes.
|
|
897
|
+
name_save(str): Specific name added to final extraction results file.
|
|
898
|
+
padding(str): padding mode.
|
|
899
|
+
|
|
900
|
+
Returns:
|
|
901
|
+
None.
|
|
902
|
+
"""
|
|
903
|
+
self.config = config
|
|
904
|
+
self.energy_distance = energy_distance
|
|
905
|
+
self.energy_image = energy_image
|
|
906
|
+
self.name_save = name_save
|
|
907
|
+
self.orthogonal_rot = orthogonal_rot
|
|
908
|
+
self.padding = padding
|
|
909
|
+
self.rot_invariance = rot_invariance
|
|
910
|
+
|
|
911
|
+
def init_from_json(self, params: Dict) -> None:
|
|
912
|
+
"""
|
|
913
|
+
Updates class attributes from json file.
|
|
914
|
+
|
|
915
|
+
Args:
|
|
916
|
+
params(Dict): Dictionary of the laws filter parameters.
|
|
917
|
+
|
|
918
|
+
Returns:
|
|
919
|
+
None.
|
|
920
|
+
"""
|
|
921
|
+
self.config = params['config']
|
|
922
|
+
self.energy_distance = params['energy_distance']
|
|
923
|
+
self.energy_image = params['energy_image']
|
|
924
|
+
self.name_save = params['name_save']
|
|
925
|
+
self.orthogonal_rot = params['orthogonal_rot']
|
|
926
|
+
self.padding = params['padding']
|
|
927
|
+
self.rot_invariance = params['rot_invariance']
|
|
928
|
+
|
|
929
|
+
|
|
930
|
+
class Wavelet:
|
|
931
|
+
"""Organizes the Wavelet filter parameters"""
|
|
932
|
+
def __init__(
|
|
933
|
+
self, ndims: int = 0, name_save: str = '',
|
|
934
|
+
basis_function: str = '', subband: str = '', level: int = 0,
|
|
935
|
+
rot_invariance: bool = False, padding: str = ''
|
|
936
|
+
) -> None:
|
|
937
|
+
"""
|
|
938
|
+
Constructor of the Wavelet class.
|
|
939
|
+
|
|
940
|
+
Args:
|
|
941
|
+
ndims(int): Dimension of the filter.
|
|
942
|
+
name_save(str): Specific name added to final extraction results file.
|
|
943
|
+
basis_function(str): Wavelet basis function.
|
|
944
|
+
subband(str): Wavelet subband.
|
|
945
|
+
level(int): Decomposition level.
|
|
946
|
+
rot_invariance(bool): If True the filter will be rotation invariant.
|
|
947
|
+
padding(str): padding mode.
|
|
948
|
+
|
|
949
|
+
Returns:
|
|
950
|
+
None.
|
|
951
|
+
"""
|
|
952
|
+
self.basis_function = basis_function
|
|
953
|
+
self.level = level
|
|
954
|
+
self.ndims = ndims
|
|
955
|
+
self.name_save = name_save
|
|
956
|
+
self.padding = padding
|
|
957
|
+
self.rot_invariance = rot_invariance
|
|
958
|
+
self.subband = subband
|
|
959
|
+
|
|
960
|
+
def init_from_json(self, params: Dict) -> None:
|
|
961
|
+
"""
|
|
962
|
+
Updates class attributes from json file.
|
|
963
|
+
|
|
964
|
+
Args:
|
|
965
|
+
params(Dict): Dictionary of the wavelet filter parameters.
|
|
966
|
+
|
|
967
|
+
Returns:
|
|
968
|
+
None.
|
|
969
|
+
"""
|
|
970
|
+
self.basis_function = params['basis_function']
|
|
971
|
+
self.level = params['level']
|
|
972
|
+
self.ndims = params['ndims']
|
|
973
|
+
self.name_save = params['name_save']
|
|
974
|
+
self.padding = params['padding']
|
|
975
|
+
self.rot_invariance = params['rot_invariance']
|
|
976
|
+
self.subband = params['subband']
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
class Textural:
|
|
980
|
+
"""Organizes the Textural filters parameters"""
|
|
981
|
+
def __init__(
|
|
982
|
+
self,
|
|
983
|
+
family: str = '',
|
|
984
|
+
size: int = 0,
|
|
985
|
+
discretization: dict = {},
|
|
986
|
+
local: bool = False,
|
|
987
|
+
name_save: str = ''
|
|
988
|
+
) -> None:
|
|
989
|
+
"""
|
|
990
|
+
Constructor of the Textural class.
|
|
991
|
+
|
|
992
|
+
Args:
|
|
993
|
+
family (str, optional): The family of the textural filter.
|
|
994
|
+
size (int, optional): The filter size.
|
|
995
|
+
discretization (dict, optional): The discretization parameters.
|
|
996
|
+
local (bool, optional): If true, the discretization will be computed locally, else globally.
|
|
997
|
+
name_save (str, optional): Specific name added to final extraction results file.
|
|
998
|
+
|
|
999
|
+
Returns:
|
|
1000
|
+
None.
|
|
1001
|
+
"""
|
|
1002
|
+
self.family = family
|
|
1003
|
+
self.size = size
|
|
1004
|
+
self.discretization = discretization
|
|
1005
|
+
self.local = local
|
|
1006
|
+
self.name_save = name_save
|
|
1007
|
+
|
|
1008
|
+
def init_from_json(self, params: Dict) -> None:
|
|
1009
|
+
"""
|
|
1010
|
+
Updates class attributes from json file.
|
|
1011
|
+
|
|
1012
|
+
Args:
|
|
1013
|
+
params(Dict): Dictionary of the wavelet filter parameters.
|
|
1014
|
+
|
|
1015
|
+
Returns:
|
|
1016
|
+
None.
|
|
1017
|
+
"""
|
|
1018
|
+
self.family = params['family']
|
|
1019
|
+
self.size = params['size']
|
|
1020
|
+
self.discretization = params['discretization']
|
|
1021
|
+
self.local = params['local']
|
|
1022
|
+
self.name_save = params['name_save']
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
class Radiomics:
|
|
1026
|
+
"""Organizes the radiomics extraction parameters"""
|
|
1027
|
+
def __init__(self, **kwargs) -> None:
|
|
1028
|
+
"""
|
|
1029
|
+
Constructor of the Radiomics class.
|
|
1030
|
+
"""
|
|
1031
|
+
self.ih_name = kwargs['ih_name'] if 'ih_name' in kwargs else None
|
|
1032
|
+
self.ivh_name = kwargs['ivh_name'] if 'ivh_name' in kwargs else None
|
|
1033
|
+
self.glcm = self.GLCM()
|
|
1034
|
+
self.glrlm = self.GLRLM()
|
|
1035
|
+
self.ngtdm = self.NGTDM()
|
|
1036
|
+
self.name_text_types = kwargs['name_text_types'] if 'name_text_types' in kwargs else None
|
|
1037
|
+
self.processing_name = kwargs['processing_name'] if 'processing_name' in kwargs else None
|
|
1038
|
+
self.scale_name = kwargs['scale_name'] if 'scale_name' in kwargs else None
|
|
1039
|
+
self.extract = kwargs['extract'] if 'extract' in kwargs else {}
|
|
1040
|
+
|
|
1041
|
+
class GLCM:
|
|
1042
|
+
"""Organizes the GLCM features extraction parameters"""
|
|
1043
|
+
def __init__(
|
|
1044
|
+
self,
|
|
1045
|
+
dist_correction: Union[bool, str] = False,
|
|
1046
|
+
merge_method: str = "vol_merge"
|
|
1047
|
+
) -> None:
|
|
1048
|
+
"""
|
|
1049
|
+
Constructor of the GLCM class
|
|
1050
|
+
|
|
1051
|
+
Args:
|
|
1052
|
+
dist_correction(Union[bool, str]): norm for distance weighting, must be
|
|
1053
|
+
"manhattan", "euclidean" or "chebyshev". If True the norm for distance weighting
|
|
1054
|
+
is gonna be "euclidean".
|
|
1055
|
+
merge_method(str): merging method which determines how features are
|
|
1056
|
+
calculated. Must be "average", "slice_merge", "dir_merge" and "vol_merge".
|
|
1057
|
+
|
|
1058
|
+
Returns:
|
|
1059
|
+
None.
|
|
1060
|
+
"""
|
|
1061
|
+
self.dist_correction = dist_correction
|
|
1062
|
+
self.merge_method = merge_method
|
|
1063
|
+
|
|
1064
|
+
|
|
1065
|
+
class GLRLM:
|
|
1066
|
+
"""Organizes the GLRLM features extraction parameters"""
|
|
1067
|
+
def __init__(
|
|
1068
|
+
self,
|
|
1069
|
+
dist_correction: Union[bool, str] = False,
|
|
1070
|
+
merge_method: str = "vol_merge"
|
|
1071
|
+
) -> None:
|
|
1072
|
+
"""
|
|
1073
|
+
Constructor of the GLRLM class
|
|
1074
|
+
|
|
1075
|
+
Args:
|
|
1076
|
+
dist_correction(Union[bool, str]): If True the norm for distance weighting is gonna be "euclidean".
|
|
1077
|
+
merge_method(str): merging method which determines how features are
|
|
1078
|
+
calculated. Must be "average", "slice_merge", "dir_merge" and "vol_merge".
|
|
1079
|
+
|
|
1080
|
+
Returns:
|
|
1081
|
+
None.
|
|
1082
|
+
"""
|
|
1083
|
+
self.dist_correction = dist_correction
|
|
1084
|
+
self.merge_method = merge_method
|
|
1085
|
+
|
|
1086
|
+
|
|
1087
|
+
class NGTDM:
|
|
1088
|
+
"""Organizes the NGTDM features extraction parameters"""
|
|
1089
|
+
def __init__(
|
|
1090
|
+
self,
|
|
1091
|
+
dist_correction: Union[bool, str] = None
|
|
1092
|
+
) -> None:
|
|
1093
|
+
"""
|
|
1094
|
+
Constructor of the NGTDM class
|
|
1095
|
+
|
|
1096
|
+
Args:
|
|
1097
|
+
dist_correction(Union[bool, str]): If True the norm for distance weighting is gonna be "euclidean".
|
|
1098
|
+
|
|
1099
|
+
Returns:
|
|
1100
|
+
None.
|
|
1101
|
+
"""
|
|
1102
|
+
self.dist_correction = dist_correction
|
|
1103
|
+
|
|
1104
|
+
|
|
1105
|
+
class Radiomics:
|
|
1106
|
+
"""Organizes all the extracted features.
|
|
1107
|
+
"""
|
|
1108
|
+
def __init__(self, image: Dict = None, params: Dict = None) -> None:
|
|
1109
|
+
"""Constructor of the Radiomics class
|
|
1110
|
+
Args:
|
|
1111
|
+
image(Dict): Dict of the extracted features.
|
|
1112
|
+
params(Dict): Dict of the parameters used in features extraction (roi type, voxels diemension...)
|
|
1113
|
+
|
|
1114
|
+
Returns:
|
|
1115
|
+
None
|
|
1116
|
+
"""
|
|
1117
|
+
self.image = image if image else {}
|
|
1118
|
+
self.params = params if params else {}
|
|
1119
|
+
|
|
1120
|
+
def update_params(self, params: Dict) -> None:
|
|
1121
|
+
"""Updates `params` attribute from a given Dict
|
|
1122
|
+
Args:
|
|
1123
|
+
params(Dict): Dict of the parameters used in features extraction (roi type, voxels diemension...)
|
|
1124
|
+
|
|
1125
|
+
Returns:
|
|
1126
|
+
None
|
|
1127
|
+
"""
|
|
1128
|
+
self.params['roi_type'] = params['roi_type']
|
|
1129
|
+
self.params['patientID'] = params['patientID']
|
|
1130
|
+
self.params['vox_dim'] = params['vox_dim']
|
|
1131
|
+
|
|
1132
|
+
def to_json(self) -> Dict:
|
|
1133
|
+
"""Summarizes the class attributes in a Dict
|
|
1134
|
+
Args:
|
|
1135
|
+
None
|
|
1136
|
+
|
|
1137
|
+
Returns:
|
|
1138
|
+
Dict: Dictionay of radiomics structure (extracted features and extraction params)
|
|
1139
|
+
"""
|
|
1140
|
+
radiomics = {
|
|
1141
|
+
'image': self.image,
|
|
1142
|
+
'params': self.params
|
|
1143
|
+
}
|
|
1144
|
+
return radiomics
|
|
1145
|
+
|
|
1146
|
+
|
|
1147
|
+
class data:
|
|
1148
|
+
"""Organizes all imaging data (volume and ROI).
|
|
1149
|
+
|
|
1150
|
+
Attributes:
|
|
1151
|
+
volume (object): Instance of MEDscan.data.volume inner class.
|
|
1152
|
+
ROI (object): Instance of MEDscan.data.ROI inner class.
|
|
1153
|
+
orientation (str): Imaging data orientation (axial, sagittal or coronal).
|
|
1154
|
+
patient_position (str): Patient position specifies the position of the
|
|
1155
|
+
patient relative to the imaging equipment space (HFS, HFP...).
|
|
1156
|
+
|
|
1157
|
+
"""
|
|
1158
|
+
def __init__(self, orientation: str=None, patient_position: str=None) -> None:
|
|
1159
|
+
"""Constructor of the scan class
|
|
1160
|
+
|
|
1161
|
+
Args:
|
|
1162
|
+
orientation (str, optional): Imaging data orientation (axial, sagittal or coronal).
|
|
1163
|
+
patient_position (str, optional): Patient position specifies the position of the
|
|
1164
|
+
patient relative to the imaging equipment space (HFS, HFP...).
|
|
1165
|
+
|
|
1166
|
+
Returns:
|
|
1167
|
+
None.
|
|
1168
|
+
"""
|
|
1169
|
+
self.volume = self.volume()
|
|
1170
|
+
self.volume_process = self.volume_process()
|
|
1171
|
+
self.ROI = self.ROI()
|
|
1172
|
+
self.orientation = orientation
|
|
1173
|
+
self.patient_position = patient_position
|
|
1174
|
+
|
|
1175
|
+
def set_patient_position(self, patient_position):
|
|
1176
|
+
self.patient_position = patient_position
|
|
1177
|
+
|
|
1178
|
+
def set_orientation(self, orientation):
|
|
1179
|
+
self.orientation = orientation
|
|
1180
|
+
|
|
1181
|
+
def set_volume(self, volume):
|
|
1182
|
+
self.volume = volume
|
|
1183
|
+
|
|
1184
|
+
def set_ROI(self, *args):
|
|
1185
|
+
self.ROI = self.ROI(args)
|
|
1186
|
+
|
|
1187
|
+
def get_roi_from_indexes(self, key: int) -> np.ndarray:
|
|
1188
|
+
"""
|
|
1189
|
+
Extracts ROI data using the saved indexes (Indexes of non-null values).
|
|
1190
|
+
|
|
1191
|
+
Args:
|
|
1192
|
+
key (int): Key of ROI indexes list (A volume can have multiple ROIs).
|
|
1193
|
+
|
|
1194
|
+
Returns:
|
|
1195
|
+
ndarray: n-dimensional array of ROI data.
|
|
1196
|
+
|
|
1197
|
+
"""
|
|
1198
|
+
roi_volume = np.zeros_like(self.volume.array).flatten()
|
|
1199
|
+
roi_volume[self.ROI.get_indexes(key)] = 1
|
|
1200
|
+
return roi_volume.reshape(self.volume.array.shape)
|
|
1201
|
+
|
|
1202
|
+
def get_indexes_by_roi_name(self, roi_name : str) -> np.ndarray:
|
|
1203
|
+
"""
|
|
1204
|
+
Extract ROI data using the ROI name.
|
|
1205
|
+
|
|
1206
|
+
Args:
|
|
1207
|
+
roi_name (str): String of the ROI name (A volume can have multiple ROIs).
|
|
1208
|
+
|
|
1209
|
+
Returns:
|
|
1210
|
+
ndarray: n-dimensional array of the ROI data.
|
|
1211
|
+
|
|
1212
|
+
"""
|
|
1213
|
+
roi_name_key = list(self.ROI.roi_names.values()).index(roi_name)
|
|
1214
|
+
roi_volume = np.zeros_like(self.volume.array).flatten()
|
|
1215
|
+
roi_volume[self.ROI.get_indexes(roi_name_key)] = 1
|
|
1216
|
+
return roi_volume.reshape(self.volume.array.shape)
|
|
1217
|
+
|
|
1218
|
+
def display(self, _slice: int = None, roi: Union[str, int] = 0) -> None:
|
|
1219
|
+
"""Displays slices from imaging data with the ROI contour in XY-Plane.
|
|
1220
|
+
|
|
1221
|
+
Args:
|
|
1222
|
+
_slice (int, optional): Index of the slice you want to plot.
|
|
1223
|
+
roi (Union[str, int], optional): ROI name or index. If not specified will use the first ROI.
|
|
1224
|
+
|
|
1225
|
+
Returns:
|
|
1226
|
+
None.
|
|
1227
|
+
|
|
1228
|
+
"""
|
|
1229
|
+
# extract slices containing ROI
|
|
1230
|
+
size_m = self.volume.array.shape
|
|
1231
|
+
i = np.arange(0, size_m[0])
|
|
1232
|
+
j = np.arange(0, size_m[1])
|
|
1233
|
+
k = np.arange(0, size_m[2])
|
|
1234
|
+
ind_mask = np.nonzero(self.get_roi_from_indexes(roi))
|
|
1235
|
+
J, I, K = np.meshgrid(i, j, k, indexing='ij')
|
|
1236
|
+
I = I[ind_mask]
|
|
1237
|
+
J = J[ind_mask]
|
|
1238
|
+
K = K[ind_mask]
|
|
1239
|
+
slices = np.unique(K)
|
|
1240
|
+
|
|
1241
|
+
vol_data = self.volume.array.swapaxes(0, 1)[:, :, slices]
|
|
1242
|
+
roi_data = self.get_roi_from_indexes(roi).swapaxes(0, 1)[:, :, slices]
|
|
1243
|
+
|
|
1244
|
+
rows = int(np.round(np.sqrt(len(slices))))
|
|
1245
|
+
columns = int(np.ceil(len(slices) / rows))
|
|
1246
|
+
|
|
1247
|
+
plt.set_cmap(plt.gray())
|
|
1248
|
+
|
|
1249
|
+
# plot only one slice
|
|
1250
|
+
if _slice:
|
|
1251
|
+
fig, ax = plt.subplots(1, 1, figsize=(10, 5))
|
|
1252
|
+
ax.axis('off')
|
|
1253
|
+
ax.set_title(_slice)
|
|
1254
|
+
ax.imshow(vol_data[:, :, _slice])
|
|
1255
|
+
im = Image.fromarray((roi_data[:, :, _slice]))
|
|
1256
|
+
ax.contour(im, colors='red', linewidths=0.4, alpha=0.45)
|
|
1257
|
+
lps_ax = fig.add_subplot(1, columns, 1)
|
|
1258
|
+
|
|
1259
|
+
# plot multiple slices containing an ROI.
|
|
1260
|
+
else:
|
|
1261
|
+
fig, axs = plt.subplots(rows, columns+1, figsize=(20, 10))
|
|
1262
|
+
s = 0
|
|
1263
|
+
for i in range(0,rows):
|
|
1264
|
+
for j in range(0,columns):
|
|
1265
|
+
axs[i,j].axis('off')
|
|
1266
|
+
if s < len(slices):
|
|
1267
|
+
axs[i,j].set_title(str(s))
|
|
1268
|
+
axs[i,j].imshow(vol_data[:, :, s])
|
|
1269
|
+
im = Image.fromarray((roi_data[:, :, s]))
|
|
1270
|
+
axs[i,j].contour(im, colors='red', linewidths=0.4, alpha=0.45)
|
|
1271
|
+
s += 1
|
|
1272
|
+
axs[i,columns].axis('off')
|
|
1273
|
+
lps_ax = fig.add_subplot(1, columns+1, axs.shape[1])
|
|
1274
|
+
|
|
1275
|
+
fig.suptitle('XY-Plane')
|
|
1276
|
+
fig.tight_layout()
|
|
1277
|
+
|
|
1278
|
+
# add the coordinates system
|
|
1279
|
+
lps_ax.axis([-1.5, 1.5, -1.5, 1.5])
|
|
1280
|
+
lps_ax.set_title("Coordinates system")
|
|
1281
|
+
|
|
1282
|
+
lps_ax.quiver([-0.5], [0], [1.5], [0], scale_units='xy', angles='xy', scale=1.0, color='green')
|
|
1283
|
+
lps_ax.quiver([-0.5], [0], [0], [-1.5], scale_units='xy', angles='xy', scale=3, color='blue')
|
|
1284
|
+
lps_ax.quiver([-0.5], [0], [1.5], [1.5], scale_units='xy', angles='xy', scale=3, color='red')
|
|
1285
|
+
lps_ax.text(1.0, 0, "L")
|
|
1286
|
+
lps_ax.text(-0.3, -0.5, "P")
|
|
1287
|
+
lps_ax.text(0.3, 0.4, "S")
|
|
1288
|
+
|
|
1289
|
+
lps_ax.set_xticks([])
|
|
1290
|
+
lps_ax.set_yticks([])
|
|
1291
|
+
|
|
1292
|
+
plt.show()
|
|
1293
|
+
|
|
1294
|
+
def display_process(self, _slice: int = None, roi: Union[str, int] = 0) -> None:
|
|
1295
|
+
"""Displays slices from imaging data with the ROI contour in XY-Plane.
|
|
1296
|
+
|
|
1297
|
+
Args:
|
|
1298
|
+
_slice (int, optional): Index of the slice you want to plot.
|
|
1299
|
+
roi (Union[str, int], optional): ROI name or index. If not specified will use the first ROI.
|
|
1300
|
+
|
|
1301
|
+
Returns:
|
|
1302
|
+
None.
|
|
1303
|
+
|
|
1304
|
+
"""
|
|
1305
|
+
# extract slices containing ROI
|
|
1306
|
+
size_m = self.volume_process.array.shape
|
|
1307
|
+
i = np.arange(0, size_m[0])
|
|
1308
|
+
j = np.arange(0, size_m[1])
|
|
1309
|
+
k = np.arange(0, size_m[2])
|
|
1310
|
+
ind_mask = np.nonzero(self.get_roi_from_indexes(roi))
|
|
1311
|
+
J, I, K = np.meshgrid(j, i, k, indexing='ij')
|
|
1312
|
+
I = I[ind_mask]
|
|
1313
|
+
J = J[ind_mask]
|
|
1314
|
+
K = K[ind_mask]
|
|
1315
|
+
slices = np.unique(K)
|
|
1316
|
+
|
|
1317
|
+
vol_data = self.volume_process.array.swapaxes(0, 1)[:, :, slices]
|
|
1318
|
+
roi_data = self.get_roi_from_indexes(roi).swapaxes(0, 1)[:, :, slices]
|
|
1319
|
+
|
|
1320
|
+
rows = int(np.round(np.sqrt(len(slices))))
|
|
1321
|
+
columns = int(np.ceil(len(slices) / rows))
|
|
1322
|
+
|
|
1323
|
+
plt.set_cmap(plt.gray())
|
|
1324
|
+
|
|
1325
|
+
# plot only one slice
|
|
1326
|
+
if _slice:
|
|
1327
|
+
fig, ax = plt.subplots(1, 1, figsize=(10, 5))
|
|
1328
|
+
ax.axis('off')
|
|
1329
|
+
ax.set_title(_slice)
|
|
1330
|
+
ax.imshow(vol_data[:, :, _slice])
|
|
1331
|
+
im = Image.fromarray((roi_data[:, :, _slice]))
|
|
1332
|
+
ax.contour(im, colors='red', linewidths=0.4, alpha=0.45)
|
|
1333
|
+
lps_ax = fig.add_subplot(1, columns, 1)
|
|
1334
|
+
|
|
1335
|
+
# plot multiple slices containing an ROI.
|
|
1336
|
+
else:
|
|
1337
|
+
fig, axs = plt.subplots(rows, columns+1, figsize=(20, 10))
|
|
1338
|
+
s = 0
|
|
1339
|
+
for i in range(0,rows):
|
|
1340
|
+
for j in range(0,columns):
|
|
1341
|
+
axs[i,j].axis('off')
|
|
1342
|
+
if s < len(slices):
|
|
1343
|
+
axs[i,j].set_title(str(s))
|
|
1344
|
+
axs[i,j].imshow(vol_data[:, :, s])
|
|
1345
|
+
im = Image.fromarray((roi_data[:, :, s]))
|
|
1346
|
+
axs[i,j].contour(im, colors='red', linewidths=0.4, alpha=0.45)
|
|
1347
|
+
s += 1
|
|
1348
|
+
axs[i,columns].axis('off')
|
|
1349
|
+
lps_ax = fig.add_subplot(1, columns+1, axs.shape[1])
|
|
1350
|
+
|
|
1351
|
+
fig.suptitle('XY-Plane')
|
|
1352
|
+
fig.tight_layout()
|
|
1353
|
+
|
|
1354
|
+
# add the coordinates system
|
|
1355
|
+
lps_ax.axis([-1.5, 1.5, -1.5, 1.5])
|
|
1356
|
+
lps_ax.set_title("Coordinates system")
|
|
1357
|
+
|
|
1358
|
+
lps_ax.quiver([-0.5], [0], [1.5], [0], scale_units='xy', angles='xy', scale=1.0, color='green')
|
|
1359
|
+
lps_ax.quiver([-0.5], [0], [0], [-1.5], scale_units='xy', angles='xy', scale=3, color='blue')
|
|
1360
|
+
lps_ax.quiver([-0.5], [0], [1.5], [1.5], scale_units='xy', angles='xy', scale=3, color='red')
|
|
1361
|
+
lps_ax.text(1.0, 0, "L")
|
|
1362
|
+
lps_ax.text(-0.3, -0.5, "P")
|
|
1363
|
+
lps_ax.text(0.3, 0.4, "S")
|
|
1364
|
+
|
|
1365
|
+
lps_ax.set_xticks([])
|
|
1366
|
+
lps_ax.set_yticks([])
|
|
1367
|
+
|
|
1368
|
+
plt.show()
|
|
1369
|
+
|
|
1370
|
+
|
|
1371
|
+
class volume:
|
|
1372
|
+
"""Organizes all volume data and information related to imaging volume.
|
|
1373
|
+
|
|
1374
|
+
Attributes:
|
|
1375
|
+
spatialRef (imref3d): Imaging data orientation (axial, sagittal or coronal).
|
|
1376
|
+
scan_rot (ndarray): Array of the rotation applied to the XYZ points of the ROI.
|
|
1377
|
+
array (ndarray): n-dimensional of the imaging data.
|
|
1378
|
+
|
|
1379
|
+
"""
|
|
1380
|
+
def __init__(self, spatialRef: imref3d=None, scan_rot: str=None, array: np.ndarray=None) -> None:
|
|
1381
|
+
"""Organizes all volume data and information.
|
|
1382
|
+
|
|
1383
|
+
Args:
|
|
1384
|
+
spatialRef (imref3d, optional): Imaging data orientation (axial, sagittal or coronal).
|
|
1385
|
+
scan_rot (ndarray, optional): Array of the rotation applied to the XYZ points of the ROI.
|
|
1386
|
+
array (ndarray, optional): n-dimensional of the imaging data.
|
|
1387
|
+
|
|
1388
|
+
"""
|
|
1389
|
+
self.spatialRef = spatialRef
|
|
1390
|
+
self.scan_rot = scan_rot
|
|
1391
|
+
self.array = array
|
|
1392
|
+
|
|
1393
|
+
def update_spatialRef(self, spatialRef_value):
|
|
1394
|
+
self.spatialRef = spatialRef_value
|
|
1395
|
+
|
|
1396
|
+
def update_scan_rot(self, scan_rot_value):
|
|
1397
|
+
self.scan_rot = scan_rot_value
|
|
1398
|
+
|
|
1399
|
+
def update_transScanToModel(self, transScanToModel_value):
|
|
1400
|
+
self.transScanToModel = transScanToModel_value
|
|
1401
|
+
|
|
1402
|
+
def update_array(self, array):
|
|
1403
|
+
self.array = array
|
|
1404
|
+
|
|
1405
|
+
def convert_to_LPS(self):
|
|
1406
|
+
"""Convert Imaging data to LPS (Left-Posterior-Superior) coordinates system.
|
|
1407
|
+
<https://www.slicer.org/wiki/Coordinate_systems>.
|
|
1408
|
+
|
|
1409
|
+
Returns:
|
|
1410
|
+
None.
|
|
1411
|
+
|
|
1412
|
+
"""
|
|
1413
|
+
# flip x
|
|
1414
|
+
self.array = np.flip(self.array, 0)
|
|
1415
|
+
# flip y
|
|
1416
|
+
self.array = np.flip(self.array, 1)
|
|
1417
|
+
|
|
1418
|
+
def spatialRef_from_nifti(self, nifti_image_path: Union[Path, str]) -> None:
|
|
1419
|
+
"""Computes the imref3d spatialRef using a NIFTI file and
|
|
1420
|
+
updates the `spatialRef` attribute.
|
|
1421
|
+
|
|
1422
|
+
Args:
|
|
1423
|
+
nifti_image_path (str): String of the NIFTI file path.
|
|
1424
|
+
|
|
1425
|
+
Returns:
|
|
1426
|
+
None.
|
|
1427
|
+
|
|
1428
|
+
"""
|
|
1429
|
+
# Loading the nifti file:
|
|
1430
|
+
nifti_image_path = Path(nifti_image_path)
|
|
1431
|
+
nifti = nib.load(nifti_image_path)
|
|
1432
|
+
nifti_data = self.array
|
|
1433
|
+
|
|
1434
|
+
# spatialRef Creation
|
|
1435
|
+
pixelX = nifti.affine[0, 0]
|
|
1436
|
+
pixelY = nifti.affine[1, 1]
|
|
1437
|
+
sliceS = nifti.affine[2, 2]
|
|
1438
|
+
min_grid = nifti.affine[:3, 3]
|
|
1439
|
+
min_Xgrid = min_grid[0]
|
|
1440
|
+
min_Ygrid = min_grid[1]
|
|
1441
|
+
min_Zgrid = min_grid[2]
|
|
1442
|
+
size_image = np.shape(nifti_data)
|
|
1443
|
+
spatialRef = imref3d(size_image, abs(pixelX), abs(pixelY), abs(sliceS))
|
|
1444
|
+
spatialRef.XWorldLimits = (np.array(spatialRef.XWorldLimits) -
|
|
1445
|
+
(spatialRef.XWorldLimits[0] -
|
|
1446
|
+
(min_Xgrid-pixelX/2))
|
|
1447
|
+
).tolist()
|
|
1448
|
+
spatialRef.YWorldLimits = (np.array(spatialRef.YWorldLimits) -
|
|
1449
|
+
(spatialRef.YWorldLimits[0] -
|
|
1450
|
+
(min_Ygrid-pixelY/2))
|
|
1451
|
+
).tolist()
|
|
1452
|
+
spatialRef.ZWorldLimits = (np.array(spatialRef.ZWorldLimits) -
|
|
1453
|
+
(spatialRef.ZWorldLimits[0] -
|
|
1454
|
+
(min_Zgrid-sliceS/2))
|
|
1455
|
+
).tolist()
|
|
1456
|
+
|
|
1457
|
+
# Converting the results into lists
|
|
1458
|
+
spatialRef.ImageSize = spatialRef.ImageSize.tolist()
|
|
1459
|
+
spatialRef.XIntrinsicLimits = spatialRef.XIntrinsicLimits.tolist()
|
|
1460
|
+
spatialRef.YIntrinsicLimits = spatialRef.YIntrinsicLimits.tolist()
|
|
1461
|
+
spatialRef.ZIntrinsicLimits = spatialRef.ZIntrinsicLimits.tolist()
|
|
1462
|
+
|
|
1463
|
+
# update spatialRef
|
|
1464
|
+
self.update_spatialRef(spatialRef)
|
|
1465
|
+
|
|
1466
|
+
def convert_spatialRef(self):
|
|
1467
|
+
"""converts the `spatialRef` attribute from RAS to LPS coordinates system.
|
|
1468
|
+
<https://www.slicer.org/wiki/Coordinate_systems>.
|
|
1469
|
+
|
|
1470
|
+
Args:
|
|
1471
|
+
None.
|
|
1472
|
+
|
|
1473
|
+
Returns:
|
|
1474
|
+
None.
|
|
1475
|
+
|
|
1476
|
+
"""
|
|
1477
|
+
# swap x and y data
|
|
1478
|
+
temp = self.spatialRef.ImageExtentInWorldX
|
|
1479
|
+
self.spatialRef.ImageExtentInWorldX = self.spatialRef.ImageExtentInWorldY
|
|
1480
|
+
self.spatialRef.ImageExtentInWorldY = temp
|
|
1481
|
+
|
|
1482
|
+
temp = self.spatialRef.PixelExtentInWorldX
|
|
1483
|
+
self.spatialRef.PixelExtentInWorldX = self.spatialRef.PixelExtentInWorldY
|
|
1484
|
+
self.spatialRef.PixelExtentInWorldY = temp
|
|
1485
|
+
|
|
1486
|
+
temp = self.spatialRef.XIntrinsicLimits
|
|
1487
|
+
self.spatialRef.XIntrinsicLimits = self.spatialRef.YIntrinsicLimits
|
|
1488
|
+
self.spatialRef.YIntrinsicLimits = temp
|
|
1489
|
+
|
|
1490
|
+
temp = self.spatialRef.XWorldLimits
|
|
1491
|
+
self.spatialRef.XWorldLimits = self.spatialRef.YWorldLimits
|
|
1492
|
+
self.spatialRef.YWorldLimits = temp
|
|
1493
|
+
del temp
|
|
1494
|
+
|
|
1495
|
+
class volume_process:
|
|
1496
|
+
"""Organizes all volume data and information.
|
|
1497
|
+
|
|
1498
|
+
Attributes:
|
|
1499
|
+
spatialRef (imref3d): Imaging data orientation (axial, sagittal or coronal).
|
|
1500
|
+
scan_rot (ndarray): Array of the rotation applied to the XYZ points of the ROI.
|
|
1501
|
+
data (ndarray): n-dimensional of the imaging data.
|
|
1502
|
+
|
|
1503
|
+
"""
|
|
1504
|
+
def __init__(self, spatialRef: imref3d = None,
|
|
1505
|
+
scan_rot: List = None, array: np.ndarray = None,
|
|
1506
|
+
user_string: str = "") -> None:
|
|
1507
|
+
"""Organizes all volume data and information.
|
|
1508
|
+
|
|
1509
|
+
Args:
|
|
1510
|
+
spatialRef (imref3d, optional): Imaging data orientation (axial, sagittal or coronal).
|
|
1511
|
+
scan_rot (ndarray, optional): Array of the rotation applied to the XYZ points of the ROI.
|
|
1512
|
+
array (ndarray, optional): n-dimensional of the imaging data.
|
|
1513
|
+
user_string(str, optional): string explaining the processed data in the class.
|
|
1514
|
+
|
|
1515
|
+
Returns:
|
|
1516
|
+
None.
|
|
1517
|
+
|
|
1518
|
+
"""
|
|
1519
|
+
self.array = array
|
|
1520
|
+
self.scan_rot = scan_rot
|
|
1521
|
+
self.spatialRef = spatialRef
|
|
1522
|
+
self.user_string = user_string
|
|
1523
|
+
|
|
1524
|
+
def update_processed_data(self, array: np.ndarray, user_string: str = "") -> None:
|
|
1525
|
+
if user_string:
|
|
1526
|
+
self.user_string = user_string
|
|
1527
|
+
self.array = array
|
|
1528
|
+
|
|
1529
|
+
def save(self, name_save: str, path_save: Union[Path, str])-> None:
|
|
1530
|
+
"""Saves the processed data locally.
|
|
1531
|
+
|
|
1532
|
+
Args:
|
|
1533
|
+
name_save(str): Saving name of the processed data.
|
|
1534
|
+
path_save(Union[Path, str]): Path to where save the processed data.
|
|
1535
|
+
|
|
1536
|
+
Returns:
|
|
1537
|
+
None.
|
|
1538
|
+
"""
|
|
1539
|
+
path_save = Path(path_save)
|
|
1540
|
+
if not name_save:
|
|
1541
|
+
name_save = self.user_string
|
|
1542
|
+
|
|
1543
|
+
if not name_save.endswith('.npy'):
|
|
1544
|
+
name_save += '.npy'
|
|
1545
|
+
|
|
1546
|
+
with open(path_save / name_save, 'wb') as f:
|
|
1547
|
+
np.save(f, self.array)
|
|
1548
|
+
|
|
1549
|
+
def load(
|
|
1550
|
+
self,
|
|
1551
|
+
file_name: str,
|
|
1552
|
+
loading_path: Union[Path, str],
|
|
1553
|
+
update: bool=True
|
|
1554
|
+
) -> Union[None, np.ndarray]:
|
|
1555
|
+
"""Saves the processed data locally.
|
|
1556
|
+
|
|
1557
|
+
Args:
|
|
1558
|
+
file_name(str): Name file of the processed data to load.
|
|
1559
|
+
loading_path(Union[Path, str]): Path to the processed data to load.
|
|
1560
|
+
update(bool, optional): If True, updates the class attrtibutes with loaded data.
|
|
1561
|
+
|
|
1562
|
+
Returns:
|
|
1563
|
+
None.
|
|
1564
|
+
"""
|
|
1565
|
+
loading_path = Path(loading_path)
|
|
1566
|
+
|
|
1567
|
+
if not file_name.endswith('.npy'):
|
|
1568
|
+
file_name += '.npy'
|
|
1569
|
+
|
|
1570
|
+
with open(loading_path / file_name, 'rb') as f:
|
|
1571
|
+
if update:
|
|
1572
|
+
self.update_processed_data(np.load(f, allow_pickle=True))
|
|
1573
|
+
else:
|
|
1574
|
+
return np.load(f, allow_pickle=True)
|
|
1575
|
+
|
|
1576
|
+
|
|
1577
|
+
class ROI:
|
|
1578
|
+
"""Organizes all ROI data and information.
|
|
1579
|
+
|
|
1580
|
+
Attributes:
|
|
1581
|
+
indexes (Dict): Dict of the ROI indexes for each ROI name.
|
|
1582
|
+
roi_names (Dict): Dict of the ROI names.
|
|
1583
|
+
nameSet (Dict): Dict of the User-defined name for Structure Set for each ROI name.
|
|
1584
|
+
nameSetInfo (Dict): Dict of the names of the structure sets that define the areas of
|
|
1585
|
+
significance. Either 'StructureSetName', 'StructureSetDescription', 'SeriesDescription'
|
|
1586
|
+
or 'SeriesInstanceUID'.
|
|
1587
|
+
|
|
1588
|
+
"""
|
|
1589
|
+
def __init__(self, indexes: Dict=None, roi_names: Dict=None) -> None:
|
|
1590
|
+
"""Constructor of the ROI class.
|
|
1591
|
+
|
|
1592
|
+
Args:
|
|
1593
|
+
indexes (Dict, optional): Dict of the ROI indexes for each ROI name.
|
|
1594
|
+
roi_names (Dict, optional): Dict of the ROI names.
|
|
1595
|
+
|
|
1596
|
+
Returns:
|
|
1597
|
+
None.
|
|
1598
|
+
"""
|
|
1599
|
+
self.indexes = indexes if indexes else {}
|
|
1600
|
+
self.roi_names = roi_names if roi_names else {}
|
|
1601
|
+
self.nameSet = roi_names if roi_names else {}
|
|
1602
|
+
self.nameSetInfo = roi_names if roi_names else {}
|
|
1603
|
+
|
|
1604
|
+
def get_indexes(self, key):
|
|
1605
|
+
if not self.indexes or key is None:
|
|
1606
|
+
return {}
|
|
1607
|
+
else:
|
|
1608
|
+
return self.indexes[str(key)]
|
|
1609
|
+
|
|
1610
|
+
def get_roi_name(self, key):
|
|
1611
|
+
if not self.roi_names or key is None:
|
|
1612
|
+
return {}
|
|
1613
|
+
else:
|
|
1614
|
+
return self.roi_names[str(key)]
|
|
1615
|
+
|
|
1616
|
+
def get_name_set(self, key):
|
|
1617
|
+
if not self.nameSet or key is None:
|
|
1618
|
+
return {}
|
|
1619
|
+
else:
|
|
1620
|
+
return self.nameSet[str(key)]
|
|
1621
|
+
|
|
1622
|
+
def get_name_set_info(self, key):
|
|
1623
|
+
if not self.nameSetInfo or key is None:
|
|
1624
|
+
return {}
|
|
1625
|
+
else:
|
|
1626
|
+
return self.nameSetInfo[str(key)]
|
|
1627
|
+
|
|
1628
|
+
def update_indexes(self, key, indexes):
|
|
1629
|
+
try:
|
|
1630
|
+
self.indexes[str(key)] = indexes
|
|
1631
|
+
except:
|
|
1632
|
+
Warning.warn("Wrong key given in update_indexes()")
|
|
1633
|
+
|
|
1634
|
+
def update_roi_name(self, key, roi_name):
|
|
1635
|
+
try:
|
|
1636
|
+
self.roi_names[str(key)] = roi_name
|
|
1637
|
+
except:
|
|
1638
|
+
Warning.warn("Wrong key given in update_roi_name()")
|
|
1639
|
+
|
|
1640
|
+
def update_name_set(self, key, name_set):
|
|
1641
|
+
try:
|
|
1642
|
+
self.nameSet[str(key)] = name_set
|
|
1643
|
+
except:
|
|
1644
|
+
Warning.warn("Wrong key given in update_name_set()")
|
|
1645
|
+
|
|
1646
|
+
def update_name_set_info(self, key, nameSetInfo):
|
|
1647
|
+
try:
|
|
1648
|
+
self.nameSetInfo[str(key)] = nameSetInfo
|
|
1649
|
+
except:
|
|
1650
|
+
Warning.warn("Wrong key given in update_name_set_info()")
|
|
1651
|
+
|
|
1652
|
+
def convert_to_LPS(self, data: np.ndarray) -> np.ndarray:
|
|
1653
|
+
"""Converts the given volume to LPS coordinates system. For
|
|
1654
|
+
more details please refer here : https://www.slicer.org/wiki/Coordinate_systems
|
|
1655
|
+
Args:
|
|
1656
|
+
data(ndarray) : Volume data in RAS to convert to to LPS
|
|
1657
|
+
|
|
1658
|
+
Returns:
|
|
1659
|
+
ndarray: n-dimensional of `data` in LPS.
|
|
1660
|
+
"""
|
|
1661
|
+
# flip x
|
|
1662
|
+
data = np.flip(data, 0)
|
|
1663
|
+
# flip y
|
|
1664
|
+
data = np.flip(data, 1)
|
|
1665
|
+
|
|
1666
|
+
return data
|
|
1667
|
+
|
|
1668
|
+
def get_roi_from_path(self, roi_path: Union[Path, str], id: str):
|
|
1669
|
+
"""Extracts all ROI data from the given path for the given
|
|
1670
|
+
patient ID and updates all class attributes with the new extracted data.
|
|
1671
|
+
|
|
1672
|
+
Args:
|
|
1673
|
+
roi_path(Union[Path, str]): Path where the ROI data is stored.
|
|
1674
|
+
id(str): ID containing patient ID and the modality type, to identify the right file.
|
|
1675
|
+
|
|
1676
|
+
Returns:
|
|
1677
|
+
None.
|
|
1678
|
+
"""
|
|
1679
|
+
self.indexes = {}
|
|
1680
|
+
self.roi_names = {}
|
|
1681
|
+
self.nameSet = {}
|
|
1682
|
+
self.nameSetInfo = {}
|
|
1683
|
+
roi_index = 0
|
|
1684
|
+
list_of_patients = os.listdir(roi_path)
|
|
1685
|
+
|
|
1686
|
+
for file in list_of_patients:
|
|
1687
|
+
# Load the patient's ROI nifti files :
|
|
1688
|
+
if file.startswith(id) and file.endswith('nii.gz') and 'ROI' in file.split("."):
|
|
1689
|
+
roi = nib.load(roi_path + "/" + file)
|
|
1690
|
+
roi_data = self.convert_to_LPS(data=roi.get_fdata())
|
|
1691
|
+
roi_name = file[file.find("(")+1 : file.find(")")]
|
|
1692
|
+
name_set = file[file.find("_")+2 : file.find("(")]
|
|
1693
|
+
self.update_indexes(key=roi_index, indexes=np.nonzero(roi_data.flatten()))
|
|
1694
|
+
self.update_name_set(key=roi_index, name_set=name_set)
|
|
1695
|
+
self.update_roi_name(key=roi_index, roi_name=roi_name)
|
|
1696
|
+
roi_index += 1
|