mediml 0.9.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. MEDiml/MEDscan.py +1696 -0
  2. MEDiml/__init__.py +21 -0
  3. MEDiml/biomarkers/BatchExtractor.py +806 -0
  4. MEDiml/biomarkers/BatchExtractorTexturalFilters.py +840 -0
  5. MEDiml/biomarkers/__init__.py +16 -0
  6. MEDiml/biomarkers/diagnostics.py +125 -0
  7. MEDiml/biomarkers/get_oriented_bound_box.py +158 -0
  8. MEDiml/biomarkers/glcm.py +1602 -0
  9. MEDiml/biomarkers/gldzm.py +523 -0
  10. MEDiml/biomarkers/glrlm.py +1315 -0
  11. MEDiml/biomarkers/glszm.py +555 -0
  12. MEDiml/biomarkers/int_vol_hist.py +527 -0
  13. MEDiml/biomarkers/intensity_histogram.py +615 -0
  14. MEDiml/biomarkers/local_intensity.py +89 -0
  15. MEDiml/biomarkers/morph.py +1756 -0
  16. MEDiml/biomarkers/ngldm.py +780 -0
  17. MEDiml/biomarkers/ngtdm.py +414 -0
  18. MEDiml/biomarkers/stats.py +373 -0
  19. MEDiml/biomarkers/utils.py +389 -0
  20. MEDiml/filters/TexturalFilter.py +299 -0
  21. MEDiml/filters/__init__.py +9 -0
  22. MEDiml/filters/apply_filter.py +134 -0
  23. MEDiml/filters/gabor.py +215 -0
  24. MEDiml/filters/laws.py +283 -0
  25. MEDiml/filters/log.py +147 -0
  26. MEDiml/filters/mean.py +121 -0
  27. MEDiml/filters/textural_filters_kernels.py +1738 -0
  28. MEDiml/filters/utils.py +107 -0
  29. MEDiml/filters/wavelet.py +237 -0
  30. MEDiml/learning/DataCleaner.py +198 -0
  31. MEDiml/learning/DesignExperiment.py +480 -0
  32. MEDiml/learning/FSR.py +667 -0
  33. MEDiml/learning/Normalization.py +112 -0
  34. MEDiml/learning/RadiomicsLearner.py +714 -0
  35. MEDiml/learning/Results.py +2237 -0
  36. MEDiml/learning/Stats.py +694 -0
  37. MEDiml/learning/__init__.py +10 -0
  38. MEDiml/learning/cleaning_utils.py +107 -0
  39. MEDiml/learning/ml_utils.py +1015 -0
  40. MEDiml/processing/__init__.py +6 -0
  41. MEDiml/processing/compute_suv_map.py +121 -0
  42. MEDiml/processing/discretisation.py +149 -0
  43. MEDiml/processing/interpolation.py +275 -0
  44. MEDiml/processing/resegmentation.py +66 -0
  45. MEDiml/processing/segmentation.py +912 -0
  46. MEDiml/utils/__init__.py +25 -0
  47. MEDiml/utils/batch_patients.py +45 -0
  48. MEDiml/utils/create_radiomics_table.py +131 -0
  49. MEDiml/utils/data_frame_export.py +42 -0
  50. MEDiml/utils/find_process_names.py +16 -0
  51. MEDiml/utils/get_file_paths.py +34 -0
  52. MEDiml/utils/get_full_rad_names.py +21 -0
  53. MEDiml/utils/get_institutions_from_ids.py +16 -0
  54. MEDiml/utils/get_patient_id_from_scan_name.py +22 -0
  55. MEDiml/utils/get_patient_names.py +26 -0
  56. MEDiml/utils/get_radiomic_names.py +27 -0
  57. MEDiml/utils/get_scan_name_from_rad_name.py +22 -0
  58. MEDiml/utils/image_reader_SITK.py +37 -0
  59. MEDiml/utils/image_volume_obj.py +22 -0
  60. MEDiml/utils/imref.py +340 -0
  61. MEDiml/utils/initialize_features_names.py +62 -0
  62. MEDiml/utils/inpolygon.py +159 -0
  63. MEDiml/utils/interp3.py +43 -0
  64. MEDiml/utils/json_utils.py +78 -0
  65. MEDiml/utils/mode.py +31 -0
  66. MEDiml/utils/parse_contour_string.py +58 -0
  67. MEDiml/utils/save_MEDscan.py +30 -0
  68. MEDiml/utils/strfind.py +32 -0
  69. MEDiml/utils/textureTools.py +188 -0
  70. MEDiml/utils/texture_features_names.py +115 -0
  71. MEDiml/utils/write_radiomics_csv.py +47 -0
  72. MEDiml/wrangling/DataManager.py +1724 -0
  73. MEDiml/wrangling/ProcessDICOM.py +512 -0
  74. MEDiml/wrangling/__init__.py +3 -0
  75. mediml-0.9.9.dist-info/LICENSE.md +674 -0
  76. mediml-0.9.9.dist-info/METADATA +232 -0
  77. mediml-0.9.9.dist-info/RECORD +78 -0
  78. mediml-0.9.9.dist-info/WHEEL +4 -0
@@ -0,0 +1,134 @@
1
+ import numpy as np
2
+
3
+ from ..MEDscan import MEDscan
4
+ from ..utils.image_volume_obj import image_volume_obj
5
+ from .gabor import *
6
+ from .laws import *
7
+ from .log import *
8
+ from .mean import *
9
+ try:
10
+ from .TexturalFilter import TexturalFilter
11
+ except ImportError:
12
+ import_failed = True
13
+ from .wavelet import *
14
+
15
+
16
+ def apply_filter(
17
+ medscan: MEDscan,
18
+ vol_obj: Union[image_volume_obj, np.ndarray],
19
+ user_set_min_val: float = None,
20
+ feature: str = None
21
+ ) -> Union[image_volume_obj, np.ndarray]:
22
+ """Applies mean filter on the given data
23
+
24
+ Args:
25
+ medscan (MEDscan): Instance of the MEDscan class that holds the filtering params
26
+ vol_obj (image_volume_obj): Imaging data to be filtered
27
+ user_set_min_val (float, optional): The minimum value to use for the discretization. Defaults to None.
28
+ feature (str, optional): The feature to extract from the family. In batch extraction, all the features
29
+ of the family will be extracted. Defaults to None.
30
+
31
+ Returns:
32
+ image_volume_obj: Filtered imaging data.
33
+ """
34
+ filter_type = medscan.params.filter.filter_type
35
+
36
+ if filter_type.lower() == "mean":
37
+ input = np.expand_dims(vol_obj.data.astype(np.float64), axis=0) # Convert to shape : (B, W, H, D)
38
+ # Initialize filter class instance
39
+ _filter = Mean(
40
+ ndims=medscan.params.filter.mean.ndims,
41
+ size=medscan.params.filter.mean.size,
42
+ padding=medscan.params.filter.mean.padding
43
+ )
44
+ # Run convolution
45
+ result = _filter.convolve(input, orthogonal_rot=medscan.params.filter.mean.orthogonal_rot)
46
+
47
+ elif filter_type.lower() == "log":
48
+ # Initialize filter class params & instance
49
+ input = np.expand_dims(vol_obj.data.astype(np.float64), axis=0) # Convert to shape : (B, W, H, D)
50
+ voxel_length = medscan.params.process.scale_non_text[0]
51
+ sigma = medscan.params.filter.log.sigma / voxel_length
52
+ length = 2 * int(4 * sigma + 0.5) + 1
53
+ _filter = LaplacianOfGaussian(
54
+ ndims=medscan.params.filter.log.ndims,
55
+ size=length,
56
+ sigma=sigma,
57
+ padding=medscan.params.filter.log.padding
58
+ )
59
+ # Run convolution
60
+ result = _filter.convolve(input, orthogonal_rot=medscan.params.filter.log.orthogonal_rot)
61
+
62
+ elif filter_type.lower() == "laws":
63
+ # Initialize filter class instance
64
+ input = np.expand_dims(vol_obj.data.astype(np.float64), axis=0) # Convert to shape : (B, W, H, D)
65
+ _filter = Laws(
66
+ config=medscan.params.filter.laws.config,
67
+ energy_distance=medscan.params.filter.laws.energy_distance,
68
+ rot_invariance=medscan.params.filter.laws.rot_invariance,
69
+ padding=medscan.params.filter.laws.padding
70
+ )
71
+ # Run convolution
72
+ result = _filter.convolve(
73
+ input,
74
+ orthogonal_rot=medscan.params.filter.laws.orthogonal_rot,
75
+ energy_image=medscan.params.filter.laws.energy_image
76
+ )
77
+
78
+ elif filter_type.lower() == "gabor":
79
+ # Initialize filter class params & instance
80
+ input = np.expand_dims(vol_obj.data.astype(np.float64), axis=0) # Convert to shape : (B, W, H, D)
81
+ voxel_length = medscan.params.process.scale_non_text[0]
82
+ sigma = medscan.params.filter.gabor.sigma / voxel_length
83
+ lamb = medscan.params.filter.gabor._lambda / voxel_length
84
+ size = 2 * int(7 * sigma + 0.5) + 1
85
+ _filter = Gabor(size=size,
86
+ sigma=sigma,
87
+ lamb=lamb,
88
+ gamma=medscan.params.filter.gabor.gamma,
89
+ theta=-medscan.params.filter.gabor.theta,
90
+ rot_invariance=medscan.params.filter.gabor.rot_invariance,
91
+ padding=medscan.params.filter.gabor.padding
92
+ )
93
+ # Run convolution
94
+ result = _filter.convolve(input, orthogonal_rot=medscan.params.filter.gabor.orthogonal_rot)
95
+
96
+ elif filter_type.lower().startswith("wavelet"):
97
+ # Initialize filter class instance
98
+ input = np.expand_dims(vol_obj.data.astype(np.float64), axis=0) # Convert to shape : (B, W, H, D)
99
+ _filter = Wavelet(
100
+ ndims=medscan.params.filter.wavelet.ndims,
101
+ wavelet_name=medscan.params.filter.wavelet.basis_function,
102
+ rot_invariance=medscan.params.filter.wavelet.rot_invariance,
103
+ padding=medscan.params.filter.wavelet.padding
104
+ )
105
+ # Run convolution
106
+ result = _filter.convolve(
107
+ input,
108
+ _filter=medscan.params.filter.wavelet.subband,
109
+ level=medscan.params.filter.wavelet.level
110
+ )
111
+ elif filter_type.lower() == "textural":
112
+ if not import_failed:
113
+ # Initialize filter class instance
114
+ _filter = TexturalFilter(
115
+ family=medscan.params.filter.textural.family,
116
+ )
117
+ # Apply filter
118
+ vol_obj = _filter(
119
+ vol_obj,
120
+ size=medscan.params.filter.textural.size,
121
+ discretization=medscan.params.filter.textural.discretization,
122
+ local=medscan.params.filter.textural.local,
123
+ user_set_min_val=user_set_min_val,
124
+ feature=feature
125
+ )
126
+ else:
127
+ raise ValueError(
128
+ r'Filter name should either be: "mean", "log", "laws", "gabor" or "wavelet".'
129
+ )
130
+
131
+ if not filter_type.lower() == "textural":
132
+ vol_obj.data = np.squeeze(result,axis=0)
133
+
134
+ return vol_obj
@@ -0,0 +1,215 @@
1
+ import math
2
+ from itertools import product
3
+ from typing import List, Union
4
+
5
+ import numpy as np
6
+
7
+ from ..MEDscan import MEDscan
8
+ from ..utils.image_volume_obj import image_volume_obj
9
+ from .utils import convolve
10
+
11
+
12
+ class Gabor():
13
+ """
14
+ The Gabor filter class
15
+ """
16
+
17
+ def __init__(
18
+ self,
19
+ size: int,
20
+ sigma: float,
21
+ lamb: float,
22
+ gamma: float,
23
+ theta: float,
24
+ rot_invariance=False,
25
+ padding="symmetric"
26
+ ) -> None:
27
+ """
28
+ The constructor of the Gabor filter. Highly inspired by Ref 1.
29
+
30
+ Args:
31
+ size (int): An integer that represent the length along one dimension of the kernel.
32
+ sigma (float): A positive float that represent the scale of the Gabor filter
33
+ lamb (float): A positive float that represent the wavelength in the Gabor filter. (mm or pixel?)
34
+ gamma (float): A positive float that represent the spacial aspect ratio
35
+ theta (float): Angle parameter used in the rotation matrix
36
+ rot_invariance (bool): If true, rotation invariance will be done on the kernel and the kernel
37
+ will be rotate 2*pi / theta times.
38
+ padding: The padding type that will be used to produce the convolution
39
+
40
+ Returns:
41
+ None
42
+ """
43
+
44
+ assert ((size + 1) / 2).is_integer() and size > 0, "size should be a positive odd number."
45
+ assert sigma > 0, "sigma should be a positive float"
46
+ assert lamb > 0, "lamb represent the wavelength, so it should be a positive float"
47
+ assert gamma > 0, "gamma is the ellipticity of the support of the filter, so it should be a positive float"
48
+
49
+ self.dim = 2
50
+ self.padding = padding
51
+ self.size = size
52
+ self.sigma = sigma
53
+ self.lamb = lamb
54
+ self.gamma = gamma
55
+ self.theta = theta
56
+ self.rot = rot_invariance
57
+ self.create_kernel()
58
+
59
+ def create_kernel(self) -> List[np.ndarray]:
60
+ """Create the kernel of the Gabor filter
61
+
62
+ Returns:
63
+ List[ndarray]: A list of numpy 2D-array that contain the kernel of the real part and
64
+ the imaginary part respectively.
65
+ """
66
+
67
+ def compute_weight(position, theta):
68
+ k_2 = position[0]*math.cos(theta) + position[1] * math.sin(theta)
69
+ k_1 = position[1]*math.cos(theta) - position[0] * math.sin(theta)
70
+
71
+ common = math.e**(-(k_1**2 + (self.gamma*k_2)**2)/(2*self.sigma**2))
72
+ real = math.cos(2*math.pi*k_1/self.lamb)
73
+ im = math.sin(2*math.pi*k_1/self.lamb)
74
+ return common*real, common*im
75
+
76
+ # Rotation invariance
77
+ nb_rot = round(2*math.pi/abs(self.theta)) if self.rot else 1
78
+ real_list = []
79
+ im_list = []
80
+
81
+ for i in range(1, nb_rot+1):
82
+ # Initialize the kernel as tensor of zeros
83
+ real_kernel = np.zeros([self.size for _ in range(2)])
84
+ im_kernel = np.zeros([self.size for _ in range(2)])
85
+
86
+ for k in product(range(self.size), repeat=2):
87
+ real_kernel[k], im_kernel[k] = compute_weight(np.array(k)-int((self.size-1)/2), self.theta*i)
88
+
89
+ real_list.extend([real_kernel])
90
+ im_list.extend([im_kernel])
91
+
92
+ self.kernel = np.expand_dims(
93
+ np.concatenate((real_list, im_list), axis=0),
94
+ axis=1
95
+ )
96
+
97
+ def convolve(self,
98
+ images: np.ndarray,
99
+ orthogonal_rot=False,
100
+ pooling_method='mean') -> np.ndarray:
101
+ """Filter a given image using the Gabor kernel defined during the construction of this instance.
102
+
103
+ Args:
104
+ images (ndarray): A n-dimensional numpy array that represent the images to filter
105
+ orthogonal_rot (bool): If true, the 3D images will be rotated over coronal, axial and sagittal axis
106
+
107
+ Returns:
108
+ ndarray: The filtered image as a numpy ndarray
109
+ """
110
+
111
+ # Swap the second axis with the last, to convert image B, W, H, D --> B, D, H, W
112
+ image = np.swapaxes(images, 1, 3)
113
+
114
+ result = convolve(self.dim, self.kernel, image, orthogonal_rot, self.padding)
115
+
116
+ # Reshape to get real and imaginary response on the first axis.
117
+ _dim = 2 if orthogonal_rot else 1
118
+ nb_rot = int(result.shape[_dim]/2)
119
+ result = np.stack(np.array_split(result, np.array([nb_rot]), _dim), axis=0)
120
+
121
+ # 2D modulus response map
122
+ result = np.linalg.norm(result, axis=0)
123
+
124
+ # Rotation invariance.
125
+ if pooling_method == 'mean':
126
+ result = np.mean(result, axis=2) if orthogonal_rot else np.mean(result, axis=1)
127
+ elif pooling_method == 'max':
128
+ result = np.max(result, axis=2) if orthogonal_rot else np.max(result, axis=1)
129
+ else:
130
+ raise ValueError("Pooling method should be either 'mean' or 'max'.")
131
+
132
+ # Aggregate orthogonal rotation
133
+ result = np.mean(result, axis=0) if orthogonal_rot else result
134
+
135
+ return np.swapaxes(result, 1, 3)
136
+
137
+ def apply_gabor(
138
+ input_images: Union[image_volume_obj, np.ndarray],
139
+ medscan: MEDscan = None,
140
+ voxel_length: float = 0.0,
141
+ sigma: float = 0.0,
142
+ _lambda: float = 0.0,
143
+ gamma: float = 0.0,
144
+ theta: float = 0.0,
145
+ rot_invariance: bool = False,
146
+ padding: str = "symmetric",
147
+ orthogonal_rot: bool = False,
148
+ pooling_method: str = "mean"
149
+ ) -> np.ndarray:
150
+ """Apply the Gabor filter to a given imaging data.
151
+
152
+ Args:
153
+ input_images (Union[image_volume_obj, np.ndarray]): The input images to filter.
154
+ medscan (MEDscan, optional): The MEDscan object that will provide the filter parameters.
155
+ voxel_length (float, optional): The voxel size of the input image.
156
+ sigma (float, optional): A positive float that represent the scale of the Gabor filter.
157
+ _lambda (float, optional): A positive float that represent the wavelength in the Gabor filter.
158
+ gamma (float, optional): A positive float that represent the spacial aspect ratio.
159
+ theta (float, optional): Angle parameter used in the rotation matrix.
160
+ rot_invariance (bool, optional): If true, rotation invariance will be done on the kernel and the kernel
161
+ will be rotate 2*pi / theta times.
162
+ padding (str, optional): The padding type that will be used to produce the convolution. Check options
163
+ here: `numpy.pad <https://numpy.org/doc/stable/reference/generated/numpy.pad.html>`__.
164
+ orthogonal_rot (bool, optional): If true, the 3D images will be rotated over coronal, axial and sagittal axis.
165
+
166
+ Returns:
167
+ ndarray: The filtered image.
168
+ """
169
+ # Check if the input is a numpy array or a Image volume object
170
+ spatial_ref = None
171
+ if type(input_images) == image_volume_obj:
172
+ spatial_ref = input_images.spatialRef
173
+ input_images = input_images.data
174
+
175
+ # Convert to shape : (B, W, H, D)
176
+ input_images = np.expand_dims(input_images.astype(np.float64), axis=0)
177
+
178
+ if medscan:
179
+ # Initialize filter class params & instance
180
+ voxel_length = medscan.params.process.scale_non_text[0]
181
+ sigma = medscan.params.filter.gabor.sigma / voxel_length
182
+ lamb = medscan.params.filter.gabor._lambda / voxel_length
183
+ size = 2 * int(7 * sigma + 0.5) + 1
184
+ _filter = Gabor(size=size,
185
+ sigma=sigma,
186
+ lamb=lamb,
187
+ gamma=medscan.params.filter.gabor.gamma,
188
+ theta=-medscan.params.filter.gabor.theta,
189
+ rot_invariance=medscan.params.filter.gabor.rot_invariance,
190
+ padding=medscan.params.filter.gabor.padding
191
+ )
192
+ # Run convolution
193
+ result = _filter.convolve(input_images, orthogonal_rot=medscan.params.filter.gabor.orthogonal_rot)
194
+ else:
195
+ if not (voxel_length and sigma and _lambda and gamma and theta):
196
+ raise ValueError("Missing parameters to build the Gabor filter.")
197
+ # Initialize filter class params & instance
198
+ sigma = sigma / voxel_length
199
+ lamb = _lambda / voxel_length
200
+ size = 2 * int(7 * sigma + 0.5) + 1
201
+ _filter = Gabor(size=size,
202
+ sigma=sigma,
203
+ lamb=lamb,
204
+ gamma=gamma,
205
+ theta=theta,
206
+ rot_invariance=rot_invariance,
207
+ padding=padding
208
+ )
209
+ # Run convolution
210
+ result = _filter.convolve(input_images, orthogonal_rot=orthogonal_rot, pooling_method=pooling_method)
211
+
212
+ if spatial_ref:
213
+ return image_volume_obj(np.squeeze(result), spatial_ref)
214
+ else:
215
+ return np.squeeze(result)
MEDiml/filters/laws.py ADDED
@@ -0,0 +1,283 @@
1
+ import math
2
+ from itertools import permutations, product
3
+ from typing import List, Union
4
+
5
+ import numpy as np
6
+ from scipy.signal import fftconvolve
7
+
8
+ from ..MEDscan import MEDscan
9
+ from ..utils.image_volume_obj import image_volume_obj
10
+ from .utils import convolve, pad_imgs
11
+
12
+
13
+ class Laws():
14
+ """
15
+ The Laws filter class
16
+ """
17
+
18
+ def __init__(
19
+ self,
20
+ config: List = None,
21
+ energy_distance: int = 7,
22
+ rot_invariance: bool = False,
23
+ padding: str = "symmetric"):
24
+ """The constructor of the Laws filter
25
+
26
+ Args:
27
+ config (str): A string list of every 1D filter used to create the Laws kernel. Since the outer product is
28
+ not commutative, we need to use a list to specify the order of the outer product. It is not
29
+ recommended to use filter of different size to create the Laws kernel.
30
+ energy_distance (float): The distance that will be used to create the energy_kernel.
31
+ rot_invariance (bool): If true, rotation invariance will be done on the kernel.
32
+ padding (str): The padding type that will be used to produce the convolution
33
+
34
+ Returns:
35
+ None
36
+ """
37
+
38
+ ndims = len(config)
39
+
40
+ self.config = config
41
+ self.energy_dist = energy_distance
42
+ self.dim = ndims
43
+ self.padding = padding
44
+ self.rot = rot_invariance
45
+ self.energy_kernel = None
46
+ self.create_kernel()
47
+ self.__create_energy_kernel()
48
+
49
+ @staticmethod
50
+ def __get_filter(name,
51
+ pad=False) -> np.ndarray:
52
+ """This method create a 1D filter according to the given filter name.
53
+
54
+ Args:
55
+ name (float): The filter name. (Such as L3, L5, E3, E5, S3, S5, W5 or R5)
56
+ pad (bool): If true, add zero padding of length 1 each side of kernel L3, E3 and S3
57
+
58
+ Returns:
59
+ ndarray: A 1D filter that is needed to construct the Laws kernel.
60
+ """
61
+
62
+ if name == "L3":
63
+ ker = np.array([0, 1, 2, 1, 0]) if pad else np.array([1, 2, 1])
64
+ return 1/math.sqrt(6) * ker
65
+ elif name == "L5":
66
+ return 1/math.sqrt(70) * np.array([1, 4, 6, 4, 1])
67
+ elif name == "E3":
68
+ ker = np.array([0, -1, 0, 1, 0]) if pad else np.array([-1, 0, 1])
69
+ return 1 / math.sqrt(2) * ker
70
+ elif name == "E5":
71
+ return 1 / math.sqrt(10) * np.array([-1, -2, 0, 2, 1])
72
+ elif name == "S3":
73
+ ker = np.array([0, -1, 2, -1, 0]) if pad else np.array([-1, 2, -1])
74
+ return 1 / math.sqrt(6) * ker
75
+ elif name == "S5":
76
+ return 1 / math.sqrt(6) * np.array([-1, 0, 2, 0, -1])
77
+ elif name == "W5":
78
+ return 1 / math.sqrt(10) * np.array([-1, 2, 0, -2, 1])
79
+ elif name == "R5":
80
+ return 1 / math.sqrt(70) * np.array([1, -4, 6, -4, 1])
81
+ else:
82
+ raise Exception(f"{name} is not a valid filter name. "
83
+ "Choose between : L3, L5, E3, E5, S3, S5, W5 or R5")
84
+
85
+ def __verify_padding_need(self) -> bool:
86
+ """Check if we need to pad the kernels
87
+
88
+ Returns:
89
+ bool: A boolean that indicate if a kernel is smaller than at least one other.
90
+ """
91
+
92
+ ker_length = np.array([int(name[-1]) for name in self.config])
93
+
94
+ return not(ker_length.min == ker_length.max)
95
+
96
+ def create_kernel(self) -> np.ndarray:
97
+ """Create the Laws by computing the outer product of 1d filter specified in the config attribute.
98
+ Kernel = config[0] X config[1] X ... X config[n]. Where X is the outer product.
99
+
100
+ Returns:
101
+ ndarray: A numpy multi-dimensional arrays that represent the Laws kernel.
102
+ """
103
+
104
+ pad = self.__verify_padding_need()
105
+ filter_list = np.array([[self.__get_filter(name, pad) for name in self.config]])
106
+
107
+ if self.rot:
108
+ filter_list = np.concatenate((filter_list, np.flip(filter_list, axis=2)), axis=0)
109
+ prod_list = [prod for prod in product(*np.swapaxes(filter_list, 0, 1))]
110
+
111
+ perm_list = []
112
+ for i in range(len(prod_list)):
113
+ perm_list.extend([perm for perm in permutations(prod_list[i])])
114
+
115
+ filter_list = np.unique(perm_list, axis=0)
116
+
117
+ kernel_list = []
118
+ for perm in filter_list:
119
+ kernel = perm[0]
120
+ shape = kernel.shape
121
+
122
+ for i in range(1, len(perm)):
123
+ sub_kernel = perm[i]
124
+ shape += np.shape(sub_kernel)
125
+ kernel = np.outer(sub_kernel, kernel).reshape(shape)
126
+ if self.dim == 3:
127
+ kernel_list.extend([np.expand_dims(np.flip(kernel, axis=(1, 2)), axis=0)])
128
+ else:
129
+ kernel_list.extend([np.expand_dims(np.flip(kernel, axis=(0, 1)), axis=0)])
130
+
131
+ self.kernel = np.unique(kernel_list, axis=0)
132
+
133
+ def __create_energy_kernel(self) -> np.ndarray:
134
+ """Create the kernel that will be used to generate Laws texture energy images
135
+
136
+ Returns:
137
+ ndarray: A numpy multi-dimensional arrays that represent the Laws energy kernel.
138
+ """
139
+
140
+ # Initialize the kernel as tensor of zeros
141
+ kernel = np.zeros([self.energy_dist*2+1 for _ in range(self.dim)])
142
+
143
+ for k in product(range(self.energy_dist*2 + 1), repeat=self.dim):
144
+ position = np.array(k)-self.energy_dist
145
+ kernel[k] = 1 if np.max(abs(position)) <= self.energy_dist else 0
146
+
147
+ self.energy_kernel = np.expand_dims(kernel/np.prod(kernel.shape), axis=(0, 1))
148
+
149
+ def __compute_energy_image(self,
150
+ images: np.ndarray) -> np.ndarray:
151
+ """Compute the Laws texture energy images as described in (Ref 1).
152
+
153
+ Args:
154
+ images (ndarray): A n-dimensional numpy array that represent the filtered images
155
+
156
+ Returns:
157
+ ndarray: A numpy multi-dimensional array of the Laws texture energy map.
158
+ """
159
+ # If we have a 2D kernel but a 3D images, we swap dimension channel with dimension batch.
160
+ images = np.swapaxes(images, 0, 1)
161
+
162
+ # absolute image intensities are used in convolution
163
+ result = fftconvolve(np.abs(images), self.energy_kernel, mode='valid')
164
+
165
+ if self.dim == 2:
166
+ return np.swapaxes(result, axis1=0, axis2=1)
167
+ else:
168
+ return np.squeeze(result, axis=1)
169
+
170
+ def convolve(self,
171
+ images: np.ndarray,
172
+ orthogonal_rot=False,
173
+ energy_image=False):
174
+ """Filter a given image using the Laws kernel defined during the construction of this instance.
175
+
176
+ Args:
177
+ images (ndarray): A n-dimensional numpy array that represent the images to filter
178
+ orthogonal_rot (bool): If true, the 3D images will be rotated over coronal, axial and sagittal axis
179
+ energy_image (bool): If true, return also the Laws Texture Energy Images
180
+
181
+ Returns:
182
+ ndarray: The filtered image
183
+ """
184
+ images = np.swapaxes(images, 1, 3)
185
+
186
+ if orthogonal_rot:
187
+ raise NotImplementedError
188
+
189
+ result = convolve(self.dim, self.kernel, images, orthogonal_rot, self.padding)
190
+ result = np.amax(result, axis=1) if self.dim == 2 else np.amax(result, axis=0)
191
+
192
+ if energy_image:
193
+ # We pad the response map
194
+ result = np.expand_dims(result, axis=1) if self.dim == 3 else result
195
+ ndims = len(result.shape)
196
+
197
+ padding = [self.energy_dist for _ in range(2 * self.dim)]
198
+ pad_axis_list = [i for i in range(ndims - self.dim, ndims)]
199
+
200
+ response = pad_imgs(result, padding, pad_axis_list, self.padding)
201
+
202
+ # Free memory
203
+ del result
204
+
205
+ # We compute the energy map and we squeeze the second dimension of the energy maps.
206
+ energy_imgs = self.__compute_energy_image(response)
207
+
208
+ return np.swapaxes(energy_imgs, 1, 3)
209
+ else:
210
+ return np.swapaxes(result, 1, 3)
211
+
212
+ def apply_laws(
213
+ input_images: Union[np.ndarray, image_volume_obj],
214
+ medscan: MEDscan = None,
215
+ config: List[str] = [],
216
+ energy_distance: int = 7,
217
+ padding: str = "symmetric",
218
+ rot_invariance: bool = False,
219
+ orthogonal_rot: bool = False,
220
+ energy_image: bool = False,
221
+ ) -> np.ndarray:
222
+ """Apply the mean filter to the input image
223
+
224
+ Args:
225
+ input_images (ndarray): The images to filter.
226
+ medscan (MEDscan, optional): The MEDscan object that will provide the filter parameters.
227
+ config (List[str], optional): A string list of every 1D filter used to create the Laws kernel. Since the outer product is
228
+ not commutative, we need to use a list to specify the order of the outer product. It is not
229
+ recommended to use filter of different size to create the Laws kernel.
230
+ energy_distance (int, optional): The distance of the Laws energy map from the center of the image.
231
+ padding (str, optional): The padding type that will be used to produce the convolution. Check options
232
+ here: `numpy.pad <https://numpy.org/doc/stable/reference/generated/numpy.pad.html>`__.
233
+ rot_invariance (bool, optional): If true, rotation invariance will be done on the kernel.
234
+ orthogonal_rot (bool, optional): If true, the 3D images will be rotated over coronal, axial and sagittal axis.
235
+ energy_image (bool, optional): If true, will compute and return the Laws Texture Energy Images.
236
+
237
+ Returns:
238
+ ndarray: The filtered image.
239
+ """
240
+ # Check if the input is a numpy array or a Image volume object
241
+ spatial_ref = None
242
+ if type(input_images) == image_volume_obj:
243
+ spatial_ref = input_images.spatialRef
244
+ input_images = input_images.data
245
+
246
+ # Convert to shape : (B, W, H, D)
247
+ input_images = np.expand_dims(input_images.astype(np.float64), axis=0)
248
+
249
+ if medscan:
250
+ # Initialize filter class instance
251
+ _filter = Laws(
252
+ config=medscan.params.filter.laws.config,
253
+ energy_distance=medscan.params.filter.laws.energy_distance,
254
+ rot_invariance=medscan.params.filter.laws.rot_invariance,
255
+ padding=medscan.params.filter.laws.padding
256
+ )
257
+ # Run convolution
258
+ result = _filter.convolve(
259
+ input_images,
260
+ orthogonal_rot=medscan.params.filter.laws.orthogonal_rot,
261
+ energy_image=medscan.params.filter.laws.energy_image
262
+ )
263
+ elif config:
264
+ # Initialize filter class instance
265
+ _filter = Laws(
266
+ config=config,
267
+ energy_distance=energy_distance,
268
+ rot_invariance=rot_invariance,
269
+ padding=padding
270
+ )
271
+ # Run convolution
272
+ result = _filter.convolve(
273
+ input_images,
274
+ orthogonal_rot=orthogonal_rot,
275
+ energy_image=energy_image
276
+ )
277
+ else:
278
+ raise ValueError("Either medscan or config must be provided")
279
+
280
+ if spatial_ref:
281
+ return image_volume_obj(np.squeeze(result), spatial_ref)
282
+ else:
283
+ return np.squeeze(result)