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
@@ -0,0 +1,1602 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+
5
+ from copy import deepcopy
6
+ from typing import Dict, List, Union, List
7
+
8
+ import numpy as np
9
+ import pandas as pd
10
+
11
+ from ..utils.textureTools import (coord2index, get_neighbour_direction,
12
+ get_value, is_list_all_none)
13
+
14
+
15
+ def get_matrix(roi_only: np.ndarray,
16
+ levels: Union[np.ndarray, List],
17
+ dist_correction=True) -> np.ndarray:
18
+ r"""
19
+ This function computes the Gray-Level Co-occurence Matrix (GLCM) of the
20
+ region of interest (ROI) of an input volume. The input volume is assumed
21
+ to be isotropically resampled. Only one GLCM is computed per scan,
22
+ simultaneously recording (i.e. adding up) the neighboring properties of
23
+ the 26-connected neighbors of all voxels in the ROI. To account for
24
+ discretization length differences, neighbors at a distance of :math:`\sqrt{3}`
25
+ voxels around a center voxel increment the GLCM by a value of :math:`\sqrt{3}`,
26
+ neighbors at a distance of :math:`\sqrt{2}` voxels around a center voxel increment
27
+ the GLCM by a value of :math:`\sqrt{2}`, and neighbors at a distance of 1 voxels
28
+ around a center voxel increment the GLCM by a value of 1.
29
+ This matrix refers to "Grey level co-occurrence based features" (ID = LFYI)
30
+ in the `IBSI1 reference manual <https://arxiv.org/pdf/1612.07003.pdf>`_.
31
+
32
+ Args:
33
+ roi_only (ndarray): Smallest box containing the ROI, with the imaging data
34
+ ready for texture analysis computations. Voxels outside the ROI are
35
+ set to NaNs.
36
+ levels (ndarray or List): Vector containing the quantized gray-levels in the tumor region
37
+ (or reconstruction ``levels`` of quantization).
38
+ dist_correction (bool, optional): Set this variable to true in order to use
39
+ discretization length difference corrections as used by the `Institute of Physics and
40
+ Engineering in Medicine <https://doi.org/10.1088/0031-9155/60/14/5471>`_.
41
+ Set this variable to false to replicate IBSI results.
42
+
43
+ Returns:
44
+ ndarray: Gray-Level Co-occurence Matrix of ``roi_only``.
45
+
46
+ References:
47
+ [1] Haralick, R. M., Shanmugam, K., & Dinstein, I. (1973). Textural \
48
+ features for image classification. IEEE Transactions on Systems, \
49
+ Man and Cybernetics, smc 3(6), 610–621.
50
+ """
51
+ # PARSING "dist_correction" ARGUMENT
52
+ if type(dist_correction) is not bool:
53
+ # The user did not input either "true" or "false",
54
+ # so the default behavior is used.
55
+ dist_correction = True
56
+
57
+ # PRELIMINARY
58
+ roi_only = roi_only.copy()
59
+ level_temp = np.max(levels)+1
60
+ roi_only[np.isnan(roi_only)] = level_temp
61
+ #levels = np.append(levels, level_temp)
62
+ dim = np.shape(roi_only)
63
+
64
+ if np.ndim(roi_only) == 2:
65
+ dim[2] = 1
66
+
67
+ q2 = np.reshape(roi_only, (1, np.prod(dim)))
68
+
69
+ # QUANTIZATION EFFECTS CORRECTION (M. Vallieres)
70
+ # In case (for example) we initially wanted to have 64 levels, but due to
71
+ # quantization, only 60 resulted.
72
+ # qs = round(levels*adjust)/adjust;
73
+ # q2 = round(q2*adjust)/adjust;
74
+
75
+ #qs = levels
76
+ qs = levels.tolist() + [level_temp]
77
+ lqs = np.size(qs)
78
+
79
+ q3 = q2*0
80
+ for k in range(0, lqs):
81
+ q3[q2 == qs[k]] = k
82
+
83
+ q3 = np.reshape(q3, dim).astype(int)
84
+ GLCM = np.zeros((lqs, lqs))
85
+
86
+ for i in range(1, dim[0]+1):
87
+ i_min = max(1, i-1)
88
+ i_max = min(i+1, dim[0])
89
+ for j in range(1, dim[1]+1):
90
+ j_min = max(1, j-1)
91
+ j_max = min(j+1, dim[1])
92
+ for k in range(1, dim[2]+1):
93
+ k_min = max(1, k-1)
94
+ k_max = min(k+1, dim[2])
95
+ val_q3 = q3[i-1, j-1, k-1]
96
+ for I2 in range(i_min, i_max+1):
97
+ for J2 in range(j_min, j_max+1):
98
+ for K2 in range(k_min, k_max+1):
99
+ if (I2 == i) & (J2 == j) & (K2 == k):
100
+ continue
101
+ else:
102
+ val_neighbor = q3[I2-1, J2-1, K2-1]
103
+ if dist_correction:
104
+ # Discretization length correction
105
+ GLCM[val_q3, val_neighbor] += \
106
+ np.sqrt(abs(I2-i)+abs(J2-j)+abs(K2-k))
107
+ else:
108
+ GLCM[val_q3, val_neighbor] += 1
109
+
110
+ GLCM = GLCM[0:-1, 0:-1]
111
+
112
+ return GLCM
113
+
114
+ def joint_max(glcm_dict: Dict) -> Union[float, List[float]]:
115
+ """Computes joint maximum features.
116
+ This feature refers to "Fcm_joint_max" (ID = GYBY) in the `IBSI1 reference \
117
+ manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
118
+
119
+ Args:
120
+ glcm_dict (Dict): dictionary with glcm matrices, generated using :func:`get_glcm_matrices`
121
+
122
+ Returns:
123
+ Union[float, List[float]]:: List or float of the joint maximum feature(s)
124
+ """
125
+ temp = []
126
+ joint_max = []
127
+ for key in glcm_dict.keys():
128
+ for glcm in glcm_dict[key]:
129
+ df_pij, _, _, _, _, _ = glcm.get_cm_data([np.nan, np.nan])
130
+ temp.append(np.max(df_pij.pij))
131
+ if len(glcm_dict) <= 1:
132
+ return sum(temp) / len(temp)
133
+ else:
134
+ print(f'Merge method: {key}, joint max: {sum(temp) / len(temp)}')
135
+ joint_max.append(sum(temp) / len(temp))
136
+ return joint_max
137
+
138
+ def joint_avg(glcm_dict: Dict) -> Union[float, List[float]]:
139
+ """Computes joint average features.
140
+ This feature refers to "Fcm_joint_avg" (ID = 60VM) in the `IBSI1 reference \
141
+ manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
142
+
143
+ Args:
144
+ glcm_dict (Dict): dictionary with glcm matrices, generated using :func:`get_glcm_matrices`
145
+
146
+ Returns:
147
+ Union[float, List[float]]:: List or float of the joint average feature(s)
148
+ """
149
+ temp = []
150
+ joint_avg = []
151
+ for key in glcm_dict.keys():
152
+ for glcm in glcm_dict[key]:
153
+ df_pij, _, _, _, _, _ = glcm.get_cm_data([np.nan, np.nan])
154
+ temp.append(np.sum(df_pij.i * df_pij.pij))
155
+ if len(glcm_dict) <= 1:
156
+ return sum(temp) / len(temp)
157
+ else:
158
+ print(f'Merge method: {key}, joint avg: {sum(temp) / len(temp)}')
159
+ joint_avg.append(sum(temp) / len(temp))
160
+ return joint_avg
161
+
162
+ def joint_var(glcm_dict: Dict) -> Union[float, List[float]]:
163
+ """Computes joint variance features.
164
+ This feature refers to "Fcm_var" (ID = UR99) in the `IBSI1 reference \
165
+ manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
166
+
167
+ Args:
168
+ glcm_dict (Dict): dictionary with glcm matrices, generated using :func:`get_glcm_matrices`
169
+
170
+ Returns:
171
+ Union[float, List[float]]: List or float of the joint variance feature(s)
172
+ """
173
+ temp = []
174
+ joint_var = []
175
+ for key in glcm_dict.keys():
176
+ for glcm in glcm_dict[key]:
177
+ df_pij, _, _, _, _, _ = glcm.get_cm_data([np.nan, np.nan])
178
+ m_u = np.sum(df_pij.i * df_pij.pij)
179
+ temp.append(np.sum((df_pij.i - m_u) ** 2.0 * df_pij.pij))
180
+ if len(glcm_dict) <= 1:
181
+ return sum(temp) / len(temp)
182
+ else:
183
+ print(f'Merge method: {key}, joint var: {sum(temp) / len(temp)}')
184
+ joint_var.append(sum(temp) / len(temp))
185
+ return joint_var
186
+
187
+ def joint_entr(glcm_dict: Dict) -> Union[float, List[float]]:
188
+ """Computes joint entropy features.
189
+ This feature refers to "Fcm_joint_entr" (ID = TU9B) in the `IBSI1 reference \
190
+ manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
191
+
192
+ Args:
193
+ glcm_dict (Dict): dictionary with glcm matrices, generated using :func:`get_glcm_matrices`
194
+
195
+ Returns:
196
+ Union[float, List[float]]: the joint entropy features
197
+ """
198
+ temp = []
199
+ joint_entr = []
200
+ for key in glcm_dict.keys():
201
+ for glcm in glcm_dict[key]:
202
+ df_pij, _, _, _, _, _ = glcm.get_cm_data([np.nan, np.nan])
203
+ temp.append(-np.sum(df_pij.pij * np.log2(df_pij.pij)))
204
+ if len(glcm_dict) <= 1:
205
+ return sum(temp) / len(temp)
206
+ else:
207
+ print(f'Merge method: {key}, joint entr: {sum(temp) / len(temp)}')
208
+ joint_entr.append(sum(temp) / len(temp))
209
+ return joint_entr
210
+
211
+ def diff_avg(glcm_dict: Dict) -> Union[float, List[float]]:
212
+ """Computes difference average features.
213
+ This feature refers to "Fcm_diff_avg" (ID = TF7R) in the `IBSI1 reference \
214
+ manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
215
+
216
+ Args:
217
+ glcm_dict (Dict): dictionary with glcm matrices, generated using :func:`get_glcm_matrices`
218
+
219
+ Returns:
220
+ Union[float, List[float]]: the difference average features
221
+ """
222
+ temp = []
223
+ diff_avg = []
224
+ for key in glcm_dict.keys():
225
+ for glcm in glcm_dict[key]:
226
+ _, _, _, df_pimj, _, _ = glcm.get_cm_data([np.nan, np.nan])
227
+ temp.append(np.sum(df_pimj.k * df_pimj.pimj))
228
+ if len(glcm_dict) <= 1:
229
+ return sum(temp) / len(temp)
230
+ else:
231
+ print(f'Merge method: {key}, diff avg: {sum(temp) / len(temp)}')
232
+ diff_avg.append(sum(temp) / len(temp))
233
+ return diff_avg
234
+
235
+ def diff_var(glcm_dict: Dict) -> Union[float, List[float]]:
236
+ """Computes difference variance features.
237
+ This feature refers to "Fcm_diff_var" (ID = D3YU) in the `IBSI1 reference \
238
+ manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
239
+
240
+ Args:
241
+ glcm_dict (Dict): dictionary with glcm matrices, generated using :func:`get_glcm_matrices`
242
+
243
+ Returns:
244
+ Union[float, List[float]]: the difference variance features
245
+ """
246
+ temp = []
247
+ diff_var = []
248
+ for key in glcm_dict.keys():
249
+ for glcm in glcm_dict[key]:
250
+ _, _, _, df_pimj, _, _ = glcm.get_cm_data([np.nan, np.nan])
251
+ m_u = np.sum(df_pimj.k * df_pimj.pimj)
252
+ temp.append(np.sum((df_pimj.k - m_u) ** 2.0 * df_pimj.pimj))
253
+ if len(glcm_dict) <= 1:
254
+ return sum(temp) / len(temp)
255
+ else:
256
+ print(f'Merge method: {key}, diff var: {sum(temp) / len(temp)}')
257
+ diff_var.append(sum(temp) / len(temp))
258
+ return diff_var
259
+
260
+ def diff_entr(glcm_dict: Dict) -> Union[float, List[float]]:
261
+ """Computes difference entropy features.
262
+ This feature refers to "Fcm_diff_entr" (ID = NTRS) in the `IBSI1 reference \
263
+ manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
264
+
265
+ Args:
266
+ glcm_dict (Dict): dictionary with glcm matrices, generated using :func:`get_glcm_matrices`
267
+
268
+ Returns:
269
+ Union[float, List[float]]: the difference entropy features
270
+ """
271
+ temp = []
272
+ diff_entr = []
273
+ for key in glcm_dict.keys():
274
+ for glcm in glcm_dict[key]:
275
+ _, _, _, df_pimj, _, _ = glcm.get_cm_data([np.nan, np.nan])
276
+ temp.append(-np.sum(df_pimj.pimj * np.log2(df_pimj.pimj)))
277
+ if len(glcm_dict) <= 1:
278
+ return sum(temp) / len(temp)
279
+ else:
280
+ print(f'Merge method: {key}, diff entr: {sum(temp) / len(temp)}')
281
+ diff_entr.append(sum(temp) / len(temp))
282
+ return diff_entr
283
+
284
+ def sum_avg(glcm_dict: Dict) -> Union[float, List[float]]:
285
+ """Computes sum average features.
286
+ This feature refers to "Fcm_sum_avg" (ID = ZGXS) in the `IBSI1 reference \
287
+ manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
288
+
289
+ Args:
290
+ glcm_dict (Dict): dictionary with glcm matrices, generated using :func:`get_glcm_matrices`
291
+
292
+ Returns:
293
+ Union[float, List[float]]: the sum average features
294
+ """
295
+ temp = []
296
+ sum_avg = []
297
+ for key in glcm_dict.keys():
298
+ for glcm in glcm_dict[key]:
299
+ _, _, _, _, df_pipj, _ = glcm.get_cm_data([np.nan, np.nan])
300
+ temp.append(np.sum(df_pipj.k * df_pipj.pipj))
301
+ if len(glcm_dict) <= 1:
302
+ return sum(temp) / len(temp)
303
+ else:
304
+ print(f'Merge method: {key}, sum avg: {sum(temp) / len(temp)}')
305
+ sum_avg.append(sum(temp) / len(temp))
306
+ return sum_avg
307
+
308
+ def sum_var(glcm_dict: Dict) -> Union[float, List[float]]:
309
+ """Computes sum variance features.
310
+ This feature refers to "Fcm_sum_var" (ID = OEEB) in the `IBSI1 reference \
311
+ manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
312
+
313
+ Args:
314
+ glcm_dict (Dict): dictionary with glcm matrices, generated using :func:`get_glcm_matrices`
315
+
316
+ Returns:
317
+ Union[float, List[float]]: the sum variance features
318
+ """
319
+ temp = []
320
+ sum_var = []
321
+ for key in glcm_dict.keys():
322
+ for glcm in glcm_dict[key]:
323
+ _, _, _, _, df_pipj, _ = glcm.get_cm_data([np.nan, np.nan])
324
+ m_u = np.sum(df_pipj.k * df_pipj.pipj)
325
+ temp.append(np.sum((df_pipj.k - m_u) ** 2.0 * df_pipj.pipj))
326
+ if len(glcm_dict) <= 1:
327
+ return sum(temp) / len(temp)
328
+ else:
329
+ print(f'Merge method: {key}, sum var: {sum(temp) / len(temp)}')
330
+ sum_var.append(sum(temp) / len(temp))
331
+ return sum_var
332
+
333
+ def sum_entr(glcm_dict: Dict) -> Union[float, List[float]]:
334
+ """Computes sum entropy features.
335
+ This feature refers to "Fcm_sum_entr" (ID = P6QZ) in the `IBSI1 reference \
336
+ manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
337
+
338
+ Args:
339
+ glcm_dict (Dict): dictionary with glcm matrices, generated using :func:`get_glcm_matrices`
340
+
341
+ Returns:
342
+ Union[float, List[float]]: the sum entropy features
343
+ """
344
+ temp = []
345
+ sum_entr = []
346
+ for key in glcm_dict.keys():
347
+ for glcm in glcm_dict[key]:
348
+ _, _, _, _, df_pipj, _ = glcm.get_cm_data([np.nan, np.nan])
349
+ temp.append(-np.sum(df_pipj.pipj * np.log2(df_pipj.pipj)))
350
+ if len(glcm_dict) <= 1:
351
+ return sum(temp) / len(temp)
352
+ else:
353
+ print(f'Merge method: {key}, sum entr: {sum(temp) / len(temp)}')
354
+ sum_entr.append(sum(temp) / len(temp))
355
+ return sum_entr
356
+
357
+ def energy(glcm_dict: Dict) -> Union[float, List[float]]:
358
+ """Computes angular second moment features.
359
+ This feature refers to "Fcm_energy" (ID = 8ZQL) in the `IBSI1 reference \
360
+ manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
361
+
362
+ Args:
363
+ glcm_dict (Dict): dictionary with glcm matrices, generated using :func:`get_glcm_matrices`
364
+
365
+ Returns:
366
+ Union[float, List[float]]: the angular second moment features
367
+ """
368
+ temp = []
369
+ energy = []
370
+ for key in glcm_dict.keys():
371
+ for glcm in glcm_dict[key]:
372
+ df_pij, _, _, _, _, _ = glcm.get_cm_data([np.nan, np.nan])
373
+ temp.append(np.sum(df_pij.pij ** 2.0))
374
+ if len(glcm_dict) <= 1:
375
+ return sum(temp) / len(temp)
376
+ else:
377
+ print(f'Merge method: {key}, energy: {sum(temp) / len(temp)}')
378
+ energy.append(sum(temp) / len(temp))
379
+ return energy
380
+
381
+ def contrast(glcm_dict: Dict) -> Union[float, List[float]]:
382
+ """Computes constrast features.
383
+ This feature refers to "Fcm_contrast" (ID = ACUI) in the `IBSI1 reference \
384
+ manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
385
+
386
+ Args:
387
+ glcm_dict (Dict): dictionary with glcm matrices, generated using :func:`get_glcm_matrices`
388
+
389
+ Returns:
390
+ Union[float, List[float]]: the contrast features
391
+ """
392
+ temp = []
393
+ contrast = []
394
+ for key in glcm_dict.keys():
395
+ for glcm in glcm_dict[key]:
396
+ df_pij, _, _, _, _, _ = glcm.get_cm_data([np.nan, np.nan])
397
+ temp.append(np.sum((df_pij.i - df_pij.j) ** 2.0 * df_pij.pij))
398
+ if len(glcm_dict) <= 1:
399
+ return sum(temp) / len(temp)
400
+ else:
401
+ print(f'Merge method: {key}, contrast: {sum(temp) / len(temp)}')
402
+ contrast.append(sum(temp) / len(temp))
403
+ return contrast
404
+
405
+ def dissimilarity(glcm_dict: Dict) -> Union[float, List[float]]:
406
+ """Computes dissimilarity features.
407
+ This feature refers to "Fcm_dissimilarity" (ID = 8S9J) in the `IBSI1 reference \
408
+ manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
409
+
410
+ Args:
411
+ glcm_dict (Dict): dictionary with glcm matrices, generated using :func:`get_glcm_matrices`
412
+
413
+ Returns:
414
+ Union[float, List[float]]: the dissimilarity features
415
+ """
416
+ temp = []
417
+ dissimilarity = []
418
+ for key in glcm_dict.keys():
419
+ for glcm in glcm_dict[key]:
420
+ df_pij, _, _, _, _, _ = glcm.get_cm_data([np.nan, np.nan])
421
+ temp.append(np.sum(np.abs(df_pij.i - df_pij.j) * df_pij.pij))
422
+ if len(glcm_dict) <= 1:
423
+ return sum(temp) / len(temp)
424
+ else:
425
+ print(f'Merge method: {key}, dissimilarity: {sum(temp) / len(temp)}')
426
+ dissimilarity.append(sum(temp) / len(temp))
427
+ return dissimilarity
428
+
429
+ def inv_diff(glcm_dict: Dict) -> Union[float, List[float]]:
430
+ """Computes inverse difference features.
431
+ This feature refers to "Fcm_inv_diff" (ID = IB1Z) in the `IBSI1 reference \
432
+ manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
433
+
434
+ Args:
435
+ glcm_dict (Dict): dictionary with glcm matrices, generated using :func:`get_glcm_matrices`
436
+
437
+ Returns:
438
+ Union[float, List[float]]: the inverse difference features
439
+ """
440
+ temp = []
441
+ inv_diff = []
442
+ for key in glcm_dict.keys():
443
+ for glcm in glcm_dict[key]:
444
+ df_pij, _, _, _, _, _ = glcm.get_cm_data([np.nan, np.nan])
445
+ temp.append(np.sum(df_pij.pij / (1.0 + np.abs(df_pij.i - df_pij.j))))
446
+ if len(glcm_dict) <= 1:
447
+ return sum(temp) / len(temp)
448
+ else:
449
+ print(f'Merge method: {key}, inv diff: {sum(temp) / len(temp)}')
450
+ inv_diff.append(sum(temp) / len(temp))
451
+ return inv_diff
452
+
453
+ def inv_diff_norm(glcm_dict: Dict) -> Union[float, List[float]]:
454
+ """Computes inverse difference normalized features.
455
+ This feature refers to "Fcm_inv_diff_norm" (ID = NDRX) in the `IBSI1 reference \
456
+ manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
457
+
458
+ Args:
459
+ glcm_dict (Dict): dictionary with glcm matrices, generated using :func:`get_glcm_matrices`
460
+
461
+ Returns:
462
+ Union[float, List[float]]: the inverse difference normalized features
463
+ """
464
+ temp = []
465
+ inv_diff_norm = []
466
+ for key in glcm_dict.keys():
467
+ for glcm in glcm_dict[key]:
468
+ df_pij, _, _, _, _, n_g = glcm.get_cm_data([np.nan, np.nan])
469
+ temp.append(np.sum(df_pij.pij / (1.0 + np.abs(df_pij.i - df_pij.j) / n_g)))
470
+ if len(glcm_dict) <= 1:
471
+ return sum(temp) / len(temp)
472
+ else:
473
+ print(f'Merge method: {key}, inv diff norm: {sum(temp) / len(temp)}')
474
+ inv_diff_norm.append(sum(temp) / len(temp))
475
+ return inv_diff_norm
476
+
477
+ def inv_diff_mom(glcm_dict: Dict) -> Union[float, List[float]]:
478
+ """Computes inverse difference moment features.
479
+ This feature refers to "Fcm_inv_diff_mom" (ID = WF0Z) in the `IBSI1 reference \
480
+ manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
481
+
482
+ Args:
483
+ glcm_dict (Dict): dictionary with glcm matrices, generated using :func:`get_glcm_matrices`
484
+
485
+ Returns:
486
+ Union[float, List[float]]: the inverse difference moment features
487
+ """
488
+ temp = []
489
+ inv_diff_mom = []
490
+ for key in glcm_dict.keys():
491
+ for glcm in glcm_dict[key]:
492
+ df_pij, _, _, _, _, _ = glcm.get_cm_data([np.nan, np.nan])
493
+ temp.append(np.sum(df_pij.pij / (1.0 + (df_pij.i - df_pij.j) ** 2.0)))
494
+ if len(glcm_dict) <= 1:
495
+ return sum(temp) / len(temp)
496
+ else:
497
+ print(f'Merge method: {key}, inv diff mom: {sum(temp) / len(temp)}')
498
+ inv_diff_mom.append(sum(temp) / len(temp))
499
+ return inv_diff_mom
500
+
501
+ def inv_diff_mom_norm(glcm_dict: Dict) -> Union[float, List[float]]:
502
+ """Computes inverse difference moment normalized features.
503
+ This feature refers to "Fcm_inv_diff_mom_norm" (ID = 1QCO) in the `IBSI1 reference \
504
+ manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
505
+
506
+ Args:
507
+ glcm_dict (Dict): dictionary with glcm matrices, generated using :func:`get_glcm_matrices`
508
+
509
+ Returns:
510
+ Union[float, List[float]]: the inverse difference moment normalized features
511
+ """
512
+ temp = []
513
+ inv_diff_mom_norm = []
514
+ for key in glcm_dict.keys():
515
+ for glcm in glcm_dict[key]:
516
+ df_pij, _, _, _, _, n_g = glcm.get_cm_data([np.nan, np.nan])
517
+ temp.append(np.sum(df_pij.pij / (1.0 + (df_pij.i - df_pij.j)** 2.0 / n_g ** 2.0)))
518
+ if len(glcm_dict) <= 1:
519
+ return sum(temp) / len(temp)
520
+ else:
521
+ print(f'Merge method: {key}, inv diff mom norm: {sum(temp) / len(temp)}')
522
+ inv_diff_mom_norm.append(sum(temp) / len(temp))
523
+ return inv_diff_mom_norm
524
+
525
+ def inv_var(glcm_dict: Dict) -> Union[float, List[float]]:
526
+ """Computes inverse variance features.
527
+ This feature refers to "Fcm_inv_var" (ID = E8JP) in the `IBSI1 reference \
528
+ manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
529
+
530
+ Args:
531
+ glcm_dict (Dict): dictionary with glcm matrices, generated using :func:`get_glcm_matrices`
532
+
533
+ Returns:
534
+ Union[float, List[float]]: the inverse variance features
535
+ """
536
+ temp = []
537
+ inv_var = []
538
+ for key in glcm_dict.keys():
539
+ for glcm in glcm_dict[key]:
540
+ df_pij, df_pi, _, _, _, _ = glcm.get_cm_data([np.nan, np.nan])
541
+ mu_marg = np.sum(df_pi.i * df_pi.pi)
542
+ var_marg = np.sum((df_pi.i - mu_marg) ** 2.0 * df_pi.pi)
543
+ if var_marg == 0.0:
544
+ temp.append(1.0)
545
+ else:
546
+ temp.append(1.0 / var_marg * (np.sum(df_pij.i * df_pij.j * df_pij.pij) - mu_marg ** 2.0))
547
+ if len(glcm_dict) <= 1:
548
+ return sum(temp) / len(temp)
549
+ else:
550
+ print(f'Merge method: {key}, inv var: {sum(temp) / len(temp)}')
551
+ inv_var.append(sum(temp) / len(temp))
552
+ return inv_var
553
+
554
+ def corr(glcm_dict: Dict) -> Union[float, List[float]]:
555
+ """Computes correlation features.
556
+ This feature refers to "Fcm_corr" (ID = NI2N) in the `IBSI1 reference \
557
+ manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
558
+
559
+ Args:
560
+ glcm_dict (Dict): dictionary with glcm matrices, generated using :func:`get_glcm_matrices`
561
+
562
+ Returns:
563
+ Union[float, List[float]]: the correlation features
564
+ """
565
+ temp = []
566
+ corr = []
567
+ for key in glcm_dict.keys():
568
+ for glcm in glcm_dict[key]:
569
+ df_pij, df_pi, _, _, _, _ = glcm.get_cm_data([np.nan, np.nan])
570
+ mu_marg = np.sum(df_pi.i * df_pi.pi)
571
+ var_marg = np.sum((df_pi.i - mu_marg) ** 2.0 * df_pi.pi)
572
+ if var_marg == 0.0:
573
+ temp.append(1.0)
574
+ else:
575
+ temp.append(1.0 / var_marg * (np.sum(df_pij.i * df_pij.j * df_pij.pij) - mu_marg ** 2.0))
576
+ if len(glcm_dict) <= 1:
577
+ return sum(temp) / len(temp)
578
+ else:
579
+ print(f'Merge method: {key}, corr: {sum(temp) / len(temp)}')
580
+ corr.append(sum(temp) / len(temp))
581
+ return corr
582
+
583
+
584
+ def auto_corr(glcm_dict: Dict) -> Union[float, List[float]]:
585
+ """Computes autocorrelation features.
586
+ This feature refers to "Fcm_auto_corr" (ID = QWB0) in the `IBSI1 reference \
587
+ manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
588
+
589
+ Args:
590
+ glcm_dict (Dict): dictionary with glcm matrices, generated using :func:`get_glcm_matrices`
591
+
592
+ Returns:
593
+ Union[float, List[float]]: the autocorrelation features
594
+ """
595
+ temp = []
596
+ auto_corr = []
597
+ for key in glcm_dict.keys():
598
+ for glcm in glcm_dict[key]:
599
+ df_pij, _, _, _, _, _ = glcm.get_cm_data([np.nan, np.nan])
600
+ temp.append(np.sum(df_pij.i * df_pij.j * df_pij.pij))
601
+ if len(glcm_dict) <= 1:
602
+ return sum(temp) / len(temp)
603
+ else:
604
+ print(f'Merge method: {key}, auto corr: {sum(temp) / len(temp)}')
605
+ auto_corr.append(sum(temp) / len(temp))
606
+ return auto_corr
607
+
608
+ def info_corr1(glcm_dict: Dict) -> Union[float, List[float]]:
609
+ """Computes information correlation 1 features.
610
+ This feature refers to "Fcm_info_corr1" (ID = R8DG) in the `IBSI1 reference \
611
+ manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
612
+
613
+ Args:
614
+ glcm_dict (Dict): dictionary with glcm matrices, generated using :func:`get_glcm_matrices`
615
+
616
+ Returns:
617
+ Union[float, List[float]]: the information correlation 1 features
618
+ """
619
+ temp = []
620
+ info_corr1 = []
621
+ for key in glcm_dict.keys():
622
+ for glcm in glcm_dict[key]:
623
+ df_pij, df_pi, _, _, _, _ = glcm.get_cm_data([np.nan, np.nan])
624
+ hxy = -np.sum(df_pij.pij * np.log2(df_pij.pij))
625
+ hxy_1 = -np.sum(df_pij.pij * np.log2(df_pij.pi * df_pij.pj))
626
+ hx = -np.sum(df_pi.pi * np.log2(df_pi.pi))
627
+ if len(df_pij) == 1 or hx == 0.0:
628
+ temp.append(1.0)
629
+ else:
630
+ temp.append((hxy - hxy_1) / hx)
631
+ if len(glcm_dict) <= 1:
632
+ return sum(temp) / len(temp)
633
+ else:
634
+ print(f'Merge method: {key}, info corr 1: {sum(temp) / len(temp)}')
635
+ info_corr1.append(sum(temp) / len(temp))
636
+ return info_corr1
637
+
638
+ def info_corr2(glcm_dict: Dict) -> Union[float, List[float]]:
639
+ """Computes information correlation 2 features - Note: iteration over combinations of i and j
640
+ This feature refers to "Fcm_info_corr2" (ID = JN9H) in the `IBSI1 reference \
641
+ manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
642
+
643
+ Args:
644
+ glcm_dict (Dict): dictionary with glcm matrices, generated using :func:`get_glcm_matrices`
645
+
646
+ Returns:
647
+ Union[float, List[float]]: the information correlation 2 features
648
+ """
649
+ temp = []
650
+ info_corr2 = []
651
+ for key in glcm_dict.keys():
652
+ for glcm in glcm_dict[key]:
653
+ df_pij, df_pi, df_pj, _, _, _ = glcm.get_cm_data([np.nan, np.nan])
654
+ hxy = - np.sum(df_pij.pij * np.log2(df_pij.pij))
655
+ hxy_2 = - np.sum(
656
+ np.tile(df_pi.pi, len(df_pj)) * np.repeat(df_pj.pj, len(df_pi)) * \
657
+ np.log2(np.tile(df_pi.pi, len(df_pj)) * np.repeat(df_pj.pj, len(df_pi)))
658
+ )
659
+ if hxy_2 < hxy:
660
+ temp.append(0)
661
+ else:
662
+ temp.append(np.sqrt(1 - np.exp(-2.0 * (hxy_2 - hxy))))
663
+ if len(glcm_dict) <= 1:
664
+ return sum(temp) / len(temp)
665
+ else:
666
+ print(f'Merge method: {key}, info corr 2: {sum(temp) / len(temp)}')
667
+ info_corr2.append(sum(temp) / len(temp))
668
+ return info_corr2
669
+
670
+ def clust_tend(glcm_dict: Dict) -> Union[float, List[float]]:
671
+ """Computes cluster tendency features.
672
+ This feature refers to "Fcm_clust_tend" (ID = DG8W) in the `IBSI1 reference \
673
+ manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
674
+
675
+ Args:
676
+ glcm_dict (Dict): dictionary with glcm matrices, generated using :func:`get_glcm_matrices`
677
+
678
+ Returns:
679
+ Union[float, List[float]]: the cluster tendency features
680
+ """
681
+ temp = []
682
+ clust_tend = []
683
+ for key in glcm_dict.keys():
684
+ for glcm in glcm_dict[key]:
685
+ df_pij, df_pi, _, _, _, _ = glcm.get_cm_data([np.nan, np.nan])
686
+ m_u = np.sum(df_pi.i * df_pi.pi)
687
+ temp.append(np.sum((df_pij.i + df_pij.j - 2 * m_u) ** 2.0 * df_pij.pij))
688
+ if len(glcm_dict) <= 1:
689
+ return sum(temp) / len(temp)
690
+ else:
691
+ print(f'Merge method: {key}, clust tend: {sum(temp) / len(temp)}')
692
+ clust_tend.append(sum(temp) / len(temp))
693
+ return clust_tend
694
+
695
+ def clust_shade(glcm_dict: Dict) -> Union[float, List[float]]:
696
+ """Computes cluster shade features.
697
+ This feature refers to "Fcm_clust_shade" (ID = 7NFM) in the `IBSI1 reference \
698
+ manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
699
+
700
+ Args:
701
+ glcm_dict (Dict): dictionary with glcm matrices, generated using :func:`get_glcm_matrices`
702
+
703
+ Returns:
704
+ Union[float, List[float]]: the cluster shade features
705
+ """
706
+ temp = []
707
+ clust_shade = []
708
+ for key in glcm_dict.keys():
709
+ for glcm in glcm_dict[key]:
710
+ df_pij, df_pi, _, _, _, _ = glcm.get_cm_data([np.nan, np.nan])
711
+ m_u = np.sum(df_pi.i * df_pi.pi)
712
+ temp.append(np.sum((df_pij.i + df_pij.j - 2 * m_u) ** 3.0 * df_pij.pij))
713
+ if len(glcm_dict) <= 1:
714
+ return sum(temp) / len(temp)
715
+ else:
716
+ print(f'Merge method: {key}, clust shade: {sum(temp) / len(temp)}')
717
+ clust_shade.append(sum(temp) / len(temp))
718
+ return clust_shade
719
+
720
+ def clust_prom(glcm_dict: Dict) -> Union[float, List[float]]:
721
+ """Computes cluster prominence features.
722
+ This feature refers to "Fcm_clust_prom" (ID = AE86) in the `IBSI1 reference \
723
+ manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
724
+
725
+ Args:
726
+ glcm_dict (Dict): dictionary with glcm matrices, generated using :func:`get_glcm_matrices`
727
+
728
+ Returns:
729
+ Union[float, List[float]]: the cluster prominence features
730
+ """
731
+ temp = []
732
+ clust_prom = []
733
+ for key in glcm_dict.keys():
734
+ for glcm in glcm_dict[key]:
735
+ df_pij, df_pi, _, _, _, _ = glcm.get_cm_data([np.nan, np.nan])
736
+ m_u = np.sum(df_pi.i * df_pi.pi)
737
+ temp.append(np.sum((df_pij.i + df_pij.j - 2 * m_u) ** 4.0 * df_pij.pij))
738
+ if len(glcm_dict) <= 1:
739
+ return sum(temp) / len(temp)
740
+ else:
741
+ print(f'Merge method: {key}, clust prom: {sum(temp) / len(temp)}')
742
+ clust_prom.append(sum(temp) / len(temp))
743
+ return clust_prom
744
+
745
+ def extract_all(vol, dist_correction=None, merge_method="vol_merge") -> Dict:
746
+ """Computes glcm features.
747
+ This features refer to Glcm family in the `IBSI1 reference \
748
+ manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
749
+
750
+ Args:
751
+ vol (ndarray): 3D volume, isotropically resampled, quantized
752
+ (e.g. n_g = 32, levels = [1, ..., n_g]), with NaNs outside the region
753
+ of interest.
754
+ dist_correction (Union[bool, str], optional): Set this variable to true in order to use
755
+ discretization length difference corrections as used by the `Institute of Physics and
756
+ Engineering in Medicine <https://doi.org/10.1088/0031-9155/60/14/5471>`__.
757
+ Set this variable to false to replicate IBSI results.
758
+ Or use string and specify the norm for distance weighting. Weighting is
759
+ only performed if this argument is "manhattan", "euclidean" or "chebyshev".
760
+ merge_method (str, optional): merging ``method`` which determines how features are
761
+ calculated. One of "average", "slice_merge", "dir_merge" and "vol_merge".
762
+ Note that not all combinations of spatial and merge ``method`` are valid.
763
+ method (str, optional): Either 'old' (deprecated) or 'new' (faster) ``method``.
764
+
765
+ Returns:
766
+ Dict: Dict of the glcm features.
767
+
768
+ Raises:
769
+ ValueError: If `method` is not 'old' or 'new'.
770
+
771
+ Todo:
772
+
773
+ - Enable calculation of CM features using different spatial methods (2d, 2.5d, 3d)
774
+ - Enable calculation of CM features using different CM distance settings
775
+ - Enable calculation of CM features for different merge methods ("average", "slice_merge", "dir_merge" and "vol_merge")
776
+ - Provide the range of discretised intensities from a calling function and pass to :func:`get_cm_features`.
777
+ - Test if dist_correction works as expected.
778
+
779
+ """
780
+ glcm = get_cm_features(
781
+ vol=vol,
782
+ intensity_range=[np.nan, np.nan],
783
+ merge_method=merge_method,
784
+ dist_weight_norm=dist_correction
785
+ )
786
+
787
+ return glcm
788
+
789
+ def get_glcm_matrices(vol,
790
+ glcm_spatial_method="3d",
791
+ glcm_dist=1.0,
792
+ merge_method="vol_merge",
793
+ dist_weight_norm=None) -> Dict:
794
+ """Extracts co-occurrence matrices from the intensity roi mask prior to features extraction.
795
+
796
+ Note:
797
+ This code was adapted from the in-house radiomics software created at
798
+ OncoRay, Dresden, Germany.
799
+
800
+ Args:
801
+ vol (ndarray): volume with discretised intensities as 3D numpy array (x, y, z).
802
+ intensity_range (ndarray): range of potential discretised intensities,provided as a list:
803
+ [minimal discretised intensity, maximal discretised intensity].
804
+ If one or both values are unknown, replace the respective values with np.nan.
805
+ glcm_spatial_method (str, optional): spatial method which determines the way
806
+ co-occurrence matrices are calculated and how features are determined.
807
+ Must be "2d", "2.5d" or "3d".
808
+ glcm_dist (float, optional): Chebyshev distance for comparison between neighboring voxels.
809
+ merge_method (str, optional): merging method which determines how features are
810
+ calculated. One of "average", "slice_merge", "dir_merge" and "vol_merge".
811
+ Note that not all combinations of spatial and merge method are valid.
812
+ dist_weight_norm (Union[bool, str], optional): norm for distance weighting. Weighting is only
813
+ performed if this argument is either "manhattan","euclidean", "chebyshev" or bool.
814
+
815
+ Returns:
816
+ Dict: Dict of co-occurrence matrices.
817
+
818
+ Raises:
819
+ ValueError: If `glcm_spatial_method` is not "2d", "2.5d" or "3d".
820
+ """
821
+ if type(glcm_spatial_method) is not list:
822
+ glcm_spatial_method = [glcm_spatial_method]
823
+
824
+ if type(glcm_dist) is not list:
825
+ glcm_dist = [glcm_dist]
826
+
827
+ if type(merge_method) is not list:
828
+ merge_method = [merge_method]
829
+
830
+ if type(dist_weight_norm) is bool:
831
+ if dist_weight_norm:
832
+ dist_weight_norm = "euclidean"
833
+
834
+ # Get the roi in tabular format
835
+ img_dims = vol.shape
836
+ index_id = np.arange(start=0, stop=vol.size)
837
+ coords = np.unravel_index(indices=index_id, shape=img_dims)
838
+ df_img = pd.DataFrame({"index_id": index_id,
839
+ "g": np.ravel(vol),
840
+ "x": coords[0],
841
+ "y": coords[1],
842
+ "z": coords[2],
843
+ "roi_int_mask": np.ravel(np.isfinite(vol))})
844
+
845
+ # Iterate over spatial arrangements
846
+ for ii_spatial in glcm_spatial_method:
847
+ # Iterate over distances
848
+ for ii_dist in glcm_dist:
849
+ # Initiate list of glcm objects
850
+ glcm_list = []
851
+ # Perform 2D analysis
852
+ if ii_spatial.lower() in ["2d", "2.5d"]:
853
+ # Iterate over slices
854
+ for ii_slice in np.arange(0, img_dims[2]):
855
+ # Get neighbour direction and iterate over neighbours
856
+ nbrs = get_neighbour_direction(
857
+ d=1,
858
+ distance="chebyshev",
859
+ centre=False,
860
+ complete=False,
861
+ dim3=False) * int(ii_dist)
862
+ for ii_direction in np.arange(0, np.shape(nbrs)[1]):
863
+ # Add glcm matrices to list
864
+ glcm_list += [CooccurrenceMatrix(distance=int(ii_dist),
865
+ direction=nbrs[:, ii_direction],
866
+ direction_id=ii_direction,
867
+ spatial_method=ii_spatial.lower(),
868
+ img_slice=ii_slice)]
869
+
870
+ # Perform 3D analysis
871
+ elif ii_spatial.lower() == "3d":
872
+ # Get neighbour direction and iterate over neighbours
873
+ nbrs = get_neighbour_direction(d=1,
874
+ distance="chebyshev",
875
+ centre=False,
876
+ complete=False,
877
+ dim3=True) * int(ii_dist)
878
+
879
+ for ii_direction in np.arange(0, np.shape(nbrs)[1]):
880
+ # Add glcm matrices to list
881
+ glcm_list += [CooccurrenceMatrix(distance=int(ii_dist),
882
+ direction=nbrs[:, ii_direction],
883
+ direction_id=ii_direction,
884
+ spatial_method=ii_spatial.lower())]
885
+
886
+ else:
887
+ raise ValueError(
888
+ "GCLM matrices can be determined in \"2d\", \"2.5d\" and \"3d\". \
889
+ The requested method (%s) is not implemented.", ii_spatial)
890
+
891
+ # Calculate glcm matrices
892
+ for glcm in glcm_list:
893
+ glcm.calculate_cm_matrix(
894
+ df_img=df_img, img_dims=img_dims, dist_weight_norm=dist_weight_norm)
895
+
896
+ # Merge matrices according to the given method
897
+ upd_list = {}
898
+ for merge_method in merge_method:
899
+ upd_list[merge_method] = combine_matrices(
900
+ glcm_list=glcm_list, merge_method=merge_method, spatial_method=ii_spatial.lower())
901
+
902
+ # Skip if no matrices are available (due to illegal combinations of merge and spatial methods
903
+ if upd_list is None:
904
+ continue
905
+ return upd_list
906
+
907
+ def get_cm_features(vol,
908
+ intensity_range,
909
+ glcm_spatial_method="3d",
910
+ glcm_dist=1.0,
911
+ merge_method="vol_merge",
912
+ dist_weight_norm=None) -> Dict:
913
+ """Extracts co-occurrence matrix-based features from the intensity roi mask.
914
+
915
+ Note:
916
+ This code was adapted from the in-house radiomics software created at
917
+ OncoRay, Dresden, Germany.
918
+
919
+ Args:
920
+ vol (ndarray): volume with discretised intensities as 3D numpy array (x, y, z).
921
+ intensity_range (ndarray): range of potential discretised intensities,
922
+ provided as a list: [minimal discretised intensity, maximal discretised
923
+ intensity]. If one or both values are unknown, replace the respective values
924
+ with np.nan.
925
+ glcm_spatial_method (str, optional): spatial method which determines the way
926
+ co-occurrence matrices are calculated and how features are determined.
927
+ MUST BE "2d", "2.5d" or "3d".
928
+ glcm_dist (float, optional): chebyshev distance for comparison between neighbouring
929
+ voxels.
930
+ merge_method (str, optional): merging method which determines how features are
931
+ calculated. One of "average", "slice_merge", "dir_merge" and "vol_merge".
932
+ Note that not all combinations of spatial and merge method are valid.
933
+ dist_weight_norm (Union[bool, str], optional): norm for distance weighting. Weighting is only
934
+ performed if this argument is either "manhattan",
935
+ "euclidean", "chebyshev" or bool.
936
+
937
+ Returns:
938
+ Dict: Dict of the glcm features.
939
+
940
+ Raises:
941
+ ValueError: If `glcm_spatial_method` is not "2d", "2.5d" or "3d".
942
+ """
943
+ if type(glcm_spatial_method) is not list:
944
+ glcm_spatial_method = [glcm_spatial_method]
945
+
946
+ if type(glcm_dist) is not list:
947
+ glcm_dist = [glcm_dist]
948
+
949
+ if type(merge_method) is not list:
950
+ merge_method = [merge_method]
951
+
952
+ if type(dist_weight_norm) is bool:
953
+ if dist_weight_norm:
954
+ dist_weight_norm = "euclidean"
955
+
956
+ # Get the roi in tabular format
957
+ img_dims = vol.shape
958
+ index_id = np.arange(start=0, stop=vol.size)
959
+ coords = np.unravel_index(indices=index_id, shape=img_dims)
960
+ df_img = pd.DataFrame({"index_id": index_id,
961
+ "g": np.ravel(vol),
962
+ "x": coords[0],
963
+ "y": coords[1],
964
+ "z": coords[2],
965
+ "roi_int_mask": np.ravel(np.isfinite(vol))})
966
+
967
+ # Generate an empty feature list
968
+ feat_list = []
969
+
970
+ # Iterate over spatial arrangements
971
+ for ii_spatial in glcm_spatial_method:
972
+ # Iterate over distances
973
+ for ii_dist in glcm_dist:
974
+ # Initiate list of glcm objects
975
+ glcm_list = []
976
+ # Perform 2D analysis
977
+ if ii_spatial.lower() in ["2d", "2.5d"]:
978
+ # Iterate over slices
979
+ for ii_slice in np.arange(0, img_dims[2]):
980
+ # Get neighbour direction and iterate over neighbours
981
+ nbrs = get_neighbour_direction(
982
+ d=1,
983
+ distance="chebyshev",
984
+ centre=False,
985
+ complete=False,
986
+ dim3=False) * int(ii_dist)
987
+ for ii_direction in np.arange(0, np.shape(nbrs)[1]):
988
+ # Add glcm matrices to list
989
+ glcm_list += [CooccurrenceMatrix(distance=int(ii_dist),
990
+ direction=nbrs[:, ii_direction],
991
+ direction_id=ii_direction,
992
+ spatial_method=ii_spatial.lower(),
993
+ img_slice=ii_slice)]
994
+
995
+ # Perform 3D analysis
996
+ elif ii_spatial.lower() == "3d":
997
+ # Get neighbour direction and iterate over neighbours
998
+ nbrs = get_neighbour_direction(d=1,
999
+ distance="chebyshev",
1000
+ centre=False,
1001
+ complete=False,
1002
+ dim3=True) * int(ii_dist)
1003
+
1004
+ for ii_direction in np.arange(0, np.shape(nbrs)[1]):
1005
+ # Add glcm matrices to list
1006
+ glcm_list += [CooccurrenceMatrix(distance=int(ii_dist),
1007
+ direction=nbrs[:, ii_direction],
1008
+ direction_id=ii_direction,
1009
+ spatial_method=ii_spatial.lower())]
1010
+
1011
+ else:
1012
+ raise ValueError(
1013
+ "GCLM matrices can be determined in \"2d\", \"2.5d\" and \"3d\". \
1014
+ The requested method (%s) is not implemented.", ii_spatial)
1015
+
1016
+ # Calculate glcm matrices
1017
+ for glcm in glcm_list:
1018
+ glcm.calculate_cm_matrix(
1019
+ df_img=df_img, img_dims=img_dims, dist_weight_norm=dist_weight_norm)
1020
+
1021
+ # Merge matrices according to the given method
1022
+ for merge_method in merge_method:
1023
+ upd_list = combine_matrices(
1024
+ glcm_list=glcm_list, merge_method=merge_method, spatial_method=ii_spatial.lower())
1025
+
1026
+ # Skip if no matrices are available (due to illegal combinations of merge and spatial methods
1027
+ if upd_list is None:
1028
+ continue
1029
+
1030
+ # Calculate features
1031
+ feat_run_list = []
1032
+ for glcm in upd_list:
1033
+ feat_run_list += [glcm.calculate_cm_features(
1034
+ intensity_range=intensity_range)]
1035
+
1036
+ # Average feature values
1037
+ feat_list += [pd.concat(feat_run_list, axis=0).mean(axis=0, skipna=True).to_frame().transpose()]
1038
+
1039
+ # Merge feature tables into a single table and return as a dictionary
1040
+ df_feat = pd.concat(feat_list, axis=1).to_dict(orient="records")[0]
1041
+
1042
+ return df_feat
1043
+
1044
+ def combine_matrices(glcm_list: List, merge_method: str, spatial_method: str) -> List:
1045
+ """Merges co-occurrence matrices prior to feature calculation.
1046
+
1047
+ Note:
1048
+ This code was adapted from the in-house radiomics software created at
1049
+ OncoRay, Dresden, Germany.
1050
+
1051
+ Args:
1052
+ glcm_list (List): List of CooccurrenceMatrix objects.
1053
+ merge_method (str): Merging method which determines how features are calculated.
1054
+ One of "average", "slice_merge", "dir_merge" and "vol_merge". Note that not all
1055
+ combinations of spatial and merge method are valid.
1056
+ spatial_method (str): spatial method which determines the way co-occurrence
1057
+ matrices are calculated and how features are determined. One of "2d", "2.5d"
1058
+ or "3d".
1059
+
1060
+ Returns:
1061
+ List[CooccurrenceMatrix]: list of one or more merged CooccurrenceMatrix objects.
1062
+ """
1063
+ # Initiate empty list
1064
+ use_list = []
1065
+
1066
+ # For average features over direction, maintain original glcms
1067
+ if merge_method == "average" and spatial_method in ["2d", "3d"]:
1068
+ # Make copy of glcm_list
1069
+ for glcm in glcm_list:
1070
+ use_list += [glcm._copy()]
1071
+
1072
+ # Set merge method to average
1073
+ for glcm in use_list:
1074
+ glcm.merge_method = "average"
1075
+
1076
+ # Merge glcms by slice
1077
+ elif merge_method == "slice_merge" and spatial_method == "2d":
1078
+ # Find slice_ids
1079
+ slice_id = []
1080
+ for glcm in glcm_list:
1081
+ slice_id += [glcm.slice]
1082
+
1083
+ # Iterate over unique slice_ids
1084
+ for ii_slice in np.unique(slice_id):
1085
+ slice_glcm_id = np.squeeze(np.where(slice_id == ii_slice))
1086
+
1087
+ # Select all matrices within the slice
1088
+ sel_matrix_list = []
1089
+ for glcm_id in slice_glcm_id:
1090
+ sel_matrix_list += [glcm_list[glcm_id].matrix]
1091
+
1092
+ # Check if any matrix has been created for the currently selected slice
1093
+ if is_list_all_none(sel_matrix_list):
1094
+ # No matrix was created
1095
+ use_list += [CooccurrenceMatrix(distance=glcm_list[slice_glcm_id[0]].distance,
1096
+ direction=None,
1097
+ direction_id=None,
1098
+ spatial_method=spatial_method,
1099
+ img_slice=ii_slice,
1100
+ merge_method=merge_method,
1101
+ matrix=None,
1102
+ n_v=0.0)]
1103
+ else:
1104
+ # Merge matrices within the slice
1105
+ merge_cm = pd.concat(sel_matrix_list, axis=0)
1106
+ merge_cm = merge_cm.groupby(by=["i", "j"]).sum().reset_index()
1107
+
1108
+ # Update the number of voxels within the merged slice
1109
+ merge_n_v = 0.0
1110
+ for glcm_id in slice_glcm_id:
1111
+ merge_n_v += glcm_list[glcm_id].n_v
1112
+
1113
+ # Create new cooccurrence matrix
1114
+ use_list += [CooccurrenceMatrix(distance=glcm_list[slice_glcm_id[0]].distance,
1115
+ direction=None,
1116
+ direction_id=None,
1117
+ spatial_method=spatial_method,
1118
+ img_slice=ii_slice,
1119
+ merge_method=merge_method,
1120
+ matrix=merge_cm,
1121
+ n_v=merge_n_v)]
1122
+
1123
+ # Merge glcms by direction
1124
+ elif merge_method == "dir_merge" and spatial_method == "2.5d":
1125
+ # Find slice_ids
1126
+ dir_id = []
1127
+ for glcm in glcm_list:
1128
+ dir_id += [glcm.direction_id]
1129
+
1130
+ # Iterate over unique directions
1131
+ for ii_dir in np.unique(dir_id):
1132
+ dir_glcm_id = np.squeeze(np.where(dir_id == ii_dir))
1133
+
1134
+ # Select all matrices with the same direction
1135
+ sel_matrix_list = []
1136
+ for glcm_id in dir_glcm_id:
1137
+ sel_matrix_list += [glcm_list[glcm_id].matrix]
1138
+
1139
+ # Check if any matrix has been created for the currently selected direction
1140
+ if is_list_all_none(sel_matrix_list):
1141
+ # No matrix was created
1142
+ use_list += [CooccurrenceMatrix(distance=glcm_list[dir_glcm_id[0]].distance,
1143
+ direction=glcm_list[dir_glcm_id[0]].direction,
1144
+ direction_id=ii_dir,
1145
+ spatial_method=spatial_method,
1146
+ img_slice=None,
1147
+ merge_method=merge_method,
1148
+ matrix=None, n_v=0.0)]
1149
+ else:
1150
+ # Merge matrices with the same direction
1151
+ merge_cm = pd.concat(sel_matrix_list, axis=0)
1152
+ merge_cm = merge_cm.groupby(by=["i", "j"]).sum().reset_index()
1153
+
1154
+ # Update the number of voxels for the merged matrices with the same direction
1155
+ merge_n_v = 0.0
1156
+ for glcm_id in dir_glcm_id:
1157
+ merge_n_v += glcm_list[glcm_id].n_v
1158
+
1159
+ # Create new co-occurrence matrix
1160
+ use_list += [CooccurrenceMatrix(distance=glcm_list[dir_glcm_id[0]].distance,
1161
+ direction=glcm_list[dir_glcm_id[0]].direction,
1162
+ direction_id=ii_dir,
1163
+ spatial_method=spatial_method,
1164
+ img_slice=None,
1165
+ merge_method=merge_method,
1166
+ matrix=merge_cm,
1167
+ n_v=merge_n_v)]
1168
+
1169
+ # Merge all glcms into a single representation
1170
+ elif merge_method == "vol_merge" and spatial_method in ["2.5d", "3d"]:
1171
+ # Select all matrices within the slice
1172
+ sel_matrix_list = []
1173
+ for glcm_id in np.arange(len(glcm_list)):
1174
+ sel_matrix_list += [glcm_list[glcm_id].matrix]
1175
+
1176
+ # Check if any matrix was created
1177
+ if is_list_all_none(sel_matrix_list):
1178
+ # In case no matrix was created
1179
+ use_list += [CooccurrenceMatrix(distance=glcm_list[0].distance,
1180
+ direction=None,
1181
+ direction_id=None,
1182
+ spatial_method=spatial_method,
1183
+ img_slice=None,
1184
+ merge_method=merge_method,
1185
+ matrix=None,
1186
+ n_v=0.0)]
1187
+ else:
1188
+ # Merge co-occurrence matrices
1189
+ merge_cm = pd.concat(sel_matrix_list, axis=0)
1190
+ merge_cm = merge_cm.groupby(by=["i", "j"]).sum().reset_index()
1191
+
1192
+ # Update the number of voxels
1193
+ merge_n_v = 0.0
1194
+ for glcm_id in np.arange(len(glcm_list)):
1195
+ merge_n_v += glcm_list[glcm_id].n_v
1196
+
1197
+ # Create new co-occurrence matrix
1198
+ use_list += [CooccurrenceMatrix(distance=glcm_list[0].distance,
1199
+ direction=None,
1200
+ direction_id=None,
1201
+ spatial_method=spatial_method,
1202
+ img_slice=None,
1203
+ merge_method=merge_method,
1204
+ matrix=merge_cm,
1205
+ n_v=merge_n_v)]
1206
+ else:
1207
+ use_list = None
1208
+
1209
+ return use_list
1210
+
1211
+
1212
+ class CooccurrenceMatrix:
1213
+ """ Class that contains a single co-occurrence ``matrix``.
1214
+
1215
+ Note:
1216
+ Code was adapted from the in-house radiomics software created at
1217
+ OncoRay, Dresden, Germany.
1218
+
1219
+ Attributes:
1220
+ distance (int): Chebyshev ``distance``.
1221
+ direction (ndarray): Direction along which neighbouring voxels are found.
1222
+ direction_id (int): Direction index to identify unique ``direction`` vectors.
1223
+ spatial_method (str): Spatial method used to calculate the co-occurrence
1224
+ ``matrix``: "2d", "2.5d" or "3d".
1225
+ img_slice (ndarray): Corresponding slice index (only if the co-occurrence
1226
+ ``matrix`` corresponds to a 2d image slice).
1227
+ merge_method (str): Method for merging the co-occurrence ``matrix`` with other
1228
+ co-occurrence matrices.
1229
+ matrix (pandas.DataFrame): The actual co-occurrence ``matrix`` in sparse format
1230
+ (row, column, count).
1231
+ n_v (int): The number of voxels in the volume.
1232
+ """
1233
+
1234
+ def __init__(self,
1235
+ distance: int,
1236
+ direction: np.ndarray,
1237
+ direction_id: int,
1238
+ spatial_method: str,
1239
+ img_slice: np.ndarray=None,
1240
+ merge_method: str=None,
1241
+ matrix: pd.DataFrame=None,
1242
+ n_v: int=None) -> None:
1243
+ """Constructor of the CooccurrenceMatrix class
1244
+
1245
+ Args:
1246
+ distance (int): Chebyshev ``distance``.
1247
+ direction (ndarray): Direction along which neighbouring voxels are found.
1248
+ direction_id (int): Direction index to identify unique ``direction`` vectors.
1249
+ spatial_method (str): Spatial method used to calculate the co-occurrence
1250
+ ``matrix``: "2d", "2.5d" or "3d".
1251
+ img_slice (ndarray, optional): Corresponding slice index (only if the
1252
+ co-occurrence ``matrix`` corresponds to a 2d image slice).
1253
+ merge_method (str, optional): Method for merging the co-occurrence ``matrix``
1254
+ with other co-occurrence matrices.
1255
+ matrix (pandas.DataFrame, optional): The actual co-occurrence ``matrix`` in
1256
+ sparse format (row, column, count).
1257
+ n_v (int, optional): The number of voxels in the volume.
1258
+
1259
+ Returns:
1260
+ None.
1261
+ """
1262
+ # Distance used
1263
+ self.distance = distance
1264
+
1265
+ # Direction and slice for which the current matrix is extracted
1266
+ self.direction = direction
1267
+ self.direction_id = direction_id
1268
+ self.img_slice = img_slice
1269
+
1270
+ # Spatial analysis method (2d, 2.5d, 3d) and merge method (average, slice_merge, dir_merge, vol_merge)
1271
+ self.spatial_method = spatial_method
1272
+ self.merge_method = merge_method
1273
+
1274
+ # Place holders
1275
+ self.matrix = matrix
1276
+ self.n_v = n_v
1277
+
1278
+ def _copy(self):
1279
+ """
1280
+ Returns a copy of the co-occurrence matrix object.
1281
+ """
1282
+ return deepcopy(self)
1283
+
1284
+ def calculate_cm_matrix(self, df_img: pd.DataFrame, img_dims: np.ndarray, dist_weight_norm: str) -> None:
1285
+ """Function that calculates a co-occurrence matrix for the settings provided during
1286
+ initialisation and the input image.
1287
+
1288
+ Args:
1289
+ df_img (pandas.DataFrame): Data table containing image intensities, x, y and z coordinates,
1290
+ and mask labels corresponding to voxels in the volume.
1291
+ img_dims (ndarray, List[float]): Dimensions of the image volume.
1292
+ dist_weight_norm (str): Norm for distance weighting. Weighting is only
1293
+ performed if this parameter is either "manhattan", "euclidean" or "chebyshev".
1294
+
1295
+ Returns:
1296
+ None. Assigns the created image table (cm matrix) to the `matrix` attribute.
1297
+
1298
+ Raises:
1299
+ ValueError:
1300
+ If `self.spatial_method` is not "2d", "2.5d" or "3d".
1301
+ Also, if ``dist_weight_norm`` is not "manhattan", "euclidean" or "chebyshev".
1302
+
1303
+ """
1304
+ # Check if the roi contains any masked voxels. If this is not the case, don't construct the glcm.
1305
+ if not np.any(df_img.roi_int_mask):
1306
+ self.n_v = 0
1307
+ self.matrix = None
1308
+
1309
+ return None
1310
+
1311
+ # Create local copies of the image table
1312
+ if self.spatial_method == "3d":
1313
+ df_cm = deepcopy(df_img)
1314
+ elif self.spatial_method in ["2d", "2.5d"]:
1315
+ df_cm = deepcopy(df_img[df_img.z == self.img_slice])
1316
+ df_cm["index_id"] = np.arange(0, len(df_cm))
1317
+ df_cm["z"] = 0
1318
+ df_cm = df_cm.reset_index(drop=True)
1319
+ else:
1320
+ raise ValueError(
1321
+ "The spatial method for grey level co-occurrence matrices should be one of \"2d\", \"2.5d\" or \"3d\".")
1322
+
1323
+ # Set grey level of voxels outside ROI to NaN
1324
+ df_cm.loc[df_cm.roi_int_mask == False, "g"] = np.nan
1325
+
1326
+ # Determine potential transitions
1327
+ df_cm["to_index"] = coord2index(x=df_cm.x.values + self.direction[0],
1328
+ y=df_cm.y.values + self.direction[1],
1329
+ z=df_cm.z.values + self.direction[2],
1330
+ dims=img_dims)
1331
+
1332
+ # Get grey levels from transitions
1333
+ df_cm["to_g"] = get_value(x=df_cm.g.values, index=df_cm.to_index.values)
1334
+
1335
+ # Check if any transitions exist.
1336
+ if np.all(np.isnan(df_cm[["to_g"]])):
1337
+ self.n_v = 0
1338
+ self.matrix = None
1339
+
1340
+ return None
1341
+
1342
+ # Count occurrences of grey level transitions
1343
+ df_cm = df_cm.groupby(by=["g", "to_g"]).size().reset_index(name="n")
1344
+
1345
+ # Append grey level transitions in opposite direction
1346
+ df_cm_inv = pd.DataFrame({"g": df_cm.to_g, "to_g": df_cm.g, "n": df_cm.n})
1347
+ df_cm = df_cm.append(df_cm_inv, ignore_index=True)
1348
+
1349
+ # Sum occurrences of grey level transitions
1350
+ df_cm = df_cm.groupby(by=["g", "to_g"]).sum().reset_index()
1351
+
1352
+ # Rename columns
1353
+ df_cm.columns = ["i", "j", "n"]
1354
+
1355
+ if dist_weight_norm in ["manhattan", "euclidean", "chebyshev"]:
1356
+ if dist_weight_norm == "manhattan":
1357
+ weight = sum(abs(self.direction))
1358
+ elif dist_weight_norm == "euclidean":
1359
+ weight = np.sqrt(sum(np.power(self.direction, 2.0)))
1360
+ elif dist_weight_norm == "chebyshev":
1361
+ weight = np.max(abs(self.direction))
1362
+ df_cm.n /= weight
1363
+
1364
+ # Set the number of voxels
1365
+ self.n_v = np.sum(df_cm.n)
1366
+
1367
+ # Add matrix and number of voxels to object
1368
+ self.matrix = df_cm
1369
+
1370
+ def get_cm_data(self, intensity_range: np.ndarray):
1371
+ """Computes the probability distribution for the elements of the GLCM
1372
+ (diagonal probability, cross-diagonal probability...) and number of gray-levels.
1373
+
1374
+ Args:
1375
+ intensity_range (ndarray): Range of potential discretised intensities, provided as a list:
1376
+ [minimal discretised intensity, maximal discretised intensity].
1377
+ If one or both values are unknown,replace the respective values with np.nan.
1378
+
1379
+ Returns:
1380
+ Typle[pd.DataFrame, pd.DataFrame, pd.DataFrame, float]:
1381
+ - Occurence data frame
1382
+ - Diagonal probabilty
1383
+ - Cross-diagonal probabilty
1384
+ - Number of gray levels
1385
+ """
1386
+ # Occurrence data frames
1387
+ df_pij = deepcopy(self.matrix)
1388
+ df_pij["pij"] = df_pij.n / sum(df_pij.n)
1389
+ df_pi = df_pij.groupby(by="i")["pij"].agg(np.sum).reset_index().rename(columns={"pij": "pi"})
1390
+ df_pj = df_pij.groupby(by="j")["pij"].agg(np.sum).reset_index().rename(columns={"pij": "pj"})
1391
+
1392
+ # Diagonal probilities p(i-j)
1393
+ df_pimj = deepcopy(df_pij)
1394
+ df_pimj["k"] = np.abs(df_pimj.i - df_pimj.j)
1395
+ df_pimj = df_pimj.groupby(by="k")["pij"].agg(np.sum).reset_index().rename(columns={"pij": "pimj"})
1396
+
1397
+ # Cross-diagonal probabilities p(i+j)
1398
+ df_pipj = deepcopy(df_pij)
1399
+ df_pipj["k"] = df_pipj.i + df_pipj.j
1400
+ df_pipj = df_pipj.groupby(by="k")["pij"].agg(np.sum).reset_index().rename(columns={"pij": "pipj"})
1401
+
1402
+ # Merger of df.p_ij, df.p_i and df.p_j
1403
+ df_pij = pd.merge(df_pij, df_pi, on="i")
1404
+ df_pij = pd.merge(df_pij, df_pj, on="j")
1405
+
1406
+ # Constant definitions
1407
+ intensity_range_loc = deepcopy(intensity_range)
1408
+ if np.isnan(intensity_range[0]):
1409
+ intensity_range_loc[0] = np.min(df_pi.i) * 1.0
1410
+ if np.isnan(intensity_range[1]):
1411
+ intensity_range_loc[1] = np.max(df_pi.i) * 1.0
1412
+ # Number of grey levels
1413
+ n_g = intensity_range_loc[1] - intensity_range_loc[0] + 1.0
1414
+
1415
+ return df_pij, df_pi, df_pj, df_pimj, df_pipj, n_g
1416
+
1417
+ def calculate_cm_features(self, intensity_range: np.ndarray) -> pd.DataFrame:
1418
+ """Wrapper to json.dump function.
1419
+
1420
+ Args:
1421
+ intensity_range (np.ndarray): Range of potential discretised intensities,
1422
+ provided as a list: [minimal discretised intensity, maximal discretised intensity].
1423
+ If one or both values are unknown,replace the respective values with np.nan.
1424
+
1425
+ Returns:
1426
+ pandas.DataFrame: Data frame with values for each feature.
1427
+ """
1428
+ # Create feature table
1429
+ feat_names = ["Fcm_joint_max", "Fcm_joint_avg", "Fcm_joint_var", "Fcm_joint_entr",
1430
+ "Fcm_diff_avg", "Fcm_diff_var", "Fcm_diff_entr",
1431
+ "Fcm_sum_avg", "Fcm_sum_var", "Fcm_sum_entr",
1432
+ "Fcm_energy", "Fcm_contrast", "Fcm_dissimilarity",
1433
+ "Fcm_inv_diff", "Fcm_inv_diff_norm", "Fcm_inv_diff_mom",
1434
+ "Fcm_inv_diff_mom_norm", "Fcm_inv_var", "Fcm_corr",
1435
+ "Fcm_auto_corr", "Fcm_clust_tend", "Fcm_clust_shade",
1436
+ "Fcm_clust_prom", "Fcm_info_corr1", "Fcm_info_corr2"]
1437
+
1438
+ df_feat = pd.DataFrame(np.full(shape=(1, len(feat_names)), fill_value=np.nan))
1439
+ df_feat.columns = feat_names
1440
+
1441
+ # Don't return data for empty slices or slices without a good matrix
1442
+ if self.matrix is None:
1443
+ # Update names
1444
+ #df_feat.columns += self._parse_names()
1445
+ return df_feat
1446
+ elif len(self.matrix) == 0:
1447
+ # Update names
1448
+ #df_feat.columns += self._parse_names()
1449
+ return df_feat
1450
+
1451
+ df_pij, df_pi, df_pj, df_pimj, df_pipj, n_g = self.get_cm_data(intensity_range)
1452
+
1453
+ ###############################################
1454
+ ###### glcm features ######
1455
+ ###############################################
1456
+ # Joint maximum
1457
+ df_feat.loc[0, "Fcm_joint_max"] = np.max(df_pij.pij)
1458
+
1459
+ # Joint average
1460
+ df_feat.loc[0, "Fcm_joint_avg"] = np.sum(df_pij.i * df_pij.pij)
1461
+
1462
+ # Joint variance
1463
+ m_u = np.sum(df_pij.i * df_pij.pij)
1464
+ df_feat.loc[0, "Fcm_joint_var"] = np.sum((df_pij.i - m_u) ** 2.0 * df_pij.pij)
1465
+
1466
+ # Joint entropy
1467
+ df_feat.loc[0, "Fcm_joint_entr"] = -np.sum(df_pij.pij * np.log2(df_pij.pij))
1468
+
1469
+ # Difference average
1470
+ df_feat.loc[0, "Fcm_diff_avg"] = np.sum(df_pimj.k * df_pimj.pimj)
1471
+
1472
+ # Difference variance
1473
+ m_u = np.sum(df_pimj.k * df_pimj.pimj)
1474
+ df_feat.loc[0, "Fcm_diff_var"] = np.sum((df_pimj.k - m_u) ** 2.0 * df_pimj.pimj)
1475
+
1476
+ # Difference entropy
1477
+ df_feat.loc[0, "Fcm_diff_entr"] = -np.sum(df_pimj.pimj * np.log2(df_pimj.pimj))
1478
+
1479
+ # Sum average
1480
+ df_feat.loc[0, "Fcm_sum_avg"] = np.sum(df_pipj.k * df_pipj.pipj)
1481
+
1482
+ # Sum variance
1483
+ m_u = np.sum(df_pipj.k * df_pipj.pipj)
1484
+ df_feat.loc[0, "Fcm_sum_var"] = np.sum((df_pipj.k - m_u) ** 2.0 * df_pipj.pipj)
1485
+
1486
+ # Sum entropy
1487
+ df_feat.loc[0, "Fcm_sum_entr"] = -np.sum(df_pipj.pipj * np.log2(df_pipj.pipj))
1488
+
1489
+ # Angular second moment
1490
+ df_feat.loc[0, "Fcm_energy"] = np.sum(df_pij.pij ** 2.0)
1491
+
1492
+ # Contrast
1493
+ df_feat.loc[0, "Fcm_contrast"] = np.sum((df_pij.i - df_pij.j) ** 2.0 * df_pij.pij)
1494
+
1495
+ # Dissimilarity
1496
+ df_feat.loc[0, "Fcm_dissimilarity"] = np.sum(np.abs(df_pij.i - df_pij.j) * df_pij.pij)
1497
+
1498
+ # Inverse difference
1499
+ df_feat.loc[0, "Fcm_inv_diff"] = np.sum(df_pij.pij / (1.0 + np.abs(df_pij.i - df_pij.j)))
1500
+
1501
+ # Inverse difference normalised
1502
+ df_feat.loc[0, "Fcm_inv_diff_norm"] = np.sum(df_pij.pij / (1.0 + np.abs(df_pij.i - df_pij.j) / n_g))
1503
+
1504
+ # Inverse difference moment
1505
+ df_feat.loc[0, "Fcm_inv_diff_mom"] = np.sum(df_pij.pij / (1.0 + (df_pij.i - df_pij.j) ** 2.0))
1506
+
1507
+ # Inverse difference moment normalised
1508
+ df_feat.loc[0, "Fcm_inv_diff_mom_norm"] = np.sum(df_pij.pij / (1.0 + (df_pij.i - df_pij.j)
1509
+ ** 2.0 / n_g ** 2.0))
1510
+
1511
+ # Inverse variance
1512
+ df_sel = df_pij[df_pij.i != df_pij.j]
1513
+ df_feat.loc[0, "Fcm_inv_var"] = np.sum(df_sel.pij / (df_sel.i - df_sel.j) ** 2.0)
1514
+ del df_sel
1515
+
1516
+ # Correlation
1517
+ mu_marg = np.sum(df_pi.i * df_pi.pi)
1518
+ var_marg = np.sum((df_pi.i - mu_marg) ** 2.0 * df_pi.pi)
1519
+
1520
+ if var_marg == 0.0:
1521
+ df_feat.loc[0, "Fcm_corr"] = 1.0
1522
+ else:
1523
+ df_feat.loc[0, "Fcm_corr"] = 1.0 / var_marg * (np.sum(df_pij.i * df_pij.j * df_pij.pij) - mu_marg ** 2.0)
1524
+
1525
+ del mu_marg, var_marg
1526
+
1527
+ # Autocorrelation
1528
+ df_feat.loc[0, "Fcm_auto_corr"] = np.sum(df_pij.i * df_pij.j * df_pij.pij)
1529
+
1530
+ # Information correlation 1
1531
+ hxy = -np.sum(df_pij.pij * np.log2(df_pij.pij))
1532
+ hxy_1 = -np.sum(df_pij.pij * np.log2(df_pij.pi * df_pij.pj))
1533
+ hx = -np.sum(df_pi.pi * np.log2(df_pi.pi))
1534
+ if len(df_pij) == 1 or hx == 0.0:
1535
+ df_feat.loc[0, "Fcm_info_corr1"] = 1.0
1536
+ else:
1537
+ df_feat.loc[0, "Fcm_info_corr1"] = (hxy - hxy_1) / hx
1538
+ del hxy, hxy_1, hx
1539
+
1540
+ # Information correlation 2 - Note: iteration over combinations of i and j
1541
+ hxy = - np.sum(df_pij.pij * np.log2(df_pij.pij))
1542
+ hxy_2 = - np.sum(
1543
+ np.tile(df_pi.pi, len(df_pj)) * np.repeat(df_pj.pj, len(df_pi)) * \
1544
+ np.log2(np.tile(df_pi.pi, len(df_pj)) * np.repeat(df_pj.pj, len(df_pi)))
1545
+ )
1546
+
1547
+ if hxy_2 < hxy:
1548
+ df_feat.loc[0, "Fcm_info_corr2"] = 0
1549
+ else:
1550
+ df_feat.loc[0, "Fcm_info_corr2"] = np.sqrt(1 - np.exp(-2.0 * (hxy_2 - hxy)))
1551
+ del hxy, hxy_2
1552
+
1553
+ # Cluster tendency
1554
+ m_u = np.sum(df_pi.i * df_pi.pi)
1555
+ df_feat.loc[0, "Fcm_clust_tend"] = np.sum((df_pij.i + df_pij.j - 2 * m_u) ** 2.0 * df_pij.pij)
1556
+ del m_u
1557
+
1558
+ # Cluster shade
1559
+ m_u = np.sum(df_pi.i * df_pi.pi)
1560
+ df_feat.loc[0, "Fcm_clust_shade"] = np.sum((df_pij.i + df_pij.j - 2 * m_u) ** 3.0 * df_pij.pij)
1561
+ del m_u
1562
+
1563
+ # Cluster prominence
1564
+ m_u = np.sum(df_pi.i * df_pi.pi)
1565
+ df_feat.loc[0, "Fcm_clust_prom"] = np.sum((df_pij.i + df_pij.j - 2 * m_u) ** 4.0 * df_pij.pij)
1566
+
1567
+ del df_pi, df_pj, df_pij, df_pimj, df_pipj, n_g
1568
+
1569
+ # Update names
1570
+ # df_feat.columns += self._parse_names()
1571
+
1572
+ return df_feat
1573
+
1574
+ def _parse_names(self) -> str:
1575
+ """"Adds additional settings-related identifiers to each feature.
1576
+ Not used currently, as the use of different settings for the
1577
+ co-occurrence matrix is not supported.
1578
+
1579
+ Returns:
1580
+ str: String of the features indetifier.
1581
+ """
1582
+ parse_str = ""
1583
+
1584
+ # Add distance
1585
+ parse_str += "_d" + str(np.round(self.distance, 1))
1586
+
1587
+ # Add spatial method
1588
+ if self.spatial_method is not None:
1589
+ parse_str += "_" + self.spatial_method
1590
+
1591
+ # Add merge method
1592
+ if self.merge_method is not None:
1593
+ if self.merge_method == "average":
1594
+ parse_str += "_avg"
1595
+ if self.merge_method == "slice_merge":
1596
+ parse_str += "_s_mrg"
1597
+ if self.merge_method == "dir_merge":
1598
+ parse_str += "_d_mrg"
1599
+ if self.merge_method == "vol_merge":
1600
+ parse_str += "_v_mrg"
1601
+
1602
+ return parse_str