Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH: Implement dynamic binning #414

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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