Source code for zrad.radiomics.extractor
from ..preprocessing import RoiData
from .extraction_context import ExtractionContext
from .extraction_preparation import build_extraction_metadata, prepare_extraction_data
from .feature_registry import resolve_groups
[docs]
class Radiomics:
"""Extract radiomics features from prepared ROI data.
``Radiomics`` consumes a fully prepared ``RoiData`` instance. Preprocessing
steps are responsible for building the intensity mask, applying
re-segmentation, and preparing texture or IVH intensity images before
extraction.
Supported feature families are:
* ``"morphology"``
* ``"local_intensity"``
* ``"intensity_statistics"``
* ``"intensity_histogram"``
* ``"glcm"``
* ``"glrlm"``
* ``"glszm"``
* ``"gldzm"``
* ``"ngtdm"``
* ``"ngldm"``
* ``"ivh"``
``"morphology"`` includes Moran's I and Geary's C for 3D ROIs, including
default extraction. Both share an exact hybrid FFT or blocked calculation.
They can also be selected by their individual feature names.
Parameters
----------
aggr_dim : {"2D", "2.5D", "3D"}, default="3D"
Spatial aggregation dimensionality for texture features. This affects
GLCM, GLRLM, GLSZM, GLDZM, NGTDM, and NGLDM feature names and values.
aggr_method : {"MERG", "AVER", "SLICE_MERG", "DIR_MERG"}, default="AVER"
Texture aggregation strategy across directions and slices. This is used
by GLCM and GLRLM features.
slice_weighting : bool, default=False
Weight 2D slice-wise texture averages by slice ROI size.
slice_median : bool, default=False
Aggregate 2D slice-wise texture values by median instead of mean.
"""
def __init__(
self,
aggr_dim='3D',
aggr_method='AVER',
slice_weighting=False,
slice_median=False,
):
if slice_weighting and slice_median:
raise ValueError('Slice median averaging is not supported with weighting strategy.')
if aggr_dim not in ['2D', '2.5D', '3D']:
raise ValueError(f"Wrong aggregation dim {aggr_dim}. Available '2D', '2.5D', and '3D'.")
if aggr_method not in ['MERG', 'AVER', 'SLICE_MERG', 'DIR_MERG']:
raise ValueError(
f"Wrong aggregation method {aggr_method}. Available 'MERG', 'AVER', 'SLICE_MERG', and 'DIR_MERG'."
)
self.aggr_dim = aggr_dim
self.aggr_method = aggr_method
self.slice_weighting = slice_weighting
self.slice_median = slice_median
[docs]
def extract_features(
self,
roi_data=None,
families=None,
features=None,
include_metadata=False,
):
"""Run radiomics feature extraction.
Parameters
----------
roi_data : RoiData
Prepared ROI data containing at least ``image``,
``morphological_mask``, and ``intensity_mask``. Texture and IVH
feature families additionally require their corresponding prepared
fields on ``RoiData``.
families : str or sequence of str, optional
Feature families to extract. Supported names are:
``"morphology"``, ``"local_intensity"``,
``"intensity_statistics"``, ``"intensity_histogram"``,
``"glcm"``, ``"glrlm"``, ``"glszm"``, ``"gldzm"``,
``"ngtdm"``, ``"ngldm"``, and ``"ivh"``.
If omitted, all default-enabled families supported by the prepared
``RoiData`` are extracted. Use ``"all"`` to extract every
supported family. Morphology includes Moran's I and Geary's C.
Repeated family selections are calculated once.
features : str or sequence of str, optional
Individual feature names to extract. Names may come from one or
more feature families. Use either ``families`` or ``features``,
not both. For texture features, either configured output names or
base feature names can be supplied. Base names are mapped to the
configured output names for the current aggregation settings.
include_metadata : bool, default=False
If ``True``, append extraction metadata to the returned dictionary.
Metadata currently includes the minimum bounding-box side length,
voxel count, and number of discretized texture bins.
Returns
-------
features : dict
Flat dictionary mapping feature names to calculated values.
Raises
------
TypeError
If ``roi_data`` is not a ``RoiData`` instance.
ValueError
If required ROI fields are missing, if an unknown family or feature
is requested, or if both ``families`` and ``features`` are set.
DataStructureError
If a requested family is not supported for the current image shape
or prepared ROI data.
Notes
-----
Feature availability depends on the prepared ``RoiData``:
* ``"morphology"`` requires a 3D ROI.
* ``"intensity_histogram"`` and texture families require
``texture_discretized_image``.
* ``"ivh"`` requires ``ivh_intensity_image`` and IVH discretization
metadata.
* ``"local_intensity"`` and ``"intensity_statistics"`` use the
non-discretized intensity mask.
"""
context = self._build_context(roi_data)
groups, selected_features = resolve_groups(context, families=families, features=features)
prepared_data = prepare_extraction_data(
context=context,
groups=groups,
include_metadata=include_metadata,
)
extracted = {}
for group in groups:
if selected_features is None:
extracted.update(group.calculate(context, prepared_data))
else:
group_features = [name for name in selected_features if name in group.output_names(context)]
extracted.update(group.calculate_selected(context, prepared_data, group_features))
if selected_features is not None:
extracted = {name: extracted[name] for name in selected_features}
if include_metadata:
extracted.update(build_extraction_metadata(prepared_data))
return extracted
def _build_context(self, roi_data):
self._validate_roi_data(roi_data)
return ExtractionContext(
roi_data=roi_data,
is_slice_2d_image=roi_data.image.shape[2] == 1,
aggr_dim=self.aggr_dim,
aggr_method=self.aggr_method,
slice_weighting=self.slice_weighting,
slice_median=self.slice_median,
)
@staticmethod
def _validate_roi_data(roi_data):
if not isinstance(roi_data, RoiData):
raise TypeError("roi_data must be an instance of zrad.preprocessing.RoiData.")
required_fields = {
"image": roi_data.image,
"morphological_mask": roi_data.morphological_mask,
"intensity_mask": roi_data.intensity_mask,
}
missing = [name for name, value in required_fields.items() if value is None]
if missing:
raise ValueError(f"roi_data is missing required field(s): {', '.join(missing)}.")