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

C matrices #158

Closed
wants to merge 31 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
9b84a6f
C matrix calculation for:
JoostJM Nov 14, 2016
2e5642b
ENH: Implement C calculation in featureClasses
JoostJM Nov 14, 2016
8184194
ENH: Implement C calculation in glszm
JoostJM Nov 14, 2016
12581ce
ENH: Remove unsused import statements
JoostJM Nov 14, 2016
8df1819
BUG: Numpy needed in setup
JoostJM Nov 14, 2016
1039ca7
BUG: Change C GLSZM calculation
JoostJM Nov 15, 2016
1926009
ENH: Improve search strategy in C
JoostJM Nov 15, 2016
3f05ccb
ENH: Add testing for matrix equality
JoostJM Nov 16, 2016
ae87b6d
ENH: Make C code consistent with C89 convention
JoostJM Nov 17, 2016
27454ae
ENH: Add C implementation of GLRLM
JoostJM Nov 21, 2016
38df7c7
ENH: Make use of C implementation optional
JoostJM Nov 22, 2016
854cad2
ENH: Implement surface area in C
JoostJM Nov 22, 2016
a0ecb0b
STYL: Add docstrings to _cmatrices and _cshape
JoostJM Nov 22, 2016
224281d
BUG: Try import _cmatrices after initialization of logger
JoostJM Nov 23, 2016
73bd5d6
STYL: setup.py: Fix style to be PEP8 compliant and improve readability
jcfr Nov 23, 2016
c5540f5
STYL: setup.py: Add keywords and classifiers
jcfr Nov 23, 2016
98455d8
STYL: setup.py: Introduce requirements.txt and requirements-dev.txt
jcfr Nov 23, 2016
405e42d
STYL: ci: Simplify circle.yml installing dependencies using requireme…
jcfr Nov 23, 2016
24c982c
setup: use scikit-build to building of cmatrices and cshape extensions
jcfr Nov 23, 2016
9db151e
ENH: Load C for matrices and shape during radiomics.__init__
JoostJM Nov 24, 2016
2bd874f
STYL: Update .gitignore
JoostJM Nov 25, 2016
d94dd00
ENH: Add C implementation for GLDZM calculation
JoostJM Nov 22, 2016
5cdaf7b
ENH: Add C support to GLDZM featureclass
JoostJM Nov 28, 2016
c3a38ae
BUG: Fix bug in GLSZM calculation in C
JoostJM Nov 30, 2016
a8830c7
STYL: Convert "tests" into a package to allow relative import of test…
jcfr Dec 4, 2016
950094b
STYL: tests: Fix order of package imports and remove unused ones
jcfr Dec 4, 2016
68578be
BUG: Import "cMatrices" and "cShape" C extensions from radiomics package
jcfr Dec 4, 2016
7da5b76
STYL: radiomics: More package imports order fixes, use of relative im…
jcfr Dec 4, 2016
f5a5e6d
ENH: Add support for "python setup.py test"
jcfr Dec 4, 2016
df7c8d8
DEL: Remove experimental/unstable feature classes
JoostJM Dec 21, 2016
0fc72a0
BUG: Fix getFeatureClasses
JoostJM Dec 22, 2016
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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ __pycache__/

# Distribution / packaging
.Python
_skbuild/
env/
build/
develop-eggs/
Expand Down Expand Up @@ -59,3 +60,7 @@ docs/_build/

# PyBuilder
target/

# scikit-build
_skbuild
MANIFEST
10 changes: 10 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
cmake_minimum_required(VERSION 3.7)

project(radiomics)

find_package(PythonInterp REQUIRED)
find_package(PythonLibs REQUIRED)
find_package(PythonExtensions REQUIRED)

add_subdirectory(radiomics/src)

12 changes: 5 additions & 7 deletions circle.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
dependencies:
pre:
- pip install numpy==1.11.0
- pip install --trusted-host www.simpleitk.org -f http://www.simpleitk.org/SimpleITK/resources/software.html SimpleITK==0.9.1
- pip install nose-parameterized==0.5.0
- pip install tqdm==4.7.1
- pip install PyWavelets==0.4.0
override:
- pip install -r requirements.txt
- pip install -U cmake scikit-build
test:
override:
- nosetests --with-xunit --logging-level=DEBUG --verbosity=3 tests/test_features.py
- pip install .
- python setup.py test --args="--with-xunit --logging-level=DEBUG"
- cp nosetests.xml $CIRCLE_TEST_REPORTS
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
76 changes: 76 additions & 0 deletions radiomics/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
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

from . import base


def debug(debug_on=True):
"""
Expand All @@ -32,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 @@ -41,10 +101,26 @@ 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
cMatsEnabled = True
_cMatLoaded = True
except Exception:
logger.warning("Error loading C Matrices, switching to python calculation\n%s", traceback.format_exc())
cMatrices = None
cShape = None
cMatsEnabled = False
_cMatLoaded = False

# For convenience, import the most used packages into the "pyradiomics" namespace
import collections, numpy

from ._version import get_versions

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

getFeatureClasses()
6 changes: 3 additions & 3 deletions radiomics/base.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import inspect
import logging
import traceback
import SimpleITK as sitk

import numpy
import inspect
from radiomics import imageoperations
import SimpleITK as sitk


class RadiomicsFeaturesBase(object):
Expand Down
42 changes: 10 additions & 32 deletions radiomics/featureextractor.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
# -*- coding: utf-8 -*-
import os
import logging
import collections
from itertools import chain
import logging
import os

import numpy
import SimpleITK as sitk
import pkgutil
import inspect
import pykwalify.core
import radiomics
from radiomics import base, imageoperations, generalinfo
import SimpleITK as sitk

from itertools import chain

from . import imageoperations, generalinfo, getFeatureClasses


class RadiomicsFeaturesExtractor:
Expand Down Expand Up @@ -64,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 @@ -136,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 @@ -561,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
5 changes: 2 additions & 3 deletions radiomics/firstorder.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import numpy
import collections
from radiomics import base, imageoperations
import SimpleITK as sitk

from . import base, imageoperations


class RadiomicsFirstOrder(base.RadiomicsFeaturesBase):
Expand Down
7 changes: 4 additions & 3 deletions radiomics/generalinfo.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import collections
import logging
import numpy

import SimpleITK as sitk
import radiomics

from . import __version__


class GeneralInfo():
Expand Down Expand Up @@ -115,7 +116,7 @@ def getVersionValue(self):
"""
Return the current version of this package.
"""
return radiomics.__version__
return __version__

def getVolumeNumValue(self):
"""
Expand Down
66 changes: 53 additions & 13 deletions radiomics/glcm.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import numpy
import collections
from radiomics import base, imageoperations
import SimpleITK as sitk

from tqdm import trange

from . import base, cMatrices, cMatsEnabled, imageoperations

class RadiomicsGLCM(base.RadiomicsFeaturesBase):
r"""
Expand Down Expand Up @@ -116,13 +115,16 @@ def __init__(self, inputImage, inputMask, **kwargs):
self.weightingNorm = kwargs.get('weightingNorm', None) # manhattan, euclidean, infinity

self.coefficients = {}
self.P_glcm = {}
self.P_glcm = None

# binning
self.matrix, self.histogram = imageoperations.binImage(self.binWidth, self.matrix, self.matrixCoordinates)
self.coefficients['Ng'] = self.histogram[1].shape[0] - 1

self._calculateGLCM()
if cMatsEnabled:
self.P_glcm = self._calculateCGLCM()
else:
self.P_glcm = self._calculateGLCM()
self._calculateCoefficients()

def _calculateGLCM(self):
Expand All @@ -140,7 +142,7 @@ def _calculateGLCM(self):
size = numpy.max(self.matrixCoordinates, 1) - numpy.min(self.matrixCoordinates, 1) + 1
angles = imageoperations.generateAngles(size)

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

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

Expand All @@ -165,12 +167,12 @@ def _calculateGLCM(self):
# 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))
self.P_glcm[i - 1, j - 1, a_idx] = count
P_glcm[i - 1, j - 1, a_idx] = count
if self.verbose: bar.close()

# Optionally make GLCMs symmetrical for each angle
if self.symmetricalGLCM:
self.P_glcm += numpy.transpose(self.P_glcm, (1, 0, 2))
P_glcm += numpy.transpose(P_glcm, (1, 0, 2))

# Optionally apply a weighting factor
if not self.weightingNorm is None:
Expand All @@ -189,17 +191,55 @@ def _calculateGLCM(self):
self.logger.warning('weigthing norm "%s" is unknown, W is set to 1', self.weightingNorm)
weights[a_idx] = 1

self.P_glcm = numpy.sum(self.P_glcm * weights[None, None, :], 2, keepdims=True)
P_glcm = numpy.sum(P_glcm * weights[None, None, :], 2, keepdims=True)

sumGlcm = numpy.sum(P_glcm, (0, 1), keepdims=True) # , keepdims=True)

# Delete empty angles if no weighting is applied
if P_glcm.shape[2] > 1:
P_glcm = numpy.delete(P_glcm, numpy.where(sumGlcm == 0), 2)
sumGlcm = numpy.delete(sumGlcm, numpy.where(sumGlcm == 0), 0)

# Normalize each glcm
return P_glcm/sumGlcm

def _calculateCGLCM(self):
size = numpy.max(self.matrixCoordinates, 1) - numpy.min(self.matrixCoordinates, 1) + 1
angles = imageoperations.generateAngles(size)
Ng = self.coefficients['Ng']

P_glcm = cMatrices.calculate_glcm(self.matrix, self.maskArray, angles, Ng)

# Optionally make GLCMs symmetrical for each angle
if self.symmetricalGLCM:
P_glcm += numpy.transpose(P_glcm, (1, 0, 2))

# Optionally apply a weighting factor
if not self.weightingNorm is None:
pixelSpacing = self.inputImage.GetSpacing()[::-1]
weights = numpy.empty(len(angles))
for a_idx, a in enumerate(angles):
if self.weightingNorm == 'infinity':
weights[a_idx] = numpy.exp(-max(numpy.abs(a) * pixelSpacing) ** 2)
elif self.weightingNorm == 'euclidean':
weights[a_idx] = numpy.exp(-numpy.sum((numpy.abs(a) * pixelSpacing) ** 2)) # sqrt ^ 2 = 1
elif self.weightingNorm == 'manhattan':
weights[a_idx] = numpy.exp(-numpy.sum(numpy.abs(a) * pixelSpacing) ** 2)
else:
self.logger.warning('weigthing norm "%s" is unknown, W is set to 1', self.weightingNorm)
weights[a_idx] = 1

P_glcm = numpy.sum(P_glcm * weights[None, None, :], 2, keepdims=True)

sumGlcm = numpy.sum(self.P_glcm, (0, 1), keepdims=True) # , keepdims=True)
sumGlcm = numpy.sum(P_glcm, (0, 1), keepdims=True) # , keepdims=True)

# Delete empty angles if no weighting is applied
if self.P_glcm.shape[2] > 1:
self.P_glcm = numpy.delete(self.P_glcm, numpy.where(sumGlcm == 0), 2)
if P_glcm.shape[2] > 1:
P_glcm = numpy.delete(P_glcm, numpy.where(sumGlcm == 0), 2)
sumGlcm = numpy.delete(sumGlcm, numpy.where(sumGlcm == 0), 0)

# Normalize each glcm
self.P_glcm = self.P_glcm / sumGlcm
return P_glcm / sumGlcm

# check if ivector and jvector can be replaced
def _calculateCoefficients(self):
Expand Down
Loading