import numpy as np
from ..exceptions import DataStructureError
from .base import BaseFeatureGroup
from .texture_aggregation import format_texture_feature_names
from .texture_base import TEXTURE_ATTRIBUTE_NAMES, ZoneMatrixFeatureBase
GLSZM_FEATURE_NAMES = (
'szm_sze',
'szm_lze',
'szm_lgze',
'szm_hgze',
'szm_szlge',
'szm_szhge',
'szm_lzlge',
'szm_lzhge',
'szm_glnu',
'szm_glnu_norm',
'szm_zsnu',
'szm_zsnu_norm',
'szm_z_perc',
'szm_gl_var',
'szm_zs_var',
'szm_zs_entr',
)
[docs]
class GLSZM(ZoneMatrixFeatureBase):
"""Grey level size zone matrix features.
GLSZM features describe connected zones of equal discretized grey level and
their sizes. They quantify small versus large zones and low- versus
high-grey-level zone patterns.
Parameters
----------
aggr_dim : {"2D", "2.5D", "3D"}
Spatial dimensionality used to define connected zones.
slice_weight : bool, default=False
Weight 2D slice-wise averages by slice ROI voxel count.
slice_median : bool, default=False
Aggregate 2D slice-wise values by median instead of mean.
"""
def __init__(self, aggr_dim, slice_weight=False, slice_median=False):
super().__init__(slice_weight=slice_weight, slice_median=slice_median)
self.aggr_dim = aggr_dim
[docs]
def get_params(self):
"""Return the configuration parameters of this GLSZM calculator.
Returns
-------
dict
Parameter names mapped to their configured values.
"""
return {
'aggr_dim': self.aggr_dim,
'slice_weight': self.slice_weight,
'slice_median': self.slice_median,
}
[docs]
def get_feature_names(self):
"""Return the GLSZM feature names produced by this calculator.
Returns
-------
list of str
Feature names defined for the GLSZM family.
"""
return list(GLSZM_FEATURE_NAMES)
@staticmethod
def _map_feature_names(values):
return dict(zip(GLSZM_FEATURE_NAMES, [values[name] for name in TEXTURE_ATTRIBUTE_NAMES]))
def _calc_2d_features(self, matrices, roi_voxel_counts, total_roi_voxels):
feature_dicts = []
weights = []
for slice_index, matrix in enumerate(matrices):
if self.slice_weight:
if total_roi_voxels == 0:
raise DataStructureError(' Denominator is zero in calc_2d_glszm_features.')
weights.append(roi_voxel_counts[slice_index] / total_roi_voxels)
else:
weights.append(1.0)
feature_dicts.append(self._matrix_feature_values(matrix, roi_voxel_counts[slice_index]))
return self._aggregate_feature_dicts(feature_dicts, None if self.slice_median else weights)
def _calc_2_5d_features(self, matrices, roi_voxel_counts):
matrix = np.sum(matrices, axis=0)
return self._matrix_feature_values(matrix, np.sum(roi_voxel_counts))
def _calc_3d_features(self, matrix, total_roi_voxels):
return self._matrix_feature_values(matrix, total_roi_voxels)
[docs]
def calculate_features(self, discretized_image_array):
"""Calculate GLSZM features for prepared discretized intensities.
Parameters
----------
discretized_image_array : numpy.ndarray
Prepared discretized intensity array with voxels outside the ROI set
to ``NaN``.
Returns
-------
dict
Mapping of GLSZM feature names to calculated values.
"""
discretized_image_array = np.asarray(discretized_image_array)
lvl = int(np.nanmax(discretized_image_array) + 1)
if self.aggr_dim == '3D':
glszm_matrix, total_roi_voxels = self._calc_glsz_3d_matrix(discretized_image_array, lvl)
return self._map_feature_names(self._calc_3d_features(glszm_matrix, total_roi_voxels))
glszm_matrices, roi_voxel_counts = self._calc_glsz_2d_matrices(discretized_image_array, lvl)
total_roi_voxels = np.sum(roi_voxel_counts)
if self.aggr_dim == '2.5D':
values = self._calc_2_5d_features(glszm_matrices, roi_voxel_counts)
else:
values = self._calc_2d_features(glszm_matrices, roi_voxel_counts, total_roi_voxels)
return self._map_feature_names(values)
class GLSZMFeatureGroup(BaseFeatureGroup):
family = 'glszm'
requirements = frozenset({'discretized_intensity_image'})
def supports(self, context):
return context.roi_data.texture_discretized_image is not None
def output_names(self, context):
return format_texture_feature_names(GLSZM_FEATURE_NAMES, context.aggr_dim)
def feature_aliases(self, context):
output_names = self.output_names(context)
aliases = {name: name for name in output_names}
aliases.update(dict(zip(GLSZM_FEATURE_NAMES, output_names)))
return aliases
def calculate(self, context, prepared_data):
glszm = GLSZM(
aggr_dim=context.aggr_dim,
slice_weight=context.slice_weighting,
slice_median=context.slice_median,
)
feature_values = glszm.calculate_features(prepared_data.require_discretized_intensity_image().array.T)
return {
output_name: feature_values[base_name]
for output_name, base_name in zip(self.output_names(context), GLSZM_FEATURE_NAMES)
}