From dd7e831424d48026ce63fc440527c238fe94132a Mon Sep 17 00:00:00 2001 From: Joost van Griethuysen Date: Wed, 8 Mar 2017 18:29:05 +0100 Subject: [PATCH 1/3] MATH: Revise Compactness 1 Flip exponent (2/3 -> 3/2) of Compactness 1 feature formula. The previous exponent was most likely an error, as this resulted in a formula that was not dimensionless, as the description indicates. The formula with the new exponent is dimensionless. Compactness1, Compactness2, Sphericity and Spherical disproportion are mathematically correlated. Update the documentation to reflect this (provide equations specifying the correlation between the different features). Also disable Compactness 1 and Compactness 2 in the default/example parameter file in the bin folder to emphasize the redundancy of these features. Updated the formulas and documentation thereof to have simplified formulas where possible. --- bin/Params.yaml | 15 ++++++- radiomics/shape.py | 106 +++++++++++++++++++++++++++------------------ 2 files changed, 78 insertions(+), 43 deletions(-) diff --git a/bin/Params.yaml b/bin/Params.yaml index 1cb7bbc7..f511eb78 100644 --- a/bin/Params.yaml +++ b/bin/Params.yaml @@ -33,8 +33,19 @@ inputImage: # Featureclasses, from which features must be calculated. If a featureclass is not mentioned, no features are calculated # for that class. Otherwise, the specified features are calculated, or, if none are specified, all are calculated. featureClass: - shape: # for lists none values are allowed, in this case, all features are enabled + shape: ['Volume', + 'SurfaceArea', + 'SurfaceVolumeRatio', + 'Sphericity', + 'SphericalDisproportion', + 'Maximum3DDiameter', + 'Maximum2DDiameterSlice', + 'Maximum2DDiameterColumn', + 'Maximum2DDiameterRow', + 'Elongation', + 'Flatness', + 'Roundness'] # Only enable these shape descriptors (disables redundant Compactness 1 and Compactness 2) firstorder: [] # specifying an empty list has the same effect as specifying nothing. - glcm: + glcm: # for lists none values are allowed, in this case, all features are enabled glrlm: glszm: diff --git a/radiomics/shape.py b/radiomics/shape.py index 77cd6419..402cf693 100644 --- a/radiomics/shape.py +++ b/radiomics/shape.py @@ -8,7 +8,8 @@ class RadiomicsShape(base.RadiomicsFeaturesBase): r""" In this group of features we included descriptors of the three-dimensional size and shape of the tumor region. - Let in the following definitions denote :math:`V` the volume and :math:`A` the surface area of the volume of interest. + Let in the following definitions denote :math:`V` the volume (mm\ :sup:`3`) and :math:`A` the surface area of the + volume (mm\ :sup:`2`) of interest. """ def __init__(self, inputImage, inputMask, **kwargs): @@ -163,13 +164,13 @@ def getVolumeFeatureValue(self): def getSurfaceAreaFeatureValue(self): r""" - Calculate the surface area of the tumor region in square millimeters. + Calculate the surface area of the tumor region in square millimeters using a marching cubes algorithm. :math:`A = \displaystyle\sum^{N}_{i=1}{\frac{1}{2}|\textbf{a}_i\textbf{b}_i \times \textbf{a}_i\textbf{c}_i|}` Where: - :math:`N` is the number of triangles forming the surface of the volume + :math:`N` is the number of triangles forming the surface mesh of the volume (ROI) :math:`a_ib_i` and :math:`a_ic_i` are the edges of the :math:`i`\ :sup:`th` triangle formed by points :math:`a_i`, :math:`b_i` and :math:`c_i` @@ -182,36 +183,85 @@ def getSurfaceVolumeRatioFeatureValue(self): Calculate the surface area to volume ratio of the tumor region :math:`surface\ to\ volume\ ratio = \frac{A}{V}` + + Here, a lower value indicates a more compact (sphere-like) shape. """ return (self.SurfaceArea / self.Volume) + def getSphericityFeatureValue(self): + r""" + Calculate the Sphericity of the tumor region. + + :math:`sphericity = \frac{\sqrt[3]{36 \pi V^2}}{A}` + + Sphericity is a measure of the roundness of the shape of the tumor region relative to a sphere. It is a + dimensionless measure, independent of scale and orientation. The value range is :math:`0 < sphericity \leq 1`, where + a value of 1 indicates a perfect sphere (a sphere has the smallest possible surface area for a given volume, + compared to other solids). + + **N.B. This feature is correlated to Compactness 1, Compactness 2 and Spherical Disproportion. In the default + parameter file provided in the** ``pyradiomics\\bin`` **folder, only Compactness 1 and Compactness 2 are therefore + disabled.** + """ + return (36 * numpy.pi * self.Volume ** 2) ** (1.0 / 3.0) / self.SurfaceArea + def getCompactness1FeatureValue(self): r""" Calculate the compactness (1) of the tumor region. - :math:`compactness\ 1 = \frac{V}{\sqrt{\pi}A^{\frac{2}{3}}}` + :math:`compactness\ 1 = \frac{V}{\sqrt{\pi A^3}}` + + Similar to Sphericity, Compactness 1 is a measure of how compact the shape of the tumor is relative to a sphere + (most compact). It is therefore correlated to Sphericity and redundant. It is provided here for completeness. + The value range is :math:`0 < compactness\ 1 \leq \frac{1}{6 \pi}`, where a value of :math:`\frac{1}{6 \pi}` + indicates a perfect sphere. - Compactness 1 is a measure of how compact the shape of the tumor is - relative to a sphere (most compact). It is a dimensionless measure, - independent of scale and orientation. Compactness 1 is defined as the - ratio of volume to the :math:`\sqrt{\text{surface area}^3}`. This is a measure of the - compactness of the shape of the image ROI + By definition, :math:`compactness\ 1 = \frac{1}{6 \pi}\sqrt{compactness\ 2} = + \frac{1}{6 \pi}\sqrt{sphericity^3}`. + + **N.B. This feature is correlated to Compactness 2, Sphericity and Spherical Disproportion. In the default + parameter file provided in the** ``pyradiomics\\bin`` **folder, only Compactness 1 and Compactness 2 are therefore + disabled.** """ - return ((self.Volume) / ((self.SurfaceArea) ** (2.0 / 3.0) * numpy.sqrt(numpy.pi))) + return ((self.Volume) / ((self.SurfaceArea) ** (3.0 / 2.0) * numpy.sqrt(numpy.pi))) def getCompactness2FeatureValue(self): r""" Calculate the Compactness (2) of the tumor region. - :math:`compactness\ 2 = 36\pi\frac{V^2}{A^3}` + :math:`compactness\ 2 = 36 \pi \frac{V^2}{A^3}` + + Similar to Sphericity and Compactness 1, Compactness 2 is a measure of how compact the shape of the tumor is + relative to a sphere (most compact). It is a dimensionless measure, independent of scale and orientation. The value + range is :math:`0 < compactness\ 2 \leq 1`, where a value of 1 indicates a perfect sphere. + + By definition, :math:`compactness\ 2 = (sphericity)^3` - Compactness 2 is a measure of how compact the shape of the tumor is - relative to a sphere (most compact). It is a dimensionless measure, - independent of scale and orientation. This is a measure of the compactness - of the shape of the image ROI. + **N.B. This feature is correlated to Compactness 1, Sphericity and Spherical Disproportion. In the default + parameter file provided in the** ``pyradiomics\\bin`` **folder, only Compactness 1 and Compactness 2 are therefore + disabled.** """ return ((36.0 * numpy.pi) * ((self.Volume) ** 2.0) / ((self.SurfaceArea) ** 3.0)) + def getSphericalDisproportionFeatureValue(self): + r""" + Calculate the Spherical Disproportion of the tumor region. + + :math:`spherical\ disproportion = \frac{A}{4\pi R^2} = \frac{A}{\sqrt[3]{36 \pi V^2}}` + + Where :math:`R` is the radius of a sphere with the same volume as the tumor, and equal to + :math:`\sqrt[3]{\frac{3V}{4\pi}}`. + + Spherical Disproportion is the ratio of the surface area of the tumor region to the surface area of a sphere with + the same volume as the tumor region, and by definition, the inverse of Sphericity. Therefore, the value range is + :math:`spherical\ disproportion \geq 1`, with a value of 1 indicating a perfect sphere. + + **N.B. This feature is correlated to Compactness 1, Sphericity and Spherical Disproportion. In the default + parameter file provided in the** ``pyradiomics\\bin`` **folder, only Compactness 1 and Compactness 2 are therefore + disabled.** + """ + return self.SurfaceArea / (36 * numpy.pi * self.Volume ** 2) ** (1.0 / 3.0) + def getMaximum3DDiameterFeatureValue(self): r""" Calculate the largest pairwise euclidean distance between tumor surface voxels. @@ -240,32 +290,6 @@ def getMaximum2DDiameterRowFeatureValue(self): return self._getMaximum2Ddiameter(2) - def getSphericalDisproportionFeatureValue(self): - r""" - Calculate the Spherical Disproportion of the tumor region. - - :math:`spherical\ disproportion = \frac{A}{4\pi R^2}` - - Where :math:`R` is the radius of a sphere with the same volume as the tumor. - - Spherical Disproportion is the ratio of the surface area of the - tumor region to the surface area of a sphere with the same - volume as the tumor region. - """ - R = self.lssif.GetEquivalentSphericalRadius(self.label) - return ((self.SurfaceArea) / (4.0 * numpy.pi * (R ** 2.0))) - - def getSphericityFeatureValue(self): - r""" - Calculate the Sphericity of the tumor region. - - :math:`sphericity = \frac{\pi^{\frac{1}{3}}(6V)^{\frac{2}{3}}}{A}` - - Sphericity is a measure of the roundness of the shape of the tumor region - relative to a sphere. This is another measure of the compactness of a tumor. - """ - return (((numpy.pi) ** (1.0 / 3.0) * (6.0 * self.Volume) ** (2.0 / 3.0)) / (self.SurfaceArea)) - def getElongationFeatureValue(self): """ From 7290f880d6171d159c81bebc1eaf556d9dd7c15c Mon Sep 17 00:00:00 2001 From: Joost van Griethuysen Date: Thu, 9 Mar 2017 14:37:35 +0100 Subject: [PATCH 2/3] DEL: Remove Roundness Feature Remove the Roundness Feature, as this expresses the same formula as Sphericity, but is calculated by Simple ITK's LabelShapeStatisticsImageFilter (LSSIF). It is documented here: http://www.insight-journal.org/browse/publication/176. The calculated values differ however, but this is due to the fact that SimpleITK calculates the surface area differently. The surface area calculated by SimpleITK can be found by calling LSSIF.GetPerimeter(). Calculation of Roundness and Surface Area is not done using SimpleITK, as the calculated surface area can be smaller than the surface area of a sphere with the same volume as the ROI (and therefore causes Roundness > 1). Example in lung1 testcase: * SA (pyradiomics) 782.24 * SA (SimpleITK) 576.06 * SA of a sphere with volume 1361.20: 593.97 --- radiomics/shape.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/radiomics/shape.py b/radiomics/shape.py index 402cf693..dfcb80a6 100644 --- a/radiomics/shape.py +++ b/radiomics/shape.py @@ -16,8 +16,6 @@ def __init__(self, inputImage, inputMask, **kwargs): super(RadiomicsShape, self).__init__(inputImage, inputMask, **kwargs) self.pixelSpacing = numpy.array(inputImage.GetSpacing()[::-1]) - z, x, y = self.pixelSpacing - self.cubicMMPerVoxel = z * x * y # Use SimpleITK for some shape features self.lssif = sitk.LabelShapeStatisticsImageFilter() @@ -302,12 +300,6 @@ def getFlatnessFeatureValue(self): """ return self.lssif.GetFlatness(self.label) - def getRoundnessFeatureValue(self): - """ - - """ - return self.lssif.GetRoundness(self.label) - def _interpolate(self, grid, p1, p2): diff = (.5 - self.maskArray[tuple(grid[p1])]) / (self.maskArray[tuple(grid[p2])] - self.maskArray[tuple(grid[p1])]) return (grid[p1] + ((grid[p2] - grid[p1]) * diff)) * self.pixelSpacing From ac234d08eb53becd7210628f4ea3ec26acaebc5c Mon Sep 17 00:00:00 2001 From: Joost van Griethuysen Date: Thu, 9 Mar 2017 14:57:55 +0100 Subject: [PATCH 3/3] TEST: Update baseline Update baseline to reflect change in Compactness 1 formula and the removal of the Roundness feature. --- data/baseline/baseline_shape.csv | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/data/baseline/baseline_shape.csv b/data/baseline/baseline_shape.csv index 4701e032..6ec9c173 100644 --- a/data/baseline/baseline_shape.csv +++ b/data/baseline/baseline_shape.csv @@ -1,6 +1,6 @@ -Patient ID,general_info_BoundingBox,general_info_GeneralSettings,general_info_ImageHash,general_info_ImageSpacing,general_info_InputImages,general_info_MaskHash,general_info_Version,general_info_VolumeNum,general_info_VoxelNum,Maximum3DDiameter,Compactness2,Maximum2DDiameterSlice,Sphericity,Compactness1,Elongation,SurfaceVolumeRatio,Volume,Flatness,SphericalDisproportion,Roundness,SurfaceArea,Maximum2DDiameterColumn,Maximum2DDiameterRow -brain1,(162; 84; 11; 47; 70; 7),{'verbose': False; 'voxelArrayShift': 2000; 'binWidth': 25; 'label': 1; 'interpolator': None; 'symmetricalGLCM': False; 'resampledPixelSpacing': None; 'gldm_a': 0; 'weightingNorm': None; 'padDistance': 5},5c9ce3ca174f0f8324aa4d277e0fef82dc5ac566,(0.7812499999999999; 0.7812499999999999; 6.499999999999998),{'original': {}},9dc2c3137b31fd872997d92c9a92d5178126d9d3,0.post403.dev0+gd611f07,2,4137,65.53661458728622,0.11412770190085314,47.218791363317415,0.48506174422170256,26.754678721506981,1.7789885567018646,0.39230826186319256,16412.65869140624,1.2191850589688844,2.0615932134671486,0.6146906661500379,6438.8216037794018,44.548790405155771,61.580176713472483 -brain2,(205; 155; 8; 20; 15; 3),{'verbose': False; 'voxelArrayShift': 2000; 'binWidth': 25; 'label': 1; 'interpolator': None; 'symmetricalGLCM': False; 'resampledPixelSpacing': None; 'gldm_a': 0; 'weightingNorm': None; 'padDistance': 5},f2b8fbc4d5d1da08a1a70e79a301f8a830139438,(0.7812499999999999; 0.7812499999999999; 6.499999999999998),{'original': {}},b41049c71633e194bee4891750392b72eabd8800,0.post403.dev0+gd611f07,1,453,19.654138400092737,0.45448842446195314,15.566295972790057,0.76884880229711317,10.643947668070684,1.3499482848728939,0.51734387202502263,1797.1801757812489,1.1970744957871078,1.3006458448166522,1.0348881179911384,929.76015086528207,18.584714191036131,14.779664291265206 -breast1,(21; 64; 8; 9; 12; 3),{'verbose': False; 'voxelArrayShift': 2000; 'binWidth': 25; 'label': 1; 'interpolator': None; 'symmetricalGLCM': False; 'resampledPixelSpacing': None; 'gldm_a': 0; 'weightingNorm': None; 'padDistance': 5},016951a8f9e8e5de21092d9d62b77262f92e04a5,(0.664062; 0.664062; 2.1),{'original': {}},5aa7d57fd57e83125b605c036c40f4a0d0cfd3e4,0.post403.dev0+gd611f07,1,143,7.772702166226878,0.38380439201415423,7.7726464321122961,0.72672479835852832,2.407442715594033,1.4286044695895188,1.3055392937755954,132.4257954551532,1.0230321043228265,1.3760367091624301,0.8556418393749154,172.88707947619218,5.3539199187231041,7.3046820000000006 -lung1,(206; 347; 32; 24; 26; 3),{'verbose': False; 'voxelArrayShift': 2000; 'binWidth': 25; 'label': 1; 'interpolator': None; 'symmetricalGLCM': False; 'resampledPixelSpacing': None; 'gldm_a': 0; 'weightingNorm': None; 'padDistance': 5},34dca4200809a5e76c702d6b9503d958093057a3,(0.5703125; 0.5703125; 5.0),{'original': {}},054d887740012177bd1f9031ddac2b67170af0f3,0.post403.dev0+gd611f07,1,837,18.182594712754316,0.43779658729410265,15.978930906357792,0.7593187496870597,9.0459132938754774,1.3912249269789807,0.57467140294582819,1361.1978149414062,1.3975132116994384,1.3169699818582548,1.031094246578549,782.2414579991738,16.044440540748841,13.537563480922259 -lung2,(318; 333; 15; 87; 66; 11),{'verbose': False; 'voxelArrayShift': 2000; 'binWidth': 25; 'label': 1; 'interpolator': None; 'symmetricalGLCM': False; 'resampledPixelSpacing': None; 'gldm_a': 0; 'weightingNorm': None; 'padDistance': 5},14f57fd04838eb8c9cca2a0dd871d29971585975,(0.6269531; 0.6269531; 5.0),{'original': {}},e284ff05593bc6cb2747261882e452d4efbccb3a,0.post403.dev0+gd611f07,1,24644,65.4490741679312,0.30580544578561636,55.235955786717163,0.67372356547818002,60.760123315042613,1.3452677171012488,0.19691793910543032,48434.10876246395,1.3057448083030738,1.4842882915788209,0.8768796050137607,9537.5448799126643,61.55885055061254,57.721196463628289 +Patient ID,general_info_BoundingBox,general_info_GeneralSettings,general_info_ImageHash,general_info_ImageSpacing,general_info_InputImages,general_info_MaskHash,general_info_Version,general_info_VolumeNum,general_info_VoxelNum,Maximum3DDiameter,Compactness2,Maximum2DDiameterSlice,Sphericity,Compactness1,Elongation,SurfaceVolumeRatio,Volume,Flatness,SphericalDisproportion,SurfaceArea,Maximum2DDiameterColumn,Maximum2DDiameterRow +brain1,(162; 84; 11; 47; 70; 7),{'verbose': False; 'voxelArrayShift': 2000; 'enableCExtensions': True; 'interpolator': None; 'resampledPixelSpacing': None; 'gldm_a': 0; 'weightingNorm': None; 'normalizeScale': 1; 'normalize': False; 'additionalInfo': True; 'removeOutliers': None; 'binWidth': 25; 'label': 1; 'symmetricalGLCM': False; 'padDistance': 5},5c9ce3ca174f0f8324aa4d277e0fef82dc5ac566,(0.7812499999999999; 0.7812499999999999; 6.499999999999998),{'original': {}},9dc2c3137b31fd872997d92c9a92d5178126d9d3,0.post403.dev0+gd611f07,2,4137,65.53661458728622,0.11412770190085314,47.218791363317415,0.48506174422170256,0.017922327666112708,1.7789885567018646,0.39230826186319256,16412.65869140624,1.2191850589688844,2.0615932134671486,6438.8216037794018,44.548790405155771,61.580176713472483 +brain2,(205; 155; 8; 20; 15; 3),{'verbose': False; 'voxelArrayShift': 2000; 'enableCExtensions': True; 'interpolator': None; 'resampledPixelSpacing': None; 'gldm_a': 0; 'weightingNorm': None; 'normalizeScale': 1; 'normalize': False; 'additionalInfo': True; 'removeOutliers': None; 'binWidth': 25; 'label': 1; 'symmetricalGLCM': False; 'padDistance': 5},f2b8fbc4d5d1da08a1a70e79a301f8a830139438,(0.7812499999999999; 0.7812499999999999; 6.499999999999998),{'original': {}},b41049c71633e194bee4891750392b72eabd8800,0.post403.dev0+gd611f07,1,453,19.654138400092737,0.45448842446195314,15.566295972790057,0.76884880229711317,0.035765169710140834,1.3499482848728939,0.51734387202502263,1797.1801757812489,1.1970744957871078,1.3006458448166522,929.76015086528207,18.584714191036131,14.779664291265206 +breast1,(21; 64; 8; 9; 12; 3),{'verbose': False; 'voxelArrayShift': 2000; 'enableCExtensions': True; 'interpolator': None; 'resampledPixelSpacing': None; 'gldm_a': 0; 'weightingNorm': None; 'normalizeScale': 1; 'normalize': False; 'additionalInfo': True; 'removeOutliers': None; 'binWidth': 25; 'label': 1; 'symmetricalGLCM': False; 'padDistance': 5},016951a8f9e8e5de21092d9d62b77262f92e04a5,(0.664062; 0.664062; 2.1),{'original': {}},5aa7d57fd57e83125b605c036c40f4a0d0cfd3e4,0.post403.dev0+gd611f07,1,143,7.772702166226878,0.38380439201415423,7.7726464321122961,0.72672479835852832,0.032866529447821202,1.4286044695895188,1.3055392937755954,132.4257954551532,1.0230321043228265,1.3760367091624301,172.88707947619218,5.3539199187231041,7.3046820000000006 +lung1,(206; 347; 32; 24; 26; 3),{'verbose': False; 'voxelArrayShift': 2000; 'enableCExtensions': True; 'interpolator': None; 'resampledPixelSpacing': None; 'gldm_a': 0; 'weightingNorm': None; 'normalizeScale': 1; 'normalize': False; 'additionalInfo': True; 'removeOutliers': None; 'binWidth': 25; 'label': 1; 'symmetricalGLCM': False; 'padDistance': 5},34dca4200809a5e76c702d6b9503d958093057a3,(0.5703125; 0.5703125; 5.0),{'original': {}},054d887740012177bd1f9031ddac2b67170af0f3,0.post403.dev0+gd611f07,1,837,18.182594712754316,0.43779658729410265,15.978930906357792,0.7593187496870597,0.035102258719353144,1.3912249269789807,0.57467140294582819,1361.1978149414062,1.3975132116994384,1.3169699818582548,782.2414579991738,16.044440540748841,13.537563480922259 +lung2,(318; 333; 15; 87; 66; 11),{'verbose': False; 'voxelArrayShift': 2000; 'enableCExtensions': True; 'interpolator': None; 'resampledPixelSpacing': None; 'gldm_a': 0; 'weightingNorm': None; 'normalizeScale': 1; 'normalize': False; 'additionalInfo': True; 'removeOutliers': None; 'binWidth': 25; 'label': 1; 'symmetricalGLCM': False; 'padDistance': 5},14f57fd04838eb8c9cca2a0dd871d29971585975,(0.6269531; 0.6269531; 5.0),{'original': {}},e284ff05593bc6cb2747261882e452d4efbccb3a,0.post403.dev0+gd611f07,1,24644,65.4490741679312,0.30580544578561636,55.235955786717163,0.67372356547818002,0.029337390690641122,1.3452677171012488,0.19691793910543032,48434.10876246395,1.3057448083030738,1.4842882915788209,9537.5448799126643,61.55885055061254,57.721196463628289