From af7f32b27d0ec11c91fa12ebf3d244c0600daf69 Mon Sep 17 00:00:00 2001 From: Joost van Griethuysen Date: Fri, 24 Aug 2018 09:38:29 +0200 Subject: [PATCH] ENH: Implement dynamic binning Add the possibility to enable dynamic binning, which scales the bin width by the ratio between the range of intensities seen in the derived and original images (only including intensities in the ROI). This is only done if dynamic binning is enabled (new parameter `dynamicBinning`, boolean, default false) and no custom bin width has been defined for that filter. This also has no effect if a fixed bin count is used. Addresses issue #49. --- docs/customization.rst | 3 +++ radiomics/featureextractor.py | 9 +++++++++ radiomics/imageoperations.py | 8 +++++++- radiomics/schemas/paramSchema.yaml | 2 ++ 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/docs/customization.rst b/docs/customization.rst index 2f132d0a..7c51d212 100644 --- a/docs/customization.rst +++ b/docs/customization.rst @@ -290,6 +290,9 @@ Feature Class Level - ``binCount`` [None]: integer, > 0, specifies the number of bins to create. The width of the bin is then determined by the range in the ROI. No definitive evidence is available on which method of discretization is superior, we advise a fixed bin width. See more :ref:`here `. +- ``dynamicBinning`` [False]: Boolean, if set to true, scales the bin width for derived images by the ratio of the range + in the image (ROI) and the range of the original image (ROI). This setting has no effect when a fixed bin count is + used, or when a custom bin width has been specified for the filter. See also :py:func:`getBinEdges()`. *Forced 2D extraction* diff --git a/radiomics/featureextractor.py b/radiomics/featureextractor.py index 7af568b1..a7a0cb06 100644 --- a/radiomics/featureextractor.py +++ b/radiomics/featureextractor.py @@ -468,6 +468,13 @@ def execute(self, imageFilepath, maskFilepath, label=None, label_channel=None, v if not resegmentShape and resegmentedMask is not None: mask = resegmentedMask + dynamic_ref_range = None + if self.settings.get('dynamicBinning', False): + im_arr = sitk.GetArrayFromImage(image) + ma_arr = sitk.GetArrayFromImage(mask) == self.settings.get('label', 1) + target_voxel_arr = im_arr[ma_arr] + dynamic_ref_range = max(target_voxel_arr) - min(target_voxel_arr) + # 6. Calculate other enabled feature classes using enabled image types # Make generators for all enabled image types self.logger.debug('Creating image type iterator') @@ -476,6 +483,8 @@ def execute(self, imageFilepath, maskFilepath, label=None, label_channel=None, v args = self.settings.copy() args.update(customKwargs) self.logger.info('Adding image type "%s" with custom settings: %s' % (imageType, str(customKwargs))) + if 'binWidth' not in customKwargs and imageType != 'Original': + args['dynamic_ref_range'] = dynamic_ref_range imageGenerators = chain(imageGenerators, getattr(imageoperations, 'get%sImage' % imageType)(image, mask, **args)) self.logger.debug('Extracting features') diff --git a/radiomics/imageoperations.py b/radiomics/imageoperations.py index bae6e03a..4930cce2 100644 --- a/radiomics/imageoperations.py +++ b/radiomics/imageoperations.py @@ -74,7 +74,7 @@ def getBinEdges(parameterValues, **kwargs): \mod W = 0`, the maximum intensity will be encoded as numBins + 1, therefore the maximum number of gray level intensities in the ROI after binning is number of bins + 1. - If dynamic binning is enabled (parameter `dynamicBinning`), and no custom binwidth has been defined for the filter, + If dynamic binning is enabled (parameter ``dynamicBinning``), and no custom binwidth has been defined for the filter, the actual bin width used (:math:`W_{dyn}`) is defined as: .. math:: @@ -118,6 +118,7 @@ def getBinEdges(parameterValues, **kwargs): global logger binWidth = kwargs.get('binWidth', 25) binCount = kwargs.get('binCount') + dynamic_ref_range = kwargs.get('dynamic_ref_range', None) if binCount is not None: binEdges = numpy.histogram(parameterValues, binCount)[1] @@ -125,6 +126,11 @@ def getBinEdges(parameterValues, **kwargs): else: minimum = min(parameterValues) maximum = max(parameterValues) + if dynamic_ref_range is not None and dynamic_ref_range > 0 and minimum < maximum: + range_scale = (maximum - minimum) / dynamic_ref_range + binWidth = binWidth * range_scale + logger.debug('Applied dynamic binning (reference range %g, current range %g), scaled bin width to %g', + dynamic_ref_range, maximum - minimum, binWidth) # Start binning form the first value lesser than or equal to the minimum value and evenly dividable by binwidth lowBound = minimum - (minimum % binWidth) diff --git a/radiomics/schemas/paramSchema.yaml b/radiomics/schemas/paramSchema.yaml index 943fd980..9162b7dd 100644 --- a/radiomics/schemas/paramSchema.yaml +++ b/radiomics/schemas/paramSchema.yaml @@ -39,6 +39,8 @@ mapping: type: int range: min-ex: 0 + dynamicBinning: + type: bool normalize: type: bool normalizeScale: