-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathbase.py
247 lines (198 loc) · 11.1 KB
/
base.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
import inspect
import logging
import traceback
import numpy
import SimpleITK as sitk
import six
from radiomics import deprecated, getProgressReporter, imageoperations
class RadiomicsFeaturesBase(object):
"""
This is the abstract class, which defines the common interface for the feature classes. All feature classes inherit
(directly of indirectly) from this class.
At initialization, image and labelmap are passed as SimpleITK image objects (``inputImage`` and ``inputMask``,
respectively.) The motivation for using SimpleITK images as input is to keep the possibility of reusing the
optimized feature calculators implemented in SimpleITK in the future. If either the image or the mask is None,
initialization fails and a warning is logged (does not raise an error).
Logging is set up using a child logger from the parent 'radiomics' logger. This retains the toolbox structure in
the generated log. The child logger is named after the module containing the feature class (e.g. 'radiomics.glcm').
Any pre calculations needed before the feature functions are called can be added by overriding the
``_initSegmentBasedCalculation`` function, which prepares the input for feature extraction. If image discretization is
needed, this can be implemented by adding a call to ``_applyBinning`` to this initialization function, which also
instantiates coefficients holding the maximum ('Ng') and unique ('GrayLevels') that can be found inside the ROI after
binning. This function also instantiates the `matrix` variable, which holds the discretized image (the `imageArray`
variable will hold only original gray levels).
The following variables are instantiated at initialization:
- kwargs: dictionary holding all customized settings passed to this feature class.
- label: label value of Region of Interest (ROI) in labelmap. If key is not present, a default value of 1 is used.
- featureNames: list containing the names of features defined in the feature class. See :py:func:`getFeatureNames`
- inputImage: SimpleITK image object of the input image (dimensions x, y, z)
The following variables are instantiated by the ``_initSegmentBasedCalculation`` function:
- inputMask: SimpleITK image object of the input labelmap (dimensions x, y, z)
- imageArray: numpy array of the gray values in the input image (dimensions z, y, x)
- maskArray: numpy boolean array with elements set to ``True`` where labelmap = label, ``False`` otherwise,
(dimensions z, y, x).
- labelledVoxelCoordinates: tuple of 3 numpy arrays containing the z, x and y coordinates of the voxels included in
the ROI, respectively. Length of each array is equal to total number of voxels inside ROI.
- matrix: copy of the imageArray variable, with gray values inside ROI discretized using the specified binWidth.
This variable is only instantiated if a call to ``_applyBinning`` is added to an override of
``_initSegmentBasedCalculation`` in the feature class.
.. note::
Although some variables listed here have similar names to customization settings, they do *not* represent all the
possible settings on the feature class level. These variables are listed here to help developers develop new feature
classes, which make use of these variables. For more information on customization, see
:ref:`radiomics-customization-label`, which includes a comprehensive list of all possible settings, including
default values and explanation of usage.
"""
def __init__(self, inputImage, inputMask, **kwargs):
self.logger = logging.getLogger(self.__module__)
self.logger.debug('Initializing feature class')
if inputImage is None or inputMask is None:
raise ValueError('Missing input image or mask')
self.progressReporter = getProgressReporter
self.settings = kwargs
self.label = kwargs.get('label', 1)
self.voxelBased = kwargs.get('voxelBased', False)
self.coefficients = {}
# all features are disabled by default
self.enabledFeatures = {}
self.featureValues = {}
self.featureNames = self.getFeatureNames()
self.inputImage = inputImage
self.inputMask = inputMask
self.imageArray = sitk.GetArrayFromImage(self.inputImage)
if self.voxelBased:
self._initVoxelBasedCalculation()
else:
self._initSegmentBasedCalculation()
def _initSegmentBasedCalculation(self):
self.maskArray = (sitk.GetArrayFromImage(self.inputMask) == self.label) # boolean array
def _initVoxelBasedCalculation(self):
self.masked = self.settings.get('maskedKernel', True)
maskArray = sitk.GetArrayFromImage(self.inputMask) == self.label # boolean array
self.labelledVoxelCoordinates = numpy.array(numpy.where(maskArray))
# Set up the mask array for the gray value discretization
if self.masked:
self.maskArray = maskArray
else:
# This will cause the discretization to use the entire image
self.maskArray = numpy.ones(self.imageArray.shape, dtype='bool')
def _initCalculation(self, voxelCoordinates=None):
"""
Last steps to prepare the class for extraction. This function calculates the texture matrices and coefficients in
the respective feature classes
"""
pass
def _applyBinning(self, matrix):
matrix, _ = imageoperations.binImage(matrix, self.maskArray, **self.settings)
self.coefficients['grayLevels'] = numpy.unique(matrix[self.maskArray])
self.coefficients['Ng'] = int(numpy.max(self.coefficients['grayLevels'])) # max gray level in the ROI
return matrix
def enableFeatureByName(self, featureName, enable=True):
"""
Enables or disables feature specified by ``featureName``. If feature is not present in this class, a lookup error is
raised. ``enable`` specifies whether to enable or disable the feature.
"""
if featureName not in self.featureNames:
raise LookupError('Feature not found: ' + featureName)
if self.featureNames[featureName]:
self.logger.warning('Feature %s is deprecated, use with caution!', featureName)
self.enabledFeatures[featureName] = enable
def enableAllFeatures(self):
"""
Enables all features found in this class for calculation.
.. note::
Features that have been marked "deprecated" are not enabled by this function. They can still be enabled manually by
a call to :py:func:`~radiomics.base.RadiomicsBase.enableFeatureByName()`,
:py:func:`~radiomics.featureextractor.RadiomicsFeaturesExtractor.enableFeaturesByName()`
or in the parameter file (by specifying the feature by name, not when enabling all features).
However, in most cases this will still result only in a deprecation warning.
"""
for featureName, is_deprecated in six.iteritems(self.featureNames):
# only enable non-deprecated features here
if not is_deprecated:
self.enableFeatureByName(featureName, True)
def disableAllFeatures(self):
"""
Disables all features. Additionally resets any calculated features.
"""
self.enabledFeatures = {}
self.featureValues = {}
@classmethod
def getFeatureNames(cls):
"""
Dynamically enumerates features defined in the feature class. Features are identified by the
``get<Feature>FeatureValue`` signature, where <Feature> is the name of the feature (unique on the class level).
Found features are returned as a dictionary of the feature names, where the value ``True`` if the
feature is deprecated, ``False`` otherwise (``{<Feature1>:<deprecated>, <Feature2>:<deprecated>, ...}``).
This function is called at initialization, found features are stored in the ``featureNames`` variable.
"""
attributes = inspect.getmembers(cls)
features = {a[0][3:-12]: getattr(a[1], '_is_deprecated', False) for a in attributes
if a[0].startswith('get') and a[0].endswith('FeatureValue')}
return features
def execute(self):
"""
Calculates all features enabled in ``enabledFeatures``. A feature is enabled if it's key is present in this
dictionary and it's value is True.
Calculated values are stored in the ``featureValues`` dictionary, with feature name as key and the calculated
feature value as value. If an exception is thrown during calculation, the error is logged, and the value is set to
NaN.
"""
if len(self.enabledFeatures) == 0:
self.enableAllFeatures()
if self.voxelBased:
self._calculateVoxels()
else:
self._calculateSegment()
return self.featureValues
@deprecated
def calculateFeatures(self):
self.logger.warning('calculateFeatures() is deprecated, use execute() instead.')
self.execute()
def _calculateVoxels(self):
initValue = self.settings.get('initValue', 0)
voxelBatch = self.settings.get('voxelBatch', -1)
# Initialize the output with empty numpy arrays
for feature, enabled in six.iteritems(self.enabledFeatures):
if enabled:
self.featureValues[feature] = numpy.full(list(self.inputImage.GetSize())[::-1], initValue, dtype='float')
# Calculate the feature values for all enabled features
voxel_count = self.labelledVoxelCoordinates.shape[1]
voxel_batch_idx = 0
if voxelBatch < 0:
voxelBatch = voxel_count
n_batches = numpy.ceil(float(voxel_count) / voxelBatch)
while voxel_batch_idx < voxel_count:
self.logger.debug('Calculating voxel batch no. %i/%i', int(voxel_batch_idx / voxelBatch) + 1, n_batches)
voxelCoords = self.labelledVoxelCoordinates[:, voxel_batch_idx:voxel_batch_idx + voxelBatch]
# Calculate the feature values for the current kernel
for success, featureName, featureValue in self._calculateFeatures(voxelCoords):
if success:
self.featureValues[featureName][tuple(voxelCoords)] = featureValue
voxel_batch_idx += voxelBatch
# Convert the output to simple ITK image objects
for feature, enabled in six.iteritems(self.enabledFeatures):
if enabled:
self.featureValues[feature] = sitk.GetImageFromArray(self.featureValues[feature])
self.featureValues[feature].CopyInformation(self.inputImage)
def _calculateSegment(self):
# Get the feature values using the current segment.
for success, featureName, featureValue in self._calculateFeatures():
# Always store the result. In case of an error, featureValue will be NaN
self.featureValues[featureName] = numpy.squeeze(featureValue)
def _calculateFeatures(self, voxelCoordinates=None):
# Initialize the calculation
# This function serves to calculate the texture matrices where applicable
self._initCalculation(voxelCoordinates)
self.logger.debug('Calculating features')
for feature, enabled in six.iteritems(self.enabledFeatures):
if enabled:
try:
# Use getattr to get the feature calculation methods, then use '()' to evaluate those methods
yield True, feature, getattr(self, 'get%sFeatureValue' % feature)()
except DeprecationWarning as deprecatedFeature:
# Add a debug log message, as a warning is usually shown and would entail a too verbose output
self.logger.debug('Feature %s is deprecated: %s', feature, deprecatedFeature.message)
except Exception:
self.logger.error('FAILED: %s', traceback.format_exc())
yield False, feature, numpy.nan