Source code for zrad.preprocessing.masks

import numpy as np

from ..exceptions import DataStructureError


[docs] class RoiMaskValidator: """Validate ROI mask geometry for 2D, 2.5D, or 3D feature extraction. The validator enforces minimum ROI extent and voxel-count requirements before feature extraction. In slice-wise modes, invalid slices are removed; in 3D mode, the full volume must satisfy the checks. Parameters ---------- aggregation_dimension : {"2D", "2.5D", "3D", None}, optional Feature aggregation mode that determines the geometric validity checks. ``"3D"`` validates the full ROI volume. Other non-``None`` values are treated as slice-wise modes and remove invalid slices. If ``None``, the mask is copied without geometric validation. """ def __init__(self, aggregation_dimension=None): self.aggregation_dimension = aggregation_dimension
[docs] def get_params(self): """Return validation parameters mapped to their configured values. Returns ------- params : dict Dictionary containing ``aggregation_dimension``. """ return { 'aggregation_dimension': self.aggregation_dimension, }
[docs] def apply(self, mask): """Validate a mask and return a validated mask copy. Parameters ---------- mask : Image Binary ROI mask to validate. Returns ------- validated_mask : Image Copy of the input mask. In slice-wise modes, slices that fail the minimum requirements are set to zero. """ validated_mask = mask.copy() if self.aggregation_dimension is None: return validated_mask masked_array = validated_mask.array min_box_size = 3 min_voxel_number_3d = 27 min_voxel_number_2d = 9 if self.aggregation_dimension == '3D': valid_coords = np.where(masked_array != 0) if len(valid_coords[0]) == 0: raise DataStructureError("No valid voxels in 3D array.") zmin, zmax = valid_coords[0].min(), valid_coords[0].max() + 1 ymin, ymax = valid_coords[1].min(), valid_coords[1].max() + 1 xmin, xmax = valid_coords[2].min(), valid_coords[2].max() + 1 bbox_shape = (zmax - zmin, ymax - ymin, xmax - xmin) no_valid_voxels = len(valid_coords[0]) if min(bbox_shape) < min_box_size: raise DataStructureError(f"3D bounding box dimension < {min_box_size}.") if no_valid_voxels < min_voxel_number_3d: raise DataStructureError(f"Valid voxel count < {min_voxel_number_3d}.") else: for z_idx in range(masked_array.shape[0]): slice_arr = masked_array[z_idx, :, :] if not np.any(slice_arr): continue valid_coords = np.where(slice_arr != 0) no_valid_voxels = len(valid_coords[0]) if no_valid_voxels == 0: continue ymin, ymax = valid_coords[0].min(), valid_coords[0].max() + 1 xmin, xmax = valid_coords[1].min(), valid_coords[1].max() + 1 height = ymax - ymin width = xmax - xmin if min(height, width) < min_box_size or no_valid_voxels < min_voxel_number_2d: slice_arr[:, :] = 0 if not np.any(masked_array): raise DataStructureError( "Not a single slice meets the minimum 2D/2.5D requirements. " "Consider finer resampling or check the data." ) validated_mask.array = masked_array return validated_mask