.. _api_utils:

=====
Utils
=====

.. currentmodule:: medtda.utils

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

.. contents:: Contents
   :local:
   :depth: 2

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

.. autofunction:: medtda.utils.resample_image

Resample an image to target voxel spacing.

**Signature:**

.. code-block:: python

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

.. autofunction:: medtda.utils.normalize_image

Normalize image intensities using various methods.

**Signature:**

.. code-block:: python

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

.. autofunction:: medtda.utils.apply_mask

Apply a binary mask to an image.

**Signature:**

.. code-block:: python

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

.. autofunction:: medtda.utils.extract_label_from_mask

Extract a single label from a multi-label mask.

**Signature:**

.. code-block:: python

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

.. autofunction:: medtda.utils.crop_to_roi

Crop image to ROI bounding box with optional padding.

**Signature:**

.. code-block:: python

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

.. autofunction:: medtda.utils.get_image_statistics

Compute image statistics.

**Signature:**

.. code-block:: python

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

.. autofunction:: medtda.utils.check_array_dimensions

Validate array dimensions.

**Signature:**

.. code-block:: python

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

.. autofunction:: medtda.utils.safe_divide

Safely divide two arrays, handling division by zero.

**Signature:**

.. code-block:: python

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

* :doc:`preprocessor` - High-level preprocessing class
* :doc:`loaders` - Image loading and validation
* :doc:`../user_guide/preprocessing` - Preprocessing guide
* :doc:`featureextractor` - Complete feature extraction
