Utils

The utils module provides utility functions for image processing, including resampling, normalization, masking, and cropping.

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

medtda.utils.resample_image(image_array, original_spacing, target_spacing, interpolator='linear')[source]

Resample image to a new voxel spacing using SimpleITK.

This function resamples a 3D or 4D image to match the target spacing, which is useful for standardizing images from different scanners or acquisition protocols.

Parameters:
  • image_array (numpy.ndarray) – Input image array (3D or 4D).

  • original_spacing (tuple of float) – Original voxel spacing (x, y, z) or (x, y, z, t). For SimpleITK, this is typically (width, height, depth).

  • target_spacing (tuple of float) – Target voxel spacing (x, y, z) or (x, y, z, t).

  • interpolator ({'linear', 'nearest', 'bspline'}, default='linear') – Interpolation method: - ‘linear’: Linear interpolation (good for images) - ‘nearest’: Nearest neighbor (good for masks/labels) - ‘bspline’: B-spline interpolation (smooth but slower)

Returns:

Resampled image array.

Return type:

numpy.ndarray

Raises:
  • ImportError – If SimpleITK is not installed.

  • ValueError – If image dimensions or spacing don’t match.

Examples

>>> import numpy as np
>>> image = np.random.rand(100, 100, 50)
>>> # Resample from 1mm × 1mm × 2mm to isotropic 1mm³
>>> resampled = resample_image(
...     image,
...     original_spacing=(1.0, 1.0, 2.0),
...     target_spacing=(1.0, 1.0, 1.0)
... )
>>> print(resampled.shape)
(100, 100, 100)

Resample an image to target voxel spacing.

Signature:

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

medtda.utils.normalize_image(image_array, method='minmax', mask=None, label=1, clip_percentiles=None)[source]

Normalize image array.

Parameters:
  • image_array (numpy.ndarray) – Input image array to normalize.

  • method ({'minmax', 'zscore', 'robust'}, default='minmax') – Normalization method: - ‘minmax’: Min-max scaling to [0, 1] - ‘zscore’: Z-score normalization (mean=0, std=1) - ‘robust’: Robust scaling using median and IQR

  • mask (numpy.ndarray, optional) – Mask array. If provided, statistics are computed only within the masked region.

  • label (int or None, default=1) – For multi-label masks, which label value to consider as foreground. If None, any non-zero value is considered foreground.

  • clip_percentiles (tuple of float, optional) – If provided, clip values to (lower_percentile, upper_percentile) before normalization. For example, (1, 99) clips to 1st and 99th percentiles, removing extreme outliers.

Returns:

Normalized image array.

Return type:

numpy.ndarray

Raises:

ValueError – If method is not recognized or arrays have incompatible shapes.

Normalize image intensities using various methods.

Signature:

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

medtda.utils.apply_mask(image_array, mask, background_value=0, label=1)[source]

Apply mask to image array.

Sets values outside the mask to background_value.

Parameters:
  • image_array (numpy.ndarray) – Input image array.

  • mask (numpy.ndarray) – Mask array (same shape as image).

  • background_value (float, default=0) – Value to assign to pixels outside the mask.

  • label (int or None, default=1) – For multi-label masks, which label value to consider as foreground. If None, any non-zero value is considered foreground.

Returns:

Masked image array.

Return type:

numpy.ndarray

Raises:

ValueError – If mask shape doesn’t match image shape.

Apply a binary mask to an image.

Signature:

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

medtda.utils.extract_label_from_mask(mask, label=None)[source]

Convert multi-label mask to binary mask for specified label.

Parameters:
  • mask (numpy.ndarray) – Input mask array (can be binary or multi-label).

  • label (int or None, default=None) – Label value to extract. If None, treat mask as binary (any non-zero value is considered foreground).

Returns:

Binary mask where specified label = 1, others = 0.

Return type:

numpy.ndarray

Examples

>>> multi_label_mask = np.array([[0, 1, 2], [1, 2, 3]])
>>> binary_mask = extract_label_from_mask(multi_label_mask, label=2)
>>> print(binary_mask)
[[0 0 1]
 [0 1 0]]

Extract a single label from a multi-label mask.

Signature:

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

medtda.utils.crop_to_roi(image, mask, padding=1, label=1)[source]

Crop image and mask to the bounding box of the Region of Interest (ROI).

This function finds the smallest bounding box containing all non-zero mask values and crops both image and mask to this region with optional padding. This reduces memory usage and speeds up persistent homology computation.

Parameters:
  • image (numpy.ndarray) – Input image array.

  • mask (numpy.ndarray) – Mask array (same shape as image).

  • padding (int, default=1) – Number of background pixels to include on all sides of the ROI. This ensures the ROI is not cut too tightly.

  • label (int or None, default=1) – For multi-label masks, which label value to consider as foreground. If None, any non-zero value is considered foreground.

Return type:

Tuple[ndarray, ndarray, dict]

Returns:

  • cropped_image (numpy.ndarray) – Cropped image array.

  • cropped_mask (numpy.ndarray) – Cropped mask array.

  • crop_info (dict) – Dictionary containing cropping information: - ‘bbox’: Tuple of slices defining the bounding box - ‘original_shape’: Original image shape - ‘cropped_shape’: Shape after cropping - ‘roi_size’: Number of voxels in ROI before padding - ‘reduction_factor’: Ratio of cropped size to original size

Raises:

ValueError – If mask shape doesn’t match image shape or if mask is all zeros.

Examples

>>> import numpy as np
>>> image = np.random.rand(100, 100, 100)
>>> mask = np.zeros((100, 100, 100))
>>> mask[40:60, 40:60, 40:60] = 1  # Small ROI
>>> cropped_img, cropped_mask, info = crop_to_roi(image, mask, padding=1)
>>> print(f"Original: {image.shape}, Cropped: {cropped_img.shape}")
Original: (100, 100, 100), Cropped: (22, 22, 22)
>>> print(f"Reduction: {info['reduction_factor']:.2f}x smaller")
Reduction: 10.65x smaller

Crop image to ROI bounding box with optional padding.

Signature:

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

medtda.utils.get_image_statistics(image_array, mask=None, label=1)[source]

Compute basic statistics for image array.

Parameters:
  • image_array (numpy.ndarray) – Input image array.

  • mask (numpy.ndarray, optional) – Mask array. If provided, statistics are computed only within the masked region.

  • label (int or None, default=1) – For multi-label masks, which label value to consider as foreground. If None, any non-zero value is considered foreground.

Returns:

Dictionary containing: - ‘min’: Minimum value - ‘max’: Maximum value - ‘mean’: Mean value - ‘std’: Standard deviation - ‘median’: Median value - ‘q1’: First quartile - ‘q3’: Third quartile

Return type:

dict

Compute image statistics.

Signature:

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

medtda.utils.check_array_dimensions(array, expected_dims, name='array')[source]

Check if array has expected dimensions.

Parameters:
  • array (numpy.ndarray) – Array to check.

  • expected_dims (int or tuple of int) – Expected number of dimensions. If int, array must have exactly that many dimensions. If tuple, array must have one of those dimensions.

  • name (str, default='array') – Name of the array for error messages.

Raises:

ValueError – If array dimensions don’t match expectations.

Return type:

None

Validate array dimensions.

Signature:

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

medtda.utils.safe_divide(numerator, denominator)[source]

Safely divide two arrays, handling division by zero.

Parameters:
Returns:

Result of division with zeros where denominator is zero.

Return type:

numpy.ndarray

Safely divide two arrays, handling division by zero.

Signature:

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