Skip to content

Commit

Permalink
BUG: Fix getFeatureClasses
Browse files Browse the repository at this point in the history
Commit 	7da5b76 caused the dynamic lookup of feature classes in `featureextractor`  to break.
Fix the function and move this lookup functionality to `__init__.py`, as it is used more extensively throughout the package.
  • Loading branch information
JoostJM committed Dec 23, 2016
1 parent df0dad6 commit 81638df
Show file tree
Hide file tree
Showing 7 changed files with 87 additions and 73 deletions.
4 changes: 2 additions & 2 deletions data/schemaFuncs.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import pywt
from radiomics.featureextractor import RadiomicsFeaturesExtractor
from radiomics import getFeatureClasses

featureClasses = RadiomicsFeaturesExtractor.getFeatureClasses()
featureClasses = getFeatureClasses()


def checkWavelet(value, rule_obj, path):
Expand Down
81 changes: 62 additions & 19 deletions radiomics/__init__.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,17 @@
import sys
import traceback
import pkgutil
import inspect
import os
import importlib

if sys.version_info < (2, 6, 0):
raise ImportError("pyradiomics > 0.9.7 requires python 2.6 or later")
in_py3 = sys.version_info[0] > 2

import logging

def pythonMatrixCalculation(usePython = False):
"""
By default, calculation of matrices is done in C, using extension ``_cmatrices.py``
If an error occurs during loading of this extension, a warning is logged and the extension is disabled,
matrices are then calculated in python.
Calculation in python can be forced by calling this function, specifying ``pyMatEnabled = True``
Re-enabling use of C implementation is also done by this function, but if extension is not loaded correctly,
a warning is logged and matrix calculation uses python.
"""
global cMatsEnabled
if usePython:
cMatsEnabled = False
elif _cMatLoaded:
cMatsEnabled = True
else:
logger.warning("C Matrices not loaded correctly, cannot calculate matrices in C")
cMatsEnabled = False
from . import base


def debug(debug_on=True):
Expand Down Expand Up @@ -53,6 +39,59 @@ def debug(debug_on=True):
debugging = False


def getFeatureClasses():
"""
Iterates over all modules of the radiomics package using pkgutil and subsequently imports those modules.
Return a dictionary of all modules containing featureClasses, with modulename as key, abstract
class object of the featureClass as value. Assumes only one featureClass per module
This is achieved by inspect.getmembers. Modules are added if it contains a member that is a class,
with name starting with 'Radiomics' and is inherited from :py:class:`radiomics.base.RadiomicsFeaturesBase`.
This iteration only runs once, subsequent calls return the dictionary created by the first call.
"""
global _featureClasses
if _featureClasses is not None:
return _featureClasses

featureClasses = {}
for _, mod, _ in pkgutil.iter_modules([os.path.dirname(base.__file__)]):
if str(mod).startswith('_'): # Do not load _version, _cmatrices and _cshape here
continue
__import__('radiomics.' + mod)
module = sys.modules['radiomics.' + mod]
attributes = inspect.getmembers(module, inspect.isclass)
for a in attributes:
if a[0].startswith('Radiomics'):
if base.RadiomicsFeaturesBase in inspect.getmro(a[1])[1:]:
featureClasses[mod] = a[1]
_featureClasses = featureClasses

return featureClasses


def pythonMatrixCalculation(usePython = False):
"""
By default, calculation of matrices is done in C, using extension ``_cmatrices.py``
If an error occurs during loading of this extension, a warning is logged and the extension is disabled,
matrices are then calculated in python.
Calculation in python can be forced by calling this function, specifying ``pyMatEnabled = True``
Re-enabling use of C implementation is also done by this function, but if extension is not loaded correctly,
a warning is logged and matrix calculation uses python.
"""
global cMatsEnabled
if usePython:
cMatsEnabled = False
elif _cMatLoaded:
cMatsEnabled = True
else:
logger.warning("C Matrices not loaded correctly, cannot calculate matrices in C")
cMatsEnabled = False


debugging = True
logger = logging.getLogger(__name__)
handler = logging.StreamHandler()
Expand All @@ -62,6 +101,8 @@ def debug(debug_on=True):
logger.addHandler(handler)
debug(False) # force level=WARNING, in case logging default is set differently (issue 102)

_featureClasses = None

try:
from . import _cmatrices as cMatrices
from . import _cshape as cShape
Expand All @@ -81,3 +122,5 @@ def debug(debug_on=True):

__version__ = get_versions()['version']
del get_versions

getFeatureClasses()
30 changes: 3 additions & 27 deletions radiomics/featureextractor.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
# -*- coding: utf-8 -*-
import collections
import inspect
import logging
import os
import pkgutil

import numpy
import pykwalify.core
import SimpleITK as sitk

from itertools import chain

from . import imageoperations, generalinfo
from . import imageoperations, generalinfo, getFeatureClasses


class RadiomicsFeaturesExtractor:
Expand Down Expand Up @@ -66,7 +64,7 @@ class RadiomicsFeaturesExtractor:
def __init__(self, *args, **kwargs):
self.logger = logging.getLogger(__name__)

self.featureClasses = self.getFeatureClasses()
self.featureClasses = getFeatureClasses()

self.kwargs = {}
self.provenance_on = True
Expand Down Expand Up @@ -138,7 +136,7 @@ def loadParams(self, paramsFile):
If supplied params file does not match the requirements, a pykwalify error is raised.
"""
dataDir = os.path.abspath(os.path.join(radiomics.__path__[0], '..', 'data'))
dataDir = os.path.abspath(os.path.join(self.__path__[0], '..', 'data'))
schemaFile = os.path.join(dataDir, 'paramSchema.yaml')
schemaFuncs = os.path.join(dataDir, 'schemaFuncs.py')
c = pykwalify.core.Core(source_file=paramsFile, schema_files=[schemaFile], extensions=[schemaFuncs])
Expand Down Expand Up @@ -563,28 +561,6 @@ def getInputImageTypes(self):
"""
return [member[9:] for member in dir(self) if member.startswith('generate_')]

@classmethod
def getFeatureClasses(cls):
"""
Iterates over all modules of the radiomics package using pkgutil and subsequently imports those modules.
Return a dictionary of all modules containing featureClasses, with modulename as key, abstract
class object of the featureClass as value. Assumes only one featureClass per module
This is achieved by inspect.getmembers. Modules are added if it contains a memeber that is a class,
with name starting with 'Radiomics' and is inherited from :py:class:`radiomics.base.RadiomicsFeaturesBase`.
"""
featureClasses = {}
for _, mod, _ in pkgutil.iter_modules(radiomics.__path__):
__import__('radiomics.' + mod)
attributes = inspect.getmembers(eval('radiomics.' + mod), inspect.isclass)
for a in attributes:
if a[0].startswith('Radiomics'):
if radiomics.base.RadiomicsFeaturesBase in inspect.getmro(a[1])[1:]:
featureClasses[mod] = a[1]

return featureClasses

def getFeatureClassNames(self):
return self.featureClasses.keys()

Expand Down
2 changes: 2 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import test_cmatrices, test_features, test_docstrings
import radiomics
25 changes: 9 additions & 16 deletions tests/test_cmatrices.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from nose_parameterized import parameterized
from radiomics import glcm, glrlm, glszm

from .testUtils import RadiomicsTestUtils
from .testUtils import RadiomicsTestUtils, custom_name_func


testUtils = RadiomicsTestUtils()
Expand All @@ -20,13 +20,6 @@
testCases = defaultTestCases
features = ["glcm", "glrlm", "glszm"] # defaultFeatures

# set testing arguments
kwargs = {}
kwargs['binWidth'] = 25
kwargs['symmetricalGLCM'] = False # Current baseline is based upon assymetrical GLCM
kwargs['verbose'] = False

testUtils.setResampling(resampledPixelSpacing= None,interpolator= sitk.sitkBSpline) # resampledPixelSpacing= [3, 3, 3]

class TestFeatures:

Expand All @@ -36,19 +29,19 @@ def generate_scenarios():

for testCase in testCases:
for feature in features:
assert(feature != None)
assert(feature is not None)
logging.debug('generate_scenarios: featureClass = %s', feature)
yield testCase, feature

global testUtils
@parameterized.expand(generate_scenarios(), testcase_func_name=testUtils.custom_name_func)
@parameterized.expand(generate_scenarios(), testcase_func_name=custom_name_func)
def test_scenario(self, testCase, featureClassName):
print("")
global testUtils

logging.debug('test_scenario: testCase = %s, featureClassName = %s', testCase, featureClassName)

testUtils.setTestCase(testCase)
testUtils.setFeatureClassAndTestCase(featureClassName, testCase)

testImage = testUtils.getImage()
testMask = testUtils.getMask()
Expand All @@ -61,25 +54,25 @@ def test_scenario(self, testCase, featureClassName):
logging.debug('No C implementation in firstorder, skipping test')
elif featureClassName == 'glcm':
logging.debug('Init GLCM')
featureClass = glcm.RadiomicsGLCM(testImage, testMask, **kwargs)
featureClass = glcm.RadiomicsGLCM(testImage, testMask, **testUtils.getKwargs())
cMat = featureClass.P_glcm
pyMat = featureClass._calculateGLCM()
elif featureClassName == 'glrlm':
logging.debug('Init GLRLM')
featureClass = glrlm.RadiomicsGLRLM(testImage, testMask, **kwargs)
featureClass = glrlm.RadiomicsGLRLM(testImage, testMask, **testUtils.getKwargs())
cMat = featureClass.P_glrlm
pyMat = featureClass._calculateGLRLM()
elif featureClassName == 'shape':
logging.debug('No C implementation in glrlm, skipping test')
# No C implementation yet, will follow
# logging.debug('Init Shape')
# featureClass = shape.RadiomicsShape(testImage, testMask, **kwargs)
# featureClass = shape.RadiomicsShape(testImage, testMask, **testUtils.getKwargs())
elif featureClassName == 'glszm':
logging.debug('Init GLSZM')
featureClass = glszm.RadiomicsGLSZM(testImage, testMask, **kwargs)
featureClass = glszm.RadiomicsGLSZM(testImage, testMask, **testUtils.getKwargs())
cMat = featureClass.P_glszm
pyMat = featureClass._calculateGLSZM()

assert (featureClass != None)
assert (featureClass is not None)
# Check if the calculated arrays match
assert numpy.max(numpy.abs(pyMat - cMat)) < 1e-3
14 changes: 7 additions & 7 deletions tests/test_docstrings.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,37 @@
import logging

from nose_parameterized import parameterized
from radiomics.featureextractor import RadiomicsFeaturesExtractor
from radiomics import getFeatureClasses
from .testUtils import custom_name_func

featureClasses = RadiomicsFeaturesExtractor.getFeatureClasses()
featureClasses = getFeatureClasses()

def setup_module(module):
# runs before anything in this file
print ("") # this is to get a newline after the dots
print ("") # this is to get a newline after the dots
return

class TestDocStrings:
def setup(self):
# setup before each test method
print ("") # this is to get a newline after the dots
print ("") # this is to get a newline after the dots

@classmethod
def setup_class(self):
# called before any methods in this class
print ("") # this is to get a newline after the dots
print ("") # this is to get a newline after the dots

@classmethod
def teardown_class(self):
# run after any methods in this class
print ("") # this is to get a newline after the dots
print ("") # this is to get a newline after the dots

def generate_scenarios():
global featureClasses
for featureClassName, featureClass in featureClasses.iteritems():
logging.info('generate_scenarios %s', featureClassName)
doc = featureClass.__doc__
assert(doc != None)
assert(doc is not None)

featureNames = featureClass.getFeatureNames()
for f in featureNames:
Expand Down
4 changes: 2 additions & 2 deletions tests/test_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def generate_scenarios():
for testCase in testCases:
for featureClassName in featureClassNames:
featureNames = extractor.featureClasses[featureClassName].getFeatureNames()
assert (featureNames != None)
assert (featureNames is not None)
assert (len(featureNames) > 0)
logging.debug('generate_scenarios: featureNames = %s', featureNames)
for featureName in featureNames:
Expand All @@ -54,7 +54,7 @@ def test_scenario(self, testCase, featureClassName, featureName):
logging.debug('Init %s' % (featureClassName))
featureClass = extractor.featureClasses[featureClassName](testImage, testMask, **testUtils.getKwargs())

assert (featureClass != None)
assert (featureClass is not None)

featureClass.disableAllFeatures()
featureClass.enableFeatureByName(featureName)
Expand Down

0 comments on commit 81638df

Please sign in to comment.