.. _api_utils: ===== Utils ===== .. currentmodule:: medtda.utils The ``utils`` module provides utility functions for image processing, including resampling, normalization, masking, and cropping. .. contents:: Contents :local: :depth: 2 Overview ======== This module contains low-level utility functions used by the ``Preprocessor`` class. These functions can also be used standalone for custom workflows. **Key Functions:** * ``resample_image`` - Resample to target spacing * ``normalize_image`` - Intensity normalization (3 methods) * ``apply_mask`` - Apply binary mask to image * ``crop_to_roi`` - Crop to ROI bounding box * ``extract_label_from_mask`` - Extract single label * ``get_image_statistics`` - Compute image statistics * ``check_array_dimensions`` - Validate array dimensions Functions ========= Image Resampling ---------------- resample_image ~~~~~~~~~~~~~~ .. autofunction:: medtda.utils.resample_image Resample an image to target voxel spacing. **Signature:** .. code-block:: python def resample_image( image_array: np.ndarray, original_spacing: Tuple[float, ...], target_spacing: Tuple[float, ...], interpolator: str = 'linear' ) -> np.ndarray **Parameters:** * **image_array** (*np.ndarray*) - Input image (3D or 4D) * **original_spacing** (*tuple*) - Current voxel spacing * **target_spacing** (*tuple*) - Desired voxel spacing * **interpolator** (*str*) - Interpolation method: ``'linear'``, ``'nearest'``, or ``'bspline'`` **Returns:** * **resampled_image** (*np.ndarray*) - Resampled image **Example:** :: from medtda.utils import resample_image # Resample to isotropic 1mm spacing resampled = resample_image( image, original_spacing=(0.5, 0.5, 2.0), target_spacing=(1.0, 1.0, 1.0), interpolator='linear' ) print(f"Original shape: {image.shape}") print(f"Resampled shape: {resampled.shape}") Normalization ------------- normalize_image ~~~~~~~~~~~~~~~ .. autofunction:: medtda.utils.normalize_image Normalize image intensities using various methods. **Signature:** .. code-block:: python def normalize_image( image_array: np.ndarray, method: str = 'minmax', mask: Optional[np.ndarray] = None, label: Optional[int] = 1, clip_percentiles: Optional[Tuple[float, float]] = None ) -> np.ndarray **Parameters:** * **image_array** (*np.ndarray*) - Input image array * **method** (*str*) - Normalization method: ``'minmax'``, ``'zscore'``, or ``'robust'`` * **mask** (*np.ndarray or None*) - Optional mask (normalize masked region only) * **label** (*int or None*) - For multi-label masks, which label to consider as foreground * **clip_percentiles** (*tuple or None*) - If provided, clip values to (lower_percentile, upper_percentile) before normalization. For example, (1, 99) clips to 1st and 99th percentiles. Set to None to disable clipping. **Returns:** * **normalized_image** (*np.ndarray*) - Normalized image **Methods:** * **minmax:** Scales to [0, 1] range * **zscore:** Mean=0, std=1 * **robust:** Median=0, IQR=1 **Example:** :: from medtda.utils import normalize_image # Min-max normalization norm_minmax = normalize_image(image, method='minmax') # Z-score normalization norm_zscore = normalize_image(image, method='zscore') # Robust normalization (good for outliers) norm_robust = normalize_image(image, method='robust') # Normalize only inside mask norm_masked = normalize_image( image, method='minmax', mask=roi_mask ) Masking ------- apply_mask ~~~~~~~~~~ .. autofunction:: medtda.utils.apply_mask Apply a binary mask to an image. **Signature:** .. code-block:: python def apply_mask( image: np.ndarray, mask: np.ndarray, background_value: float = 0.0 ) -> np.ndarray **Parameters:** * **image** (*np.ndarray*) - Input image * **mask** (*np.ndarray*) - Binary mask * **background_value** (*float*) - Value for masked-out pixels **Returns:** * **masked_image** (*np.ndarray*) - Image with mask applied **Example:** :: from medtda.utils import apply_mask # Set background to 0 masked = apply_mask(image, mask, background_value=0.0) # Use image minimum as background masked = apply_mask(image, mask, background_value=image.min()) extract_label_from_mask ~~~~~~~~~~~~~~~~~~~~~~~~ .. autofunction:: medtda.utils.extract_label_from_mask Extract a single label from a multi-label mask. **Signature:** .. code-block:: python def extract_label_from_mask( mask: np.ndarray, label: int ) -> np.ndarray **Parameters:** * **mask** (*np.ndarray*) - Multi-label mask * **label** (*int*) - Label value to extract **Returns:** * **binary_mask** (*np.ndarray*) - Binary mask for the specified label **Example:** :: from medtda.utils import extract_label_from_mask # Multi-label mask: 0=background, 1=liver, 2=tumor liver_mask = extract_label_from_mask(multi_mask, label=1) tumor_mask = extract_label_from_mask(multi_mask, label=2) ROI Cropping ------------ crop_to_roi ~~~~~~~~~~~ .. autofunction:: medtda.utils.crop_to_roi Crop image to ROI bounding box with optional padding. **Signature:** .. code-block:: python def crop_to_roi( image: np.ndarray, mask: np.ndarray, padding: int = 0 ) -> Tuple[np.ndarray, np.ndarray] **Parameters:** * **image** (*np.ndarray*) - Input image * **mask** (*np.ndarray*) - Binary mask defining ROI * **padding** (*int*) - Padding in pixels on all sides **Returns:** * **cropped_image** (*np.ndarray*) - Cropped image * **cropped_mask** (*np.ndarray*) - Cropped mask **Example:** :: from medtda.utils import crop_to_roi # Crop to ROI bounding box cropped_img, cropped_mask = crop_to_roi(image, mask) # Crop with 10-pixel padding cropped_img, cropped_mask = crop_to_roi( image, mask, padding=10 ) print(f"Original shape: {image.shape}") print(f"Cropped shape: {cropped_img.shape}") Statistics ---------- get_image_statistics ~~~~~~~~~~~~~~~~~~~~~ .. autofunction:: medtda.utils.get_image_statistics Compute image statistics. **Signature:** .. code-block:: python def get_image_statistics( image: np.ndarray, mask: Optional[np.ndarray] = None ) -> dict **Parameters:** * **image** (*np.ndarray*) - Input image * **mask** (*np.ndarray or None*) - Optional mask **Returns:** * **stats** (*dict*) - Dictionary with statistics: * ``'mean'``, ``'std'``, ``'min'``, ``'max'`` * ``'median'``, ``'q25'``, ``'q75'`` * ``'shape'``, ``'dtype'`` **Example:** :: from medtda.utils import get_image_statistics # Get statistics stats = get_image_statistics(image) print(f"Mean: {stats['mean']:.2f}") print(f"Std: {stats['std']:.2f}") print(f"Range: [{stats['min']:.2f}, {stats['max']:.2f}]") # Statistics inside mask only stats_roi = get_image_statistics(image, mask=roi_mask) Validation ---------- check_array_dimensions ~~~~~~~~~~~~~~~~~~~~~~ .. autofunction:: medtda.utils.check_array_dimensions Validate array dimensions. **Signature:** .. code-block:: python def check_array_dimensions( array: np.ndarray, expected_ndim: int, name: str = 'array' ) -> None **Raises:** * **ValueError** if dimensions don't match **Example:** :: from medtda.utils import check_array_dimensions # Validate 3D image check_array_dimensions(image, expected_ndim=3, name='image') # Validate 2D mask check_array_dimensions(mask_2d, expected_ndim=2, name='mask') safe_divide ~~~~~~~~~~~ .. autofunction:: medtda.utils.safe_divide Safely divide two arrays, handling division by zero. **Signature:** .. code-block:: python def safe_divide( numerator: np.ndarray, denominator: np.ndarray ) -> np.ndarray **Returns:** * **result** (*np.ndarray*) - Division result with zeros where denominator is zero **Example:** :: from medtda.utils import safe_divide # Avoid division by zero result = safe_divide(numerator, denominator) Complete Examples ================= Example 1: Complete Preprocessing Pipeline ------------------------------------------- :: from medtda.utils import ( resample_image, normalize_image, extract_label_from_mask, crop_to_roi ) # Step 1: Resample to isotropic spacing resampled = resample_image( image, original_spacing=(0.5, 0.5, 2.0), target_spacing=(1.0, 1.0, 1.0) ) # Step 2: Extract liver from multi-label mask liver_mask = extract_label_from_mask(multi_mask, label=1) # Step 3: Crop to liver ROI cropped, cropped_mask = crop_to_roi( resampled, liver_mask, padding=5 ) # Step 4: Normalize normalized = normalize_image( cropped, method='robust', mask=cropped_mask ) print(f"Final shape: {normalized.shape}") Example 2: Comparing Normalization Methods ------------------------------------------- :: from medtda.utils import normalize_image, get_image_statistics methods = ['minmax', 'zscore', 'robust'] for method in methods: normalized = normalize_image(image, method=method) stats = get_image_statistics(normalized) print(f"\n{method} normalization:") print(f" Mean: {stats['mean']:.4f}") print(f" Std: {stats['std']:.4f}") print(f" Range: [{stats['min']:.4f}, {stats['max']:.4f}]") Example 3: ROI-Based Processing -------------------------------- :: from medtda.utils import ( extract_label_from_mask, crop_to_roi, apply_mask, get_image_statistics ) # Extract specific organ organ_mask = extract_label_from_mask(multi_label_mask, label=2) # Get statistics before cropping stats_full = get_image_statistics(image, mask=organ_mask) # Crop to organ bounding box cropped_img, cropped_mask = crop_to_roi( image, organ_mask, padding=10 ) # Apply mask to cropped image masked = apply_mask( cropped_img, cropped_mask, background_value=0.0 ) # Get statistics after cropping stats_cropped = get_image_statistics(masked) print(f"Volume reduction: {image.size / masked.size:.2f}x") Example 4: Custom Resampling Strategy -------------------------------------- :: from medtda.utils import resample_image # Original anisotropic spacing original_spacing = (0.7, 0.7, 3.0) # Strategy 1: Isotropic 1mm iso_1mm = resample_image( image, original_spacing, (1.0, 1.0, 1.0) ) # Strategy 2: Keep in-plane, resample through-plane semi_iso = resample_image( image, original_spacing, (0.7, 0.7, 1.0) ) # Strategy 3: Downsample for speed downsampled = resample_image( image, original_spacing, (2.0, 2.0, 2.0) ) print(f"Original: {image.shape}") print(f"Isotropic 1mm: {iso_1mm.shape}") print(f"Semi-isotropic: {semi_iso.shape}") print(f"Downsampled: {downsampled.shape}") Example 5: Multi-Label Processing ---------------------------------- :: from medtda.utils import extract_label_from_mask import numpy as np # Get all labels unique_labels = np.unique(multi_label_mask) print(f"Labels: {unique_labels}") # Extract and process each label label_results = {} for label in unique_labels[1:]: # Skip background (0) # Extract label binary_mask = extract_label_from_mask(multi_label_mask, label) # Crop to this label's ROI cropped_img, cropped_mask = crop_to_roi( image, binary_mask, padding=5 ) # Get statistics stats = get_image_statistics(cropped_img, mask=cropped_mask) label_results[label] = { 'shape': cropped_img.shape, 'stats': stats } # Print results for label, result in label_results.items(): print(f"\nLabel {label}:") print(f" Cropped shape: {result['shape']}") print(f" Mean intensity: {result['stats']['mean']:.2f}") Common Patterns =============== Pattern 1: Quick Normalization ------------------------------- :: from medtda.utils import normalize_image normalized = normalize_image(image, method='minmax') Pattern 2: Masked Normalization -------------------------------- :: from medtda.utils import normalize_image, apply_mask # Normalize inside mask normalized = normalize_image(image, method='robust', mask=roi_mask) # Apply mask to result final = apply_mask(normalized, roi_mask) Pattern 3: Resampling and Normalization ---------------------------------------- :: from medtda.utils import resample_image, normalize_image # Resample resampled = resample_image( image, (0.5, 0.5, 2.0), (1.0, 1.0, 1.0) ) # Normalize normalized = normalize_image(resampled, method='minmax') Pattern 4: Label Extraction and Cropping ----------------------------------------- :: from medtda.utils import extract_label_from_mask, crop_to_roi # Extract specific label binary = extract_label_from_mask(multi_mask, label=2) # Crop to bounding box cropped_img, cropped_mask = crop_to_roi(image, binary, padding=10) Performance Tips ================ 1. **Interpolation method:** * ``interpolator='nearest'``: Fastest, use for masks/labels * ``interpolator='linear'``: Good balance, use for most images * ``interpolator='bspline'``: Slower, higher quality 2. **Crop before normalize:** Reduces computation:: # Faster cropped, _ = crop_to_roi(image, mask) normalized = normalize_image(cropped) # Slower normalized = normalize_image(image) cropped, _ = crop_to_roi(normalized, mask) 3. **Reuse masks:** Extract labels once:: # Extract once tumor_mask = extract_label_from_mask(multi_mask, label=2) # Use multiple times cropped1, _ = crop_to_roi(image1, tumor_mask) cropped2, _ = crop_to_roi(image2, tumor_mask) See Also ======== * :doc:`preprocessor` - High-level preprocessing class * :doc:`loaders` - Image loading and validation * :doc:`../user_guide/preprocessing` - Preprocessing guide * :doc:`featureextractor` - Complete feature extraction