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.
Files changed (78) hide show
  1. MEDiml/MEDscan.py +1696 -0
  2. MEDiml/__init__.py +21 -0
  3. MEDiml/biomarkers/BatchExtractor.py +806 -0
  4. MEDiml/biomarkers/BatchExtractorTexturalFilters.py +840 -0
  5. MEDiml/biomarkers/__init__.py +16 -0
  6. MEDiml/biomarkers/diagnostics.py +125 -0
  7. MEDiml/biomarkers/get_oriented_bound_box.py +158 -0
  8. MEDiml/biomarkers/glcm.py +1602 -0
  9. MEDiml/biomarkers/gldzm.py +523 -0
  10. MEDiml/biomarkers/glrlm.py +1315 -0
  11. MEDiml/biomarkers/glszm.py +555 -0
  12. MEDiml/biomarkers/int_vol_hist.py +527 -0
  13. MEDiml/biomarkers/intensity_histogram.py +615 -0
  14. MEDiml/biomarkers/local_intensity.py +89 -0
  15. MEDiml/biomarkers/morph.py +1756 -0
  16. MEDiml/biomarkers/ngldm.py +780 -0
  17. MEDiml/biomarkers/ngtdm.py +414 -0
  18. MEDiml/biomarkers/stats.py +373 -0
  19. MEDiml/biomarkers/utils.py +389 -0
  20. MEDiml/filters/TexturalFilter.py +299 -0
  21. MEDiml/filters/__init__.py +9 -0
  22. MEDiml/filters/apply_filter.py +134 -0
  23. MEDiml/filters/gabor.py +215 -0
  24. MEDiml/filters/laws.py +283 -0
  25. MEDiml/filters/log.py +147 -0
  26. MEDiml/filters/mean.py +121 -0
  27. MEDiml/filters/textural_filters_kernels.py +1738 -0
  28. MEDiml/filters/utils.py +107 -0
  29. MEDiml/filters/wavelet.py +237 -0
  30. MEDiml/learning/DataCleaner.py +198 -0
  31. MEDiml/learning/DesignExperiment.py +480 -0
  32. MEDiml/learning/FSR.py +667 -0
  33. MEDiml/learning/Normalization.py +112 -0
  34. MEDiml/learning/RadiomicsLearner.py +714 -0
  35. MEDiml/learning/Results.py +2237 -0
  36. MEDiml/learning/Stats.py +694 -0
  37. MEDiml/learning/__init__.py +10 -0
  38. MEDiml/learning/cleaning_utils.py +107 -0
  39. MEDiml/learning/ml_utils.py +1015 -0
  40. MEDiml/processing/__init__.py +6 -0
  41. MEDiml/processing/compute_suv_map.py +121 -0
  42. MEDiml/processing/discretisation.py +149 -0
  43. MEDiml/processing/interpolation.py +275 -0
  44. MEDiml/processing/resegmentation.py +66 -0
  45. MEDiml/processing/segmentation.py +912 -0
  46. MEDiml/utils/__init__.py +25 -0
  47. MEDiml/utils/batch_patients.py +45 -0
  48. MEDiml/utils/create_radiomics_table.py +131 -0
  49. MEDiml/utils/data_frame_export.py +42 -0
  50. MEDiml/utils/find_process_names.py +16 -0
  51. MEDiml/utils/get_file_paths.py +34 -0
  52. MEDiml/utils/get_full_rad_names.py +21 -0
  53. MEDiml/utils/get_institutions_from_ids.py +16 -0
  54. MEDiml/utils/get_patient_id_from_scan_name.py +22 -0
  55. MEDiml/utils/get_patient_names.py +26 -0
  56. MEDiml/utils/get_radiomic_names.py +27 -0
  57. MEDiml/utils/get_scan_name_from_rad_name.py +22 -0
  58. MEDiml/utils/image_reader_SITK.py +37 -0
  59. MEDiml/utils/image_volume_obj.py +22 -0
  60. MEDiml/utils/imref.py +340 -0
  61. MEDiml/utils/initialize_features_names.py +62 -0
  62. MEDiml/utils/inpolygon.py +159 -0
  63. MEDiml/utils/interp3.py +43 -0
  64. MEDiml/utils/json_utils.py +78 -0
  65. MEDiml/utils/mode.py +31 -0
  66. MEDiml/utils/parse_contour_string.py +58 -0
  67. MEDiml/utils/save_MEDscan.py +30 -0
  68. MEDiml/utils/strfind.py +32 -0
  69. MEDiml/utils/textureTools.py +188 -0
  70. MEDiml/utils/texture_features_names.py +115 -0
  71. MEDiml/utils/write_radiomics_csv.py +47 -0
  72. MEDiml/wrangling/DataManager.py +1724 -0
  73. MEDiml/wrangling/ProcessDICOM.py +512 -0
  74. MEDiml/wrangling/__init__.py +3 -0
  75. mediml-0.9.9.dist-info/LICENSE.md +674 -0
  76. mediml-0.9.9.dist-info/METADATA +232 -0
  77. mediml-0.9.9.dist-info/RECORD +78 -0
  78. 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