mediml 0.9.9__py3-none-any.whl

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