Skip to content

Commit

Permalink
ENH: Implement dynamic binning
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
JoostJM committed Apr 11, 2019
1 parent 3efae04 commit af7f32b
Show file tree
Hide file tree
Showing 4 changed files with 21 additions and 1 deletion.
3 changes: 3 additions & 0 deletions docs/customization.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <radiomics_fixed_bin_width>`.
- ``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*

Expand Down
9 changes: 9 additions & 0 deletions radiomics/featureextractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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')
Expand Down
8 changes: 7 additions & 1 deletion radiomics/imageoperations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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::
Expand Down Expand Up @@ -118,13 +118,19 @@ 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]
binEdges[-1] += 1 # Ensures that the maximum value is included in the topmost bin when using numpy.digitize
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)
Expand Down
2 changes: 2 additions & 0 deletions radiomics/schemas/paramSchema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ mapping:
type: int
range:
min-ex: 0
dynamicBinning:
type: bool
normalize:
type: bool
normalizeScale:
Expand Down

0 comments on commit af7f32b

Please sign in to comment.