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,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
|
MEDiml/filters/gabor.py
ADDED
|
@@ -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)
|