=============
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)

.. code-block:: python

   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)

.. code-block:: python

   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:

.. code-block:: python

   # 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
--------------------

.. code-block:: python

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

Normalization Methods
---------------------

**Min-Max Normalization** (``minmax``)

Scales values to [0, 1]:

.. math::

   x_{norm} = \frac{x - x_{min}}{x_{max} - x_{min}}

.. code-block:: python

   extractor = FeatureExtractor(
       normalize=True,
       normalize_method='minmax'
   )

**Pros:** Preserves relative differences, bounded output

**Z-Score Normalization** (``zscore``)

Standardizes to zero mean and unit variance:

.. math::

   x_{norm} = \frac{x - \mu}{\sigma}

.. code-block:: python

   extractor = FeatureExtractor(
       normalize=True,
       normalize_method='zscore'
   )

**Pros:** Removes scale, handles outliers better than minmax

**Robust Scaling** (``robust``)

Uses median and interquartile range:

.. math::

   x_{norm} = \frac{x - median(x)}{IQR(x)}

.. code-block:: python

   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):

.. code-block:: python

   # 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:

.. code-block:: python

   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:**

.. code-block:: python

   # 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:**

.. code-block:: python

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

Automatic Detection
-------------------

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

.. code-block:: python

   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):

.. code-block:: python

   # 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
-----------------

.. list-table::
   :header-rows: 1
   :widths: 30 20 20 30

   * - 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:

.. code-block:: python

   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:

.. code-block:: python

   # 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:

.. code-block:: python

   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:

.. code-block:: python

   # 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
-------------------

.. code-block:: python

   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:

.. code-block:: python

   # 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:

.. code-block:: python

   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:

.. code-block:: python

   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:

.. code-block:: python

   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
==========

* :doc:`persistent_homology` - Understanding PH computation
* :doc:`vectorization` - Choosing vectorization methods
* :doc:`batch_processing` - Processing multiple images
* :doc:`../api/preprocessor` - Preprocessor API reference
