Basic Usage

This example demonstrates the fundamental usage patterns of MedTDA for feature extraction.

Simple Feature Extraction

Minimal Example

Extract features from a single image with default settings:

from medtda import FeatureExtractor

# Create extractor with defaults
extractor = FeatureExtractor()

# Extract features
features = extractor.execute('path/to/image.nii.gz')

print(f"Number of features extracted: {len(features)}")

Output:

Number of features extracted: 114

With Custom Parameters

Configure preprocessing and vectorization:

from medtda import FeatureExtractor

extractor = FeatureExtractor(
    # Preprocessing
    normalize=True,
    normalize_method='robust',

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

    # Vectorization
    vectorization_method='PersImage'
)

features = extractor.execute('ct_scan.nii.gz')

# features is a flat dict: {'PersImage_H0_f0': ..., 'PersImage_H0_f1': ..., ...}
print(f"Number of persistence image features: {len(features)}")

Output:

Number of persistence image features: 1200

Using Masks

Binary Mask

Process only the region of interest:

from medtda import FeatureExtractor

extractor = FeatureExtractor(
    normalize=True,
    crop_to_roi=True,
    roi_padding=5
)

features = extractor.execute(
    image='liver_scan.nii.gz',
    mask='liver_mask.nii.gz'
)

What this does:

  1. Loads image and mask

  2. Crops to liver bounding box (with 5-pixel padding)

  3. Applies mask (sets background to 0)

  4. Normalizes intensities

  5. Computes persistent homology

  6. Returns feature vector

Multi-Label Mask

Extract specific label from multi-label segmentation:

from medtda import FeatureExtractor

# Multi-label mask: 0=background, 1=liver, 2=tumor
extractor = FeatureExtractor(
    label=2,  # Extract tumor only
    crop_to_roi=True,
    normalize=True
)

features = extractor.execute(
    image='ct_abdomen.nii.gz',
    mask='organs_mask.nii.gz'  # Multi-label
)

print("Extracted features from tumor region only")

Different Image Formats

3D NIFTI Images

Most common medical imaging format:

from medtda import FeatureExtractor
from medtda.loaders import load_image

# Load and inspect
image, metadata = load_image('brain_mri.nii.gz')
print(f"Image shape: {image.shape}")
print(f"Spacing: {metadata.get('spacing')}")

# Extract features
extractor = FeatureExtractor()
features = extractor.execute(image)  # Can pass array directly

Output:

Image shape: (256, 256, 180)
Spacing: (1.0, 1.0, 1.0)

2D PNG/TIFF Images

For microscopy or 2D radiographs:

from medtda import FeatureExtractor

# 2D images → only H0 and H1 dimensions available
extractor = FeatureExtractor(
    max_dimension=1,  # H0 and H1 only
    normalize=True
)

features = extractor.execute('microscopy_image.png')

print("2D image processed - H0 and H1 features extracted")

NumPy Arrays

Work directly with arrays:

import numpy as np
from medtda import FeatureExtractor

# Create or load image as numpy array
image_array = np.random.rand(100, 100, 100)

# Process array directly
extractor = FeatureExtractor(normalize=False)
features = extractor.execute(image_array)

Accessing Barcodes

Get Raw Persistence Barcodes

from medtda import FeatureExtractor

extractor = FeatureExtractor(
    vectorization_method='PersStats',
    return_barcodes=True  # Return barcodes too
)

features, barcodes = extractor.execute('image.nii.gz')

# Access barcodes by dimension
h0_barcode = barcodes['H0']  # Connected components
h1_barcode = barcodes['H1']  # Loops

print(f"H0 features: {len(h0_barcode)}")
print(f"H1 features: {len(h1_barcode)}")

# Analyze persistence
h1_persistence = h1_barcode[:, 1] - h1_barcode[:, 0]
print(f"Mean H1 persistence: {h1_persistence.mean():.3f}")

Output:

H0 features: 47
H1 features: 12
Mean H1 persistence: 0.234

Filter Barcodes by Persistence

import numpy as np

# Get barcodes
_, barcodes = extractor.execute('image.nii.gz')

# Filter H1 features
h1 = barcodes['H1']
persistence = h1[:, 1] - h1[:, 0]

# Keep only significant features
threshold = 0.1
significant_features = h1[persistence > threshold]

print(f"Total H1 features: {len(h1)}")
print(f"Significant (p > {threshold}): {len(significant_features)}")

Multiple Vectorization Methods

Single Method

from medtda import FeatureExtractor

extractor = FeatureExtractor(
    vectorization_method='PersImage'
)

features = extractor.execute('image.nii.gz')

# features is a dict with keys like 'PersImage_H0_f0', 'PersImage_H0_f1', etc.
print(f\"Extracted {len(features)} persistence image features\")

Multiple Methods

from medtda import FeatureExtractor

extractor = FeatureExtractor()

# Enable multiple methods
extractor.enable_vectorization_methods([
    'PersStats',
    'BettiCurve',
    'PersImage'
])

features = extractor.execute('image.nii.gz')

# features is a flat dict with all method features combined
# Keys: 'PersStats_H0_mean', 'BettiCurve_H0_f0', 'PersImage_H0_f0', ...
stats_features = {k: v for k, v in features.items() if k.startswith('PersStats')}
betti_features = {k: v for k, v in features.items() if k.startswith('BettiCurve')}
pi_features = {k: v for k, v in features.items() if k.startswith('PersImage')}
print(f"PersStats features: {len(stats_features)}")
print(f"BettiCurve features: {len(betti_features)}")
print(f"PersImage features: {len(pi_features)}")

Output:

PersStats features: 26
BettiCurve features: 200
PersImage features: 800

Configure Method Parameters

from medtda import FeatureExtractor

extractor = FeatureExtractor()
extractor.enable_vectorization_methods([
    'BettiCurve',
    'PersImage'
])

# Configure individual methods
extractor.set_vectorization_params(
    'BettiCurve',
    resolution=200  # Higher resolution
)

extractor.set_vectorization_params(
    'PersImage',
    resolution=30,    # Finer grid
    bandwidth=0.1     # Smaller bandwidth
)

features = extractor.execute('image.nii.gz')

Complete Workflow Examples

Example 1: CT Liver Analysis

from medtda import FeatureExtractor
from medtda.loaders import load_image, load_mask
import numpy as np

# Load data
image, img_meta = load_image('liver_ct.nii.gz')
mask, mask_meta = load_mask('liver_segmentation.nii.gz')

print(f"Image shape: {image.shape}")
print(f"Image spacing: {img_meta.get('spacing')}")

# Configure for CT
extractor = FeatureExtractor(
    # CT-specific preprocessing
    spacing=(1.0, 1.0, 1.0),      # Resample to isotropic
    window=(40, 400),              # Soft tissue window
    normalize=True,
    normalize_method='robust',

    # ROI processing
    crop_to_roi=True,
    roi_padding=5,

    # PH parameters
    filtration_type='sublevel',
    max_dimension=2,

    # Vectorization
    vectorization_method='PersImage'
)

# Extract features
features = extractor.execute(image, mask)

# features is a flat dict: {'PersImage_H0_f0': ..., 'PersImage_H1_f0': ..., ...}
feature_vector = np.array(list(features.values()))
print(f"Feature vector size: {len(feature_vector)}")

# Could now use for ML:
# model.predict(feature_vector.reshape(1, -1))

Example 2: Brain MRI Tumor Classification

from medtda import FeatureExtractor
import numpy as np

# Configure for brain MRI
extractor = FeatureExtractor(
    # Preprocessing
    normalize=True,
    normalize_method='zscore',  # Good for MRI
    crop_to_roi=True,

    # Use multiple methods for classification
    vectorization_method=[
        'PersStats',
        'BettiCurve',
        'PersImage'
    ]
)

# Process
features = extractor.execute(
    'brain_t1.nii.gz',
    'tumor_mask.nii.gz'
)

# features is a flat dict with all method features combined
# Keys: 'PersStats_H0_mean', 'BettiCurve_H0_f0', 'PersImage_H0_f0', ...
all_features = np.array(list(features.values()))

print(f"Combined feature vector: {len(all_features)} dimensions")

Example 3: Microscopy Image Analysis

from medtda import FeatureExtractor
from medtda.loaders import load_image

# Load 2D microscopy image
image, metadata = load_image('histology.tiff')

# Configure for 2D
extractor = FeatureExtractor(
    normalize=True,
    max_dimension=1,  # Only H0 and H1 for 2D
    filtration_type='sublevel',
    vectorization_method='PersImage',
    return_barcodes=True
)

# Extract
features, barcodes = extractor.execute(image)

# Analyze
h0_persistence = barcodes['H0'][:, 1] - barcodes['H0'][:, 0]
h1_persistence = barcodes['H1'][:, 1] - barcodes['H1'][:, 0]

print(f"Cells/nuclei (H0): {np.sum(h0_persistence > 0.05)}")
print(f"Glands/structures (H1): {np.sum(h1_persistence > 0.1)}")

Example 4: Quick Feature Inspection

from medtda import FeatureExtractor

# Fast inspection with minimal features
extractor = FeatureExtractor(
    normalize=True,
    max_dimension=1,  # Faster
    vectorization_method='PersStats'  # Fastest
)

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

# Access named statistics directly
print("Quick topology summary:")
print(f"  H0 features detected: {features.get('PersStats_H0_count', 'N/A')}")
print(f"  H0 mean persistence: {features.get('PersStats_H0_mean', 'N/A')}")
print(f"  H0 max persistence: {features.get('PersStats_H0_max', 'N/A')}")

Using with Machine Learning

Scikit-learn Integration

from medtda import FeatureExtractor
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
import numpy as np

# Configure extractor
extractor = FeatureExtractor(
    normalize=True,
    vectorization_method='PersImage'
)

# Process training data
X = []
y = []

for image_path, label in training_data:
    features = extractor.execute(image_path)
    feature_vector = np.array(list(features.values()))
    X.append(feature_vector)
    y.append(label)

X = np.array(X)
y = np.array(y)

# Train model
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)

clf = RandomForestClassifier(n_estimators=100)
clf.fit(X_train, y_train)

# Evaluate
score = clf.score(X_test, y_test)
print(f"Accuracy: {score:.3f}")

Tips and Best Practices

  1. Start with Default Settings

    extractor = FeatureExtractor()
    features = extractor.execute(image)
    
  2. Always Normalize

    extractor = FeatureExtractor(normalize=True)
    
  3. Use ROI Cropping for Speed

    extractor = FeatureExtractor(crop_to_roi=True)
    
  4. Check Image Properties First

    from medtda.loaders import load_image
    
    image, metadata = load_image('scan.nii.gz')
    print(f"Shape: {image.shape}")
    print(f"Value range: [{image.min()}, {image.max()}]")
    print(f"Spacing: {metadata.get('spacing')}")
    
  5. Start with Fast Methods

    # For initial exploration
    extractor = FeatureExtractor(
        max_dimension=1,  # Skip H2
       vectorization_method='PersStats'  # Fastest
    

Common Pitfalls

Issue: Shape Mismatch

# Wrong: mask doesn't match image
features = extractor.execute('image.nii.gz', 'wrong_mask.nii.gz')
# Error: ValueError: Image and mask shapes don't match

# Fix: Verify compatibility
from medtda.loaders import load_image, load_mask, validate_compatibility

image, _ = load_image('image.nii.gz')
mask, _ = load_mask('mask.nii.gz')
validate_compatibility(image, mask)

Issue: Empty Barcodes

# If ROI is too small or homogeneous
features, barcodes = extractor.execute(image, mask)

# Check for empty barcodes
for dim, barcode in barcodes.items():
    if len(barcode) == 0:
        print(f"Warning: No H{dim} features detected")

Issue: Memory Error on Large Images

# Problem: 3D image too large

# Solution 1: Downsample
extractor = FeatureExtractor(spacing=(2.0, 2.0, 2.0))

# Solution 2: Crop to ROI
extractor = FeatureExtractor(crop_to_roi=True)

# Solution 3: Process 2D slices instead

Next Steps

See Also