Skip to content

Commit fc84120

Browse files
JoostJMjcfr
authored andcommitted
ENH: Implement extension in C for calculation of texture matrices.
Affects GLCM, GLRLM and GLSZM calculation. On initialization, extension is loaded and enabled by default. If an error occurs, a warning is logged and matrices will be calculated in full-python mode. Alternatively, full-python mode can be forced by a call to `radiomics.enableCExtensions(False)`. Added `requirements-setup.txt` to specify dependencies needed during setup (requires numpy and cython)
1 parent cc78841 commit fc84120

File tree

9 files changed

+903
-26
lines changed

9 files changed

+903
-26
lines changed

radiomics/__init__.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
# For convenience, import collection and numpy
32
# into the "pyradiomics" namespace
43

@@ -8,6 +7,7 @@
87
import os
98
import pkgutil
109
import sys
10+
import traceback
1111

1212
import numpy # noqa: F401
1313

@@ -42,6 +42,34 @@ def debug(debug_on=True):
4242
debugging = False
4343

4444

45+
def enableCExtensions(enabled=True):
46+
"""
47+
By default, calculation of GLCM, GLRLM and GLSZM is done in C, using extension ``_cmatrices.py``
48+
49+
If an error occurs during loading of this extension, a warning is logged and the extension is disabled,
50+
matrices are then calculated in python.
51+
The C extension can be disabled by calling this function as ``enableCExtensions(False)``, which forces the calculation
52+
of the matrices to full-python mode.
53+
54+
Re-enabling use of C implementation is also done by this function, but if the extension is not loaded correctly,
55+
a warning is logged and matrix calculation is forced to full-python mode.
56+
"""
57+
global _cMatsState, logger
58+
if enabled:
59+
if _cMatsState == 1: # Extension loaded but not enabled
60+
logger.info("Enabling C extensions")
61+
_cMatsState = 2 # Enables matrix calculation in C
62+
else: # _cMatsState = 0; Extension not loaded correctly, do not enable matrix calculation in C and log warning
63+
logger.warning("C Matrices not loaded correctly, cannot calculate matrices in C")
64+
elif _cMatsState == 2:
65+
logger.info("Disabling C extensions")
66+
_cMatsState = 1
67+
68+
69+
def cMatsEnabled():
70+
return _cMatsState == 2
71+
72+
4573
def getFeatureClasses():
4674
"""
4775
Iterates over all modules of the radiomics package using pkgutil and subsequently imports those modules.
@@ -102,6 +130,18 @@ def getInputImageTypes():
102130
_featureClasses = None
103131
_inputImages = None
104132

133+
# Indicates status of C extensions: 0 = not loaded, 1 = loaded but not enabled, 2 = enabled
134+
_cMatsState = 0
135+
136+
try:
137+
logger.debug("Loading C extensions")
138+
from radiomics import _cmatrices as cMatrices
139+
_cMatsState = 1
140+
enableCExtensions()
141+
except Exception:
142+
logger.warning("Error loading C extensions, switching to python calculation:\n%s", traceback.format_exc())
143+
cMatrices = None # set cMatrices to None to prevent an import error in the feature classes.
144+
105145
from ._version import get_versions
106146

107147
__version__ = get_versions()['version']

radiomics/glcm.py

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from six.moves import range
33
from tqdm import trange
44

5-
from radiomics import base, imageoperations
5+
from radiomics import base, cMatrices, cMatsEnabled, imageoperations
66

77

88
class RadiomicsGLCM(base.RadiomicsFeaturesBase):
@@ -124,10 +124,14 @@ def __init__(self, inputImage, inputMask, **kwargs):
124124
self.matrix, self.histogram = imageoperations.binImage(self.binWidth, self.matrix, self.matrixCoordinates)
125125
self.coefficients['Ng'] = self.histogram[1].shape[0] - 1
126126

127-
self._calculateGLCM()
127+
if cMatsEnabled():
128+
self.P_glcm = self._calculateCMatrix()
129+
else:
130+
self.P_glcm = self._calculateMatrix()
131+
128132
self._calculateCoefficients()
129133

130-
def _calculateGLCM(self):
134+
def _calculateMatrix(self):
131135
r"""
132136
Compute GLCMs for the input image for every direction in 3D.
133137
Calculated GLCMs are placed in array P_glcm with shape (i/j, a)
@@ -142,7 +146,7 @@ def _calculateGLCM(self):
142146
size = numpy.max(self.matrixCoordinates, 1) - numpy.min(self.matrixCoordinates, 1) + 1
143147
angles = imageoperations.generateAngles(size)
144148

145-
self.P_glcm = numpy.zeros((Ng, Ng, int(angles.shape[0])), dtype='float64')
149+
P_glcm = numpy.zeros((Ng, Ng, int(angles.shape[0])), dtype='float64')
146150

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

@@ -167,12 +171,32 @@ def _calculateGLCM(self):
167171
# that are also a neighbour of a voxel with gray level i for angle a.
168172
# The number of indices is then equal to the total number of pairs with gray level i and j for angle a
169173
count = len(neighbour_indices.intersection(j_indices))
170-
self.P_glcm[i - 1, j - 1, a_idx] = count
174+
P_glcm[i - 1, j - 1, a_idx] = count
171175
if self.verbose: bar.close()
172176

177+
P_glcm = self._applyMatrixOptions(P_glcm, angles)
178+
179+
return P_glcm
180+
181+
def _calculateCMatrix(self):
182+
size = numpy.max(self.matrixCoordinates, 1) - numpy.min(self.matrixCoordinates, 1) + 1
183+
angles = imageoperations.generateAngles(size)
184+
Ng = self.coefficients['Ng']
185+
186+
P_glcm = cMatrices.calculate_glcm(self.matrix, self.maskArray, angles, Ng)
187+
P_glcm = self._applyMatrixOptions(P_glcm, angles)
188+
189+
return P_glcm
190+
191+
def _applyMatrixOptions(self, P_glcm, angles):
192+
"""
193+
Further process calculated matrix by optionally making it symmetrical and/or applying a weighting factor.
194+
Finally, delete empty angles and normalize the GLCM by dividing it by the sum of its elements.
195+
"""
196+
173197
# Optionally make GLCMs symmetrical for each angle
174198
if self.symmetricalGLCM:
175-
self.P_glcm += numpy.transpose(self.P_glcm, (1, 0, 2))
199+
P_glcm += numpy.transpose(P_glcm, (1, 0, 2))
176200

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

194-
self.P_glcm = numpy.sum(self.P_glcm * weights[None, None, :], 2, keepdims=True)
218+
P_glcm = numpy.sum(P_glcm * weights[None, None, :], 2, keepdims=True)
195219

196-
sumP_glcm = numpy.sum(self.P_glcm, (0, 1), keepdims=True) # , keepdims=True)
220+
sumP_glcm = numpy.sum(P_glcm, (0, 1), keepdims=True)
197221

198222
# Delete empty angles if no weighting is applied
199-
if self.P_glcm.shape[2] > 1:
200-
self.P_glcm = numpy.delete(self.P_glcm, numpy.where(sumP_glcm == 0), 2)
223+
if P_glcm.shape[2] > 1:
224+
P_glcm = numpy.delete(P_glcm, numpy.where(sumP_glcm == 0), 2)
201225
sumP_glcm = numpy.delete(sumP_glcm, numpy.where(sumP_glcm == 0), 2)
202226

203227
# Normalize each glcm
204-
self.P_glcm = self.P_glcm / sumP_glcm
228+
return P_glcm / sumP_glcm
205229

206230
# check if ivector and jvector can be replaced
207231
def _calculateCoefficients(self):

radiomics/glrlm.py

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import numpy
44
from six.moves import range
55

6-
from radiomics import base, imageoperations
6+
from radiomics import base, cMatrices, cMatsEnabled, imageoperations
77

88

99
class RadiomicsGLRLM(base.RadiomicsFeaturesBase):
@@ -93,10 +93,14 @@ def __init__(self, inputImage, inputMask, **kwargs):
9393
self.coefficients['Nr'] = numpy.max(self.matrix.shape)
9494
self.coefficients['Np'] = self.targetVoxelArray.size
9595

96-
self._calculateGLRLM()
96+
if cMatsEnabled():
97+
self.P_glrlm = self._calculateCMatrix()
98+
else:
99+
self.P_glrlm = self._calculateMatrix()
100+
97101
self._calculateCoefficients()
98102

99-
def _calculateGLRLM(self):
103+
def _calculateMatrix(self):
100104
Ng = self.coefficients['Ng']
101105
Nr = self.coefficients['Nr']
102106

@@ -152,11 +156,34 @@ def _calculateGLRLM(self):
152156
if not isMultiElement:
153157
P[:] = 0
154158

159+
P_glrlm = self._applyMatrixOptions(P_glrlm, angles)
160+
161+
return P_glrlm
162+
163+
def _calculateCMatrix(self):
164+
Ng = self.coefficients['Ng']
165+
Nr = self.coefficients['Nr']
166+
167+
size = numpy.max(self.matrixCoordinates, 1) - numpy.min(self.matrixCoordinates, 1) + 1
168+
angles = imageoperations.generateAngles(size)
169+
170+
P_glrlm = cMatrices.calculate_glrlm(self.matrix, self.maskArray, angles, Ng, Nr)
171+
P_glrlm = self._applyMatrixOptions(P_glrlm, angles)
172+
173+
return P_glrlm
174+
175+
def _applyMatrixOptions(self, P_glrlm, angles):
176+
"""
177+
Further process the calculated matrix by cropping the matrix to between minimum and maximum observed gray-levels and
178+
up to maximum observed run-length. Optionally apply a weighting factor. Finally delete empty angles and store the
179+
sum of the matrix in ``self.coefficients``.
180+
"""
181+
155182
# Crop gray-level axis of GLRLMs to between minimum and maximum observed gray-levels
156183
# Crop run-length axis of GLRLMs up to maximum observed run-length
157184
P_glrlm_bounds = numpy.argwhere(P_glrlm)
158185
(xstart, ystart, zstart), (xstop, ystop, zstop) = P_glrlm_bounds.min(0), P_glrlm_bounds.max(0) + 1 # noqa: F841
159-
self.P_glrlm = P_glrlm[xstart:xstop, :ystop, :]
186+
P_glrlm = P_glrlm[xstart:xstop, :ystop, :]
160187

161188
# Optionally apply a weighting factor
162189
if self.weightingNorm is not None:
@@ -175,17 +202,19 @@ def _calculateGLRLM(self):
175202
self.logger.warning('weigthing norm "%s" is unknown, weighting factor is set to 1', self.weightingNorm)
176203
weights[a_idx] = 1
177204

178-
self.P_glrlm = numpy.sum(self.P_glrlm * weights[None, None, :], 2, keepdims=True)
205+
P_glrlm = numpy.sum(P_glrlm * weights[None, None, :], 2, keepdims=True)
179206

180-
sumP_glrlm = numpy.sum(self.P_glrlm, (0, 1))
207+
sumP_glrlm = numpy.sum(P_glrlm, (0, 1))
181208

182209
# Delete empty angles if no weighting is applied
183-
if self.P_glrlm.shape[2] > 1:
184-
self.P_glrlm = numpy.delete(self.P_glrlm, numpy.where(sumP_glrlm == 0), 2)
210+
if P_glrlm.shape[2] > 1:
211+
P_glrlm = numpy.delete(P_glrlm, numpy.where(sumP_glrlm == 0), 2)
185212
sumP_glrlm = numpy.delete(sumP_glrlm, numpy.where(sumP_glrlm == 0), 0)
186213

187214
self.coefficients['sumP_glrlm'] = sumP_glrlm
188215

216+
return P_glrlm
217+
189218
def _calculateCoefficients(self):
190219

191220
pr = numpy.sum(self.P_glrlm, 0)

radiomics/glszm.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from six.moves import range
33
from tqdm import trange
44

5-
from radiomics import base, imageoperations
5+
from radiomics import base, cMatrices, cMatsEnabled, imageoperations
66

77

88
class RadiomicsGLSZM(base.RadiomicsFeaturesBase):
@@ -66,10 +66,14 @@ def __init__(self, inputImage, inputMask, **kwargs):
6666
self.coefficients['Ng'] = self.histogram[1].shape[0] - 1
6767
self.coefficients['Np'] = self.targetVoxelArray.size
6868

69-
self._calculateGLSZM()
69+
if cMatsEnabled():
70+
self.P_glszm = self._calculateCMatrix()
71+
else:
72+
self.P_glszm = self._calculateMatrix()
73+
7074
self._calculateCoefficients()
7175

72-
def _calculateGLSZM(self):
76+
def _calculateMatrix(self):
7377
"""
7478
Number of times a region with a
7579
gray level and voxel count occurs in an image. P_glszm[level, voxel_count] = # occurrences
@@ -131,7 +135,15 @@ def _calculateGLSZM(self):
131135
# Crop size-zone area axis of GLSZM matrix up to maximum observed size-zone area
132136
P_glszm_bounds = numpy.argwhere(P_glszm)
133137
(xstart, ystart), (xstop, ystop) = P_glszm_bounds.min(0), P_glszm_bounds.max(0) + 1 # noqa: F841
134-
self.P_glszm = P_glszm[xstart:xstop, :ystop]
138+
return P_glszm[xstart:xstop, :ystop]
139+
140+
def _calculateCMatrix(self):
141+
size = numpy.max(self.matrixCoordinates, 1) - numpy.min(self.matrixCoordinates, 1) + 1
142+
angles = imageoperations.generateAngles(size)
143+
Ng = self.coefficients['Ng']
144+
Ns = self.coefficients['Np']
145+
146+
return cMatrices.calculate_glszm(self.matrix, self.maskArray, angles, Ng, Ns)
135147

136148
def _calculateCoefficients(self):
137149
sumP_glszm = numpy.sum(self.P_glszm, (0, 1))

0 commit comments

Comments
 (0)