mediml 0.9.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- MEDiml/MEDscan.py +1696 -0
- MEDiml/__init__.py +21 -0
- MEDiml/biomarkers/BatchExtractor.py +806 -0
- MEDiml/biomarkers/BatchExtractorTexturalFilters.py +840 -0
- MEDiml/biomarkers/__init__.py +16 -0
- MEDiml/biomarkers/diagnostics.py +125 -0
- MEDiml/biomarkers/get_oriented_bound_box.py +158 -0
- MEDiml/biomarkers/glcm.py +1602 -0
- MEDiml/biomarkers/gldzm.py +523 -0
- MEDiml/biomarkers/glrlm.py +1315 -0
- MEDiml/biomarkers/glszm.py +555 -0
- MEDiml/biomarkers/int_vol_hist.py +527 -0
- MEDiml/biomarkers/intensity_histogram.py +615 -0
- MEDiml/biomarkers/local_intensity.py +89 -0
- MEDiml/biomarkers/morph.py +1756 -0
- MEDiml/biomarkers/ngldm.py +780 -0
- MEDiml/biomarkers/ngtdm.py +414 -0
- MEDiml/biomarkers/stats.py +373 -0
- MEDiml/biomarkers/utils.py +389 -0
- MEDiml/filters/TexturalFilter.py +299 -0
- MEDiml/filters/__init__.py +9 -0
- MEDiml/filters/apply_filter.py +134 -0
- MEDiml/filters/gabor.py +215 -0
- MEDiml/filters/laws.py +283 -0
- MEDiml/filters/log.py +147 -0
- MEDiml/filters/mean.py +121 -0
- MEDiml/filters/textural_filters_kernels.py +1738 -0
- MEDiml/filters/utils.py +107 -0
- MEDiml/filters/wavelet.py +237 -0
- MEDiml/learning/DataCleaner.py +198 -0
- MEDiml/learning/DesignExperiment.py +480 -0
- MEDiml/learning/FSR.py +667 -0
- MEDiml/learning/Normalization.py +112 -0
- MEDiml/learning/RadiomicsLearner.py +714 -0
- MEDiml/learning/Results.py +2237 -0
- MEDiml/learning/Stats.py +694 -0
- MEDiml/learning/__init__.py +10 -0
- MEDiml/learning/cleaning_utils.py +107 -0
- MEDiml/learning/ml_utils.py +1015 -0
- MEDiml/processing/__init__.py +6 -0
- MEDiml/processing/compute_suv_map.py +121 -0
- MEDiml/processing/discretisation.py +149 -0
- MEDiml/processing/interpolation.py +275 -0
- MEDiml/processing/resegmentation.py +66 -0
- MEDiml/processing/segmentation.py +912 -0
- MEDiml/utils/__init__.py +25 -0
- MEDiml/utils/batch_patients.py +45 -0
- MEDiml/utils/create_radiomics_table.py +131 -0
- MEDiml/utils/data_frame_export.py +42 -0
- MEDiml/utils/find_process_names.py +16 -0
- MEDiml/utils/get_file_paths.py +34 -0
- MEDiml/utils/get_full_rad_names.py +21 -0
- MEDiml/utils/get_institutions_from_ids.py +16 -0
- MEDiml/utils/get_patient_id_from_scan_name.py +22 -0
- MEDiml/utils/get_patient_names.py +26 -0
- MEDiml/utils/get_radiomic_names.py +27 -0
- MEDiml/utils/get_scan_name_from_rad_name.py +22 -0
- MEDiml/utils/image_reader_SITK.py +37 -0
- MEDiml/utils/image_volume_obj.py +22 -0
- MEDiml/utils/imref.py +340 -0
- MEDiml/utils/initialize_features_names.py +62 -0
- MEDiml/utils/inpolygon.py +159 -0
- MEDiml/utils/interp3.py +43 -0
- MEDiml/utils/json_utils.py +78 -0
- MEDiml/utils/mode.py +31 -0
- MEDiml/utils/parse_contour_string.py +58 -0
- MEDiml/utils/save_MEDscan.py +30 -0
- MEDiml/utils/strfind.py +32 -0
- MEDiml/utils/textureTools.py +188 -0
- MEDiml/utils/texture_features_names.py +115 -0
- MEDiml/utils/write_radiomics_csv.py +47 -0
- MEDiml/wrangling/DataManager.py +1724 -0
- MEDiml/wrangling/ProcessDICOM.py +512 -0
- MEDiml/wrangling/__init__.py +3 -0
- mediml-0.9.9.dist-info/LICENSE.md +674 -0
- mediml-0.9.9.dist-info/METADATA +232 -0
- mediml-0.9.9.dist-info/RECORD +78 -0
- mediml-0.9.9.dist-info/WHEEL +4 -0
|
@@ -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
|