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,1756 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import copy
5
+ import logging
6
+ from typing import Dict, List, Tuple, Union
7
+
8
+ import numpy as np
9
+ import pandas as pd
10
+ import scipy.spatial as sc
11
+ from scipy.spatial import ConvexHull
12
+ from skimage.measure import marching_cubes
13
+
14
+ from ..biomarkers.get_oriented_bound_box import rot_matrix, sig_proc_find_peaks
15
+
16
+
17
+ def get_mesh(mask: np.ndarray,
18
+ res: Union[np.ndarray, List]) -> Tuple[np.ndarray,
19
+ np.ndarray,
20
+ np.ndarray]:
21
+ """Compute Mesh.
22
+
23
+ Note:
24
+ Make sure the `mask` is padded with a layer of 0's in all
25
+ dimensions to reduce potential isosurface computation errors.
26
+
27
+ Args:
28
+ mask (ndarray): Contains only 0's and 1's.
29
+ res (ndarray or List): [a,b,c] vector specifying the resolution of the volume in mm.
30
+ xyz resolution (world), or JIK resolution (intrinsic matlab).
31
+
32
+ Returns:
33
+ Tuple[np.ndarray, np.ndarray, np.ndarray]:
34
+ - Array of the [X,Y,Z] positions of the ROI.
35
+ - Array of the spatial coordinates for `mask` unique mesh vertices.
36
+ - Array of triangular faces via referencing vertex indices from vertices.
37
+ """
38
+ # Getting the grid of X,Y,Z positions, where the coordinate reference
39
+ # system (0,0,0) is located at the upper left corner of the first voxel
40
+ # (-0.5: half a voxel distance). For the whole volume defining the mask,
41
+ # no matter if it is a 1 or a 0.
42
+ mask = mask.copy()
43
+ res = res.copy()
44
+
45
+ x = res[0]*((np.arange(1, np.shape(mask)[0]+1))-0.5)
46
+ y = res[1]*((np.arange(1, np.shape(mask)[1]+1))-0.5)
47
+ z = res[2]*((np.arange(1, np.shape(mask)[2]+1))-0.5)
48
+ X, Y, Z = np.meshgrid(x, y, z, indexing='ij')
49
+
50
+ # Getting the isosurface of the mask
51
+ vertices, faces, _, _ = marching_cubes(volume=mask, level=0.5, spacing=res)
52
+
53
+ # Getting the X,Y,Z positions of the ROI (i.e. 1's) of the mask
54
+ X = np.reshape(X, (np.size(X), 1), order='F')
55
+ Y = np.reshape(Y, (np.size(Y), 1), order='F')
56
+ Z = np.reshape(Z, (np.size(Z), 1), order='F')
57
+
58
+ xyz = np.concatenate((X, Y, Z), axis=1)
59
+ xyz = xyz[np.where(np.reshape(mask, np.size(mask), order='F') == 1)[0], :]
60
+
61
+ return xyz, faces, vertices
62
+
63
+ def get_com(xgl_int: np.ndarray,
64
+ xgl_morph: np.ndarray,
65
+ xyz_int: np.ndarray,
66
+ xyz_morph: np.ndarray) -> Union[float,
67
+ np.ndarray]:
68
+ """Calculates center of mass shift (in mm, since resolution is in mm).
69
+
70
+ Note:
71
+ Row positions of "x_gl" and "xyz" must correspond for each point.
72
+
73
+ Args:
74
+ xgl_int (ndarray): Vector of intensity values in the volume to analyze
75
+ (only values in the intensity mask).
76
+ xgl_morph (ndarray): Vector of intensity values in the volume to analyze
77
+ (only values in the morphological mask).
78
+ xyz_int (ndarray): [n_points X 3] matrix of three column vectors, defining the [X,Y,Z]
79
+ positions of the points in the ROI (1's) of the mask volume (In mm).
80
+ (Mesh-based volume calculated from the ROI intensity mesh)
81
+ xyz_morph (ndarray): [n_points X 3] matrix of three column vectors, defining the [X,Y,Z]
82
+ positions of the points in the ROI (1's) of the mask volume (In mm).
83
+ (Mesh-based volume calculated from the ROI morphological mesh)
84
+
85
+ Returns:
86
+ Union[float, np.ndarray]: The ROI volume centre of mass.
87
+
88
+ """
89
+
90
+ # Getting the geometric centre of mass
91
+ n_v = np.size(xgl_morph)
92
+
93
+ com_geom = np.sum(xyz_morph, 0)/n_v # [1 X 3] vector
94
+
95
+ # Getting the density centre of mass
96
+ xyz_int[:, 0] = xgl_int*xyz_int[:, 0]
97
+ xyz_int[:, 1] = xgl_int*xyz_int[:, 1]
98
+ xyz_int[:, 2] = xgl_int*xyz_int[:, 2]
99
+ com_gl = np.sum(xyz_int, 0)/np.sum(xgl_int, 0) # [1 X 3] vector
100
+
101
+ # Calculating the shift
102
+ com = np.linalg.norm(com_geom - com_gl)
103
+
104
+ return com
105
+
106
+ def get_area_dens_approx(a: float,
107
+ b: float,
108
+ c: float,
109
+ n: float) -> float:
110
+ """Computes area density - minimum volume enclosing ellipsoid
111
+
112
+ Args:
113
+ a (float): Major semi-axis length.
114
+ b (float): Minor semi-axis length.
115
+ c (float): Least semi-axis length.
116
+ n (int): Number of iterations.
117
+
118
+ Returns:
119
+ float: Area density - minimum volume enclosing ellipsoid.
120
+
121
+ """
122
+ alpha = np.sqrt(1 - b**2/a**2)
123
+ beta = np.sqrt(1 - c**2/a**2)
124
+ ab = alpha * beta
125
+ point = (alpha**2+beta**2) / (2*ab)
126
+ a_ell = 0
127
+
128
+ for v in range(0, n+1):
129
+ coef = [0]*v + [1]
130
+ legen = np.polynomial.legendre.legval(x=point, c=coef)
131
+ a_ell = a_ell + ab**v / (1-4*v**2) * legen
132
+
133
+ a_ell = a_ell * 4 * np.pi * a * b
134
+
135
+ return a_ell
136
+
137
+ def get_axis_lengths(xyz: np.ndarray) -> Tuple[float, float, float]:
138
+ """Computes AxisLengths.
139
+
140
+ Args:
141
+ xyz (ndarray): Array of three column vectors, defining the [X,Y,Z]
142
+ positions of the points in the ROI (1's) of the mask volume. In mm.
143
+
144
+ Returns:
145
+ Tuple[float, float, float]: Array of three column vectors
146
+ [Major axis lengths, Minor axis lengths, Least axis lengths].
147
+
148
+ """
149
+ xyz = xyz.copy()
150
+
151
+ # Getting the geometric centre of mass
152
+ com_geom = np.sum(xyz, 0)/np.shape(xyz)[0] # [1 X 3] vector
153
+
154
+ # Subtracting the centre of mass
155
+ xyz[:, 0] = xyz[:, 0] - com_geom[0]
156
+ xyz[:, 1] = xyz[:, 1] - com_geom[1]
157
+ xyz[:, 2] = xyz[:, 2] - com_geom[2]
158
+
159
+ # Getting the covariance matrix
160
+ cov_mat = np.cov(xyz, rowvar=False)
161
+
162
+ # Getting the eigenvalues
163
+ eig_val, _ = np.linalg.eig(cov_mat)
164
+ eig_val = np.sort(eig_val)
165
+
166
+ major = eig_val[2]
167
+ minor = eig_val[1]
168
+ least = eig_val[0]
169
+
170
+ return major, minor, least
171
+
172
+ def min_oriented_bound_box(pos_mat: np.ndarray) -> np.ndarray:
173
+ """Computes the minimum bounding box of an arbitrary solid: an iterative approach.
174
+ This feature refers to "Volume density (oriented minimum bounding box)" (ID = ZH1A)
175
+ in the `IBSI1 reference manual <https://arxiv.org/pdf/1612.07003.pdf>`_.
176
+
177
+ Args:
178
+ pos_mat (ndarray): matrix position
179
+
180
+ Returns:
181
+ ndarray: return bounding box dimensions
182
+ """
183
+
184
+ ##########################
185
+ # Internal functions
186
+ ##########################
187
+
188
+ def calc_rot_aabb_surface(theta: float,
189
+ hull_mat: np.ndarray) -> np.ndarray:
190
+ """Function to calculate surface of the axis-aligned bounding box of a rotated 2D contour
191
+
192
+ Args:
193
+ theta (float): angle in radian
194
+ hull_mat (nddarray): convex hull matrix
195
+
196
+ Returns:
197
+ ndarray: the surface of the axis-aligned bounding box of a rotated 2D contour
198
+ """
199
+
200
+ # Create rotation matrix and rotate over theta
201
+ rot_mat = rot_matrix(theta=theta, dim=2)
202
+ rot_hull = np.dot(rot_mat, hull_mat)
203
+
204
+ # Calculate bounding box surface of the rotated contour
205
+ rot_aabb_dims = np.max(rot_hull, axis=1) - np.min(rot_hull, axis=1)
206
+ rot_aabb_area = np.product(rot_aabb_dims)
207
+
208
+ return rot_aabb_area
209
+
210
+ def approx_min_theta(hull_mat: np.ndarray,
211
+ theta_sel: float,
212
+ res: float,
213
+ max_rep: int=5) -> np.ndarray:
214
+ """Iterative approximator for finding angle theta that minimises surface area
215
+
216
+ Args:
217
+ hull_mat (ndarray): convex hull matrix
218
+ theta_sel (float): angle in radian
219
+ res (float): value in radian
220
+ max_rep (int, optional): maximum repetition. Defaults to 5.
221
+
222
+ Returns:
223
+ ndarray: the angle theta that minimises surfae area
224
+ """
225
+
226
+ for i in np.arange(0, max_rep):
227
+
228
+ # Select new thetas in vicinity of
229
+ theta = np.array([theta_sel-res, theta_sel-0.5*res,
230
+ theta_sel, theta_sel+0.5*res, theta_sel+res])
231
+
232
+ # Calculate projection areas for current angles theta
233
+ rot_area = np.array(
234
+ list(map(lambda x: calc_rot_aabb_surface(theta=x, hull_mat=hull_mat), theta)))
235
+
236
+ # Find global minimum and corresponding angle theta_sel
237
+ theta_sel = theta[np.argmin(rot_area)]
238
+
239
+ # Shrink resolution and iterate
240
+ res = res / 2.0
241
+
242
+ return theta_sel
243
+
244
+ def rotate_minimal_projection(input_pos: float,
245
+ rot_axis: int,
246
+ n_minima: int=3,
247
+ res_init: float=5.0):
248
+ """Function to that rotates input_pos to find the rotation that
249
+ minimises the projection of input_pos on the
250
+ plane normal to the rot_axis
251
+
252
+ Args:
253
+ input_pos (float): input position value
254
+ rot_axis (int): rotation axis value
255
+ n_minima (int, optional): _description_. Defaults to 3.
256
+ res_init (float, optional): _description_. Defaults to 5.0.
257
+
258
+ Returns:
259
+ _type_: _description_
260
+ """
261
+
262
+
263
+ # Find axis aligned bounding box of the point set
264
+ aabb_max = np.max(input_pos, axis=0)
265
+ aabb_min = np.min(input_pos, axis=0)
266
+
267
+ # Center the point set at the AABB center
268
+ output_pos = input_pos - 0.5 * (aabb_min + aabb_max)
269
+
270
+ # Project model to plane
271
+ proj_pos = copy.deepcopy(output_pos)
272
+ proj_pos = np.delete(proj_pos, [rot_axis], axis=1)
273
+
274
+ # Calculate 2D convex hull of the model projection in plane
275
+ if np.shape(proj_pos)[0] >= 10:
276
+ hull_2d = ConvexHull(points=proj_pos)
277
+ hull_mat = proj_pos[hull_2d.vertices, :]
278
+ del hull_2d, proj_pos
279
+ else:
280
+ hull_mat = proj_pos
281
+ del proj_pos
282
+
283
+ # Transpose hull_mat so that the array is (ndim, npoints) instead of (npoints, ndim)
284
+ hull_mat = np.transpose(hull_mat)
285
+
286
+ # Calculate bounding box surface of a series of rotated contours
287
+ # Note we can program a min-search algorithm here as well
288
+
289
+ # Calculate initial surfaces
290
+ theta_init = np.arange(start=0.0, stop=90.0 +
291
+ res_init, step=res_init) * np.pi / 180.0
292
+ rot_area = np.array(
293
+ list(map(lambda x: calc_rot_aabb_surface(theta=x, hull_mat=hull_mat), theta_init)))
294
+
295
+ # Find local minima
296
+ df_min = sig_proc_find_peaks(x=rot_area, ddir="neg")
297
+
298
+ # Check if any minimum was generated
299
+ if len(df_min) > 0:
300
+ # Investigate up to n_minima number of local minima, starting with the global minimum
301
+ df_min = df_min.sort_values(by="val", ascending=True)
302
+
303
+ # Determine max number of minima evaluated
304
+ max_iter = np.min([n_minima, len(df_min)])
305
+
306
+ # Initialise place holder array
307
+ theta_min = np.zeros(max_iter)
308
+
309
+ # Iterate over local minima
310
+ for k in np.arange(0, max_iter):
311
+
312
+ # Find initial angle corresponding to i-th minimum
313
+ sel_ind = df_min.ind.values[k]
314
+ theta_curr = theta_init[sel_ind]
315
+
316
+ # Zoom in to improve the approximation of theta
317
+ theta_min[k] = approx_min_theta(
318
+ hull_mat=hull_mat, theta_sel=theta_curr, res=res_init*np.pi/180.0)
319
+
320
+ # Calculate surface areas corresponding to theta_min and theta that
321
+ # minimises the surface
322
+ rot_area = np.array(
323
+ list(map(lambda x: calc_rot_aabb_surface(theta=x, hull_mat=hull_mat), theta_min)))
324
+ theta_sel = theta_min[np.argmin(rot_area)]
325
+
326
+ else:
327
+ theta_sel = theta_init[0]
328
+
329
+ # Rotate original point along the angle that minimises the projected AABB area
330
+ output_pos = np.transpose(output_pos)
331
+ rot_mat = rot_matrix(theta=theta_sel, dim=3, rot_axis=rot_axis)
332
+ output_pos = np.dot(rot_mat, output_pos)
333
+
334
+ # Rotate output_pos back to (npoints, ndim)
335
+ output_pos = np.transpose(output_pos)
336
+
337
+ return output_pos
338
+
339
+ ##########################
340
+ # Main function
341
+ ##########################
342
+
343
+ rot_df = pd.DataFrame({"rot_axis_0": np.array([0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2]),
344
+ "rot_axis_1": np.array([1, 2, 1, 2, 0, 2, 0, 2, 0, 1, 0, 1]),
345
+ "rot_axis_2": np.array([2, 1, 0, 0, 2, 0, 1, 1, 1, 0, 2, 2]),
346
+ "aabb_axis_0": np.zeros(12),
347
+ "aabb_axis_1": np.zeros(12),
348
+ "aabb_axis_2": np.zeros(12),
349
+ "vol": np.zeros(12)})
350
+
351
+ # Rotate over different sequences
352
+ for i in np.arange(0, len(rot_df)):
353
+ # Create a local copy
354
+ work_pos = copy.deepcopy(pos_mat)
355
+
356
+ # Rotate over sequence of rotation axes
357
+ work_pos = rotate_minimal_projection(
358
+ input_pos=work_pos, rot_axis=rot_df.rot_axis_0[i])
359
+ work_pos = rotate_minimal_projection(
360
+ input_pos=work_pos, rot_axis=rot_df.rot_axis_1[i])
361
+ work_pos = rotate_minimal_projection(
362
+ input_pos=work_pos, rot_axis=rot_df.rot_axis_2[i])
363
+
364
+ # Determine resultant minimum bounding box
365
+ aabb_dims = np.max(work_pos, axis=0) - np.min(work_pos, axis=0)
366
+ rot_df.loc[i, "aabb_axis_0"] = aabb_dims[0]
367
+ rot_df.loc[i, "aabb_axis_1"] = aabb_dims[1]
368
+ rot_df.loc[i, "aabb_axis_2"] = aabb_dims[2]
369
+ rot_df.loc[i, "vol"] = np.product(aabb_dims)
370
+
371
+ del work_pos, aabb_dims
372
+
373
+ # Find minimal volume of all rotations and return bounding box dimensions
374
+ idxmin = rot_df.vol.idxmin()
375
+ sel_row = rot_df.loc[idxmin]
376
+ ombb_dims = np.array(
377
+ [sel_row.aabb_axis_0, sel_row.aabb_axis_1, sel_row.aabb_axis_2])
378
+
379
+ return ombb_dims
380
+
381
+ def get_moran_i(vol: np.ndarray,
382
+ res: List[float]) -> float:
383
+ """Computes Moran's Index.
384
+ This feature refers to "Moran’s I index" (ID = N365)
385
+ in the `IBSI1 reference manual <https://arxiv.org/pdf/1612.07003.pdf>`_.
386
+
387
+ Args:
388
+ vol (ndarray): 3D volume, NON-QUANTIZED, continous imaging intensity distribution.
389
+ res (List[float]): [a,b,c] vector specfying the resolution of the volume in mm.
390
+ XYZ resolution (world) or JIK resolution (intrinsic matlab).
391
+
392
+ Returns:
393
+ float: Value of Moran's Index.
394
+
395
+ """
396
+ vol = vol.copy()
397
+ res = res.copy()
398
+
399
+ # Find the location(s) of all non NaNs voxels
400
+ I, J, K = np.nonzero(~np.isnan(vol))
401
+ n_vox = np.size(I)
402
+
403
+ # Get the mean
404
+ u = np.mean(vol[~np.isnan(vol[:])])
405
+ vol_mean = vol.copy() - u # (x_gl,i - u)
406
+ vol_m_mean_s = np.power((vol.copy() - u), 2) # (x_gl,i - u).^2
407
+ # Sum of (x_gl,i - u).^2 over all i
408
+ sum_s = np.sum(vol_m_mean_s[~np.isnan(vol_m_mean_s[:])])
409
+
410
+ # Get a meshgrid first
411
+ x = res[0]*((np.arange(1, np.shape(vol)[0]+1))-0.5)
412
+ y = res[1]*((np.arange(1, np.shape(vol)[1]+1))-0.5)
413
+ z = res[2]*((np.arange(1, np.shape(vol)[2]+1))-0.5)
414
+ X, Y, Z = np.meshgrid(x, y, z, indexing='ij')
415
+
416
+ temp = 0
417
+ sum_w = 0
418
+ for i in range(1, n_vox+1):
419
+ # Distance mesh
420
+ temp_x = X - X[I[i-1], J[i-1], K[i-1]]
421
+ temp_y = Y - Y[I[i-1], J[i-1], K[i-1]]
422
+ temp_z = Z - Z[I[i-1], J[i-1], K[i-1]]
423
+
424
+ # meshgrid of weigths
425
+ temp_dist_mesh = 1 / np.sqrt(temp_x**2 + temp_y**2 + temp_z**2)
426
+
427
+ # Removing NaNs
428
+ temp_dist_mesh[np.isnan(vol)] = np.NaN
429
+ temp_dist_mesh[I[i-1], J[i-1], K[i-1]] = np.NaN
430
+ # Running sum of weights
431
+ w_sum = np.sum(temp_dist_mesh[~np.isnan(temp_dist_mesh[:])])
432
+ sum_w = sum_w + w_sum
433
+
434
+ # Inside sum calculation
435
+ # Removing NaNs
436
+ temp_vol = vol_mean.copy()
437
+ temp_vol[I[i-1], J[i-1], K[i-1]] = np.NaN
438
+ temp_vol = temp_dist_mesh * temp_vol # (wij .* (x_gl,j - u))
439
+ # Summing (wij .* (x_gl,j - u)) over all j
440
+ sum_val = np.sum(temp_vol[~np.isnan(temp_vol[:])])
441
+ # Running sum of (x_gl,i - u)*(wij .* (x_gl,j - u)) over all i
442
+ temp = temp + vol_mean[I[i-1], J[i-1], K[i-1]] * sum_val
443
+
444
+ moran_i = temp*n_vox/sum_s/sum_w
445
+
446
+ return moran_i
447
+
448
+ def get_mesh_volume(faces: np.ndarray,
449
+ vertices:np.ndarray) -> float:
450
+ """Computes MeshVolume feature.
451
+ This feature refers to "Volume (mesh)" (ID = RNU0)
452
+ in the `IBSI1 reference manual <https://arxiv.org/pdf/1612.07003.pdf>`_.
453
+
454
+ Args:
455
+ faces (np.ndarray): matrix of three column vectors, defining the [X,Y,Z]
456
+ positions of the ``faces`` of the isosurface or convex hull of the mask
457
+ (output from "isosurface.m" or "convhull.m" functions of MATLAB).
458
+ --> These are more precisely indexes to ``vertices``
459
+ vertices (np.ndarray): matrix of three column vectors, defining the
460
+ [X,Y,Z] positions of the ``vertices`` of the isosurface of the mask (output
461
+ from "isosurface.m" function of MATLAB).
462
+ --> In mm.
463
+
464
+ Returns:
465
+ float: Mesh volume
466
+ """
467
+ faces = faces.copy()
468
+ vertices = vertices.copy()
469
+
470
+ # Getting vectors for the three vertices
471
+ # (with respect to origin) of each face
472
+ a = vertices[faces[:, 0], :]
473
+ b = vertices[faces[:, 1], :]
474
+ c = vertices[faces[:, 2], :]
475
+
476
+ # Calculating volume
477
+ v_cross = np.cross(b, c)
478
+ v_dot = np.sum(a.conj()*v_cross, axis=1)
479
+ volume = np.abs(np.sum(v_dot))/6
480
+
481
+ return volume
482
+
483
+ def get_mesh_area(faces: np.ndarray,
484
+ vertices: np.ndarray) -> float:
485
+ """Computes the surface area (mesh) feature from the ROI mesh by
486
+ summing over the triangular face surface areas.
487
+ This feature refers to "Surface area (mesh)" (ID = C0JK)
488
+ in the `IBSI1 reference manual <https://arxiv.org/pdf/1612.07003.pdf>`_.
489
+
490
+ Args:
491
+ faces (np.ndarray): matrix of three column vectors, defining the [X,Y,Z]
492
+ positions of the ``faces`` of the isosurface or convex hull of the mask
493
+ (output from "isosurface.m" or "convhull.m" functions of MATLAB).
494
+ --> These are more precisely indexes to ``vertices``
495
+ vertices (np.ndarray): matrix of three column vectors,
496
+ defining the [X,Y,Z]
497
+ positions of the ``vertices`` of the isosurface of the mask (output
498
+ from "isosurface.m" function of MATLAB).
499
+ --> In mm.
500
+
501
+ Returns:
502
+ float: Mesh area.
503
+ """
504
+
505
+ faces = faces.copy()
506
+ vertices = vertices.copy()
507
+
508
+ # Getting two vectors of edges for each face
509
+ a = vertices[faces[:, 1], :] - vertices[faces[:, 0], :]
510
+ b = vertices[faces[:, 2], :] - vertices[faces[:, 0], :]
511
+
512
+ # Calculating the surface area of each face and summing it up all at once.
513
+ c = np.cross(a, b)
514
+ area = 1/2 * np.sum(np.sqrt(np.sum(np.power(c, 2), 1)))
515
+
516
+ return area
517
+
518
+ def get_geary_c(vol: np.ndarray,
519
+ res: np.ndarray) -> float:
520
+ """Computes Geary'C measure (Assesses intensity differences between voxels).
521
+ This feature refers to "Geary's C measure" (ID = NPT7)
522
+ in the `IBSI1 reference manual <https://arxiv.org/pdf/1612.07003.pdf>`_.
523
+
524
+ Args:
525
+ vol (ndarray): 3D volume, NON-QUANTIZED, continous imaging intensity distribution.
526
+ res (ndarray): [a,b,c] vector specfying the resolution of the volume in mm.
527
+
528
+ Returns:
529
+ float: computes value of Geary'C measure.
530
+ """
531
+ vol = vol.copy()
532
+ res = res.copy()
533
+
534
+ # Find the location(s) of all non NaNs voxels
535
+ I, J, K = np.nonzero(~np.isnan(vol))
536
+ n_vox = np.size(I)
537
+
538
+ # Get the mean
539
+ u = np.mean(vol[~np.isnan(vol[:])])
540
+ vol_m_mean_s = np.power((vol.copy() - u), 2) # (x_gl,i - u).^2
541
+
542
+ # Sum of (x_gl,i - u).^2 over all i
543
+ sum_s = np.sum(vol_m_mean_s[~np.isnan(vol_m_mean_s[:])])
544
+
545
+ # Get a meshgrid first
546
+ x = res[0]*((np.arange(1, np.shape(vol)[0]+1))-0.5)
547
+ y = res[1]*((np.arange(1, np.shape(vol)[1]+1))-0.5)
548
+ z = res[2]*((np.arange(1, np.shape(vol)[2]+1))-0.5)
549
+ X, Y, Z = np.meshgrid(x, y, z, indexing='ij')
550
+
551
+ temp = 0
552
+ sum_w = 0
553
+
554
+ for i in range(1, n_vox+1):
555
+ # Distance mesh
556
+ temp_x = X - X[I[i-1], J[i-1], K[i-1]]
557
+ temp_y = Y - Y[I[i-1], J[i-1], K[i-1]]
558
+ temp_z = Z - Z[I[i-1], J[i-1], K[i-1]]
559
+
560
+ # meshgrid of weigths
561
+ temp_dist_mesh = 1/np.sqrt(temp_x**2 + temp_y**2 + temp_z**2)
562
+
563
+ # Removing NaNs
564
+ temp_dist_mesh[np.isnan(vol)] = np.NaN
565
+ temp_dist_mesh[I[i-1], J[i-1], K[i-1]] = np.NaN
566
+
567
+ # Running sum of weights
568
+ w_sum = np.sum(temp_dist_mesh[~np.isnan(temp_dist_mesh[:])])
569
+ sum_w = sum_w + w_sum
570
+
571
+ # Inside sum calculation
572
+ val = vol[I[i-1], J[i-1], K[i-1]].copy() # x_gl,i
573
+ # wij.*(x_gl,i - x_gl,j).^2
574
+ temp_vol = temp_dist_mesh*(vol - val)**2
575
+
576
+ # Removing i voxel to be sure;
577
+ temp_vol[I[i-1], J[i-1], K[i-1]] = np.NaN
578
+
579
+ # Sum of wij.*(x_gl,i - x_gl,j).^2 over all j
580
+ sum_val = np.sum(temp_vol[~np.isnan(temp_vol[:])])
581
+
582
+ # Running sum of (sum of wij.*(x_gl,i - x_gl,j).^2 over all j) over all i
583
+ temp = temp + sum_val
584
+
585
+ geary_c = temp * (n_vox-1) / sum_s / (2*sum_w)
586
+
587
+ return geary_c
588
+
589
+ def min_vol_ellipse(P: np.ndarray,
590
+ tolerance: np.ndarray) -> Tuple[np.ndarray,
591
+ np.ndarray]:
592
+ """Computes min_vol_ellipse.
593
+
594
+ Finds the minimum volume enclsing ellipsoid (MVEE) of a set of data
595
+ points stored in matrix P. The following optimization problem is solved:
596
+
597
+ minimize $$log(det(A))$$ subject to $$(P_i - c)' * A * (P_i - c) <= 1$$
598
+
599
+ in variables A and c, where `P_i` is the `i-th` column of the matrix `P`.
600
+ The solver is based on Khachiyan Algorithm, and the final solution
601
+ is different from the optimal value by the pre-spesified amount of
602
+ `tolerance`.
603
+
604
+ Note:
605
+ Adapted from MATLAB code of Nima Moshtagh (nima@seas.upenn.edu)
606
+ University of Pennsylvania.
607
+
608
+ Args:
609
+ P (ndarray): (d x N) dimnesional matrix containing N points in R^d.
610
+ tolerance (ndarray): error in the solution with respect to the optimal value.
611
+
612
+ Returns:
613
+ 2-element tuple containing
614
+
615
+ - A: (d x d) matrix of the ellipse equation in the 'center form': \
616
+ $$(x-c)' * A * (x-c) = 1$$ \
617
+ where d is shape of `P` along 0-axis.
618
+
619
+ - c: d-dimensional vector as the center of the ellipse.
620
+
621
+ Examples:
622
+
623
+ >>>P = rand(5,100)
624
+
625
+ >>>[A, c] = :func:`min_vol_ellipse(P, .01)`
626
+
627
+ To reduce the computation time, work with the boundary points only:
628
+
629
+ >>>K = :func:`convhulln(P)`
630
+
631
+ >>>K = :func:`unique(K(:))`
632
+
633
+ >>>Q = :func:`P(:,K)`
634
+
635
+ >>>[A, c] = :func:`min_vol_ellipse(Q, .01)`
636
+ """
637
+
638
+ # Solving the Dual problem
639
+ # data points
640
+ d, N = np.shape(P)
641
+ Q = np.ones((d+1, N))
642
+ Q[:-1, :] = P[:, :]
643
+
644
+ # initializations
645
+ err = 1
646
+ u = np.ones(N)/N # 1st iteration
647
+ new_u = np.zeros(N)
648
+
649
+ # Khachiyan Algorithm
650
+
651
+ while (err > tolerance):
652
+ diag_u = np.diag(u)
653
+ trans_q = np.transpose(Q)
654
+ X = Q @ diag_u @ trans_q
655
+
656
+ # M the diagonal vector of an NxN matrix
657
+ inv_x = np.linalg.inv(X)
658
+ M = np.diag(trans_q @ inv_x @ Q)
659
+ maximum = np.max(M)
660
+ j = np.argmax(M)
661
+
662
+ step_size = (maximum - d - 1)/((d+1)*(maximum-1))
663
+ new_u = (1 - step_size)*u.copy()
664
+ new_u[j] = new_u[j] + step_size
665
+ err = np.linalg.norm(new_u - u)
666
+ u = new_u.copy()
667
+
668
+ # Computing the Ellipse parameters
669
+ # Finds the ellipse equation in the 'center form':
670
+ # (x-c)' * A * (x-c) = 1
671
+ # It computes a dxd matrix 'A' and a d dimensional vector 'c' as the center
672
+ # of the ellipse.
673
+ U = np.diag(u)
674
+
675
+ # the A matrix for the ellipse
676
+ c = P @ u
677
+ c = np.reshape(c, (np.size(c), 1), order='F') # center of the ellipse
678
+
679
+ pup_t = P @ U @ np.transpose(P)
680
+ cct = c @ np.transpose(c)
681
+ a_inv = np.linalg.inv(pup_t - cct)
682
+ A = (1/d) * a_inv
683
+
684
+ return A, c
685
+
686
+ def padding(vol: np.ndarray,
687
+ mask_int: np.ndarray,
688
+ mask_morph: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
689
+ """Padding the volume and masks.
690
+
691
+ Args:
692
+ vol (ndarray): 3D volume, NON-QUANTIZED, continous imaging intensity distribution.
693
+ mask_int (ndarray): Intensity mask.
694
+ mask_morph (ndarray): Morphological mask.
695
+
696
+ Returns:
697
+ tuple of 3 ndarray: Volume and masks after padding.
698
+ """
699
+
700
+ # PADDING THE VOLUME WITH A LAYER OF NaNs
701
+ # (reduce mesh computation errors of associated mask)
702
+ vol = vol.copy()
703
+ vol = np.pad(vol, pad_width=1, mode="constant", constant_values=np.NaN)
704
+ # PADDING THE MASKS WITH A LAYER OF 0's
705
+ # (reduce mesh computation errors of associated mask)
706
+ mask_int = mask_int.copy()
707
+ mask_int = np.pad(mask_int, pad_width=1, mode="constant", constant_values=0.0)
708
+ mask_morph = mask_morph.copy()
709
+ mask_morph = np.pad(mask_morph, pad_width=1, mode="constant", constant_values=0.0)
710
+
711
+ return vol, mask_int, mask_morph
712
+
713
+ def get_variables(vol: np.ndarray,
714
+ mask_int: np.ndarray,
715
+ mask_morph: np.ndarray,
716
+ res: np.ndarray) -> Tuple[np.ndarray,
717
+ np.ndarray,
718
+ np.ndarray,
719
+ np.ndarray,
720
+ np.ndarray,
721
+ np.ndarray,
722
+ np.ndarray]:
723
+ """Compute variables usefull to calculate morphological features.
724
+
725
+ Args:
726
+ vol (ndarray): 3D volume, NON-QUANTIZED, continous imaging intensity distribution.
727
+ mask_int (ndarray): Intensity mask.
728
+ mask_morph (ndarray): Morphological mask.
729
+ res (ndarray): [a,b,c] vector specfying the resolution of the volume in mm.
730
+ XYZ resolution (world), or JIK resolution (intrinsic matlab).
731
+
732
+ Returns:
733
+ tuple of 7 ndarray: Variables usefull to calculate morphological features.
734
+ """
735
+ # GETTING IMPORTANT VARIABLES
736
+ xgl_int = np.reshape(vol, np.size(vol), order='F')[np.where(
737
+ np.reshape(mask_int, np.size(mask_int), order='F') == 1)[0]].copy()
738
+ xgl_morph = np.reshape(vol, np.size(vol), order='F')[np.where(
739
+ np.reshape(mask_morph, np.size(mask_morph), order='F') == 1)[0]].copy()
740
+ # XYZ refers to [Xc,Yc,Zc] in ref. [1].
741
+ xyz_int, _, _ = get_mesh(mask_int, res)
742
+ # XYZ refers to [Xc,Yc,Zc] in ref. [1].
743
+ xyz_morph, faces, vertices = get_mesh(mask_morph, res)
744
+ # [X,Y,Z] points of the convex hull.
745
+ # conv_hull Matlab is conv_hull.simplices
746
+ conv_hull = sc.ConvexHull(vertices)
747
+
748
+ return xgl_int, xgl_morph, xyz_int, xyz_morph, faces, vertices, conv_hull
749
+
750
+ def extract_all(vol: np.ndarray,
751
+ mask_int: np.ndarray,
752
+ mask_morph: np.ndarray,
753
+ res: np.ndarray,
754
+ intensity_type: str,
755
+ compute_moran_i: bool=False,
756
+ compute_geary_c: bool=False) -> Dict:
757
+ """Compute Morphological Features.
758
+ This features refer to Morphological family in
759
+ the `IBSI1 reference manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
760
+
761
+ Note:
762
+ Moran's Index and Geary's C measure takes so much computation time. Please
763
+ use `compute_moran_i` `compute_geary_c` carefully.
764
+
765
+ Args:
766
+ vol (ndarray): 3D volume, NON-QUANTIZED, continous imaging intensity distribution.
767
+ mask_int (ndarray): Intensity mask.
768
+ mask_morph (ndarray): Morphological mask.
769
+ res (ndarray): [a,b,c] vector specfying the resolution of the volume in mm.
770
+ XYZ resolution (world), or JIK resolution (intrinsic matlab).
771
+ intensity_type (str): Type of intensity to compute. Can be "arbitrary", "definite" or "filtered".
772
+ Will compute all features for "definite" intensity type and all but intergrated intensity for
773
+ "arbitrary" intensity type.
774
+ compute_moran_i (bool, optional): True to compute Moran's Index.
775
+ compute_geary_c (bool, optional): True to compute Geary's C measure.
776
+
777
+ Raises:
778
+ ValueError: If `intensity_type` is not "arbitrary", "definite" or "filtered".
779
+ """
780
+ assert intensity_type in ["arbitrary", "definite", "filtered"], \
781
+ "intensity_type must be 'arbitrary', 'definite' or 'filtered'"
782
+
783
+ # Initialization of final structure (Dictionary) containing all features.
784
+ morph = {'Fmorph_vol': [],
785
+ 'Fmorph_approx_vol': [],
786
+ 'Fmorph_area': [],
787
+ 'Fmorph_av': [],
788
+ 'Fmorph_comp_1': [],
789
+ 'Fmorph_comp_2': [],
790
+ 'Fmorph_sph_dispr': [],
791
+ 'Fmorph_sphericity': [],
792
+ 'Fmorph_asphericity': [],
793
+ 'Fmorph_com': [],
794
+ 'Fmorph_diam': [],
795
+ 'Fmorph_pca_major': [],
796
+ 'Fmorph_pca_minor': [],
797
+ 'Fmorph_pca_least': [],
798
+ 'Fmorph_pca_elongation': [],
799
+ 'Fmorph_pca_flatness': [], # until here
800
+ 'Fmorph_v_dens_aabb': [],
801
+ 'Fmorph_a_dens_aabb': [],
802
+ 'Fmorph_v_dens_ombb': [],
803
+ 'Fmorph_a_dens_ombb': [],
804
+ 'Fmorph_v_dens_aee': [],
805
+ 'Fmorph_a_dens_aee': [],
806
+ 'Fmorph_v_dens_mvee': [],
807
+ 'Fmorph_a_dens_mvee': [],
808
+ 'Fmorph_v_dens_conv_hull': [],
809
+ 'Fmorph_a_dens_conv_hull': [],
810
+ 'Fmorph_integ_int': [],
811
+ 'Fmorph_moran_i': [],
812
+ 'Fmorph_geary_c': []
813
+ }
814
+ #Initialization
815
+ vol, mask_int, mask_morph = padding(vol, mask_int, mask_morph)
816
+ xgl_int, xgl_morph, xyz_int, xyz_morph, faces, vertices, conv_hull = get_variables(vol, mask_int, mask_morph, res)
817
+
818
+ # STARTING COMPUTATION
819
+ if intensity_type != "filtered":
820
+ # Volume in mm^3
821
+ volume = get_mesh_volume(faces, vertices)
822
+ morph['Fmorph_vol'] = volume # Volume
823
+
824
+ # Approximate Volume
825
+ morph['Fmorph_approx_vol'] = np.sum(mask_morph[:]) * np.prod(res)
826
+
827
+ # Surface area in mm^2
828
+ area = get_mesh_area(faces, vertices)
829
+ morph['Fmorph_area'] = area
830
+
831
+ # Surface to volume ratio
832
+ morph['Fmorph_av'] = area / volume
833
+
834
+ # Compactness 1
835
+ morph['Fmorph_comp_1'] = volume / ((np.pi**(1/2))*(area**(3/2)))
836
+
837
+ # Compactness 2
838
+ morph['Fmorph_comp_2'] = 36*np.pi*(volume**2) / (area**3)
839
+
840
+ # Spherical disproportion
841
+ morph['Fmorph_sph_dispr'] = area / (36*np.pi*volume**2)**(1/3)
842
+
843
+ # Sphericity
844
+ morph['Fmorph_sphericity'] = ((36*np.pi*volume**2)**(1/3)) / area
845
+
846
+ # Asphericity
847
+ morph['Fmorph_asphericity'] = ((area**3) / (36*np.pi*volume**2))**(1/3) - 1
848
+
849
+ # Centre of mass shift
850
+ morph['Fmorph_com'] = get_com(xgl_int, xgl_morph, xyz_int, xyz_morph)
851
+
852
+ # Maximum 3D diameter
853
+ morph['Fmorph_diam'] = np.max(sc.distance.pdist(conv_hull.points[conv_hull.vertices]))
854
+
855
+ # Major axis length
856
+ [major, minor, least] = get_axis_lengths(xyz_morph)
857
+ morph['Fmorph_pca_major'] = 4 * np.sqrt(major)
858
+
859
+ # Minor axis length
860
+ morph['Fmorph_pca_minor'] = 4 * np.sqrt(minor)
861
+
862
+ # Least axis length
863
+ morph['Fmorph_pca_least'] = 4 * np.sqrt(least)
864
+
865
+ # Elongation
866
+ morph['Fmorph_pca_elongation'] = np.sqrt(minor / major)
867
+
868
+ # Flatness
869
+ morph['Fmorph_pca_flatness'] = np.sqrt(least / major)
870
+
871
+ # Volume density - axis-aligned bounding box
872
+ xc_aabb = np.max(vertices[:, 0]) - np.min(vertices[:, 0])
873
+ yc_aabb = np.max(vertices[:, 1]) - np.min(vertices[:, 1])
874
+ zc_aabb = np.max(vertices[:, 2]) - np.min(vertices[:, 2])
875
+ v_aabb = xc_aabb * yc_aabb * zc_aabb
876
+ morph['Fmorph_v_dens_aabb'] = volume / v_aabb
877
+
878
+ # Area density - axis-aligned bounding box
879
+ a_aabb = 2*xc_aabb*yc_aabb + 2*xc_aabb*zc_aabb + 2*yc_aabb*zc_aabb
880
+ morph['Fmorph_a_dens_aabb'] = area / a_aabb
881
+
882
+ # Volume density - oriented minimum bounding box
883
+ # Implementation of Chan and Tan's algorithm (C.K. Chan, S.T. Tan.
884
+ # Determination of the minimum bounding box of an
885
+ # arbitrary solid: an iterative approach.
886
+ # Comp Struc 79 (2001) 1433-1449
887
+ bound_box_dims = min_oriented_bound_box(vertices)
888
+ vol_bb = np.prod(bound_box_dims)
889
+ morph['Fmorph_v_dens_ombb'] = volume / vol_bb
890
+
891
+ # Area density - oriented minimum bounding box
892
+ a_ombb = 2 * (bound_box_dims[0]*bound_box_dims[1] +
893
+ bound_box_dims[0]*bound_box_dims[2] +
894
+ bound_box_dims[1]*bound_box_dims[2])
895
+ morph['Fmorph_a_dens_ombb'] = area / a_ombb
896
+
897
+ # Volume density - approximate enclosing ellipsoid
898
+ a = 2*np.sqrt(major)
899
+ b = 2*np.sqrt(minor)
900
+ c = 2*np.sqrt(least)
901
+ v_aee = (4*np.pi*a*b*c) / 3
902
+ morph['Fmorph_v_dens_aee'] = volume / v_aee
903
+
904
+ # Area density - approximate enclosing ellipsoid
905
+ a_aee = get_area_dens_approx(a, b, c, 20)
906
+ morph['Fmorph_a_dens_aee'] = area / a_aee
907
+
908
+ # Volume density - minimum volume enclosing ellipsoid
909
+ # (Rotate the volume first??)
910
+ # Copyright (c) 2009, Nima Moshtagh
911
+ # http://www.mathworks.com/matlabcentral/fileexchange/
912
+ # 9542-minimum-volume-enclosing-ellipsoid
913
+ # Subsequent singular value decomposition of matrix A and and
914
+ # taking the inverse of the square root of the diagonal of the
915
+ # sigma matrix will produce respective semi-axis lengths.
916
+ # Subsequent singular value decomposition of matrix A and
917
+ # taking the inverse of the square root of the diagonal of the
918
+ # sigma matrix will produce respective semi-axis lengths.
919
+ p = np.stack((conv_hull.points[conv_hull.simplices[:, 0], 0],
920
+ conv_hull.points[conv_hull.simplices[:, 1], 1],
921
+ conv_hull.points[conv_hull.simplices[:, 2], 2]), axis=1)
922
+ A, _ = min_vol_ellipse(np.transpose(p), 0.01)
923
+ # New semi-axis lengths
924
+ _, Q, _ = np.linalg.svd(A)
925
+ a = 1/np.sqrt(Q[2])
926
+ b = 1/np.sqrt(Q[1])
927
+ c = 1/np.sqrt(Q[0])
928
+ v_mvee = (4*np.pi*a*b*c)/3
929
+ morph['Fmorph_v_dens_mvee'] = volume / v_mvee
930
+
931
+ # Area density - minimum volume enclosing ellipsoid
932
+ # Using a new set of (a,b,c), see Volume density - minimum
933
+ # volume enclosing ellipsoid
934
+ a_mvee = get_area_dens_approx(a, b, c, 20)
935
+ morph['Fmorph_a_dens_mvee'] = area / a_mvee
936
+
937
+ # Volume density - convex hull
938
+ v_convex = conv_hull.volume
939
+ morph['Fmorph_v_dens_conv_hull'] = volume / v_convex
940
+
941
+ # Area density - convex hull
942
+ a_convex = conv_hull.area
943
+ morph['Fmorph_a_dens_conv_hull'] = area / a_convex
944
+
945
+ # Integrated intensity
946
+ if intensity_type == "definite":
947
+ volume = get_mesh_volume(faces, vertices)
948
+ morph['Fmorph_integ_int'] = np.mean(xgl_int) * volume
949
+
950
+ # Moran's I index
951
+ if compute_moran_i:
952
+ vol_mor = vol.copy()
953
+ vol_mor[mask_int == 0] = np.NaN
954
+ morph['Fmorph_moran_i'] = get_moran_i(vol_mor, res)
955
+
956
+ # Geary's C measure
957
+ if compute_geary_c:
958
+ morph['Fmorph_geary_c'] = get_geary_c(vol_mor, res)
959
+
960
+ return morph
961
+
962
+ def vol(vol: np.ndarray,
963
+ mask_int: np.ndarray,
964
+ mask_morph: np.ndarray,
965
+ res: np.ndarray) -> float:
966
+ """Computes morphological volume feature.
967
+ This feature refers to "Fmorph_vol" (ID = RNUO) in
968
+ the `IBSI1 reference manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
969
+
970
+ Args:
971
+ vol (ndarray): 3D volume, NON-QUANTIZED, continous imaging intensity distribution.
972
+ mask_int (ndarray): Intensity mask.
973
+ mask_morph (ndarray): Morphological mask.
974
+ res (ndarray): [a,b,c] vector specfying the resolution of the volume in mm.
975
+ XYZ resolution (world), or JIK resolution (intrinsic matlab).
976
+
977
+ Returns:
978
+ float: Value of the morphological volume feature.
979
+ """
980
+ vol, mask_int, mask_morph = padding(vol, mask_int, mask_morph)
981
+ _, _, _, _, faces, vertices, _ = get_variables(vol, mask_int, mask_morph, res)
982
+ volume = get_mesh_volume(faces, vertices)
983
+
984
+ return volume # Morphological volume feature
985
+
986
+ def approx_vol(vol: np.ndarray,
987
+ mask_int: np.ndarray,
988
+ mask_morph: np.ndarray,
989
+ res: np.ndarray) -> float:
990
+ """Computes morphological approximate volume feature.
991
+ This feature refers to "Fmorph_approx_vol" (ID = YEKZ) in
992
+ the `IBSI1 reference manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
993
+
994
+ Args:
995
+ vol (ndarray): 3D volume, NON-QUANTIZED, continous imaging intensity distribution.
996
+ mask_int (ndarray): Intensity mask.
997
+ mask_morph (ndarray): Morphological mask.
998
+ res (ndarray): [a,b,c] vector specfying the resolution of the volume in mm.
999
+ XYZ resolution (world), or JIK resolution (intrinsic matlab).
1000
+
1001
+ Returns:
1002
+ float: Value of the morphological approximate volume feature.
1003
+ """
1004
+ vol, mask_int, mask_morph = padding(vol, mask_int, mask_morph)
1005
+ volume_appro = np.sum(mask_morph[:]) * np.prod(res)
1006
+
1007
+ return volume_appro # Morphological approximate volume feature
1008
+
1009
+ def area(vol: np.ndarray,
1010
+ mask_int: np.ndarray,
1011
+ mask_morph: np.ndarray,
1012
+ res: np.ndarray) -> float:
1013
+ """Computes Surface area feature.
1014
+ This feature refers to "Fmorph_area" (ID = COJJK) in
1015
+ the `IBSI1 reference manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
1016
+
1017
+ Args:
1018
+ vol (ndarray): 3D volume, NON-QUANTIZED, continous imaging intensity distribution.
1019
+ mask_int (ndarray): Intensity mask.
1020
+ mask_morph (ndarray): Morphological mask.
1021
+ res (ndarray): [a,b,c] vector specfying the resolution of the volume in mm.
1022
+ XYZ resolution (world), or JIK resolution (intrinsic matlab).
1023
+
1024
+ Returns:
1025
+ float: Value of the surface area feature.
1026
+ """
1027
+ vol, mask_int, mask_morph = padding(vol, mask_int, mask_morph)
1028
+ _, _, _, _, faces, vertices, _ = get_variables(vol, mask_int, mask_morph, res)
1029
+ area = get_mesh_area(faces, vertices)
1030
+
1031
+ return area # Surface area
1032
+
1033
+ def av(vol: np.ndarray,
1034
+ mask_int: np.ndarray,
1035
+ mask_morph: np.ndarray,
1036
+ res: np.ndarray) -> float:
1037
+ """Computes Surface to volume ratio feature.
1038
+ This feature refers to "Fmorph_av" (ID = 2PR5) in
1039
+ the `IBSI1 reference manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
1040
+
1041
+ Args:
1042
+ vol (ndarray): 3D volume, NON-QUANTIZED, continous imaging intensity distribution.
1043
+ mask_int (ndarray): Intensity mask.
1044
+ mask_morph (ndarray): Morphological mask.
1045
+ res (ndarray): [a,b,c] vector specfying the resolution of the volume in mm.
1046
+ XYZ resolution (world), or JIK resolution (intrinsic matlab).
1047
+
1048
+ Returns:
1049
+ float: Value of the Surface to volume ratio feature.
1050
+ """
1051
+ _, _, _, _, faces, vertices, _ = get_variables(vol, mask_int, mask_morph, res)
1052
+ volume = get_mesh_volume(faces, vertices)
1053
+ area = get_mesh_area(faces, vertices)
1054
+ ratio = area / volume
1055
+
1056
+ return ratio # Surface to volume ratio
1057
+
1058
+ def comp_1(vol: np.ndarray,
1059
+ mask_int: np.ndarray,
1060
+ mask_morph: np.ndarray,
1061
+ res: np.ndarray) -> float:
1062
+ """Computes Compactness 1 feature.
1063
+ This feature refers to "Fmorph_comp_1" (ID = SKGS) in
1064
+ the `IBSI1 reference manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
1065
+
1066
+ Args:
1067
+ vol (ndarray): 3D volume, NON-QUANTIZED, continous imaging intensity distribution.
1068
+ mask_int (ndarray): Intensity mask.
1069
+ mask_morph (ndarray): Morphological mask.
1070
+ res (ndarray): [a,b,c] vector specfying the resolution of the volume in mm.
1071
+ XYZ resolution (world), or JIK resolution (intrinsic matlab).
1072
+
1073
+ Returns:
1074
+ float: Value of the Compactness 1 feature.
1075
+ """
1076
+ _, _, _, _, faces, vertices, _ = get_variables(vol, mask_int, mask_morph, res)
1077
+ volume = get_mesh_volume(faces, vertices)
1078
+ area = get_mesh_area(faces, vertices)
1079
+ comp_1 = volume / ((np.pi**(1/2))*(area**(3/2)))
1080
+
1081
+ return comp_1 # Compactness 1
1082
+
1083
+ def comp_2(vol: np.ndarray,
1084
+ mask_int: np.ndarray,
1085
+ mask_morph: np.ndarray,
1086
+ res: np.ndarray) -> float:
1087
+ """Computes Compactness 2 feature.
1088
+ This feature refers to "Fmorph_comp_2" (ID = BQWJ) in
1089
+ the `IBSI1 reference manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
1090
+
1091
+ Args:
1092
+ vol (ndarray): 3D volume, NON-QUANTIZED, continous imaging intensity distribution.
1093
+ mask_int (ndarray): Intensity mask.
1094
+ mask_morph (ndarray): Morphological mask.
1095
+ res (ndarray): [a,b,c] vector specfying the resolution of the volume in mm.
1096
+ XYZ resolution (world), or JIK resolution (intrinsic matlab).
1097
+
1098
+ Returns:
1099
+ float: Value of the Compactness 2 feature.
1100
+ """
1101
+ _, _, _, _, faces, vertices, _ = get_variables(vol, mask_int, mask_morph, res)
1102
+ volume = get_mesh_volume(faces, vertices)
1103
+ area = get_mesh_area(faces, vertices)
1104
+ comp_2 = 36*np.pi*(volume**2) / (area**3)
1105
+
1106
+ return comp_2 # Compactness 2
1107
+
1108
+ def sph_dispr(vol: np.ndarray,
1109
+ mask_int: np.ndarray,
1110
+ mask_morph: np.ndarray,
1111
+ res: np.ndarray) -> float:
1112
+ """Computes Spherical disproportion feature.
1113
+ This feature refers to "Fmorph_sph_dispr" (ID = KRCK) in
1114
+ the `IBSI1 reference manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
1115
+
1116
+ Args:
1117
+ vol (ndarray): 3D volume, NON-QUANTIZED, continous imaging intensity distribution.
1118
+ mask_int (ndarray): Intensity mask.
1119
+ mask_morph (ndarray): Morphological mask.
1120
+ res (ndarray): [a,b,c] vector specfying the resolution of the volume in mm.
1121
+ XYZ resolution (world), or JIK resolution (intrinsic matlab).
1122
+
1123
+ Returns:
1124
+ float: Value of the Spherical disproportion feature.
1125
+ """
1126
+ _, _, _, _, faces, vertices, _ = get_variables(vol, mask_int, mask_morph, res)
1127
+ volume = get_mesh_volume(faces, vertices)
1128
+ area = get_mesh_area(faces, vertices)
1129
+ sph_dispr = area / (36*np.pi*volume**2)**(1/3)
1130
+
1131
+ return sph_dispr # Spherical disproportion
1132
+
1133
+ def sphericity(vol: np.ndarray,
1134
+ mask_int: np.ndarray,
1135
+ mask_morph: np.ndarray,
1136
+ res: np.ndarray) -> float:
1137
+ """Computes Sphericity feature.
1138
+ This feature refers to "Fmorph_sphericity" (ID = QCFX) in
1139
+ the `IBSI1 reference manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
1140
+
1141
+ Args:
1142
+ vol (ndarray): 3D volume, NON-QUANTIZED, continous imaging intensity distribution.
1143
+ mask_int (ndarray): Intensity mask.
1144
+ mask_morph (ndarray): Morphological mask.
1145
+ res (ndarray): [a,b,c] vector specfying the resolution of the volume in mm.
1146
+ XYZ resolution (world), or JIK resolution (intrinsic matlab).
1147
+
1148
+ Returns:
1149
+ float: Value of the Sphericity feature.
1150
+ """
1151
+ _, _, _, _, faces, vertices, _ = get_variables(vol, mask_int, mask_morph, res)
1152
+ volume = get_mesh_volume(faces, vertices)
1153
+ area = get_mesh_area(faces, vertices)
1154
+ sphericity = ((36*np.pi*volume**2)**(1/3)) / area
1155
+
1156
+ return sphericity # Sphericity
1157
+
1158
+ def asphericity(vol: np.ndarray,
1159
+ mask_int: np.ndarray,
1160
+ mask_morph: np.ndarray,
1161
+ res: np.ndarray) -> float:
1162
+ """Computes Asphericity feature.
1163
+ This feature refers to "Fmorph_asphericity" (ID = 25C) in
1164
+ the `IBSI1 reference manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
1165
+
1166
+ Args:
1167
+ vol (ndarray): 3D volume, NON-QUANTIZED, continous imaging intensity distribution.
1168
+ mask_int (ndarray): Intensity mask.
1169
+ mask_morph (ndarray): Morphological mask.
1170
+ res (ndarray): [a,b,c] vector specfying the resolution of the volume in mm.
1171
+ XYZ resolution (world), or JIK resolution (intrinsic matlab).
1172
+
1173
+ Returns:
1174
+ float: Value of the Asphericity feature.
1175
+ """
1176
+ _, _, _, _, faces, vertices, _ = get_variables(vol, mask_int, mask_morph, res)
1177
+ volume = get_mesh_volume(faces, vertices)
1178
+ area = get_mesh_area(faces, vertices)
1179
+ asphericity = ((area**3) / (36*np.pi*volume**2))**(1/3) - 1
1180
+
1181
+ return asphericity # Asphericity
1182
+
1183
+ def com(vol: np.ndarray,
1184
+ mask_int: np.ndarray,
1185
+ mask_morph: np.ndarray,
1186
+ res: np.ndarray) -> float:
1187
+ """Computes Centre of mass shift feature.
1188
+ This feature refers to "Fmorph_com" (ID = KLM) in
1189
+ the `IBSI1 reference manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
1190
+
1191
+ Args:
1192
+ vol (ndarray): 3D volume, NON-QUANTIZED, continous imaging intensity distribution.
1193
+ mask_int (ndarray): Intensity mask.
1194
+ mask_morph (ndarray): Morphological mask.
1195
+ res (ndarray): [a,b,c] vector specfying the resolution of the volume in mm.
1196
+ XYZ resolution (world), or JIK resolution (intrinsic matlab).
1197
+
1198
+ Returns:
1199
+ float: Value of the Centre of mass shift feature.
1200
+ """
1201
+ vol, mask_int, mask_morph = padding(vol, mask_int, mask_morph)
1202
+ xgl_int, xgl_morph, xyz_int, xyz_morph, _, _, _ = get_variables(vol, mask_int, mask_morph, res)
1203
+ com = get_com(xgl_int, xgl_morph, xyz_int, xyz_morph)
1204
+
1205
+ return com # Centre of mass shift
1206
+
1207
+ def diam(vol: np.ndarray,
1208
+ mask_int: np.ndarray,
1209
+ mask_morph: np.ndarray,
1210
+ res: np.ndarray) -> float:
1211
+ """Computes Maximum 3D diameter feature.
1212
+ This feature refers to "Fmorph_diam" (ID = L0JK) in
1213
+ the `IBSI1 reference manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
1214
+
1215
+ Args:
1216
+ vol (ndarray): 3D volume, NON-QUANTIZED, continous imaging intensity distribution.
1217
+ mask_int (ndarray): Intensity mask.
1218
+ mask_morph (ndarray): Morphological mask.
1219
+ res (ndarray): [a,b,c] vector specfying the resolution of the volume in mm.
1220
+ XYZ resolution (world), or JIK resolution (intrinsic matlab).
1221
+
1222
+ Returns:
1223
+ float: Value of the Maximum 3D diameter feature.
1224
+ """
1225
+ vol, mask_int, mask_morph = padding(vol, mask_int, mask_morph)
1226
+ _, _, _, _, _, _, conv_hull = get_variables(vol, mask_int, mask_morph, res)
1227
+ diam = np.max(sc.distance.pdist(conv_hull.points[conv_hull.vertices]))
1228
+
1229
+ return diam # Maximum 3D diameter
1230
+
1231
+ def pca_major(vol: np.ndarray,
1232
+ mask_int: np.ndarray,
1233
+ mask_morph: np.ndarray,
1234
+ res: np.ndarray) -> float:
1235
+ """Computes Major axis length feature.
1236
+ This feature refers to "Fmorph_pca_major" (ID = TDIC) in
1237
+ the `IBSI1 reference manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
1238
+
1239
+ Args:
1240
+ vol (ndarray): 3D volume, NON-QUANTIZED, continous imaging intensity distribution.
1241
+ mask_int (ndarray): Intensity mask.
1242
+ mask_morph (ndarray): Morphological mask.
1243
+ res (ndarray): [a,b,c] vector specfying the resolution of the volume in mm.
1244
+ XYZ resolution (world), or JIK resolution (intrinsic matlab).
1245
+
1246
+ Returns:
1247
+ float: Value of the Major axis length feature.
1248
+ """
1249
+ vol, mask_int, mask_morph = padding(vol, mask_int, mask_morph)
1250
+ _, _, _, xyz_morph, _, _, _ = get_variables(vol, mask_int, mask_morph, res)
1251
+ [major, _, _] = get_axis_lengths(xyz_morph)
1252
+ pca_major = 4 * np.sqrt(major)
1253
+
1254
+ return pca_major # Major axis length
1255
+
1256
+ def pca_minor(vol: np.ndarray,
1257
+ mask_int: np.ndarray,
1258
+ mask_morph: np.ndarray,
1259
+ res: np.ndarray) -> float:
1260
+ """Computes Minor axis length feature.
1261
+ This feature refers to "Fmorph_pca_minor" (ID = P9VJ) in
1262
+ the `IBSI1 reference manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
1263
+
1264
+ Args:
1265
+ vol (ndarray): 3D volume, NON-QUANTIZED, continous imaging intensity distribution.
1266
+ mask_int (ndarray): Intensity mask.
1267
+ mask_morph (ndarray): Morphological mask.
1268
+ res (ndarray): [a,b,c] vector specfying the resolution of the volume in mm.
1269
+ XYZ resolution (world), or JIK resolution (intrinsic matlab).
1270
+
1271
+ Returns:
1272
+ float: Value of the Minor axis length feature.
1273
+ """
1274
+ vol, mask_int, mask_morph = padding(vol, mask_int, mask_morph)
1275
+ _, _, _, xyz_morph, _, _, _ = get_variables(vol, mask_int, mask_morph, res)
1276
+ [_, minor, _] = get_axis_lengths(xyz_morph)
1277
+ pca_minor = 4 * np.sqrt(minor)
1278
+
1279
+ return pca_minor # Minor axis length
1280
+
1281
+ def pca_least(vol: np.ndarray,
1282
+ mask_int: np.ndarray,
1283
+ mask_morph: np.ndarray,
1284
+ res: np.ndarray) -> float:
1285
+ """Computes Least axis length feature.
1286
+ This feature refers to "Fmorph_pca_least" (ID = 7J51) in
1287
+ the `IBSI1 reference manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
1288
+
1289
+ Args:
1290
+ vol (ndarray): 3D volume, NON-QUANTIZED, continous imaging intensity distribution.
1291
+ mask_int (ndarray): Intensity mask.
1292
+ mask_morph (ndarray): Morphological mask.
1293
+ res (ndarray): [a,b,c] vector specfying the resolution of the volume in mm.
1294
+ XYZ resolution (world), or JIK resolution (intrinsic matlab).
1295
+
1296
+ Returns:
1297
+ float: Value of the Least axis length feature.
1298
+ """
1299
+ vol, mask_int, mask_morph = padding(vol, mask_int, mask_morph)
1300
+ _, _, _, xyz_morph, _, _, _ = get_variables(vol, mask_int, mask_morph, res)
1301
+ [_, _, least] = get_axis_lengths(xyz_morph)
1302
+ pca_least = 4 * np.sqrt(least)
1303
+
1304
+ return pca_least # Least axis length
1305
+
1306
+ def pca_elongation(vol: np.ndarray,
1307
+ mask_int: np.ndarray,
1308
+ mask_morph: np.ndarray,
1309
+ res: np.ndarray) -> float:
1310
+ """Computes Elongation feature.
1311
+ This feature refers to "Fmorph_pca_elongation" (ID = Q3CK) in
1312
+ the `IBSI1 reference manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
1313
+
1314
+ Args:
1315
+ vol (ndarray): 3D volume, NON-QUANTIZED, continous imaging intensity distribution.
1316
+ mask_int (ndarray): Intensity mask.
1317
+ mask_morph (ndarray): Morphological mask.
1318
+ res (ndarray): [a,b,c] vector specfying the resolution of the volume in mm.
1319
+ XYZ resolution (world), or JIK resolution (intrinsic matlab).
1320
+
1321
+ Returns:
1322
+ float: Value of the Elongation feature.
1323
+ """
1324
+ vol, mask_int, mask_morph = padding(vol, mask_int, mask_morph)
1325
+ _, _, _, xyz_morph, _, _, _ = get_variables(vol, mask_int, mask_morph, res)
1326
+ [major, minor, _] = get_axis_lengths(xyz_morph)
1327
+ pca_elongation = np.sqrt(minor / major)
1328
+
1329
+ return pca_elongation # Elongation
1330
+
1331
+ def pca_flatness(vol: np.ndarray,
1332
+ mask_int: np.ndarray,
1333
+ mask_morph: np.ndarray,
1334
+ res: np.ndarray) -> float:
1335
+ """Computes Flatness feature.
1336
+ This feature refers to "Fmorph_pca_flatness" (ID = N17B) in
1337
+ the `IBSI1 reference manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
1338
+
1339
+ Args:
1340
+ vol (ndarray): 3D volume, NON-QUANTIZED, continous imaging intensity distribution.
1341
+ mask_int (ndarray): Intensity mask.
1342
+ mask_morph (ndarray): Morphological mask.
1343
+ res (ndarray): [a,b,c] vector specfying the resolution of the volume in mm.
1344
+ XYZ resolution (world), or JIK resolution (intrinsic matlab).
1345
+
1346
+ Returns:
1347
+ float: Value of the Flatness feature.
1348
+ """
1349
+ vol, mask_int, mask_morph = padding(vol, mask_int, mask_morph)
1350
+ _, _, _, xyz_morph, _, _, _ = get_variables(vol, mask_int, mask_morph, res)
1351
+ [major, _, least] = get_axis_lengths(xyz_morph)
1352
+ pca_flatness = np.sqrt(least / major)
1353
+
1354
+ return pca_flatness # Flatness
1355
+
1356
+ def v_dens_aabb(vol: np.ndarray,
1357
+ mask_int: np.ndarray,
1358
+ mask_morph: np.ndarray,
1359
+ res: np.ndarray) -> float:
1360
+ """Computes Volume density - axis-aligned bounding box feature.
1361
+ This feature refers to "Fmorph_v_dens_aabb" (ID = PBX1) in
1362
+ the `IBSI1 reference manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
1363
+
1364
+ Args:
1365
+ vol (ndarray): 3D volume, NON-QUANTIZED, continous imaging intensity distribution.
1366
+ mask_int (ndarray): Intensity mask.
1367
+ mask_morph (ndarray): Morphological mask.
1368
+ res (ndarray): [a,b,c] vector specfying the resolution of the volume in mm.
1369
+ XYZ resolution (world), or JIK resolution (intrinsic matlab).
1370
+
1371
+ Returns:
1372
+ float: Value of the Volume density - axis-aligned bounding box feature.
1373
+ """
1374
+ vol, mask_int, mask_morph = padding(vol, mask_int, mask_morph)
1375
+ _, _, _, _, faces, vertices, _ = get_variables(vol, mask_int, mask_morph, res)
1376
+ volume = get_mesh_volume(faces, vertices)
1377
+ xc_aabb = np.max(vertices[:, 0]) - np.min(vertices[:, 0])
1378
+ yc_aabb = np.max(vertices[:, 1]) - np.min(vertices[:, 1])
1379
+ zc_aabb = np.max(vertices[:, 2]) - np.min(vertices[:, 2])
1380
+ v_aabb = xc_aabb * yc_aabb * zc_aabb
1381
+ v_dens_aabb = volume / v_aabb
1382
+
1383
+ return v_dens_aabb # Volume density - axis-aligned bounding box
1384
+
1385
+ def a_dens_aabb(vol: np.ndarray,
1386
+ mask_int: np.ndarray,
1387
+ mask_morph: np.ndarray,
1388
+ res: np.ndarray) -> float:
1389
+ """Computes Area density - axis-aligned bounding box feature.
1390
+ This feature refers to "Fmorph_a_dens_aabb" (ID = R59B) in
1391
+ the `IBSI1 reference manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
1392
+
1393
+ Args:
1394
+ vol (ndarray): 3D volume, NON-QUANTIZED, continous imaging intensity distribution.
1395
+ mask_int (ndarray): Intensity mask.
1396
+ mask_morph (ndarray): Morphological mask.
1397
+ res (ndarray): [a,b,c] vector specfying the resolution of the volume in mm.
1398
+ XYZ resolution (world), or JIK resolution (intrinsic matlab).
1399
+
1400
+ Returns:
1401
+ float: Value of the Area density - axis-aligned bounding box feature.
1402
+ """
1403
+ vol, mask_int, mask_morph = padding(vol, mask_int, mask_morph)
1404
+ _, _, _, _, faces, vertices, _ = get_variables(vol, mask_int, mask_morph, res)
1405
+ area = get_mesh_area(faces, vertices)
1406
+ xc_aabb = np.max(vertices[:, 0]) - np.min(vertices[:, 0])
1407
+ yc_aabb = np.max(vertices[:, 1]) - np.min(vertices[:, 1])
1408
+ zc_aabb = np.max(vertices[:, 2]) - np.min(vertices[:, 2])
1409
+ a_aabb = 2*xc_aabb*yc_aabb + 2*xc_aabb*zc_aabb + 2*yc_aabb*zc_aabb
1410
+ a_dens_aabb = area / a_aabb
1411
+
1412
+ return a_dens_aabb # Area density - axis-aligned bounding box
1413
+
1414
+ def v_dens_ombb(vol: np.ndarray,
1415
+ mask_int: np.ndarray,
1416
+ mask_morph: np.ndarray,
1417
+ res: np.ndarray) -> float:
1418
+ """Computes Volume density - oriented minimum bounding box feature.
1419
+ Implementation of Chan and Tan's algorithm (C.K. Chan, S.T. Tan.
1420
+ Determination of the minimum bounding box of an
1421
+ arbitrary solid: an iterative approach.
1422
+ Comp Struc 79 (2001) 1433-1449.
1423
+ This feature refers to "Fmorph_v_dens_ombb" (ID = ZH1A) in
1424
+ the `IBSI1 reference manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
1425
+
1426
+ Args:
1427
+ vol (ndarray): 3D volume, NON-QUANTIZED, continous imaging intensity distribution.
1428
+ mask_int (ndarray): Intensity mask.
1429
+ mask_morph (ndarray): Morphological mask.
1430
+ res (ndarray): [a,b,c] vector specfying the resolution of the volume in mm.
1431
+ XYZ resolution (world), or JIK resolution (intrinsic matlab).
1432
+
1433
+ Returns:
1434
+ float: Value of the Volume density - oriented minimum bounding box feature.
1435
+ """
1436
+ vol, mask_int, mask_morph = padding(vol, mask_int, mask_morph)
1437
+ _, _, _, _, faces, vertices, _ = get_variables(vol, mask_int, mask_morph, res)
1438
+ volume = get_mesh_volume(faces, vertices)
1439
+ bound_box_dims = min_oriented_bound_box(vertices)
1440
+ vol_bb = np.prod(bound_box_dims)
1441
+ v_dens_ombb = volume / vol_bb
1442
+
1443
+ return v_dens_ombb # Volume density - oriented minimum bounding box
1444
+
1445
+ def a_dens_ombb(vol: np.ndarray,
1446
+ mask_int: np.ndarray,
1447
+ mask_morph: np.ndarray,
1448
+ res: np.ndarray) -> float:
1449
+ """Computes Area density - oriented minimum bounding box feature.
1450
+ Implementation of Chan and Tan's algorithm (C.K. Chan, S.T. Tan.
1451
+ Determination of the minimum bounding box of an
1452
+ arbitrary solid: an iterative approach.
1453
+ This feature refers to "Fmorph_a_dens_ombb" (ID = IQYR) in
1454
+ the `IBSI1 reference manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
1455
+
1456
+ Args:
1457
+ vol (ndarray): 3D volume, NON-QUANTIZED, continous imaging intensity distribution.
1458
+ mask_int (ndarray): Intensity mask.
1459
+ mask_morph (ndarray): Morphological mask.
1460
+ res (ndarray): [a,b,c] vector specfying the resolution of the volume in mm.
1461
+ XYZ resolution (world), or JIK resolution (intrinsic matlab).
1462
+
1463
+ Returns:
1464
+ float: Value of the Area density - oriented minimum bounding box feature.
1465
+ """
1466
+ vol, mask_int, mask_morph = padding(vol, mask_int, mask_morph)
1467
+ _, _, _, _, faces, vertices, _ = get_variables(vol, mask_int, mask_morph, res)
1468
+ area = get_mesh_area(faces, vertices)
1469
+
1470
+ bound_box_dims = min_oriented_bound_box(vertices)
1471
+ a_ombb = 2 * (bound_box_dims[0] * bound_box_dims[1]
1472
+ + bound_box_dims[0] * bound_box_dims[2]
1473
+ + bound_box_dims[1] * bound_box_dims[2])
1474
+ a_dens_ombb = area / a_ombb
1475
+
1476
+ return a_dens_ombb # Area density - oriented minimum bounding box
1477
+
1478
+ def v_dens_aee(vol: np.ndarray,
1479
+ mask_int: np.ndarray,
1480
+ mask_morph: np.ndarray,
1481
+ res: np.ndarray) -> float:
1482
+ """Computes Volume density - approximate enclosing ellipsoid feature.
1483
+ This feature refers to "Fmorph_v_dens_aee" (ID = 6BDE) in
1484
+ the `IBSI1 reference manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
1485
+
1486
+ Args:
1487
+ vol (ndarray): 3D volume, NON-QUANTIZED, continous imaging intensity distribution.
1488
+ mask_int (ndarray): Intensity mask.
1489
+ mask_morph (ndarray): Morphological mask.
1490
+ res (ndarray): [a,b,c] vector specfying the resolution of the volume in mm.
1491
+ XYZ resolution (world), or JIK resolution (intrinsic matlab).
1492
+
1493
+ Returns:
1494
+ float: Value of the Volume density - approximate enclosing ellipsoid feature.
1495
+ """
1496
+ vol, mask_int, mask_morph = padding(vol, mask_int, mask_morph)
1497
+ _, _, _, xyz_morph, faces, vertices, _ = get_variables(vol, mask_int, mask_morph, res)
1498
+ volume = get_mesh_volume(faces, vertices)
1499
+ [major, minor, least] = get_axis_lengths(xyz_morph)
1500
+ a = 2*np.sqrt(major)
1501
+ b = 2*np.sqrt(minor)
1502
+ c = 2*np.sqrt(least)
1503
+ v_aee = (4*np.pi*a*b*c) / 3
1504
+ v_dens_aee = volume / v_aee
1505
+
1506
+ return v_dens_aee # Volume density - approximate enclosing ellipsoid
1507
+
1508
+ def a_dens_aee(vol: np.ndarray,
1509
+ mask_int: np.ndarray,
1510
+ mask_morph: np.ndarray,
1511
+ res: np.ndarray) -> float:
1512
+ """Computes Area density - approximate enclosing ellipsoid feature.
1513
+ This feature refers to "Fmorph_a_dens_aee" (ID = RDD2) in
1514
+ the `IBSI1 reference manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
1515
+
1516
+ Args:
1517
+ vol (ndarray): 3D volume, NON-QUANTIZED, continous imaging intensity distribution.
1518
+ mask_int (ndarray): Intensity mask.
1519
+ mask_morph (ndarray): Morphological mask.
1520
+ res (ndarray): [a,b,c] vector specfying the resolution of the volume in mm.
1521
+ XYZ resolution (world), or JIK resolution (intrinsic matlab).
1522
+
1523
+ Returns:
1524
+ float: Value of the Area density - approximate enclosing ellipsoid feature.
1525
+ """
1526
+ vol, mask_int, mask_morph = padding(vol, mask_int, mask_morph)
1527
+ _, _, _, xyz_morph, faces, vertices, _ = get_variables(vol, mask_int, mask_morph, res)
1528
+ area = get_mesh_area(faces, vertices)
1529
+ [major, minor, least] = get_axis_lengths(xyz_morph)
1530
+ a = 2*np.sqrt(major)
1531
+ b = 2*np.sqrt(minor)
1532
+ c = 2*np.sqrt(least)
1533
+ a_aee = get_area_dens_approx(a, b, c, 20)
1534
+ a_dens_aee = area / a_aee
1535
+
1536
+ return a_dens_aee # Area density - approximate enclosing ellipsoid
1537
+
1538
+ def v_dens_mvee(vol: np.ndarray,
1539
+ mask_int: np.ndarray,
1540
+ mask_morph: np.ndarray,
1541
+ res: np.ndarray) -> float:
1542
+ """Computes Volume density - minimum volume enclosing ellipsoid feature.
1543
+ Subsequent singular value decomposition of matrix A and and
1544
+ taking the inverse of the square root of the diagonal of the
1545
+ sigma matrix will produce respective semi-axis lengths.
1546
+ Subsequent singular value decomposition of matrix A and
1547
+ taking the inverse of the square root of the diagonal of the
1548
+ sigma matrix will produce respective semi-axis lengths.
1549
+ This feature refers to "Fmorph_v_dens_mvee" (ID = SWZ1) in
1550
+ the `IBSI1 reference manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
1551
+
1552
+ Args:
1553
+ vol (ndarray): 3D volume, NON-QUANTIZED, continous imaging intensity distribution.
1554
+ mask_int (ndarray): Intensity mask.
1555
+ mask_morph (ndarray): Morphological mask.
1556
+ res (ndarray): [a,b,c] vector specfying the resolution of the volume in mm.
1557
+ XYZ resolution (world), or JIK resolution (intrinsic matlab).
1558
+
1559
+ Returns:
1560
+ float: Value of the Volume density - minimum volume enclosing ellipsoid feature.
1561
+ """
1562
+ vol, mask_int, mask_morph = padding(vol, mask_int, mask_morph)
1563
+ _, _, _, _, faces, vertices, conv_hull = get_variables(vol, mask_int, mask_morph, res)
1564
+ volume = get_mesh_volume(faces, vertices)
1565
+ p = np.stack((conv_hull.points[conv_hull.simplices[:, 0], 0],
1566
+ conv_hull.points[conv_hull.simplices[:, 1], 1],
1567
+ conv_hull.points[conv_hull.simplices[:, 2], 2]), axis=1)
1568
+ A, _ = min_vol_ellipse(np.transpose(p), 0.01)
1569
+ # New semi-axis lengths
1570
+ _, Q, _ = np.linalg.svd(A)
1571
+ a = 1/np.sqrt(Q[2])
1572
+ b = 1/np.sqrt(Q[1])
1573
+ c = 1/np.sqrt(Q[0])
1574
+ v_mvee = (4*np.pi*a*b*c) / 3
1575
+ v_dens_mvee = volume / v_mvee
1576
+
1577
+ return v_dens_mvee # Volume density - minimum volume enclosing ellipsoid
1578
+
1579
+ def a_dens_mvee(vol: np.ndarray,
1580
+ mask_int: np.ndarray,
1581
+ mask_morph: np.ndarray,
1582
+ res: np.ndarray) -> float:
1583
+ """Computes Area density - minimum volume enclosing ellipsoid feature.
1584
+ Subsequent singular value decomposition of matrix A and and
1585
+ taking the inverse of the square root of the diagonal of the
1586
+ sigma matrix will produce respective semi-axis lengths.
1587
+ Subsequent singular value decomposition of matrix A and
1588
+ taking the inverse of the square root of the diagonal of the
1589
+ sigma matrix will produce respective semi-axis lengths.
1590
+ This feature refers to "Fmorph_a_dens_mvee" (ID = BRI8) in
1591
+ the `IBSI1 reference manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
1592
+
1593
+ Args:
1594
+ vol (ndarray): 3D volume, NON-QUANTIZED, continous imaging intensity distribution.
1595
+ mask_int (ndarray): Intensity mask.
1596
+ mask_morph (ndarray): Morphological mask.
1597
+ res (ndarray): [a,b,c] vector specfying the resolution of the volume in mm.
1598
+ XYZ resolution (world), or JIK resolution (intrinsic matlab).
1599
+
1600
+ Returns:
1601
+ float: Value of the Area density - minimum volume enclosing ellipsoid feature.
1602
+ """
1603
+ vol, mask_int, mask_morph = padding(vol, mask_int, mask_morph)
1604
+ _, _, _, _, faces, vertices, conv_hull = get_variables(vol, mask_int, mask_morph, res)
1605
+ area = get_mesh_area(faces, vertices)
1606
+ p = np.stack((conv_hull.points[conv_hull.simplices[:, 0], 0],
1607
+ conv_hull.points[conv_hull.simplices[:, 1], 1],
1608
+ conv_hull.points[conv_hull.simplices[:, 2], 2]), axis=1)
1609
+ A, _ = min_vol_ellipse(np.transpose(p), 0.01)
1610
+ # New semi-axis lengths
1611
+ _, Q, _ = np.linalg.svd(A)
1612
+ a = 1/np.sqrt(Q[2])
1613
+ b = 1/np.sqrt(Q[1])
1614
+ c = 1/np.sqrt(Q[0])
1615
+ a_mvee = get_area_dens_approx(a, b, c, 20)
1616
+ a_dens_mvee = area / a_mvee
1617
+
1618
+ return a_dens_mvee # Area density - minimum volume enclosing ellipsoid
1619
+
1620
+ def v_dens_conv_hull(vol: np.ndarray,
1621
+ mask_int: np.ndarray,
1622
+ mask_morph: np.ndarray,
1623
+ res: np.ndarray) -> float:
1624
+ """Computes Volume density - convex hull feature.
1625
+ This feature refers to "Fmorph_v_dens_conv_hull" (ID = R3ER) in
1626
+ the `IBSI1 reference manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
1627
+
1628
+ Args:
1629
+ vol (ndarray): 3D volume, NON-QUANTIZED, continous imaging intensity distribution.
1630
+ mask_int (ndarray): Intensity mask.
1631
+ mask_morph (ndarray): Morphological mask.
1632
+ res (ndarray): [a,b,c] vector specfying the resolution of the volume in mm.
1633
+ XYZ resolution (world), or JIK resolution (intrinsic matlab).
1634
+
1635
+ Returns:
1636
+ float: Value of the Volume density - convex hull feature.
1637
+ """
1638
+ vol, mask_int, mask_morph = padding(vol, mask_int, mask_morph)
1639
+ _, _, _, _, faces, vertices, conv_hull = get_variables(vol, mask_int, mask_morph, res)
1640
+ volume = get_mesh_volume(faces, vertices)
1641
+ v_convex = conv_hull.volume
1642
+ v_dens_conv_hull = volume / v_convex
1643
+
1644
+ return v_dens_conv_hull # Volume density - convex hull
1645
+
1646
+ def a_dens_conv_hull(vol: np.ndarray,
1647
+ mask_int: np.ndarray,
1648
+ mask_morph: np.ndarray,
1649
+ res: np.ndarray) -> float:
1650
+ """Computes Area density - convex hull feature.
1651
+ This feature refers to "Fmorph_a_dens_conv_hull" (ID = 7T7F) in
1652
+ the `IBSI1 reference manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
1653
+
1654
+ Args:
1655
+ vol (ndarray): 3D volume, NON-QUANTIZED, continous imaging intensity distribution.
1656
+ mask_int (ndarray): Intensity mask.
1657
+ mask_morph (ndarray): Morphological mask.
1658
+ res (ndarray): [a,b,c] vector specfying the resolution of the volume in mm.
1659
+ XYZ resolution (world), or JIK resolution (intrinsic matlab).
1660
+
1661
+ Returns:
1662
+ float: Value of the Area density - convex hull feature.
1663
+ """
1664
+ vol, mask_int, mask_morph = padding(vol, mask_int, mask_morph)
1665
+ _, _, _, _, faces, vertices, conv_hull = get_variables(vol, mask_int, mask_morph, res)
1666
+ area = get_mesh_area(faces, vertices)
1667
+ v_convex = conv_hull.area
1668
+ a_dens_conv_hull = area / v_convex
1669
+
1670
+ return a_dens_conv_hull # Area density - convex hull
1671
+
1672
+ def integ_int(vol: np.ndarray,
1673
+ mask_int: np.ndarray,
1674
+ mask_morph: np.ndarray,
1675
+ res: np.ndarray) -> float:
1676
+ """Computes Integrated intensity feature.
1677
+ This feature refers to "Fmorph_integ_int" (ID = 99N0) in
1678
+ the `IBSI1 reference manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
1679
+
1680
+ Args:
1681
+ vol (ndarray): 3D volume, NON-QUANTIZED, continous imaging intensity distribution.
1682
+ mask_int (ndarray): Intensity mask.
1683
+ mask_morph (ndarray): Morphological mask.
1684
+ res (ndarray): [a,b,c] vector specfying the resolution of the volume in mm.
1685
+ XYZ resolution (world), or JIK resolution (intrinsic matlab).
1686
+
1687
+ Returns:
1688
+ float: Value of the Integrated intensity feature.
1689
+
1690
+ """
1691
+ vol, mask_int, mask_morph = padding(vol, mask_int, mask_morph)
1692
+ xgl_int, _, _, _, faces, vertices, _ = get_variables(vol, mask_int, mask_morph, res)
1693
+ volume = get_mesh_volume(faces, vertices)
1694
+ integ_int = np.mean(xgl_int) * volume
1695
+
1696
+ return integ_int # Integrated intensity
1697
+
1698
+ def moran_i(vol: np.ndarray,
1699
+ mask_int: np.ndarray,
1700
+ mask_morph: np.ndarray,
1701
+ res: np.ndarray,
1702
+ compute_moran_i: bool=False) -> float:
1703
+ """Computes Moran's I index feature.
1704
+ This feature refers to "Fmorph_moran_i" (ID = N365) in
1705
+ the `IBSI1 reference manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
1706
+
1707
+ Args:
1708
+ vol (ndarray): 3D volume, NON-QUANTIZED, continous imaging intensity distribution.
1709
+ mask_int (ndarray): Intensity mask.
1710
+ mask_morph (ndarray): Morphological mask.
1711
+ res (ndarray): [a,b,c] vector specfying the resolution of the volume in mm.
1712
+ XYZ resolution (world), or JIK resolution (intrinsic matlab).
1713
+ compute_moran_i (bool, optional): True to compute Moran's Index.
1714
+
1715
+ Returns:
1716
+ float: Value of the Moran's I index feature.
1717
+
1718
+ """
1719
+ vol, mask_int, mask_morph = padding(vol, mask_int, mask_morph)
1720
+
1721
+ if compute_moran_i:
1722
+ vol_mor = vol.copy()
1723
+ vol_mor[mask_int == 0] = np.NaN
1724
+ moran_i = get_moran_i(vol_mor, res)
1725
+
1726
+ return moran_i # Moran's I index
1727
+
1728
+ def geary_c(vol: np.ndarray,
1729
+ mask_int: np.ndarray,
1730
+ mask_morph: np.ndarray,
1731
+ res: np.ndarray,
1732
+ compute_geary_c: bool=False) -> float:
1733
+ """Computes Geary's C measure feature.
1734
+ This feature refers to "Fmorph_geary_c" (ID = NPT7) in
1735
+ the `IBSI1 reference manual <https://arxiv.org/pdf/1612.07003.pdf>`__.
1736
+
1737
+ Args:
1738
+ vol (ndarray): 3D volume, NON-QUANTIZED, continous imaging intensity distribution.
1739
+ mask_int (ndarray): Intensity mask.
1740
+ mask_morph (ndarray): Morphological mask.
1741
+ res (ndarray): [a,b,c] vector specfying the resolution of the volume in mm.
1742
+ XYZ resolution (world), or JIK resolution (intrinsic matlab).
1743
+ compute_geary_c (bool, optional): True to compute Geary's C measure.
1744
+
1745
+ Returns:
1746
+ float: Value of the Geary's C measure feature.
1747
+
1748
+ """
1749
+ vol, mask_int, mask_morph = padding(vol, mask_int, mask_morph)
1750
+
1751
+ if compute_geary_c:
1752
+ vol_mor = vol.copy()
1753
+ vol_mor[mask_int == 0] = np.NaN
1754
+ geary_c = get_geary_c(vol_mor, res)
1755
+
1756
+ return geary_c # Geary's C measure