mediml 0.9.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- MEDiml/MEDscan.py +1696 -0
- MEDiml/__init__.py +21 -0
- MEDiml/biomarkers/BatchExtractor.py +806 -0
- MEDiml/biomarkers/BatchExtractorTexturalFilters.py +840 -0
- MEDiml/biomarkers/__init__.py +16 -0
- MEDiml/biomarkers/diagnostics.py +125 -0
- MEDiml/biomarkers/get_oriented_bound_box.py +158 -0
- MEDiml/biomarkers/glcm.py +1602 -0
- MEDiml/biomarkers/gldzm.py +523 -0
- MEDiml/biomarkers/glrlm.py +1315 -0
- MEDiml/biomarkers/glszm.py +555 -0
- MEDiml/biomarkers/int_vol_hist.py +527 -0
- MEDiml/biomarkers/intensity_histogram.py +615 -0
- MEDiml/biomarkers/local_intensity.py +89 -0
- MEDiml/biomarkers/morph.py +1756 -0
- MEDiml/biomarkers/ngldm.py +780 -0
- MEDiml/biomarkers/ngtdm.py +414 -0
- MEDiml/biomarkers/stats.py +373 -0
- MEDiml/biomarkers/utils.py +389 -0
- MEDiml/filters/TexturalFilter.py +299 -0
- MEDiml/filters/__init__.py +9 -0
- MEDiml/filters/apply_filter.py +134 -0
- MEDiml/filters/gabor.py +215 -0
- MEDiml/filters/laws.py +283 -0
- MEDiml/filters/log.py +147 -0
- MEDiml/filters/mean.py +121 -0
- MEDiml/filters/textural_filters_kernels.py +1738 -0
- MEDiml/filters/utils.py +107 -0
- MEDiml/filters/wavelet.py +237 -0
- MEDiml/learning/DataCleaner.py +198 -0
- MEDiml/learning/DesignExperiment.py +480 -0
- MEDiml/learning/FSR.py +667 -0
- MEDiml/learning/Normalization.py +112 -0
- MEDiml/learning/RadiomicsLearner.py +714 -0
- MEDiml/learning/Results.py +2237 -0
- MEDiml/learning/Stats.py +694 -0
- MEDiml/learning/__init__.py +10 -0
- MEDiml/learning/cleaning_utils.py +107 -0
- MEDiml/learning/ml_utils.py +1015 -0
- MEDiml/processing/__init__.py +6 -0
- MEDiml/processing/compute_suv_map.py +121 -0
- MEDiml/processing/discretisation.py +149 -0
- MEDiml/processing/interpolation.py +275 -0
- MEDiml/processing/resegmentation.py +66 -0
- MEDiml/processing/segmentation.py +912 -0
- MEDiml/utils/__init__.py +25 -0
- MEDiml/utils/batch_patients.py +45 -0
- MEDiml/utils/create_radiomics_table.py +131 -0
- MEDiml/utils/data_frame_export.py +42 -0
- MEDiml/utils/find_process_names.py +16 -0
- MEDiml/utils/get_file_paths.py +34 -0
- MEDiml/utils/get_full_rad_names.py +21 -0
- MEDiml/utils/get_institutions_from_ids.py +16 -0
- MEDiml/utils/get_patient_id_from_scan_name.py +22 -0
- MEDiml/utils/get_patient_names.py +26 -0
- MEDiml/utils/get_radiomic_names.py +27 -0
- MEDiml/utils/get_scan_name_from_rad_name.py +22 -0
- MEDiml/utils/image_reader_SITK.py +37 -0
- MEDiml/utils/image_volume_obj.py +22 -0
- MEDiml/utils/imref.py +340 -0
- MEDiml/utils/initialize_features_names.py +62 -0
- MEDiml/utils/inpolygon.py +159 -0
- MEDiml/utils/interp3.py +43 -0
- MEDiml/utils/json_utils.py +78 -0
- MEDiml/utils/mode.py +31 -0
- MEDiml/utils/parse_contour_string.py +58 -0
- MEDiml/utils/save_MEDscan.py +30 -0
- MEDiml/utils/strfind.py +32 -0
- MEDiml/utils/textureTools.py +188 -0
- MEDiml/utils/texture_features_names.py +115 -0
- MEDiml/utils/write_radiomics_csv.py +47 -0
- MEDiml/wrangling/DataManager.py +1724 -0
- MEDiml/wrangling/ProcessDICOM.py +512 -0
- MEDiml/wrangling/__init__.py +3 -0
- mediml-0.9.9.dist-info/LICENSE.md +674 -0
- mediml-0.9.9.dist-info/METADATA +232 -0
- mediml-0.9.9.dist-info/RECORD +78 -0
- mediml-0.9.9.dist-info/WHEEL +4 -0
MEDiml/utils/imref.py
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
from typing import Tuple, Union
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def intrinsicToWorld(R, xIntrinsic: float, yIntrinsic: float, zIntrinsic:float) -> Tuple[float, float, float]:
|
|
10
|
+
"""Convert from intrinsic to world coordinates.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
R (imref3d): imref3d object (same functionality of MATLAB imref3d class)
|
|
14
|
+
xIntrinsic (float): Coordinates along the x-dimension in the intrinsic coordinate system
|
|
15
|
+
yIntrinsic (float): Coordinates along the y-dimension in the intrinsic coordinate system
|
|
16
|
+
zIntrinsic (float): Coordinates along the z-dimension in the intrinsic coordinate system
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
float: world coordinates
|
|
20
|
+
"""
|
|
21
|
+
return R.intrinsicToWorld(xIntrinsic=xIntrinsic, yIntrinsic=yIntrinsic, zIntrinsic=zIntrinsic)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def worldToIntrinsic(R, xWorld: float, yWorld: float, zWorld: float) -> Tuple[float, float, float] :
|
|
25
|
+
"""Convert from world coordinates to intrinsic.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
R (imref3d): imref3d object (same functionality of MATLAB imref3d class)
|
|
29
|
+
xWorld (float): Coordinates along the x-dimension in the intrinsic coordinate system
|
|
30
|
+
yWorld (float): Coordinates along the y-dimension in the intrinsic coordinate system
|
|
31
|
+
zWorld (float): Coordinates along the z-dimension in the intrinsic coordinate system
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
_type_: intrinsic coordinates
|
|
35
|
+
"""
|
|
36
|
+
return R.worldToIntrinsic(xWorld=xWorld, yWorld=yWorld, zWorld=zWorld)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def sizes_match(R, A):
|
|
40
|
+
"""Compares whether the two imref3d objects have the same size.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
R (imref3d): First imref3d object.
|
|
44
|
+
A (imref3d): Second imref3d object.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
bool: True if ``R`` and ``A`` have the same size, and false if not.
|
|
48
|
+
|
|
49
|
+
"""
|
|
50
|
+
return np.all(R.imageSize == A.imageSize)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class imref3d:
|
|
54
|
+
"""This class mirrors the functionality of the matlab imref3d class
|
|
55
|
+
|
|
56
|
+
An `imref3d object <https://www.mathworks.com/help/images/ref/imref3d.html>`_
|
|
57
|
+
stores the relationship between the intrinsic coordinates
|
|
58
|
+
anchored to the columns, rows, and planes of a 3-D image and the spatial
|
|
59
|
+
location of the same column, row, and plane locations in a world coordinate system.
|
|
60
|
+
|
|
61
|
+
The image is sampled regularly in the planar world-x, world-y, and world-z coordinates
|
|
62
|
+
of the coordinate system such that intrinsic-x, -y and -z values align with world-x, -y
|
|
63
|
+
and -z values, respectively. The resolution in each dimension can be different.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
ImageSize (ndarray, optional): Number of elements in each spatial dimension,
|
|
67
|
+
specified as a 3-element positive row vector.
|
|
68
|
+
PixelExtentInWorldX (float, optional): Size of a single pixel in the x-dimension
|
|
69
|
+
measured in the world coordinate system.
|
|
70
|
+
PixelExtentInWorldY (float, optional): Size of a single pixel in the y-dimension
|
|
71
|
+
measured in the world coordinate system.
|
|
72
|
+
PixelExtentInWorldZ (float, optional): Size of a single pixel in the z-dimension
|
|
73
|
+
measured in the world coordinate system.
|
|
74
|
+
xWorldLimits (ndarray, optional): Limits of image in world x, specified as a 2-element row vector,
|
|
75
|
+
[xMin xMax].
|
|
76
|
+
yWorldLimits (ndarray, optional): Limits of image in world y, specified as a 2-element row vector,
|
|
77
|
+
[yMin yMax].
|
|
78
|
+
zWorldLimits (ndarray, optional): Limits of image in world z, specified as a 2-element row vector,
|
|
79
|
+
[zMin zMax].
|
|
80
|
+
|
|
81
|
+
Attributes:
|
|
82
|
+
ImageSize (ndarray): Number of elements in each spatial dimension,
|
|
83
|
+
specified as a 3-element positive row vector.
|
|
84
|
+
PixelExtentInWorldX (float): Size of a single pixel in the x-dimension
|
|
85
|
+
measured in the world coordinate system.
|
|
86
|
+
PixelExtentInWorldY (float): Size of a single pixel in the y-dimension
|
|
87
|
+
measured in the world coordinate system.
|
|
88
|
+
PixelExtentInWorldZ (float): Size of a single pixel in the z-dimension
|
|
89
|
+
measured in the world coordinate system.
|
|
90
|
+
XIntrinsicLimits (ndarray): Limits of image in intrinsic units in the x-dimension,
|
|
91
|
+
specified as a 2-element row vector [xMin xMax].
|
|
92
|
+
YIntrinsicLimits (ndarray): Limits of image in intrinsic units in the y-dimension,
|
|
93
|
+
specified as a 2-element row vector [yMin yMax].
|
|
94
|
+
ZIntrinsicLimits (ndarray): Limits of image in intrinsic units in the z-dimension,
|
|
95
|
+
specified as a 2-element row vector [zMin zMax].
|
|
96
|
+
ImageExtentInWorldX (float): Span of image in the x-dimension in
|
|
97
|
+
the world coordinate system.
|
|
98
|
+
ImageExtentInWorldY (float): Span of image in the y-dimension in
|
|
99
|
+
the world coordinate system.
|
|
100
|
+
ImageExtentInWorldZ (float): Span of image in the z-dimension in
|
|
101
|
+
the world coordinate system.
|
|
102
|
+
xWorldLimits (ndarray): Limits of image in world x, specified as a 2-element row vector,
|
|
103
|
+
[xMin xMax].
|
|
104
|
+
yWorldLimits (ndarray): Limits of image in world y, specified as a 2-element row vector,
|
|
105
|
+
[yMin yMax].
|
|
106
|
+
zWorldLimits (ndarray): Limits of image in world z, specified as a 2-element row vector,
|
|
107
|
+
[zMin zMax].
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
def __init__(self,
|
|
111
|
+
imageSize=None,
|
|
112
|
+
pixelExtentInWorldX=1.0,
|
|
113
|
+
pixelExtentInWorldY=1.0,
|
|
114
|
+
pixelExtentInWorldZ=1.0,
|
|
115
|
+
xWorldLimits=None,
|
|
116
|
+
yWorldLimits=None,
|
|
117
|
+
zWorldLimits=None) -> None:
|
|
118
|
+
|
|
119
|
+
# Check if imageSize is an ndarray, and cast to ndarray otherwise
|
|
120
|
+
self.ImageSize = self._parse_to_ndarray(x=imageSize, n=3)
|
|
121
|
+
|
|
122
|
+
# Size of single voxels along axis in world coordinate system.
|
|
123
|
+
# Equivalent to voxel spacing.
|
|
124
|
+
self.PixelExtentInWorldX = pixelExtentInWorldX
|
|
125
|
+
self.PixelExtentInWorldY = pixelExtentInWorldY
|
|
126
|
+
self.PixelExtentInWorldZ = pixelExtentInWorldZ
|
|
127
|
+
|
|
128
|
+
# Limits of the image in intrinsic coordinates
|
|
129
|
+
# AZ: this differs from DICOM, which assumes that the origin lies
|
|
130
|
+
# at the center of the first voxel.
|
|
131
|
+
if imageSize is not None:
|
|
132
|
+
self.XIntrinsicLimits = np.array([-0.5, imageSize[0]-0.5])
|
|
133
|
+
self.YIntrinsicLimits = np.array([-0.5, imageSize[1]-0.5])
|
|
134
|
+
self.ZIntrinsicLimits = np.array([-0.5, imageSize[2]-0.5])
|
|
135
|
+
else:
|
|
136
|
+
self.XIntrinsicLimits = None
|
|
137
|
+
self.YIntrinsicLimits = None
|
|
138
|
+
self.ZIntrinsicLimits = None
|
|
139
|
+
|
|
140
|
+
# Size of the image in world coordinates
|
|
141
|
+
if imageSize is not None:
|
|
142
|
+
self.ImageExtentInWorldX = imageSize[0] * pixelExtentInWorldX
|
|
143
|
+
self.ImageExtentInWorldY = imageSize[1] * pixelExtentInWorldY
|
|
144
|
+
self.ImageExtentInWorldZ = imageSize[2] * pixelExtentInWorldZ
|
|
145
|
+
else:
|
|
146
|
+
self.ImageExtentInWorldX = None
|
|
147
|
+
self.ImageExtentInWorldY = None
|
|
148
|
+
self.ImageExtentInWorldZ = None
|
|
149
|
+
|
|
150
|
+
# Limits of the image in the world coordinates
|
|
151
|
+
self.XWorldLimits = self._parse_to_ndarray(x=xWorldLimits, n=2)
|
|
152
|
+
self.YWorldLimits = self._parse_to_ndarray(x=yWorldLimits, n=2)
|
|
153
|
+
self.ZWorldLimits = self._parse_to_ndarray(x=zWorldLimits, n=2)
|
|
154
|
+
|
|
155
|
+
if xWorldLimits is None and imageSize is not None:
|
|
156
|
+
self.XWorldLimits = np.array([0.0, self.ImageExtentInWorldX])
|
|
157
|
+
if yWorldLimits is None and imageSize is not None:
|
|
158
|
+
self.YWorldLimits = np.array([0.0, self.ImageExtentInWorldY])
|
|
159
|
+
if zWorldLimits is None and imageSize is not None:
|
|
160
|
+
self.ZWorldLimits = np.array([0.0, self.ImageExtentInWorldZ])
|
|
161
|
+
|
|
162
|
+
def _parse_to_ndarray(self,
|
|
163
|
+
x: np.iterable,
|
|
164
|
+
n=None) -> np.ndarray:
|
|
165
|
+
"""Internal function to cast input to a numpy array.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
x (iterable): Object that supports __iter__.
|
|
169
|
+
n (int, optional): expected length.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
ndarray: iterable input as a numpy array.
|
|
173
|
+
"""
|
|
174
|
+
if x is not None:
|
|
175
|
+
# Cast to ndarray
|
|
176
|
+
if not isinstance(x, np.ndarray):
|
|
177
|
+
x = np.array(x)
|
|
178
|
+
|
|
179
|
+
# Check length
|
|
180
|
+
if n is not None:
|
|
181
|
+
if not len(x) == n:
|
|
182
|
+
raise ValueError(
|
|
183
|
+
"Length of array does not meet the expected length.", len(x), n)
|
|
184
|
+
|
|
185
|
+
return x
|
|
186
|
+
|
|
187
|
+
def intrinsicToWorld(self,
|
|
188
|
+
xIntrinsic: np.ndarray,
|
|
189
|
+
yIntrinsic: np.ndarray,
|
|
190
|
+
zIntrinsic: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
191
|
+
"""Convert from intrinsic to world coordinates.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
xIntrinsic (ndarray): Coordinates along the x-dimension in the intrinsic coordinate system.
|
|
195
|
+
yIntrinsic (ndarray): Coordinates along the y-dimension in the intrinsic coordinate system.
|
|
196
|
+
zIntrinsic (ndarray): Coordinates along the z-dimension in the intrinsic coordinate system.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
Tuple[np.ndarray, np.ndarray, np.ndarray]: [xWorld, yWorld, zWorld] in world coordinate system.
|
|
200
|
+
"""
|
|
201
|
+
xWorld = (self.XWorldLimits[0] + 0.5*self.PixelExtentInWorldX) + \
|
|
202
|
+
xIntrinsic * self.PixelExtentInWorldX
|
|
203
|
+
yWorld = (self.YWorldLimits[0] + 0.5*self.PixelExtentInWorldY) + \
|
|
204
|
+
yIntrinsic * self.PixelExtentInWorldY
|
|
205
|
+
zWorld = (self.ZWorldLimits[0] + 0.5*self.PixelExtentInWorldZ) + \
|
|
206
|
+
zIntrinsic * self.PixelExtentInWorldZ
|
|
207
|
+
|
|
208
|
+
return xWorld, yWorld, zWorld
|
|
209
|
+
|
|
210
|
+
def worldToIntrinsic(self,
|
|
211
|
+
xWorld: np.ndarray,
|
|
212
|
+
yWorld: np.ndarray,
|
|
213
|
+
zWorld: np.ndarray)-> Union[np.ndarray,
|
|
214
|
+
np.ndarray,
|
|
215
|
+
np.ndarray]:
|
|
216
|
+
"""Converts from world coordinates to intrinsic coordinates.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
xWorld (ndarray): Coordinates along the x-dimension in the world coordinate system.
|
|
220
|
+
yWorld (ndarray): Coordinates along the y-dimension in the world coordinate system.
|
|
221
|
+
zWorld (ndarray): Coordinates along the z-dimension in the world coordinate system.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
ndarray: [xIntrinsic,yIntrinsic,zIntrinsic] in intrinsic coordinate system.
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
xIntrinsic = (
|
|
228
|
+
xWorld - (self.XWorldLimits[0] + 0.5*self.PixelExtentInWorldX)) / self.PixelExtentInWorldX
|
|
229
|
+
yIntrinsic = (
|
|
230
|
+
yWorld - (self.YWorldLimits[0] + 0.5*self.PixelExtentInWorldY)) / self.PixelExtentInWorldY
|
|
231
|
+
zIntrinsic = (
|
|
232
|
+
zWorld - (self.ZWorldLimits[0] + 0.5*self.PixelExtentInWorldZ)) / self.PixelExtentInWorldZ
|
|
233
|
+
|
|
234
|
+
return xIntrinsic, yIntrinsic, zIntrinsic
|
|
235
|
+
|
|
236
|
+
def contains_point(self,
|
|
237
|
+
xWorld: np.ndarray,
|
|
238
|
+
yWorld: np.ndarray,
|
|
239
|
+
zWorld: np.ndarray) -> np.ndarray:
|
|
240
|
+
"""Determines which points defined by ``xWorld``, ``yWorld`` and ``zWorld``.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
xWorld (ndarray): Coordinates along the x-dimension in the world coordinate system.
|
|
244
|
+
yWorld (ndarray): Coordinates along the y-dimension in the world coordinate system.
|
|
245
|
+
zWorld (ndarray): Coordinates along the z-dimension in the world coordinate system.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
ndarray: boolean array for coordinate sets that are within the bounds of the image.
|
|
249
|
+
"""
|
|
250
|
+
xInside = np.logical_and(
|
|
251
|
+
xWorld >= self.XWorldLimits[0], xWorld <= self.XWorldLimits[1])
|
|
252
|
+
yInside = np.logical_and(
|
|
253
|
+
yWorld >= self.YWorldLimits[0], yWorld <= self.YWorldLimits[1])
|
|
254
|
+
zInside = np.logical_and(
|
|
255
|
+
zWorld >= self.ZWorldLimits[0], zWorld <= self.ZWorldLimits[1])
|
|
256
|
+
|
|
257
|
+
return xInside + yInside + zInside == 3
|
|
258
|
+
|
|
259
|
+
def WorldLimits(self,
|
|
260
|
+
axis=None,
|
|
261
|
+
newValue=None) -> Union[np.ndarray, None]:
|
|
262
|
+
"""Sets the WorldLimits to the new value for the given ``axis``.
|
|
263
|
+
If the newValue is None, the method returns the attribute value.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
axis (str, optional): Specify the dimension, must be 'X', 'Y' or 'Z'.
|
|
267
|
+
newValue (iterable, optional): New value for the WorldLimits attribute.
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
ndarray: Limits of image in world along the axis-dimension.
|
|
271
|
+
"""
|
|
272
|
+
if newValue is None:
|
|
273
|
+
# Get value
|
|
274
|
+
if axis == "X":
|
|
275
|
+
return self.XWorldLimits
|
|
276
|
+
elif axis == "Y":
|
|
277
|
+
return self.YWorldLimits
|
|
278
|
+
elif axis == "Z":
|
|
279
|
+
return self.ZWorldLimits
|
|
280
|
+
else:
|
|
281
|
+
# Set value
|
|
282
|
+
if axis == "X":
|
|
283
|
+
self.XWorldLimits = self._parse_to_ndarray(x=newValue, n=2)
|
|
284
|
+
elif axis == "Y":
|
|
285
|
+
self.YWorldLimits = self._parse_to_ndarray(x=newValue, n=2)
|
|
286
|
+
elif axis == "Z":
|
|
287
|
+
self.ZWorldLimits = self._parse_to_ndarray(x=newValue, n=2)
|
|
288
|
+
|
|
289
|
+
def PixelExtentInWorld(self, axis=None) -> Union[float, None]:
|
|
290
|
+
"""Returns the PixelExtentInWorld attribute value for the given ``axis``.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
axis (str, optional): Specify the dimension, must be 'X', 'Y' or 'Z'.
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
float: Size of a single pixel in the axis-dimension measured in the world coordinate system.
|
|
297
|
+
"""
|
|
298
|
+
if axis == "X":
|
|
299
|
+
return self.PixelExtentInWorldX
|
|
300
|
+
elif axis == "Y":
|
|
301
|
+
return self.PixelExtentInWorldY
|
|
302
|
+
elif axis == "Z":
|
|
303
|
+
return self.PixelExtentInWorldZ
|
|
304
|
+
|
|
305
|
+
def IntrinsicLimits(self,
|
|
306
|
+
axis=None) -> Union[np.ndarray,
|
|
307
|
+
None]:
|
|
308
|
+
"""Returns the IntrinsicLimits attribute value for the given ``axis``.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
axis (str, optional): Specify the dimension, must be 'X', 'Y' or 'Z'.
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
ndarray: Limits of image in intrinsic units in the axis-dimension, specified as a 2-element row vector [xMin xMax].
|
|
315
|
+
"""
|
|
316
|
+
if axis == "X":
|
|
317
|
+
return self.XIntrinsicLimits
|
|
318
|
+
elif axis == "Y":
|
|
319
|
+
return self.YIntrinsicLimits
|
|
320
|
+
elif axis == "Z":
|
|
321
|
+
return self.ZIntrinsicLimits
|
|
322
|
+
|
|
323
|
+
def ImageExtentInWorld(self,
|
|
324
|
+
axis=None) -> Union[float,
|
|
325
|
+
None]:
|
|
326
|
+
"""Returns the ImageExtentInWorld attribute value for the given ``axis``.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
axis (str, optional): Specify the dimension, must be 'X', 'Y' or 'Z'.
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
ndarray: Span of image in the axis-dimension in the world coordinate system.
|
|
333
|
+
|
|
334
|
+
"""
|
|
335
|
+
if axis == "X":
|
|
336
|
+
return self.ImageExtentInWorldX
|
|
337
|
+
elif axis == "Y":
|
|
338
|
+
return self.ImageExtentInWorldY
|
|
339
|
+
elif axis == "Z":
|
|
340
|
+
return self.ImageExtentInWorldZ
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
from typing import Dict, List, Tuple
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def initialize_features_names(image_space_struct: Dict) -> Tuple[List, List]:
|
|
9
|
+
"""Finds all the features names from `image_space_struct`
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
image_space_struct(Dict): Dictionary of the extracted features (Texture & Non-texture)
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
Tuple[List, List]: Two lists of the texture and non-texture features names found in the `image_space_struct`.
|
|
16
|
+
"""
|
|
17
|
+
# First entry is the names of feature types. Second entry is the name of
|
|
18
|
+
# the features for a given feature type. Third entry is the name of the
|
|
19
|
+
# extraction parameters for all features of a given feature type.
|
|
20
|
+
non_text_cell = [0] * 3
|
|
21
|
+
# First entry is the names of feature types. Second entry is the name of
|
|
22
|
+
# the features for a given feature type. Third entry is the name of the
|
|
23
|
+
# extraction parameters for all features of a given feature type.
|
|
24
|
+
text_cell = [0] * 3
|
|
25
|
+
|
|
26
|
+
# NON-TEXTURE FEATURES
|
|
27
|
+
field_non_text = [key for key in image_space_struct.keys() if key != 'texture']
|
|
28
|
+
n_non_text_type = len(field_non_text)
|
|
29
|
+
non_text_cell[0] = field_non_text
|
|
30
|
+
non_text_cell[1] = [0] * n_non_text_type
|
|
31
|
+
non_text_cell[2] = [0] * n_non_text_type
|
|
32
|
+
|
|
33
|
+
for t in range(0, n_non_text_type):
|
|
34
|
+
dic_image_space_struct_non_text = image_space_struct[non_text_cell[0][t]]
|
|
35
|
+
field_params_non_text = [
|
|
36
|
+
key for key in dic_image_space_struct_non_text.keys()]
|
|
37
|
+
dic_image_space_struct_params_non_text = image_space_struct[non_text_cell[0]
|
|
38
|
+
[t]][field_params_non_text[0]]
|
|
39
|
+
field_feat_non_text = [
|
|
40
|
+
key for key in dic_image_space_struct_params_non_text.keys()]
|
|
41
|
+
non_text_cell[1][t] = field_feat_non_text
|
|
42
|
+
non_text_cell[2][t] = field_params_non_text
|
|
43
|
+
|
|
44
|
+
# TEXTURE FEATURES
|
|
45
|
+
dic_image_space_struct_texture = image_space_struct['texture']
|
|
46
|
+
field_text = [key for key in dic_image_space_struct_texture.keys()]
|
|
47
|
+
n_text_type = len(field_text)
|
|
48
|
+
text_cell[0] = field_text
|
|
49
|
+
text_cell[1] = [0] * n_text_type
|
|
50
|
+
text_cell[2] = [0] * n_text_type
|
|
51
|
+
|
|
52
|
+
for t in range(0, n_text_type):
|
|
53
|
+
dic_image_space_struct_text = image_space_struct['texture'][text_cell[0][t]]
|
|
54
|
+
field_params_text = [key for key in dic_image_space_struct_text.keys()]
|
|
55
|
+
dic_image_space_struct_params_text = image_space_struct['texture'][text_cell[0]
|
|
56
|
+
[t]][field_params_text[0]]
|
|
57
|
+
field_feat_text = [
|
|
58
|
+
key for key in dic_image_space_struct_params_text.keys()]
|
|
59
|
+
text_cell[1][t] = field_feat_text
|
|
60
|
+
text_cell[2][t] = field_params_text
|
|
61
|
+
|
|
62
|
+
return non_text_cell, text_cell
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def inpolygon(x_q: np.ndarray,
|
|
9
|
+
y_q: np.ndarray,
|
|
10
|
+
x_v: np.ndarray,
|
|
11
|
+
y_v: np.ndarray) -> np.ndarray:
|
|
12
|
+
"""Implements similar functionality MATLAB inpolygon.
|
|
13
|
+
Finds points located inside or on edge of polygonal region.
|
|
14
|
+
|
|
15
|
+
Note:
|
|
16
|
+
Unlike matlab inpolygon, this function does not determine the
|
|
17
|
+
status of single points :math:`(x_q, y_q)`. Instead, it determines the
|
|
18
|
+
status for an entire grid by ray-casting.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
x_q (ndarray): x-coordinates of query points, in intrinsic reference system.
|
|
22
|
+
y_q (ndarray): y-coordinates of query points, in intrinsic reference system.
|
|
23
|
+
x_q (ndarray): x-coordinates of polygon vertices, in intrinsic reference system.
|
|
24
|
+
y_q (ndarray): y-coordinates of polygon vertices, in intrinsic reference system.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
ndarray: boolean array indicating if the query points are on the edge of the polygon area.
|
|
28
|
+
|
|
29
|
+
"""
|
|
30
|
+
def ray_line_intersection(ray_orig, ray_dir, vert_1, vert_2):
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
"""
|
|
34
|
+
epsilon = 0.000001
|
|
35
|
+
|
|
36
|
+
# Define edge
|
|
37
|
+
edge_line = vert_1 - vert_2
|
|
38
|
+
|
|
39
|
+
# Define ray vertices
|
|
40
|
+
r_vert_1 = ray_orig
|
|
41
|
+
r_vert_2 = ray_orig + ray_dir
|
|
42
|
+
edge_ray = - ray_dir
|
|
43
|
+
|
|
44
|
+
# Calculate determinant - if close to 0, lines are parallel and will
|
|
45
|
+
# not intersect
|
|
46
|
+
det = np.cross(edge_ray, edge_line)
|
|
47
|
+
if (det > -epsilon) and (det < epsilon):
|
|
48
|
+
return np.nan
|
|
49
|
+
|
|
50
|
+
# Calculate inverse of the determinant
|
|
51
|
+
inv_det = 1.0 / det
|
|
52
|
+
|
|
53
|
+
# Calculate determinant
|
|
54
|
+
a11 = np.cross(r_vert_1, r_vert_2)
|
|
55
|
+
a21 = np.cross(vert_1, vert_2)
|
|
56
|
+
|
|
57
|
+
# Solve for x
|
|
58
|
+
a12 = edge_ray[0]
|
|
59
|
+
a22 = edge_line[0]
|
|
60
|
+
x = np.linalg.det(np.array([[a11, a12], [a21, a22]])) * inv_det
|
|
61
|
+
|
|
62
|
+
# Solve for y
|
|
63
|
+
b12 = edge_ray[1]
|
|
64
|
+
b22 = edge_line[1]
|
|
65
|
+
y = np.linalg.det(np.array([[a11, b12], [a21, b22]])) * inv_det
|
|
66
|
+
|
|
67
|
+
t = np.array([x, y])
|
|
68
|
+
|
|
69
|
+
# Check whether the solution falls within the line segment
|
|
70
|
+
u1 = np.around(np.dot(edge_line, edge_line), 5)
|
|
71
|
+
u2 = np.around(np.dot(edge_line, vert_1-t), 5)
|
|
72
|
+
if (u2 / u1) < 0.0 or (u2 / u1) > 1.0:
|
|
73
|
+
return np.nan
|
|
74
|
+
|
|
75
|
+
# Return scalar length from ray origin
|
|
76
|
+
t_scal = np.linalg.norm(ray_orig - t)
|
|
77
|
+
|
|
78
|
+
return t_scal
|
|
79
|
+
|
|
80
|
+
# These are hacks to actually make this function work
|
|
81
|
+
spacing = np.array([1.0, 1.0])
|
|
82
|
+
origin = np.array([0.0, 0.0])
|
|
83
|
+
shape = np.array([np.max(x_q) + 1, np.max(y_q) + 1])
|
|
84
|
+
# shape = np.array([np.max(x_q), np.max(y_q)]) Original from Alex
|
|
85
|
+
vertices = np.vstack((x_v, y_v)).transpose()
|
|
86
|
+
lines = np.vstack(
|
|
87
|
+
([np.arange(0, len(x_v))], [np.arange(-1, len(x_v) - 1)])).transpose()
|
|
88
|
+
|
|
89
|
+
# Set up line vertices
|
|
90
|
+
vertex_a = vertices[lines[:, 0], :]
|
|
91
|
+
vertex_b = vertices[lines[:, 1], :]
|
|
92
|
+
|
|
93
|
+
# Remove lines with length 0 and center on the origin
|
|
94
|
+
line_mask = np.sum(np.abs(vertex_a - vertex_b), axis=1) > 0.0
|
|
95
|
+
vertex_a = vertex_a[line_mask] - origin
|
|
96
|
+
vertex_b = vertex_b[line_mask] - origin
|
|
97
|
+
|
|
98
|
+
# Find extent of contours in x
|
|
99
|
+
x_min_ind = int(
|
|
100
|
+
np.max([np.floor(np.min(vertices[:, 0]) / spacing[0]), 0.0]))
|
|
101
|
+
x_max_ind = int(
|
|
102
|
+
np.min([np.ceil(np.max(vertices[:, 0]) / spacing[0]), shape[0] * 1.0]))
|
|
103
|
+
|
|
104
|
+
# Set up voxel grid and y-span
|
|
105
|
+
vox_grid = np.zeros(shape, dtype=int)
|
|
106
|
+
vox_span = origin[1] + np.arange(0, shape[1]) * spacing[1]
|
|
107
|
+
|
|
108
|
+
# Set ray origin and direction (starts at negative y, and travels towards
|
|
109
|
+
# positive y
|
|
110
|
+
ray_origin = np.array([0.0, -1.0])
|
|
111
|
+
ray_dir = np.array([0.0, 1.0])
|
|
112
|
+
|
|
113
|
+
for x_ind in np.arange(x_min_ind, x_max_ind):
|
|
114
|
+
# Update ray origin
|
|
115
|
+
ray_origin[0] = origin[0] + x_ind * spacing[0]
|
|
116
|
+
|
|
117
|
+
# Scan both forward and backward to resolve points located on
|
|
118
|
+
# the polygon
|
|
119
|
+
vox_col_frwd = np.zeros(np.shape(vox_span), dtype=int)
|
|
120
|
+
vox_col_bkwd = np.zeros(np.shape(vox_span), dtype=int)
|
|
121
|
+
|
|
122
|
+
# Find lines that are intersected by the ray
|
|
123
|
+
ray_hit = np.sum(
|
|
124
|
+
np.sign(np.vstack((vertex_a[:, 0], vertex_b[:, 0])) - ray_origin[0]), axis=0)
|
|
125
|
+
|
|
126
|
+
# If the ray crosses a vertex, the sum of the sign is 0 when the ray
|
|
127
|
+
# does not hit an vertex point, and -1 or 1 when it does.
|
|
128
|
+
# In the latter case, we only keep of the vertices for each hit.
|
|
129
|
+
simplex_mask = np.logical_or(ray_hit == 0, ray_hit == 1)
|
|
130
|
+
|
|
131
|
+
# Go to next iterator if mask is empty
|
|
132
|
+
if np.sum(simplex_mask) == 0:
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
# Determine the selected vertices
|
|
136
|
+
selected_verts = np.squeeze(np.where(simplex_mask))
|
|
137
|
+
|
|
138
|
+
# Find intercept of rays with lines
|
|
139
|
+
t_scal = np.array([ray_line_intersection(ray_orig=ray_origin, ray_dir=ray_dir,
|
|
140
|
+
vert_1=vertex_a[ii, :], vert_2=vertex_b[ii, :]) for ii in selected_verts])
|
|
141
|
+
|
|
142
|
+
# Remove invalid results
|
|
143
|
+
t_scal = t_scal[np.isfinite(t_scal)]
|
|
144
|
+
if t_scal.size == 0:
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
# Update vox_col based on t_scal. This basically adds a 1 for all
|
|
148
|
+
# voxels that lie behind the line intersections
|
|
149
|
+
# of the ray.
|
|
150
|
+
for t_curr in t_scal:
|
|
151
|
+
vox_col_frwd[vox_span > t_curr + ray_origin[1]] += 1
|
|
152
|
+
for t_curr in t_scal:
|
|
153
|
+
vox_col_bkwd[vox_span < t_curr + ray_origin[1]] += 1
|
|
154
|
+
|
|
155
|
+
# Voxels in the roi cross an uneven number of meshes from the origin
|
|
156
|
+
vox_grid[x_ind,
|
|
157
|
+
:] += np.logical_and(vox_col_frwd % 2, vox_col_bkwd % 2)
|
|
158
|
+
|
|
159
|
+
return vox_grid.astype(dtype=bool)
|
MEDiml/utils/interp3.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
from scipy.ndimage import map_coordinates
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def interp3(v, x_q, y_q, z_q, method) -> np.ndarray:
|
|
9
|
+
"""`Interpolation for 3-D gridded data <https://www.mathworks.com/help/matlab/ref/interp3.html>`_\
|
|
10
|
+
in meshgrid format, implements similar functionality MATLAB interp3.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
X, Y, Z (ndarray) : Query points, should be intrinsic coordinates.
|
|
14
|
+
method (str): {nearest, linear, spline, cubic}, Interpolation ``method``.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
ndarray: Array of interpolated values.
|
|
18
|
+
|
|
19
|
+
Raises:
|
|
20
|
+
ValueError: If ``method`` is not 'nearest', 'linear', 'spline' or 'cubic'.
|
|
21
|
+
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
# Parse method
|
|
25
|
+
if method == "nearest":
|
|
26
|
+
spline_order = 0
|
|
27
|
+
elif method == "linear":
|
|
28
|
+
spline_order = 1
|
|
29
|
+
elif method in ["spline", "cubic"]:
|
|
30
|
+
spline_order = 3
|
|
31
|
+
else:
|
|
32
|
+
raise ValueError("Interpolator not implemented.")
|
|
33
|
+
|
|
34
|
+
size = np.size(x_q)
|
|
35
|
+
coord_X = np.reshape(x_q, size, order='F')
|
|
36
|
+
coord_Y = np.reshape(y_q, size, order='F')
|
|
37
|
+
coord_Z = np.reshape(z_q, size, order='F')
|
|
38
|
+
coordinates = np.array([coord_X, coord_Y, coord_Z]).astype(np.float32)
|
|
39
|
+
v_q = map_coordinates(input=v.astype(
|
|
40
|
+
np.float32), coordinates=coordinates, order=spline_order, mode='nearest')
|
|
41
|
+
v_q = np.reshape(v_q, np.shape(x_q), order='F')
|
|
42
|
+
|
|
43
|
+
return v_q
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import pathlib
|
|
7
|
+
from typing import Dict
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _is_jsonable(data: any, cls: object) -> bool:
|
|
11
|
+
"""Checks if the given ``data`` is JSON serializable.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
data (Any): ``Data`` that will be checked.
|
|
15
|
+
cls(object, optional): Costum JSONDecoder subclass. If not specified JSONDecoder is used.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
bool: True if the given ``data`` is serializable, False if not.
|
|
19
|
+
"""
|
|
20
|
+
try:
|
|
21
|
+
json.dumps(data, cls=cls)
|
|
22
|
+
return True
|
|
23
|
+
except (TypeError, OverflowError):
|
|
24
|
+
return False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def posix_to_string(dictionnary: Dict) -> Dict:
|
|
28
|
+
"""Converts all Pathlib.Path to str [Pathlib is not serializable].
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
dictionnary (Dict): dict with Pathlib.Path values to convert.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Dict: ``dictionnary`` with all Pathlib.Path converted to str.
|
|
35
|
+
"""
|
|
36
|
+
for key, value in dictionnary.items():
|
|
37
|
+
if type(value) is dict:
|
|
38
|
+
value = posix_to_string(value)
|
|
39
|
+
else:
|
|
40
|
+
if issubclass(type(value), (pathlib.WindowsPath, pathlib.PosixPath, pathlib.Path)):
|
|
41
|
+
dictionnary[key] = str(value)
|
|
42
|
+
|
|
43
|
+
return dictionnary
|
|
44
|
+
|
|
45
|
+
def load_json(file_path: pathlib.Path) -> Dict:
|
|
46
|
+
"""Wrapper to json.load function.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
file_path (Path): Path of the json file to load.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Dict: The loaded json file.
|
|
53
|
+
|
|
54
|
+
"""
|
|
55
|
+
with open(file_path, 'r') as fp:
|
|
56
|
+
return json.load(fp)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def save_json(file_path: pathlib.Path, data: any, cls=None) -> None:
|
|
60
|
+
"""Wrapper to json.dump function.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
file_path (Path): Path to write the json file to.
|
|
64
|
+
data (Any): Data to write to the given path. Must be serializable by JSON.
|
|
65
|
+
cls(object, optional): Costum JSONDecoder subclass. If not specified JSONDecoder is used.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
None: saves the ``data`` in JSON file to the ``file_path``.
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
TypeError: If ``data`` is not JSON serializable.
|
|
72
|
+
"""
|
|
73
|
+
if _is_jsonable(data, cls):
|
|
74
|
+
with open(file_path, 'w') as fp:
|
|
75
|
+
json.dump(data, fp, indent=4, cls=cls)
|
|
76
|
+
else:
|
|
77
|
+
raise TypeError("The given data is not JSON serializable. \
|
|
78
|
+
We rocommend using a costum encoder.")
|