.. _api_loaders:

=======
Loaders
=======

.. currentmodule:: medtda.loaders

The ``loaders`` module provides functions for loading and saving medical images and masks in various formats.

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

Overview
========

This module handles I/O operations for medical images, supporting common 2D and 3D formats including NIFTI, PNG, TIFF, and DICOM.

**Key Features:**

* Automatic format detection
* 2D and 3D image support
* Metadata preservation
* Image and mask validation
* Compatibility checking

**Supported Formats:**

* **3D:** NIFTI (.nii, .nii.gz), NRRD (.nrrd), MetaImage (.mha, .mhd)
* **2D:** PNG, TIFF, JPEG, BMP
* **DICOM:** Single files or series

Functions
=========

load_image
----------

.. autofunction:: medtda.loaders.load_image

Load a medical image from file.

**Signature:**

.. code-block:: python

   def load_image(path: Union[str, Path]) -> Tuple[np.ndarray, dict]

**Parameters:**

* **path** (*str or Path*) - Path to image file

**Returns:**

* **image** (*np.ndarray*) - Image array
* **metadata** (*dict*) - Image metadata including:
  
  * ``'spacing'`` - Voxel spacing (if available)
  * ``'origin'`` - Image origin
  * ``'direction'`` - Image orientation
  * ``'dtype'`` - Original data type

**Example:**

::

    from medtda.loaders import load_image

    # Load 3D NIFTI image
    image, metadata = load_image('ct_scan.nii.gz')
    print(f"Image shape: {image.shape}")
    print(f"Spacing: {metadata.get('spacing')}")

    # Load 2D PNG image
    image_2d, metadata_2d = load_image('xray.png')

load_mask
---------

.. autofunction:: medtda.loaders.load_mask

Load a mask (binary or multi-label) from file.

**Signature:**

.. code-block:: python

   def load_mask(path: Union[str, Path]) -> Tuple[np.ndarray, dict]

**Parameters:**

* **path** (*str or Path*) - Path to mask file

**Returns:**

* **mask** (*np.ndarray*) - Mask array (integer type)
* **metadata** (*dict*) - Mask metadata

**Example:**

::

    from medtda.loaders import load_mask

    # Binary mask
    mask, metadata = load_mask('segmentation.nii.gz')

    # Multi-label mask
    multi_mask, metadata = load_mask('multi_label.nii.gz')
    print(f"Labels present: {np.unique(multi_mask)}")

save_image
----------

.. autofunction:: medtda.loaders.save_image

Save an image array to file.

**Signature:**

.. code-block:: python

   def save_image(
       image_array: np.ndarray,
       path: Union[str, Path],
       metadata: Optional[dict] = None
   ) -> None

**Parameters:**

* **image_array** (*np.ndarray*) - Image array to save
* **path** (*str or Path*) - Output file path
* **metadata** (*dict or None*) - Optional metadata (spacing, origin, etc.)

**Example:**

::

    from medtda.loaders import save_image

    # Save processed image
    save_image(
        processed_image,
        'output.nii.gz',
        metadata={'spacing': (1.0, 1.0, 1.0)}
    )

    # Save as 2D PNG
    save_image(slice_2d, 'slice.png')

Validation Functions
====================

validate_image
--------------

.. autofunction:: medtda.loaders.validate_image

Validate an image array.

**Signature:**

.. code-block:: python

   def validate_image(image: np.ndarray) -> None

**Raises:**

* **ValueError** if image is invalid

**Example:**

::

    from medtda.loaders import validate_image

    try:
        validate_image(image_array)
        print("Image is valid")
    except ValueError as e:
        print(f"Invalid image: {e}")

validate_mask
-------------

.. autofunction:: medtda.loaders.validate_mask

Validate a mask array.

**Signature:**

.. code-block:: python

   def validate_mask(mask: np.ndarray) -> None

**Raises:**

* **ValueError** if mask is invalid

**Example:**

::

    from medtda.loaders import validate_mask

    validate_mask(mask_array)

validate_compatibility
----------------------

.. autofunction:: medtda.loaders.validate_compatibility

Validate that image and mask are compatible.

**Signature:**

.. code-block:: python

   def validate_compatibility(
       image: np.ndarray,
       mask: np.ndarray
   ) -> None

**Raises:**

* **ValueError** if image and mask are incompatible

**Example:**

::

    from medtda.loaders import validate_compatibility

    validate_compatibility(image, mask)
    # Raises ValueError if shapes don't match

Complete Examples
=================

Example 1: Basic Loading
-------------------------

::

    from medtda.loaders import load_image, load_mask

    # Load image and mask
    image, img_meta = load_image('data/patient001.nii.gz')
    mask, mask_meta = load_mask('data/patient001_mask.nii.gz')

    print(f"Image shape: {image.shape}")
    print(f"Image spacing: {img_meta.get('spacing')}")
    print(f"Mask labels: {np.unique(mask)}")

Example 2: Loading with Validation
-----------------------------------

::

    from medtda.loaders import (
        load_image,
        load_mask,
        validate_compatibility
    )

    # Load
    image, _ = load_image('scan.nii.gz')
    mask, _ = load_mask('mask.nii.gz')

    # Validate
    try:
        validate_compatibility(image, mask)
        print("Image and mask are compatible")
    except ValueError as e:
        print(f"Error: {e}")

Example 3: Processing and Saving
---------------------------------

::

    from medtda import Preprocessor
    from medtda.loaders import load_image, save_image

    # Load
    image, metadata = load_image('original.nii.gz')

    # Process
    preprocessor = Preprocessor(
        normalize=True,
        spacing=(1.0, 1.0, 1.0)
    )
    processed = preprocessor.preprocess(image)

    # Save with metadata
    save_image(
        processed,
        'processed.nii.gz',
        metadata={'spacing': (1.0, 1.0, 1.0)}
    )

Example 4: Batch Loading
-------------------------

::

    from medtda.loaders import load_image
    import glob
    from pathlib import Path

    # Load all NIFTI files
    image_paths = glob.glob('data/*.nii.gz')

    images = []
    for path in image_paths:
        image, metadata = load_image(path)
        images.append({
            'name': Path(path).stem,
            'array': image,
            'metadata': metadata
        })

    print(f"Loaded {len(images)} images")

Example 5: 2D Image Handling
-----------------------------

::

    from medtda.loaders import load_image, save_image
    import numpy as np

    # Load 2D images
    xray, metadata = load_image('xray.png')
    print(f"2D image shape: {xray.shape}")

    # Process
    normalized = (xray - xray.min()) / (xray.max() - xray.min())

    # Save as different format
    save_image(normalized, 'xray_processed.tiff')

Example 6: Multi-Label Mask
----------------------------

::

    from medtda.loaders import load_mask
    from medtda.utils import extract_label_from_mask

    # Load multi-label mask
    multi_mask, metadata = load_mask('organs.nii.gz')

    # Check labels
    labels = np.unique(multi_mask)
    print(f"Available labels: {labels}")
    # e.g., [0, 1, 2, 3] for background, liver, kidney, spleen

    # Extract single label
    liver_mask = extract_label_from_mask(multi_mask, label=1)

Example 7: Metadata Handling
-----------------------------

::

    from medtda.loaders import load_image, save_image

    # Load with metadata
    image, metadata = load_image('scan.nii.gz')

    # Inspect metadata
    print("Metadata:")
    for key, value in metadata.items():
        print(f"  {key}: {value}")

    # Modify metadata
    metadata['spacing'] = (1.0, 1.0, 1.0)  # Change to isotropic

    # Save with modified metadata
    save_image(image, 'scan_modified.nii.gz', metadata=metadata)

Supported Formats
=================

3D Formats
----------

**NIFTI (.nii, .nii.gz)**

* Most common medical imaging format
* Supports metadata (spacing, orientation)
* Recommended for 3D volumes

::

    image, meta = load_image('brain.nii.gz')

**NRRD (.nrrd)**

* Nearly Raw Raster Data
* Good metadata support

::

    image, meta = load_image('volume.nrrd')

**MetaImage (.mha, .mhd)**

* ITK format
* Separate header and data files (.mhd + .raw)

::

    image, meta = load_image('scan.mha')

2D Formats
----------

**PNG**

* Lossless compression
* Good for masks and processed images

::

    image, meta = load_image('slice.png')

**TIFF**

* Supports 16-bit grayscale
* Good for high dynamic range

::

    image, meta = load_image('microscopy.tiff')

**JPEG**

* Lossy compression
* Not recommended for analysis (lossy)

::

    image, meta = load_image('preview.jpg')

DICOM
-----

**DICOM (.dcm)**

* Medical imaging standard
* Rich metadata
* May require series loading

::

    # Single DICOM file
    image, meta = load_image('slice001.dcm')

    # Note: For DICOM series, use specialized tools

Format Detection
================

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

Format is automatically detected from file extension::

    # Automatically detects NIFTI
    img, _ = load_image('scan.nii.gz')

    # Automatically detects PNG
    img, _ = load_image('image.png')

Troubleshooting
===============

Common Issues
-------------

**Issue: Shape mismatch between image and mask**

::

    # Check shapes before processing
    image, _ = load_image('image.nii.gz')
    mask, _ = load_image('mask.nii.gz')
    
    if image.shape != mask.shape:
        print(f"Shape mismatch: {image.shape} vs {mask.shape}")

**Issue: Missing metadata**

::

    image, metadata = load_image('image.nii.gz')
    
    # Provide default spacing if missing
    spacing = metadata.get('spacing', (1.0, 1.0, 1.0))

**Issue: Unsupported format**

::

    # Check file extension
    from pathlib import Path
    
    path = Path('image.xyz')
    if path.suffix not in ['.nii', '.nii.gz', '.nrrd', '.png', '.tiff']:
        print(f"Unsupported format: {path.suffix}")

Performance Tips
================

1. **Use compressed NIFTI:**
   
   `.nii.gz` files are smaller and load quickly::
   
       # Prefer this
       img, _ = load_image('scan.nii.gz')

2. **Cache loaded images:**
   
   Avoid reloading the same file::
   
       # Load once
       image, metadata = load_image('large_scan.nii.gz')
       
       # Reuse image array
       result1 = process1(image)
       result2 = process2(image)

3. **Validate once:**
   
   Validate at load time, not during processing::
   
       from medtda.loaders import load_image, validate_image
       
       image, _ = load_image('scan.nii.gz')
       validate_image(image)  # Once only

See Also
========

* :doc:`preprocessor` - Image preprocessing
* :doc:`utils` - Image processing utilities
* :doc:`../user_guide/preprocessing` - Preprocessing guide
* :doc:`featureextractor` - Feature extraction
