import numpy as np
from ..image import Image
from .roi import RoiData
def _normalize_intensity_range(intensity_range):
if intensity_range is None:
return None
if (
not isinstance(intensity_range, (list, tuple))
or len(intensity_range) != 2
or not all(isinstance(value, (int, float)) and not isinstance(value, bool) for value in intensity_range)
):
raise ValueError("intensity_range must be a two-value numeric sequence.")
lower, upper = (float(value) for value in intensity_range)
if not np.isfinite(lower) or np.isnan(upper) or lower > upper:
raise ValueError("intensity_range must have a finite lower bound and lower <= upper.")
return lower, upper
class RangeResegmenter:
"""Remove ROI voxels outside a configured intensity range.
Range re-segmentation restricts the intensity ROI to voxels whose original
image intensities lie within user-defined absolute bounds. Excluded voxels
are represented as ``NaN`` in the intensity mask. Prepared texture and IVH
images are cleared because the valid intensity population has changed.
Parameters
----------
intensity_range : tuple[float, float] or None
Inclusive lower and upper intensity limits as ``(lower, upper)``. Voxels
outside this range are removed from ``RoiData.intensity_mask`` by
replacing them with ``NaN``. If ``None``, range re-segmentation is
skipped.
"""
def __init__(self, intensity_range):
self.intensity_range = _normalize_intensity_range(intensity_range)
def get_params(self):
"""Return re-segmentation parameters mapped to their configured values.
Returns
-------
params : dict
Dictionary containing ``intensity_range``.
"""
return {
'intensity_range': self.intensity_range,
}
def apply(self, roi_data):
"""Apply range re-segmentation to the intensity mask.
Parameters
----------
roi_data : RoiData
ROI data with an existing ``intensity_mask``.
Returns
-------
roi_data : RoiData
ROI data with voxels outside ``intensity_range`` removed from the
intensity mask.
"""
if self.intensity_range is None:
return roi_data
lower, upper = self.intensity_range
range_mask = np.where(
(roi_data.image.array <= upper) & (roi_data.image.array >= lower),
1,
0,
)
intensity_mask = roi_data.intensity_mask
return RoiData(
image=roi_data.image,
filtered_image=roi_data.filtered_image,
morphological_mask=roi_data.morphological_mask,
intensity_mask=Image(
array=np.where((range_mask > 0) & (~np.isnan(intensity_mask.array)), intensity_mask.array, np.nan),
origin=intensity_mask.origin,
spacing=intensity_mask.spacing,
direction=intensity_mask.direction,
shape=intensity_mask.shape,
),
intensity_range=self.intensity_range,
)
class OutlierResegmenter:
"""Remove ROI voxels outside a mean-centered standard-deviation range.
Outlier re-segmentation excludes voxels whose current intensity-mask value
is outside a symmetric interval around the valid intensity-mask mean. The
interval width is controlled by a standard-deviation multiplier. Prepared
texture and IVH images are cleared because the valid intensity population
has changed.
Parameters
----------
outlier_range : float, str, or None
Number of standard deviations around the ROI mean to retain. Values
outside ``mean +/- outlier_range * std`` are removed from
``RoiData.intensity_mask`` by replacing them with ``NaN``. If ``None``
is supplied, outlier re-segmentation is skipped.
"""
def __init__(self, outlier_range):
if outlier_range is not None:
try:
outlier_range = float(outlier_range)
except (TypeError, ValueError) as exc:
raise ValueError("outlier_range must be a positive number.") from exc
if not np.isfinite(outlier_range) or outlier_range <= 0:
raise ValueError("outlier_range must be a positive number.")
self.outlier_range = outlier_range
def get_params(self):
"""Return re-segmentation parameters mapped to their configured values.
Returns
-------
params : dict
Dictionary containing ``outlier_range``.
"""
return {
'outlier_range': self.outlier_range,
}
def apply(self, roi_data):
"""Apply outlier re-segmentation to the intensity mask.
Parameters
----------
roi_data : RoiData
ROI data with ``morphological_mask`` and ``intensity_mask``.
Returns
-------
roi_data : RoiData
ROI data with statistical outliers removed from the intensity mask.
"""
if self.outlier_range is None:
return roi_data
intensity_mask = roi_data.intensity_mask
valid_values = intensity_mask.array[~np.isnan(intensity_mask.array)]
mean = np.mean(valid_values)
std = np.std(valid_values)
outlier_mask = np.where(
(intensity_mask.array <= mean + self.outlier_range * std)
& (intensity_mask.array >= mean - self.outlier_range * std),
1,
0,
)
return RoiData(
image=roi_data.image,
filtered_image=roi_data.filtered_image,
morphological_mask=roi_data.morphological_mask,
intensity_mask=Image(
array=np.where((outlier_mask > 0) & (~np.isnan(intensity_mask.array)), intensity_mask.array, np.nan),
origin=intensity_mask.origin,
spacing=intensity_mask.spacing,
direction=intensity_mask.direction,
shape=intensity_mask.shape,
),
intensity_range=roi_data.intensity_range,
)
[docs]
class Resegmenter:
"""Apply range and outlier re-segmentation to ``RoiData.intensity_mask``.
Re-segmentation removes voxels from the intensity ROI by replacing excluded
voxels with ``NaN`` and clearing prepared texture and IVH images. Range
re-segmentation is evaluated on ``RoiData.image`` and applied to the
existing ``RoiData.intensity_mask``. If both criteria are configured, range
re-segmentation is applied first; outlier statistics are then calculated
from the remaining valid intensity-mask values.
Parameters
----------
intensity_range : tuple[float, float] or None, optional
Inclusive lower and upper intensity limits as ``(lower, upper)``. If
``None``, range re-segmentation is skipped.
outlier_range : float, str, or None, optional
Number of standard deviations around the ROI mean to retain. If
``None`` is supplied, outlier re-segmentation is skipped.
"""
def __init__(self, intensity_range=None, outlier_range=None):
self.intensity_range = _normalize_intensity_range(intensity_range)
self.outlier_range = outlier_range
[docs]
def get_params(self):
"""Return re-segmentation parameters mapped to their configured values.
Returns
-------
params : dict
Dictionary containing ``intensity_range`` and ``outlier_range``.
"""
return {
'intensity_range': self.intensity_range,
'outlier_range': self.outlier_range,
}
[docs]
def apply(self, roi_data):
"""Return ROI data with an updated intensity mask.
Parameters
----------
roi_data : RoiData
ROI data containing ``image``, ``morphological_mask``, and
``intensity_mask``. The intensity mask is usually created with
``IntensityMaskBuilder`` before re-segmentation.
Returns
-------
roi_data : RoiData
New ROI data with ``intensity_mask`` updated by the configured
range and outlier criteria.
"""
roi_data = RangeResegmenter(self.intensity_range).apply(roi_data)
return OutlierResegmenter(self.outlier_range).apply(roi_data)