Preprocessing

MedTDA provides comprehensive preprocessing capabilities for medical images. This guide covers all preprocessing options and how to use them effectively.

Overview

Preprocessing transforms raw medical images into a standardized format suitable for persistent homology computation. MedTDA supports:

  • Format Conversion - Load various medical image formats

  • Intensity Normalization - Standardize intensity values

  • Resampling - Change voxel spacing (3D/4D only)

  • Windowing - Apply intensity windowing (CT images)

  • Masking - Apply binary or multi-label masks

  • ROI Cropping - Reduce computation by cropping to region of interest

All preprocessing is configured through FeatureExtractor parameters.

Supported Image Formats

2D Images

  • PNG (.png)

  • JPEG (.jpg, .jpeg)

  • TIFF (.tif, .tiff)

  • BMP (.bmp)

from medtda import FeatureExtractor
import numpy as np
from PIL import Image

# Load 2D image
image = np.array(Image.open('image.png').convert('L'))

extractor = FeatureExtractor()
features = extractor.execute(image)

3D/4D Medical Images

Requires SimpleITK:

  • NIfTI (.nii, .nii.gz)

  • NRRD (.nrrd)

  • MetaImage (.mha, .mhd)

import SimpleITK as sitk
from medtda import FeatureExtractor

# Load 3D medical image
image = sitk.ReadImage('scan.nii.gz')

extractor = FeatureExtractor()
features = extractor.execute(image)

File Paths vs Arrays

MedTDA accepts both file paths and NumPy/SimpleITK arrays:

# Using file paths (recommended for 3D)
features = extractor.execute(
    image='scan.nii.gz',
    mask='mask.nii.gz'
)

# Using NumPy arrays
import numpy as np
image_array = np.random.rand(100, 100, 50)
mask_array = np.random.randint(0, 2, (100, 100, 50))
features = extractor.execute(image=image_array, mask=mask_array)

# Using SimpleITK images
import SimpleITK as sitk
image_sitk = sitk.ReadImage('scan.nii.gz')
features = extractor.execute(image=image_sitk)

Intensity Normalization

Normalization standardizes intensity values across images, crucial for consistent TDA features.

Enable Normalization

extractor = FeatureExtractor(
    normalize=True,              # Enable normalization
    normalize_method='minmax'    # Choose method
)

Normalization Methods

Min-Max Normalization (minmax)

Scales values to [0, 1]:

\[x_{norm} = \frac{x - x_{min}}{x_{max} - x_{min}}\]
extractor = FeatureExtractor(
    normalize=True,
    normalize_method='minmax'
)

Pros: Preserves relative differences, bounded output

Z-Score Normalization (zscore)

Standardizes to zero mean and unit variance:

\[x_{norm} = \frac{x - \mu}{\sigma}\]
extractor = FeatureExtractor(
    normalize=True,
    normalize_method='zscore'
)

Pros: Removes scale, handles outliers better than minmax

Robust Scaling (robust)

Uses median and interquartile range:

\[x_{norm} = \frac{x - median(x)}{IQR(x)}\]
extractor = FeatureExtractor(
    normalize=True,
    normalize_method='robust'
)

Pros: Very robust to outliers

Resampling and Spacing

Resampling changes voxel spacing, useful for standardizing spatial resolution across 3D/4D images.

Target Spacing

Specify target spacing in mm (or image units):

# Resample to 1mm isotropic (same spacing in all directions)
extractor = FeatureExtractor(
    spacing=(1.0, 1.0, 1.0)
)

# Resample to anisotropic spacing
extractor = FeatureExtractor(
    spacing=(0.5, 0.5, 2.0)  # Higher resolution in xy, lower in z
)

For 2D images:

extractor = FeatureExtractor(
    spacing=(0.25, 0.25)  # 2D spacing
)

Why Resample?

Benefits:

  • Standardizes spatial resolution across images

  • Enables fair comparison of topological features

  • Can reduce computation time (downsample large images)

  • Makes features independent of acquisition parameters

Considerations:

  • Upsampling (small spacing) increases computation time

  • Downsampling (large spacing) may lose fine details

  • Choose spacing based on feature size of interest

Common Spacing Values

CT Scans:

# Whole body: 2-3mm isotropic
extractor = FeatureExtractor(spacing=(2.0, 2.0, 2.0))

# Chest/abdomen: 1-1.5mm isotropic
extractor = FeatureExtractor(spacing=(1.0, 1.0, 1.0))

# High-resolution: 0.5-0.75mm isotropic
extractor = FeatureExtractor(spacing=(0.5, 0.5, 0.5))

MRI:

# Standard brain MRI: 1mm isotropic
extractor = FeatureExtractor(spacing=(1.0, 1.0, 1.0))

Automatic Detection

If spacing=None (default), no resampling is performed:

extractor = FeatureExtractor(spacing=None)  # Use original spacing

Windowing

Windowing applies an intensity window, commonly used for CT images to focus on specific tissue types.

Window Parameters

Specify as (center, width):

# Soft tissue window for CT
extractor = FeatureExtractor(
    window=(40, 400)  # Center=40 HU, Width=400 HU
)

# Lung window
extractor = FeatureExtractor(
    window=(-600, 1500)  # Center=-600 HU, Width=1500 HU
)

# Bone window
extractor = FeatureExtractor(
    window=(400, 1800)  # Center=400 HU, Width=1800 HU
)

Values outside the window are clipped.

Common CT Windows

Tissue Type

Center (HU)

Width (HU)

Use Case

Soft Tissue

40

400

General abdomen/pelvis

Lung

-600

1500

Chest CT, lung nodules

Bone

400

1800

Skeletal imaging

Brain

40

80

Brain CT

Liver

60

150

Liver lesions

Masking

Masks restrict analysis to specific regions of interest.

Binary Masks

Simple foreground/background masks:

extractor = FeatureExtractor(
    background_value=0,  # Value for background pixels
    crop_to_roi=True     # Crop to mask bounding box
)

features = extractor.execute(
    image='scan.nii.gz',
    mask='binary_mask.nii.gz'  # 0=background, >0=foreground
)

Multi-Label Masks

Extract specific labels from multi-label segmentations:

# Extract label 1 (e.g., tumor)
extractor = FeatureExtractor(
    label=1,             # Extract this label only
    background_value=0,  # Value for background
    crop_to_roi=True
)

features = extractor.execute('scan.nii.gz', 'multilabel_mask.nii.gz')

Process Multiple Labels

Extract features for each label separately:

labels = [1, 2, 3]  # Tumor core, edema, enhancing

results = {}
for label_id in labels:
    extractor = FeatureExtractor(
        label=label_id,
        normalize=True,
        crop_to_roi=True,
        vectorization_method='PersStats'
    )
    features = extractor.execute('scan.nii.gz', 'mask.nii.gz')
    results[f'label_{label_id}'] = features

Background Value

Control what value to assign to masked-out regions:

# Set background to 0 (common)
extractor = FeatureExtractor(background_value=0)

# Set background to minimum image value
extractor = FeatureExtractor(background_value=None)  # Auto-detect

# Set background to specific value
extractor = FeatureExtractor(background_value=-1000)  # E.g., air in CT

ROI Cropping

Cropping to the region of interest reduces memory usage and computation time.

Enable ROI Cropping

extractor = FeatureExtractor(
    crop_to_roi=True,   # Enable cropping (default)
    roi_padding=1       # Padding in pixels (default=1)
)

Padding

Add padding around the ROI bounding box:

# No padding (tight crop)
extractor = FeatureExtractor(crop_to_roi=True, roi_padding=0)

# Small padding (1-2 pixels)
extractor = FeatureExtractor(crop_to_roi=True, roi_padding=2)

# Large padding (preserve context)
extractor = FeatureExtractor(crop_to_roi=True, roi_padding=5)

Recommendations:

  • Use roi_padding=1 or 2 for most cases

  • Increase for features that depend on surrounding context

  • Decrease to 0 for maximum speed on large volumes

Disable Cropping

Process the full image:

extractor = FeatureExtractor(crop_to_roi=False)

When to disable:

  • ROI is already tightly cropped

  • Studying whole-image topology

  • Very small images

Complete Preprocessing Example

Combining all preprocessing options:

from medtda import FeatureExtractor

extractor = FeatureExtractor(
    # Resampling
    spacing=(1.0, 1.0, 1.0),      # Isotropic 1mm

    # Windowing
    window=(40, 400),              # Soft tissue window

    # Normalization
    normalize=True,                # Enable normalization
    normalize_method='minmax',     # Min-max to [0,1]

    # Masking
    label=1,                       # Extract label 1
    background_value=0,            # Background = 0

    # ROI Cropping
    crop_to_roi=True,             # Enable cropping
    roi_padding=2,                # 2-pixel padding

    # Persistent Homology
    filtration_type='sublevel',
    max_dimension=2,

    # Vectorization
    vectorization_method='PersStats'
)

# Extract features
features = extractor.execute('ct_scan.nii.gz', 'tumor_mask.nii.gz')

Using the Preprocessor Directly

For standalone preprocessing without feature extraction:

from medtda import Preprocessor

preprocessor = Preprocessor(
    spacing=(1.0, 1.0, 1.0),
    window=(40, 400),
    normalize=True,
    normalize_method='minmax',
    background_value=0,
    label=1,
    crop_to_roi=True,
    roi_padding=2
)

# Preprocess image
image_preprocessed, mask_preprocessed = preprocessor.execute(
    image='scan.nii.gz',
    mask='mask.nii.gz'
)

# Returns NumPy arrays
print(image_preprocessed.shape)

Best Practices

  1. Always normalize for consistency across images

  2. Resample to isotropic spacing for 3D images (e.g., 1mm)

  3. Use ROI cropping to improve performance

  4. Choose appropriate windowing for CT images

  5. Process each label separately for multi-label masks

  6. Document preprocessing parameters for reproducibility

Troubleshooting

Image is too large / out of memory

  • Increase spacing to downsample

  • Enable crop_to_roi

  • Reduce roi_padding

Features are inconsistent across images

  • Ensure all images use same preprocessing

  • Always enable normalize

  • Use consistent spacing across dataset

Preprocessing is slow

  • Downsample with larger spacing

  • Enable crop_to_roi

  • Skip windowing if not needed

Mask and image dimensions don’t match

  • Check that mask corresponds to the image

  • Ensure both are loaded correctly

  • Verify coordinate systems match (for medical images)

Next Steps