Skip to content

Commit ace3663

Browse files
authored
Merge pull request #209 from JoostJM/add-normalize-function
Implement normalization function in imageoperations
2 parents e5ad8f8 + 428e29b commit ace3663

File tree

3 files changed

+99
-31
lines changed

3 files changed

+99
-31
lines changed

data/paramSchema.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,16 @@ mapping:
1818
type: float
1919
range:
2020
min-ex: 0
21+
normalize:
22+
type: bool
23+
normalizeScale:
24+
type: float
25+
range:
26+
min-ex: 0
27+
removeOutliers:
28+
type: float
29+
range:
30+
min-ex: 0
2131
resampledPixelSpacing:
2232
seq:
2333
- type: int

radiomics/featureextractor.py

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616

1717
class RadiomicsFeaturesExtractor:
18-
"""
18+
r"""
1919
Wrapper class for calculation of a radiomics signature.
2020
At and after initialisation various settings can be used to customize the resultant signature.
2121
This includes which classes and features to use, as well as what should be done in terms of preprocessing the image
@@ -28,14 +28,24 @@ class RadiomicsFeaturesExtractor:
2828
It initialisation, a parameters file can be provided containing all necessary settings. This is done by passing the
2929
location of the file as the single argument in the initialization call, without specifying it as a keyword argument.
3030
If such a file location is provided, any additional kwargs are ignored.
31-
Alternatively, at initialisation, the following general settings can be specified in kwargs:
31+
Alternatively, at initialisation, the following general settings can be specified in the parameter file or ``kwargs``,
32+
with default values in brackets:
3233
3334
- verbose [False]: Boolean, set to False to disable status update printing.
3435
- enableCExtensions [True]: Boolean, set to False to force calculation to full-python mode. See also
3536
:py:func:`~radiomics.enableCExtensions()`.
3637
- additionalInfo [True]: boolean, set to False to disable inclusion of additional information on the extraction in the
3738
output. See also :py:func:`~addProvenance()`.
3839
- binWidth [25]: Float, size of the bins when making a histogram and for discretization of the image gray level.
40+
- normalize [False]: Boolean, set to True to enable normalizing of the image before any resampling. See also
41+
:py:func:`~imageoperations.normalizeImage`.
42+
- normalizeScale [1]: Float, determines the scale after normalizing the image. If normalizing is disabled, this has
43+
no effect.
44+
- removeOutliers [None]: Float, defines the outliers to remove from the image. An outlier is defined as values that
45+
differ more than :math:`n\sigma_x` from the mean, where :math:`n>0` and equal to the value of this setting. If this
46+
parameter is omitted (providing it without a value (i.e. None) in the parameter file will throw an error), no
47+
outliers are removed. If normalizing is disabled, this has no effect. See also
48+
:py:func:`~imageoperations.normalizeImage`.
3949
- resampledPixelSpacing [None]: List of 3 floats, sets the size of the voxel in (x, y, z) plane when resampling.
4050
- interpolator [sitkBSpline]: Simple ITK constant or string name thereof, sets interpolator to use for resampling.
4151
Enumerated value, possible values:
@@ -82,7 +92,10 @@ def __init__(self, *args, **kwargs):
8292
self.loadParams(args[0])
8393
else:
8494
# Set default settings and update with and changed settings contained in kwargs
85-
self.kwargs = {'resampledPixelSpacing': None, # No resampling by default
95+
self.kwargs = {'normalize': False,
96+
'normalizeScale': 1,
97+
'removeOutliers': None,
98+
'resampledPixelSpacing': None, # No resampling by default
8699
'interpolator': sitk.sitkBSpline,
87100
'padDistance': 5,
88101
'label': 1,
@@ -171,7 +184,10 @@ def loadParams(self, paramsFile):
171184
self.enabledFeatures = enabledFeatures
172185

173186
# Set default settings and update with and changed settings contained in kwargs
174-
self.kwargs = {'resampledPixelSpacing': None, # No resampling by default
187+
self.kwargs = {'normalize': False,
188+
'normalizeScale': 1,
189+
'removeOutliers': None,
190+
'resampledPixelSpacing': None, # No resampling by default
175191
'interpolator': sitk.sitkBSpline,
176192
'padDistance': 5,
177193
'label': 1,
@@ -287,11 +303,11 @@ def enableFeaturesByName(self, **enabledFeatures):
287303
def execute(self, imageFilepath, maskFilepath, label=None):
288304
"""
289305
Compute radiomics signature for provide image and mask combination.
290-
First, image and mask are loaded and resampled if necessary. Second, if enabled, provenance information is
291-
calculated and stored as part of the result. Next, shape features are calculated on a cropped (no padding) version
292-
of the original image. Then other featureclasses are calculated using all specified input images in ``inputImages``.
293-
Images are cropped to tumor mask (no padding) after application of any filter and before being passed to the feature
294-
class. Finally, the dictionary containing all calculated features is returned.
306+
First, image and mask are loaded and normalized/resampled if necessary. Second, if enabled, provenance information
307+
is calculated and stored as part of the result. Next, shape features are calculated on a cropped (no padding)
308+
version of the original image. Then other featureclasses are calculated using all specified input images in
309+
``inputImages``. Images are cropped to tumor mask (no padding) after application of any filter and before being
310+
passed to the feature class. Finally, the dictionary containing all calculated features is returned.
295311
296312
:param imageFilepath: SimpleITK Image, or string pointing to image file location
297313
:param maskFilepath: SimpleITK Image, or string pointing to labelmap file location
@@ -360,7 +376,9 @@ def loadImage(self, ImageFilePath, MaskFilePath):
360376
All other cases are ignored (nothing calculated).
361377
Equal approach is used for assignment of mask using MaskFilePath.
362378
363-
If resampling is enabled, both image and mask are resampled and cropped to the tumormask (with additional
379+
If normalizing is enabled image is first normalized before any resampling is applied.
380+
381+
If resampling is enabled, both image and mask are resampled and cropped to the tumor mask (with additional
364382
padding as specified in padDistance) after assignment of image and mask.
365383
"""
366384
if isinstance(ImageFilePath, six.string_types) and os.path.exists(ImageFilePath):
@@ -381,6 +399,9 @@ def loadImage(self, ImageFilePath, MaskFilePath):
381399
if self.kwargs['verbose']: print("Error reading mask Filepath or SimpleITK object")
382400
mask = None
383401

402+
if self.kwargs['normalize']:
403+
image = imageoperations.normalizeImage(image, self.kwargs['normalizeScale'], self.kwargs['removeOutliers'])
404+
384405
if self.kwargs['interpolator'] is not None and self.kwargs['resampledPixelSpacing'] is not None:
385406
image, mask = imageoperations.resampleImage(image, mask,
386407
self.kwargs['resampledPixelSpacing'],
@@ -436,6 +457,9 @@ def computeFeatures(self, image, mask, inputImageName, **kwargs):
436457
return featureVector
437458

438459
def getFeatureClassNames(self):
460+
"""
461+
Returns a list of all possible feature classes.
462+
"""
439463
return self.featureClasses.keys()
440464

441465
def getFeatureNames(self, featureClassName):

radiomics/imageoperations.py

Lines changed: 55 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,8 @@ def cropToTumorMask(imageNode, maskNode, label=1, boundingBox=None):
138138

139139

140140
def resampleImage(imageNode, maskNode, resampledPixelSpacing, interpolator=sitk.sitkBSpline, label=1, padDistance=5):
141-
"""Resamples image or label to the specified pixel spacing (The default interpolator is Bspline)
141+
"""
142+
Resamples image or label to the specified pixel spacing (The default interpolator is Bspline)
142143
143144
'imageNode' is a SimpleITK Object, and 'resampledPixelSpacing' is the output pixel spacing (list of 3 elements).
144145
@@ -231,6 +232,39 @@ def resampleImage(imageNode, maskNode, resampledPixelSpacing, interpolator=sitk.
231232

232233
return resampledImageNode, resampledMaskNode
233234

235+
def normalizeImage(image, scale=1, outliers=None):
236+
r"""
237+
Normalizes the image by centering it at the mean with standard deviation. Normalization is based on all gray values in
238+
the image, not just those inside the segementation.
239+
240+
:math:`f(x) = \frac{s(x - \mu_x)}{\sigma_x}`
241+
242+
Where:
243+
244+
- :math:`x` and :math:`f(x)` are the original and normalized intensity, respectively.
245+
- :math:`\mu_x` and :math:`\sigma_x` are the mean and standard deviation of the image instensity values.
246+
- :math:`s` is an optional scaling defined by ``scale``. By default, it is set to 1.
247+
248+
Optionally, outliers can be removed, in which case values for which :math:`x > \mu_x + n\sigma_x` or
249+
:math:`x < \mu_x - n\sigma_x` are set to :math:`\mu_x + n\sigma_x` and :math:`\mu_x - n\sigma_x`, respectively.
250+
Here, :math:`n>0` and defined by ``outliers``. This, in turn, is controlled by the ``removeOutliers`` parameter.
251+
Removal of outliers is done after the values of the image are normalized, but before ``scale`` is applied.
252+
"""
253+
image = sitk.Normalize(image)
254+
255+
if outliers is not None:
256+
imageArr = sitk.GetArrayFromImage(image)
257+
258+
imageArr[imageArr > outliers] = outliers
259+
imageArr[imageArr < -outliers] = -outliers
260+
261+
newImage = sitk.GetImageFromArray(imageArr)
262+
newImage.CopyInformation(image)
263+
264+
image *= scale
265+
266+
return image
267+
234268

235269
def applyThreshold(inputImage, lowerThreshold, upperThreshold, insideValue=None, outsideValue=0):
236270
# this mode is useful to generate the mask of thresholded voxels
@@ -309,32 +343,32 @@ def getLoGImage(inputImage, **kwargs):
309343

310344
def getWaveletImage(inputImage, **kwargs):
311345
"""
312-
Apply wavelet filter to image and compute signature for each filtered image.
346+
Apply wavelet filter to image and compute signature for each filtered image.
313347
314-
Following settings are possible:
348+
Following settings are possible:
315349
316-
- start_level [0]: integer, 0 based level of wavelet which should be used as first set of decompositions
317-
from which a signature is calculated
318-
- level [1]: integer, number of levels of wavelet decompositions from which a signature is calculated.
319-
- wavelet ["coif1"]: string, type of wavelet decomposition. Enumerated value, validated against possible values
320-
present in the ``pyWavelet.wavelist()``. Current possible values (pywavelet version 0.4.0) (where an
321-
aditional number is needed, range of values is indicated in []):
350+
- start_level [0]: integer, 0 based level of wavelet which should be used as first set of decompositions
351+
from which a signature is calculated
352+
- level [1]: integer, number of levels of wavelet decompositions from which a signature is calculated.
353+
- wavelet ["coif1"]: string, type of wavelet decomposition. Enumerated value, validated against possible values
354+
present in the ``pyWavelet.wavelist()``. Current possible values (pywavelet version 0.4.0) (where an
355+
aditional number is needed, range of values is indicated in []):
322356
323-
- haar
324-
- dmey
325-
- sym[2-20]
326-
- db[1-20]
327-
- coif[1-5]
328-
- bior[1.1, 1.3, 1.5, 2.2, 2.4, 2.6, 2.8, 3.1, 3.3, 3.5, 3.7, 3.9, 4.4, 5.5, 6.8]
329-
- rbio[1.1, 1.3, 1.5, 2.2, 2.4, 2.6, 2.8, 3.1, 3.3, 3.5, 3.7, 3.9, 4.4, 5.5, 6.8]
357+
- haar
358+
- dmey
359+
- sym[2-20]
360+
- db[1-20]
361+
- coif[1-5]
362+
- bior[1.1, 1.3, 1.5, 2.2, 2.4, 2.6, 2.8, 3.1, 3.3, 3.5, 3.7, 3.9, 4.4, 5.5, 6.8]
363+
- rbio[1.1, 1.3, 1.5, 2.2, 2.4, 2.6, 2.8, 3.1, 3.3, 3.5, 3.7, 3.9, 4.4, 5.5, 6.8]
330364
331-
Returned filter name reflects wavelet type:
332-
wavelet[level]-<decompositionName>
365+
Returned filter name reflects wavelet type:
366+
wavelet[level]-<decompositionName>
333367
334-
N.B. only levels greater than the first level are entered into the name.
368+
N.B. only levels greater than the first level are entered into the name.
335369
336-
:return: Yields each wavelet decomposition and final approximation, corresponding filter name and ``kwargs``
337-
"""
370+
:return: Yields each wavelet decomposition and final approximation, corresponding filter name and ``kwargs``
371+
"""
338372
global logger
339373

340374
approx, ret = _swt3(inputImage, kwargs.get('wavelet', 'coif1'), kwargs.get('level', 1), kwargs.get('start_level', 0))

0 commit comments

Comments
 (0)