CLI Workflows

This example demonstrates common workflows using the MedTDA command-line interface for batch processing, configuration management, and automation.

Basic CLI Usage

Single Image Processing

Extract features from one image:

medtda extract image.nii.gz

With mask:

medtda extract image.nii.gz --mask roi_mask.nii.gz

Specify output:

medtda extract image.nii.gz --output features.npy

Choose vectorization method:

medtda extract image.nii.gz --method persistence_image

Multiple methods:

medtda extract image.nii.gz --method persistence_stats --method betti_curve

Configuration Files

Basic Configuration

Create config.yaml:

# Preprocessing
preprocessing:
  normalize: true
  normalize_method: robust
  spacing: [1.0, 1.0, 1.0]
  crop_to_roi: true
  roi_padding: 5

# Persistent homology
persistent_homology:
  filtration_type: sublevel
  max_dimension: 2

# Vectorization
vectorization:
  method: persistence_image
  parameters:
    resolution: 20
    bandwidth: 0.1

Use configuration:

medtda extract image.nii.gz --config config.yaml

CT-Specific Configuration

Create config_ct.yaml:

preprocessing:
  window: [-60, 340]  # Soft tissue window
  spacing: [1.0, 1.0, 1.0]
  normalize: true
  normalize_method: robust
  crop_to_roi: true

persistent_homology:
  filtration_type: sublevel
  max_dimension: 2

vectorization:
  method: persistence_stats

Usage:

medtda extract ct_scan.nii.gz \
    --mask liver_mask.nii.gz \
    --config config_ct.yaml \
    --output ct_features.npy

MRI-Specific Configuration

Create config_mri.yaml:

preprocessing:
  normalize: true
  normalize_method: zscore
  spacing: [1.0, 1.0, 1.0]
  crop_to_roi: true
  clip_percentiles: [1, 99]

persistent_homology:
  filtration_type: sublevel
  max_dimension: 2

vectorization:
  method: persistence_image
  parameters:
    resolution: 30
    bandwidth: 0.08

Usage:

medtda extract brain_mri.nii.gz \
    --mask tumor_mask.nii.gz \
    --config config_mri.yaml

Batch Processing

Process Directory

Process all images in directory:

medtda batch process data/images/ --output results/

With glob pattern:

medtda batch process "data/images/*.nii.gz" --output results/

With masks:

medtda batch process data/images/ \
    --mask-dir data/masks/ \
    --output results/

Parallel processing:

medtda batch process data/images/ \
    --output results/ \
    --n-jobs 8

Using File Lists

Create file_list.txt:

data/patient001/image.nii.gz
data/patient002/image.nii.gz
data/patient003/image.nii.gz

Process from list:

medtda batch process --file-list file_list.txt --output results/

With corresponding masks:

Create files.csv:

image,mask
data/patient001/image.nii.gz,data/patient001/mask.nii.gz
data/patient002/image.nii.gz,data/patient002/mask.nii.gz
data/patient003/image.nii.gz,data/patient003/mask.nii.gz
medtda batch process --file-list files.csv --output results/

Output Formats

NumPy Format

Save as .npy (default):

medtda extract image.nii.gz --output features.npy

Load in Python:

import numpy as np
features = np.load('features.npy')

CSV Format

Save as CSV:

medtda extract image.nii.gz --output features.csv --format csv

Batch to CSV:

medtda batch process data/images/ \
    --output results.csv \
    --format csv

Example CSV output:

filename,feature_1,feature_2,...,feature_13
image001.nii.gz,0.234,0.456,...,0.789
image002.nii.gz,0.123,0.345,...,0.567

JSON Format

Save as JSON:

medtda extract image.nii.gz --output features.json --format json

Example output:

{
  "filename": "image.nii.gz",
  "method": "persistence_stats",
  "features": [0.234, 0.456, ..., 0.789],
  "metadata": {
    "n_features": 13,
    "image_shape": [256, 256, 180]
  }
}

HDF5 Format

Batch to HDF5:

medtda batch process data/images/ \
    --output dataset.h5 \
    --format hdf5

Load in Python:

import h5py

with h5py.File('dataset.h5', 'r') as f:
    features = f['features'][:]
    filenames = f['filenames'][:]

Advanced Workflows

Multi-Method Extraction

Extract multiple vectorization methods:

medtda batch process data/images/ \
    --method persistence_stats \
    --method betti_curve \
    --method persistence_image \
    --output results/

Output structure:

results/
  persistence_stats/
    image001_features.npy
    image002_features.npy
    ...
  betti_curve/
    image001_features.npy
    image002_features.npy
    ...
  persistence_image/
    image001_features.npy
    image002_features.npy
    ...

With Barcodes

Extract and save persistence barcodes:

medtda extract image.nii.gz \
    --save-barcodes \
    --output-dir results/

Output:

results/
  image_features.npy
  image_barcodes.npz

Load barcodes:

import numpy as np

barcodes = np.load('image_barcodes.npz')
h0 = barcodes['H0']
h1 = barcodes['H1']
h2 = barcodes['H2']

Quality Control

Validation Mode

Validate images before processing:

medtda batch validate data/images/ \
    --mask-dir data/masks/

Example output:

Validating 100 images...

✓ image001.nii.gz: OK
✗ image002.nii.gz: Shape mismatch with mask
✓ image003.nii.gz: OK
✗ image004.nii.gz: Empty mask
...

Summary:
  Valid: 96
  Invalid: 4

See validation_report.txt for details

Process only valid:

medtda batch process data/images/ \
    --mask-dir data/masks/ \
    --validate \
    --output results/

Error Handling

Skip errors and continue:

medtda batch process data/images/ \
    --output results/ \
    --skip-errors

Log errors:

medtda batch process data/images/ \
    --output results/ \
    --skip-errors \
    --error-log errors.log

Example error log:

image002.nii.gz: Shape mismatch with mask (256,256,180) vs (256,256,160)
image015.nii.gz: Empty ROI after masking
image043.nii.gz: Unable to load file: corrupted NIFTI header

Resume Processing

Resume interrupted batch:

medtda batch process data/images/ \
    --output results/ \
    --resume

How it works:

  • Checks which files already processed

  • Skips completed files

  • Continues from where it stopped

Filtering and Selection

By ROI Size

Only process if ROI is large enough:

medtda batch process data/images/ \
    --mask-dir data/masks/ \
    --min-roi-voxels 1000 \
    --output results/

By Label

Extract specific label from multi-label mask:

medtda extract image.nii.gz \
    --mask multilabel_mask.nii.gz \
    --label 2 \
    --output features.npy

Batch for all labels:

for label in 1 2 3; do
    medtda batch process data/images/ \
        --mask-dir data/masks/ \
        --label $label \
        --output results/label_$label/
done

Preprocessing Exploration

Different Normalizations

Compare normalization methods:

# MinMax
medtda extract image.nii.gz \
    --normalize minmax \
    --output features_minmax.npy

# Z-score
medtda extract image.nii.gz \
    --normalize zscore \
    --output features_zscore.npy

# Robust
medtda extract image.nii.gz \
    --normalize robust \
    --output features_robust.npy

Different Resolutions

Test different resamplings:

for spacing in 0.5 1.0 2.0; do
    medtda extract image.nii.gz \
        --spacing $spacing $spacing $spacing \
        --output features_spacing${spacing}.npy
done

Different Windows (CT)

Test CT windowing:

# Soft tissue
medtda extract ct_scan.nii.gz \
    --window -60 340 \
    --output features_soft_tissue.npy

# Lung
medtda extract ct_scan.nii.gz \
    --window -1000 400 \
    --output features_lung.npy

# Bone
medtda extract ct_scan.nii.gz \
    --window 300 1800 \
    --output features_bone.npy

Vectorization Parameter Tuning

Persistence Image Resolution

Test different resolutions:

for res in 10 20 30 50; do
    medtda extract image.nii.gz \
        --method persistence_image \
        --pi-resolution $res \
        --output features_pi${res}.npy
done

Betti Curve Resolution

for res in 50 100 200; do
    medtda extract image.nii.gz \
        --method betti_curve \
        --betti-resolution $res \
        --output features_betti${res}.npy
done

Landscape Layers

for layers in 3 5 10; do
    medtda extract image.nii.gz \
        --method persistence_landscape \
        --landscape-layers $layers \
        --output features_landscape${layers}.npy
done

Automation Scripts

Bash Script: Process Dataset

Create process_dataset.sh:

#!/bin/bash

# Configuration
DATA_DIR="data"
OUTPUT_DIR="results"
CONFIG="config.yaml"
N_JOBS=8

# Create output directory
mkdir -p "$OUTPUT_DIR"

# Process images
echo "Processing images..."
medtda batch process "$DATA_DIR/images/" \
    --mask-dir "$DATA_DIR/masks/" \
    --config "$CONFIG" \
    --output "$OUTPUT_DIR/features.csv" \
    --format csv \
    --n-jobs $N_JOBS \
    --skip-errors \
    --error-log "$OUTPUT_DIR/errors.log"

# Validation
if [ $? -eq 0 ]; then
    echo "✓ Processing complete!"
    echo "Results saved to $OUTPUT_DIR/features.csv"
else
    echo "✗ Processing failed. Check $OUTPUT_DIR/errors.log"
    exit 1
fi

Run:

chmod +x process_dataset.sh
./process_dataset.sh

Python Script: Multi-Configuration

Create batch_configs.py:

#!/usr/bin/env python3
import subprocess
from pathlib import Path

# Configurations to test
configs = {
    'ct_soft_tissue': 'configs/ct_soft.yaml',
    'ct_lung': 'configs/ct_lung.yaml',
    'mri_t1': 'configs/mri_t1.yaml',
    'mri_t2': 'configs/mri_t2.yaml'
}

data_dir = Path('data/images')
mask_dir = Path('data/masks')
output_base = Path('results')

for name, config in configs.items():
    print(f"\n{'='*60}")
    print(f"Processing: {name}")
    print(f"{'='*60}\n")

    output_dir = output_base / name
    output_dir.mkdir(parents=True, exist_ok=True)

    cmd = [
        'medtda', 'batch', 'process',
        str(data_dir),
        '--mask-dir', str(mask_dir),
        '--config', config,
        '--output', str(output_dir / 'features.csv'),
        '--format', 'csv',
        '--n-jobs', '8',
        '--skip-errors'
    ]

    result = subprocess.run(cmd)

    if result.returncode == 0:
        print(f"✓ {name} complete!")
    else:
        print(f"✗ {name} failed!")

Run:

python batch_configs.py

Integration with ML Pipelines

Extract and Train

#!/bin/bash

# 1. Extract features
echo "Extracting features..."
medtda batch process data/train/images/ \
    --mask-dir data/train/masks/ \
    --config config.yaml \
    --output train_features.csv \
    --format csv

medtda batch process data/test/images/ \
    --mask-dir data/test/masks/ \
    --config config.yaml \
    --output test_features.csv \
    --format csv

# 2. Train model (Python script)
echo "Training model..."
python train_model.py \
    --train train_features.csv \
    --test test_features.csv \
    --labels data/labels.csv \
    --output model.pkl

# 3. Evaluate
echo "Evaluating..."
python evaluate.py \
    --model model.pkl \
    --test test_features.csv \
    --labels data/labels.csv

Cross-Validation Pipeline

Create cv_pipeline.sh:

#!/bin/bash

N_FOLDS=5

for fold in $(seq 0 $((N_FOLDS-1))); do
    echo "Processing fold $fold..."

    # Extract train features
    medtda batch process \
        --file-list "folds/fold_${fold}_train.txt" \
        --mask-dir data/masks/ \
        --config config.yaml \
        --output "folds/fold_${fold}_train_features.csv"

    # Extract validation features
    medtda batch process \
        --file-list "folds/fold_${fold}_val.txt" \
        --mask-dir data/masks/ \
        --config config.yaml \
        --output "folds/fold_${fold}_val_features.csv"

    # Train and evaluate
    python train_fold.py --fold $fold
done

# Aggregate results
python aggregate_cv_results.py --n-folds $N_FOLDS

Profiling and Optimization

Timing

Time extraction:

time medtda extract large_image.nii.gz --output features.npy

Batch timing:

time medtda batch process data/images/ \
    --output results/ \
    --n-jobs 8

Compare configurations:

for config in config1.yaml config2.yaml config3.yaml; do
    echo "Testing $config..."
    time medtda batch process data/images/ \
        --config $config \
        --output tmp/
    echo ""
done

Memory Profiling

Monitor memory usage:

/usr/bin/time -v medtda batch process data/images/ \
    --output results/ \
    2>&1 | grep "Maximum resident set size"

Useful Aliases

Add to ~/.bashrc or ~/.zshrc:

# Quick extraction with defaults
alias medtda-quick='medtda extract --normalize --method persistence_stats'

# CT processing
alias medtda-ct='medtda extract --config ~/configs/ct.yaml'

# MRI processing
alias medtda-mri='medtda extract --config ~/configs/mri.yaml'

# Batch with masks
alias medtda-batch='medtda batch process --mask-dir masks/ --output results/'

Usage:

medtda-quick image.nii.gz
medtda-ct ct_scan.nii.gz --mask liver.nii.gz
medtda-mri brain.nii.gz --mask tumor.nii.gz

Debugging

Verbose Mode

medtda extract image.nii.gz --verbose

Output:

[INFO] Loading image: image.nii.gz
[INFO] Image shape: (256, 256, 180)
[INFO] Normalizing with method: robust
[INFO] Computing persistent homology...
[INFO] Filtration type: sublevel
[INFO] Max dimension: 2
[INFO] Vectorizing with persistence_stats...
[INFO] Feature vector size: 13
[INFO] Saving to: features.npy
[SUCCESS] Done!

Dry Run

See what would be processed without actually processing:

medtda batch process data/images/ \
    --mask-dir data/masks/ \
    --dry-run

Output:

Would process:
  image001.nii.gz -> mask001.nii.gz
  image002.nii.gz -> mask002.nii.gz
  image003.nii.gz -> mask003.nii.gz
  ...
Total: 100 images

Next Steps

See Also