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

Remove dependency for tqdm #232

Merged
merged 3 commits into from
Apr 5, 2017
Merged
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
688 changes: 600 additions & 88 deletions bin/Notebooks/helloRadiomics.ipynb

Large diffs are not rendered by default.

62 changes: 62 additions & 0 deletions bin/helloRadiomics.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,62 @@
import radiomics
from radiomics import featureextractor

def tqdmProgressbar():
"""
This function will setup the progress bar exposed by the 'tqdm' package.
Progress reporting is only used in PyRadiomics for the calculation of GLCM and GLSZM in full python mode, therefore
enable GLCM and full-python mode to show the progress bar functionality

N.B. This function will only work if the 'click' package is installed (not included in the PyRadiomics requirements)
"""
global extractor
extractor.kwargs['enableCExtensions'] = False
# Enable the GLCM class to show the progress bar
extractor.enableFeatureClassByName('glcm')

radiomics.setVerbosity(logging.INFO) # Verbosity must be at least INFO to enable progress bar

import tqdm
radiomics.progressReporter = tqdm.tqdm

def clickProgressbar():
"""
This function will setup the progress bar exposed by the 'click' package.
Progress reporting is only used in PyRadiomics for the calculation of GLCM and GLSZM in full python mode, therefore
enable GLCM and full-python mode to show the progress bar functionality.

Because the signature used to instantiate a click progress bar is different from what PyRadiomics expects, we need to
write a simple wrapper class to enable use of a click progress bar. In this case we only need to change the 'desc'
keyword argument to a 'label' keyword argument.

N.B. This function will only work if the 'click' package is installed (not included in the PyRadiomics requirements)
"""
global extractor

extractor.kwargs['enableCExtensions'] = False
# Enable the GLCM class to show the progress bar
extractor.enableFeatureClassByName('glcm')

radiomics.setVerbosity(logging.INFO) # Verbosity must be at least INFO to enable progress bar

import click

class progressWrapper():
def __init__(self, iterable, desc=''):
# For a click progressbar, the description must be provided in the 'label' keyword argument.
self.bar = click.progressbar(iterable, label=desc)

def __iter__(self):
return self.bar.__iter__() # Redirect to the __iter__ function of the click progressbar

def __enter__(self):
return self.bar.__enter__() # Redirect to the __enter__ function of the click progressbar

def __exit__(self, exc_type, exc_value, tb):
return self.bar.__exit__(exc_type, exc_value, tb) # Redirect to the __exit__ function of the click progressbar

radiomics.progressReporter = progressWrapper


testCase = 'brain1'
dataDir = os.path.join(os.path.abspath(""), "..", "data")
Expand Down Expand Up @@ -61,6 +117,12 @@
# Only enable mean and skewness in firstorder
extractor.enableFeaturesByName(firstorder=['Mean', 'Skewness'])

# Uncomment on of these functions to show how PyRadiomics can use the 'tqdm' or 'click' package to report progress when
# running in full python mode. Assumes the respective package is installed (not included in the requirements)

# tqdmProgressbar()
# clickProgressbar()

print("Active features:")
for cls, features in six.iteritems(extractor.enabledFeatures):
if len(features) == 0:
Expand Down
56 changes: 56 additions & 0 deletions docs/developers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,62 @@ returned by multiple yield statements, or yield statements inside a loop. Please
be returned on each call to yield and that ``inputImageName`` is a unique name for each returned derived image. Derived
images must have the same dimensions and occupy the same physical space to ensure compatibility with the mask.

------------------
Progress Reporting
------------------

When operating in full-python mode, the calculation of the texture matrices can take some time. Therefor PyRadiomics
provides the possibility to report the progress for calculation of GLCM and GLSZM.
This is only enabled in full-python mode when the verbosity (:py:func:`~radiomics.setVerbosity()`) is set to INFO or
DEBUG. By default, none is provided and no progress of matrix calculation will be reported.

To enable progress reporting, the ``radiomics.progressReporter`` variable should be set to a class object (NOT an
instance), which fits the following signature:

1. Accepts an iterable as the first positional argument and a keyword argument ('desc') specifying a label to display
2. Can be used in a 'with' statement (i.e. exposes a ``__enter__`` and ``__exit__`` function)
3. Is iterable (i.e. at least specifies an ``__iter__`` function, which iterates over the iterable passed at
initialization)

It is also possible to create your own progress reporter. To achieve this, additionally specify a function ``__next__``,
and have the ``__iter__`` function return ``self``. The ``__next__`` function takes no arguments and returns a call to
the ``__next__`` function of the iterable (i.e. ``return self.iterable.__next__()``). Any prints/progress reporting
calls can then be inserted in this function prior to the return statement.

In ``radiomics\__init__.py`` a dummy progress reporter (``_DummyProgressReporter``) is defined, which is used when
calculating in full-python mode, but progress reporting is not enabled (verbosity > INFO) or the ``progressReporter``
variable is not set.

To design a custom progress reporter, the following code can be adapted and used as progressReporter::

class MyProgressReporter(object):
def __init__(self, iterable, desc=''):
self.desc = desc # A description is which describes the progress that is reported
self.iterable = iterable # Iterable is required

# This function identifies the class as iterable and should return an object which exposes
# the __next__ function that should be used to iterate over the object
def __iter__(self):
return self # return self to 'intercept' the calls to __next__ to insert reporting code.

def __next__(self):
nextElement = self.iterable.__next__()
# Insert custom progress reporting code here. This is called for every iteration in the loop
# (once for each unique gray level in the ROI for GLCM and GLSZM)

# By inserting after the call `self.iterable.__next__()` the function will exit before the
# custom code is run when the stopIteration error is raised.
return nextElement

# This function is called when the 'with' statement is entered
def __enter__(self):
print (self.desc) # Print out the description upon start of the loop
return self # The __enter__ function should return itself

# This function is called when the 'with' statement is exited
def __exit__(self, exc_type, exc_value, tb):
pass # If nothing needs to be closed or handled, so just specify 'pass'

------------------------------
Addtional points for attention
------------------------------
Expand Down
52 changes: 52 additions & 0 deletions radiomics/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,58 @@ def getInputImageTypes():
return _inputImages


class _DummyProgressReporter(object):
"""
This class represents the dummy Progress reporter and is used for where progress reporting is implemented, but not
enabled (when the progressReporter is not set or verbosity level > INFO).

PyRadiomics expects that the _getProgressReporter function returns an object that takes an iterable and 'desc' keyword
argument at initialization. Furthermore, it should be iterable, where it iterates over the iterable passed at
initialization and it should be used in a 'with' statement.

In this class, the __iter__ function redirects to the __iter__ function of the iterable passed at initialization.
The __enter__ and __exit__ functions enable usage in a 'with' statement
"""
def __init__(self, iterable, desc=''):
self.desc = desc # A description is not required, but is provided by PyRadiomics
self.iterable = iterable # Iterable is required

def __iter__(self):
return self.iterable.__iter__() # Just iterate over the iterable passed at initialization

def __enter__(self):
return self # The __enter__ function should return itself

def __exit__(self, exc_type, exc_value, tb):
pass # Nothing needs to be closed or handled, so just specify 'pass'


def _getProgressReporter(*args, **kwargs):
"""
This function returns an instance of the progressReporter, or, if it is not set (None), returns a dummy progress
reporter.

To enable progress reporting, the progressReporter variable should be set to a class object (NOT an instance), which
fits the following signature:

1. Accepts an iterable as the first positional argument and a keyword argument ('desc') specifying a label to display
2. Can be used in a 'with' statement (i.e. exposes a __enter__ and __exit__ function)
3. Is iterable (i.e. at least specifies an __iter__ function, which iterates over the iterable passed at
initialization).

It is also possible to create your own progress reporter. To achieve this, additionally specify a function `__next__`,
and have the `__iter__` function return `self`. The `__next__` function takes no arguments and returns a call to the
`__next__` function of the iterable (i.e. `return self.iterable.__next__()`). Any prints/progress reporting calls can
then be inserted in this function prior to the return statement.
"""
global progressReporter
if progressReporter is None:
return _DummyProgressReporter(*args, **kwargs)
else:
return progressReporter(*args, **kwargs)

progressReporter = None

debugging = True
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO) # Set default level of logger to INFO to reflect most common setting for a log file
Expand Down
6 changes: 5 additions & 1 deletion radiomics/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,11 @@ class RadiomicsFeaturesBase(object):
def __init__(self, inputImage, inputMask, **kwargs):
self.logger = logging.getLogger(self.__module__)
self.logger.debug('Initializing feature class')
self.verbose = radiomics.handler.level <= logging.INFO # check if the handler to stderr is set to INFO or lower
# check if the handler to stderr is set and its level is INFO or lower
if logging.NOTSET < radiomics.handler.level <= logging.INFO:
self.progressReporter = radiomics._getProgressReporter
else:
self.progressReporter = radiomics._DummyProgressReporter

self.kwargs = kwargs
self.binWidth = kwargs.get('binWidth', 25)
Expand Down
48 changes: 22 additions & 26 deletions radiomics/glcm.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import numpy
from six.moves import range
from tqdm import trange

from radiomics import base, cMatrices, cMatsEnabled, imageoperations

Expand Down Expand Up @@ -151,31 +150,28 @@ def _calculateMatrix(self):

P_glcm = numpy.zeros((Ng, Ng, int(angles.shape[0])), dtype='float64')

if self.verbose: bar = trange(Ng, desc='calculate GLCM')

# iterate over gray levels for center voxel
for i in range(1, Ng + 1):
# give some progress
if self.verbose: bar.update()

# get the indices to all voxels which have the current gray level i
i_indices = numpy.where(self.matrix == i)

# iterate over gray levels for neighbouring voxel
for j in range(1, Ng + 1):
# get the indices to all voxels which have the current gray level j
j_indices = set(zip(*numpy.where(self.matrix == j)))

for a_idx, a in enumerate(angles):
# get the corresponding indices of the neighbours for angle a
neighbour_indices = set(zip(*(i_indices + a[:, None])))

# The following intersection yields the indices to voxels with gray level j
# that are also a neighbour of a voxel with gray level i for angle a.
# The number of indices is then equal to the total number of pairs with gray level i and j for angle a
count = len(neighbour_indices.intersection(j_indices))
P_glcm[i - 1, j - 1, a_idx] = count
if self.verbose: bar.close()
# If verbosity > INFO, or no progress reporter is set in radiomics.progressReporter, _dummyProgressReporter is used,
# which just iterates over the iterator without reporting progress
with self.progressReporter(range(1, Ng + 1), desc='calculate GLCM') as bar:
# iterate over gray levels for center voxel
for i in bar:
# get the indices to all voxels which have the current gray level i
i_indices = numpy.where(self.matrix == i)

# iterate over gray levels for neighbouring voxel
for j in range(1, Ng + 1):
# get the indices to all voxels which have the current gray level j
j_indices = set(zip(*numpy.where(self.matrix == j)))

for a_idx, a in enumerate(angles):
# get the corresponding indices of the neighbours for angle a
neighbour_indices = set(zip(*(i_indices + a[:, None])))

# The following intersection yields the indices to voxels with gray level j
# that are also a neighbour of a voxel with gray level i for angle a.
# The number of indices is then equal to the total number of pairs with gray level i and j for angle a
count = len(neighbour_indices.intersection(j_indices))
P_glcm[i - 1, j - 1, a_idx] = count

P_glcm = self._applyMatrixOptions(P_glcm, angles)

Expand Down
69 changes: 32 additions & 37 deletions radiomics/glszm.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import numpy
from six.moves import range
from tqdm import trange

from radiomics import base, cMatrices, cMatsEnabled, imageoperations

Expand Down Expand Up @@ -83,56 +82,52 @@ def _calculateMatrix(self):
"""
self.logger.debug('Calculating GLSZM matrix in Python')

Ng = self.coefficients['Ng']
Np = self.coefficients['Np']
size = numpy.max(self.matrixCoordinates, 1) - numpy.min(self.matrixCoordinates, 1) + 1
angles = imageoperations.generateAngles(size, **self.kwargs)

# Empty GLSZ matrix
P_glszm = numpy.zeros((self.coefficients['Ng'], self.coefficients['Np']))

# Iterate over all gray levels in the image
numGrayLevels = self.coefficients['Ng'] + 1

if self.verbose: bar = trange(numGrayLevels - 1, desc='calculate GLSZM')

for i in range(1, numGrayLevels):
# give some progress
if self.verbose: bar.update()

ind = zip(*numpy.where(self.matrix == i))
ind = list(set(ind).intersection(set(zip(*self.matrixCoordinates))))
P_glszm = numpy.zeros((Ng, Np))

while ind: # check if ind is not empty: unprocessed regions for current gray level
# Pop first coordinate of an unprocessed zone, start new stack
ind_region = [ind.pop()]
# If verbosity > INFO, or no progress reporter is set in radiomics.progressReporter, _dummyProgressReporter is used,
# which just iterates over the iterator without reporting progress
with self.progressReporter(range(1, Ng + 1), desc='calculate GLSZM') as bar:
# Iterate over all gray levels in the image
for i in bar:
ind = zip(*numpy.where(self.matrix == i))
ind = list(set(ind).intersection(set(zip(*self.matrixCoordinates))))

# Define regionSize
regionSize = 0
while ind: # check if ind is not empty: unprocessed regions for current gray level
# Pop first coordinate of an unprocessed zone, start new stack
ind_region = [ind.pop()]

# Grow zone for item popped from stack of region indices, loop until stack of region indices is exhausted
# Each loop represents one voxel belonging to current zone. Therefore, count number of loops as regionSize
while ind_region:
regionSize += 1
# Define regionSize
regionSize = 0

# Use pop to remove next node for set of unprocessed region indices
ind_node = ind_region.pop()
# Grow zone for item popped from stack of region indices, loop until stack of region indices is exhausted
# Each loop represents one voxel belonging to current zone. Therefore, count number of loops as regionSize
while ind_region:
regionSize += 1

# get all coordinates in the 26-connected region, 2 voxels per angle
region_full = [tuple(sum(a) for a in zip(ind_node, angle_i)) for angle_i in angles]
region_full += [tuple(sum(a) for a in zip(ind_node, angle_i)) for angle_i in angles * -1]
# Use pop to remove next node for set of unprocessed region indices
ind_node = ind_region.pop()

# get all unprocessed coordinates in the 26-connected region with same gray level
region_level = list(set(ind).intersection(set(region_full)))
# get all coordinates in the 26-connected region, 2 voxels per angle
region_full = [tuple(sum(a) for a in zip(ind_node, angle_i)) for angle_i in angles]
region_full += [tuple(sum(a) for a in zip(ind_node, angle_i)) for angle_i in angles * -1]

# Remove already processed indices to prevent reprocessing
ind = list(set(ind) - set(region_level))
# get all unprocessed coordinates in the 26-connected region with same gray level
region_level = list(set(ind).intersection(set(region_full)))

# Add all found neighbours to the total stack of unprocessed neighbours
ind_region.extend(region_level)
# Remove already processed indices to prevent reprocessing
ind = list(set(ind) - set(region_level))

# Update the gray level size zone matrix
P_glszm[i - 1, regionSize - 1] += 1
# Add all found neighbours to the total stack of unprocessed neighbours
ind_region.extend(region_level)

if self.verbose: bar.close()
# Update the gray level size zone matrix
P_glszm[i - 1, regionSize - 1] += 1

# Crop gray-level axis of GLSZM matrix to between minimum and maximum observed gray-levels
# Crop size-zone area axis of GLSZM matrix up to maximum observed size-zone area
Expand Down
1 change: 0 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ numpy>=1.9.2
SimpleITK>=0.9.1
nose>=1.3.7
nose-parameterized>=0.5.0
tqdm>=4.7.1
PyWavelets>=0.4.0
pykwalify>=1.6.0
six>=1.10.0
Expand Down