# Multi-Resolution Plugin Architecture

This section covers the multi-resolution plugin architecture that enables efficient processing where algorithms are computed at low resolution and applied to full resolution data.

## Overview

The scaled processing plugin architecture in ZarrNii allows for efficient multi-resolution operations. This is particularly useful for algorithms that can be computed efficiently at lower resolution and then applied to the full-resolution data. Common use cases include:

- Bias field correction
- Background estimation
- Denoising operations
- Global intensity normalization

ZarrNii uses the [pluggy](https://pluggy.readthedocs.io/) framework for its plugin system, providing a flexible and extensible architecture for creating custom processing plugins.

## Plugin Interface

All scaled processing plugins are plain Python classes that implement the required hook methods using the `@hookimpl` decorator from `zarrnii_plugin_api`. **No inheritance from any base class is required.**

### `lowres_func(lowres_array: np.ndarray) -> np.ndarray`

This function processes the downsampled data and returns a result that will be upsampled and applied to the full-resolution data.

### `highres_func(fullres_array: dask.array, upsampled_output: dask.array) -> dask.array`

This function receives the full-resolution dask array and the upsampled output (already upsampled to match the full-resolution shape), and applies the operation blockwise. The upsampling is handled internally by the `apply_scaled_processing` method.

### `scaled_processing_plugin_name() -> str`

Returns the name of the plugin.

### `scaled_processing_plugin_description() -> str`

Returns a description of what the plugin does.

## Basic Usage

```python
from zarrnii import ZarrNii, GaussianBiasFieldCorrection

# Load your data
znimg = ZarrNii.from_nifti("path/to/image.nii")

# Apply bias field correction
corrected = znimg.apply_scaled_processing(
    GaussianBiasFieldCorrection(sigma=5.0),
    downsample_factor=4
)

# Save result
corrected.to_nifti("corrected_image.nii")
```

## Built-in Plugins

### GaussianBiasFieldCorrection

A simple bias field correction plugin that estimates smooth bias fields using Gaussian smoothing at low resolution and applies correction by division.

```python
# Basic usage with default parameters
corrected = znimg.apply_scaled_processing(GaussianBiasFieldCorrection())

# Custom parameters
corrected = znimg.apply_scaled_processing(
    GaussianBiasFieldCorrection(sigma=3.0, mode='constant'),
    downsample_factor=8
)
```

Parameters:
- `sigma`: Standard deviation for Gaussian smoothing (default: 5.0)
- `mode`: Boundary condition for smoothing (default: 'reflect')

### N4BiasFieldCorrection

A more sophisticated bias field correction plugin that uses the N4 algorithm from ANTsPy for superior bias field estimation at low resolution, then applies correction by division.

**Installation**: Requires `antspyx` to be installed:
```bash
# Install with N4 support
pip install 'zarrnii[n4]'
# or install antspyx directly
pip install antspyx
```

```python
from zarrnii import N4BiasFieldCorrection

# Basic usage with default parameters
corrected = znimg.apply_scaled_processing(N4BiasFieldCorrection())

# Custom parameters for more control
corrected = znimg.apply_scaled_processing(
    N4BiasFieldCorrection(
        spline_spacing=150.0,
        convergence={'iters': [25, 25], 'tol': 0.001},
        shrink_factor=2
    ),
    downsample_factor=4
)
```

Parameters:
- `spline_spacing`: Spacing between knots for spline fitting (default: 200.0)
- `convergence`: Dictionary with 'iters' (list) and 'tol' (float) for convergence criteria (default: {'iters': [50], 'tol': 0.001})
- `shrink_factor`: Shrink factor for processing (default: 1)

## Creating Custom Plugins

You can create custom plugins as plain Python classes by implementing the required hook methods with `@hookimpl`. **No base class inheritance is required.**

```python
from zarrnii_plugin_api import hookimpl
import numpy as np
import dask.array as da
from scipy import ndimage

class CustomPlugin:
    """Custom scaled processing plugin example."""

    def __init__(self, param1=1.0):
        self.param1 = param1

    @hookimpl
    def lowres_func(self, lowres_array: np.ndarray) -> np.ndarray:
        """Process low-resolution data."""
        # Your low-resolution algorithm here
        # Example: compute some correction map
        correction_map = np.ones_like(lowres_array) * self.param1
        return correction_map

    @hookimpl
    def highres_func(self, fullres_array: da.Array, upsampled_output: da.Array) -> da.Array:
        """Apply correction to full-resolution data."""
        # The upsampling is handled internally by apply_scaled_processing
        # This example shows a simple multiplication operation
        # Apply correction directly (both arrays are same size)
        result = fullres_array * upsampled_output
        return result

    @hookimpl
    def scaled_processing_plugin_name(self) -> str:
        """Return the name of the plugin."""
        return "Custom Plugin"

    @hookimpl
    def scaled_processing_plugin_description(self) -> str:
        """Return a description of the plugin."""
        return "A custom multi-resolution processing plugin"

# Method 1: Direct usage with ZarrNii (recommended for simple cases)
result = znimg.apply_scaled_processing(CustomPlugin(param1=2.0))

# Method 2: Using the plugin manager (recommended for external plugins)
from zarrnii.plugins import get_plugin_manager

pm = get_plugin_manager()
plugin = CustomPlugin(param1=2.0)
pm.register(plugin)

# Now the plugin is available through the plugin manager
# and can be discovered by other tools
```

## External Plugin Development

External plugins allow you to package and distribute your custom plugins as separate Python packages that can be discovered and used by ZarrNii. They only need `zarrnii_plugin_api` as a dependency — no dependency on `zarrnii` core is required.

### Step 1: Create Your Plugin Package

Create a new Python package with the following structure:

```
my_zarrnii_plugin/
├── setup.py or pyproject.toml
└── my_zarrnii_plugin/
    ├── __init__.py
    └── my_plugin.py
```

### Step 2: Implement Your Plugin

In `my_plugin.py`:

```python
"""Custom external plugin for ZarrNii."""
from zarrnii_plugin_api import hookimpl
import numpy as np
import dask.array as da
from scipy import ndimage

class MyExternalPlugin:
    """An example external plugin for demonstration."""

    def __init__(self, smoothing_sigma=2.0, scale_factor=1.5):
        """Initialize the plugin with custom parameters.

        Args:
            smoothing_sigma: Sigma for Gaussian smoothing
            scale_factor: Scaling factor for the correction
        """
        self.smoothing_sigma = smoothing_sigma
        self.scale_factor = scale_factor

    @hookimpl
    def lowres_func(self, lowres_array: np.ndarray) -> np.ndarray:
        """Compute correction map at low resolution."""
        if lowres_array.size == 0:
            raise ValueError("Input array is empty")

        # Apply smoothing to estimate low-frequency components
        smoothed = ndimage.gaussian_filter(
            lowres_array.astype(np.float32),
            sigma=self.smoothing_sigma
        )

        # Scale the correction map
        correction_map = smoothed * self.scale_factor

        return correction_map

    @hookimpl
    def highres_func(self, fullres_array: da.Array, upsampled_output: da.Array) -> da.Array:
        """Apply the correction to full-resolution data."""
        # Apply the correction by division
        epsilon = np.finfo(np.float32).eps
        corrected = fullres_array / da.maximum(upsampled_output, epsilon)
        return corrected

    @hookimpl
    def scaled_processing_plugin_name(self) -> str:
        """Return plugin name."""
        return "My External Plugin"

    @hookimpl
    def scaled_processing_plugin_description(self) -> str:
        """Return plugin description."""
        return (
            f"External plugin with smoothing (sigma={self.smoothing_sigma}) "
            f"and scaling (factor={self.scale_factor})"
        )
```

### Step 3: Make Your Plugin Discoverable

In `__init__.py`:

```python
"""My ZarrNii Plugin Package."""
from .my_plugin import MyExternalPlugin

__all__ = ["MyExternalPlugin"]
```

### Step 4: Configure Your Package

In `pyproject.toml`:

```toml
[project]
name = "my-zarrnii-plugin"
version = "0.1.0"
description = "My custom ZarrNii processing plugin"
dependencies = [
    # Only zarrnii_plugin_api is needed — no zarrnii dependency!
    "zarrnii_plugin_api>=0.1.0",
    "scipy>=1.11.0",
]

[project.entry-points."zarrnii"]
my_external_plugin = "my_zarrnii_plugin:MyExternalPlugin"
```

### Step 5: Use Your External Plugin

After installing your plugin package (`pip install my-zarrnii-plugin`):

```python
from zarrnii import ZarrNii
from my_zarrnii_plugin import MyExternalPlugin

# Load your data
znimg = ZarrNii.from_nifti("input.nii")

# Use the external plugin directly
result = znimg.apply_scaled_processing(
    MyExternalPlugin(smoothing_sigma=3.0, scale_factor=2.0),
    downsample_factor=4
)

# Or register it with the plugin manager
from zarrnii.plugins import get_plugin_manager

pm = get_plugin_manager()
plugin = MyExternalPlugin(smoothing_sigma=3.0, scale_factor=2.0)
pm.register(plugin)

# Query registered plugins
for p in pm.get_plugins():
    names = pm.hook.scaled_processing_plugin_name()
    descriptions = pm.hook.scaled_processing_plugin_description()
    print(f"Plugin: {names[0]} - {descriptions[0]}")
```

## Advanced Usage

### Custom Downsampling Factors

```python
# Use different downsampling factors
result = znimg.apply_scaled_processing(
    GaussianBiasFieldCorrection(),
    downsample_factor=8  # 8x downsampling
)
```

### Custom Chunk Sizes

```python
# Specify custom spatial chunk sizes for low-resolution processing
# The chunk_size parameter accepts only spatial dimensions (Z, Y, X)
# Non-spatial dimensions (time, channel) are automatically prepended with singleton chunks (1)
result = znimg.apply_scaled_processing(
    GaussianBiasFieldCorrection(),
    chunk_size=(32, 32, 32)  # Spatial chunks only (Z, Y, X)
)
```

### Temporary File Options

The framework uses temporary OME-Zarr files to break up the dask computation graph for better performance. You can control this behavior:

```python
# Disable temporary file usage (may impact performance on large datasets)
result = znimg.apply_scaled_processing(
    GaussianBiasFieldCorrection(),
    use_temp_zarr=False
)

# Use custom temporary file location
result = znimg.apply_scaled_processing(
    GaussianBiasFieldCorrection(),
    temp_zarr_path="/custom/path/temp_processing.ome.zarr"
)
```

### Plugin Class vs Instance

```python
# Using plugin class (parameters passed as kwargs)
result1 = znimg.apply_scaled_processing(GaussianBiasFieldCorrection, sigma=3.0)

# Using plugin instance (parameters set during initialization)
plugin = GaussianBiasFieldCorrection(sigma=3.0)
result2 = znimg.apply_scaled_processing(plugin)
```

## Performance Considerations

1. **Downsampling Factor**: Higher factors reduce computation time but may reduce accuracy
2. **Chunk Sizes**: Optimize for your memory constraints and processing requirements
3. **Algorithm Complexity**: The `lowres_func` runs on small numpy arrays, while `highres_func` uses dask for scalability
4. **Temporary Files**: The default temporary OME-Zarr approach breaks up dask computation graphs for better performance on large datasets. Disable only if you have specific memory/disk constraints
5. **Dask-based Upsampling**: Uses ZarrNii's `.upsample()` method which leverages dask for efficient parallel upsampling

## Integration with Other Operations

The scaled processing plugins integrate seamlessly with other ZarrNii operations:

```python
# Chain operations
result = (znimg
    .apply_scaled_processing(GaussianBiasFieldCorrection())
    .downsample(level=1)
    .segment_otsu())

# Save to different formats
result.to_nifti("processed.nii")
result.to_ome_zarr("processed.ome.zarr")
```

## See Also

- [Segmentation Plugins](segmentation_example.md) for other plugin architectures
- [Downsampling and Upsampling](downsampling.md) for resolution operations
- [API Reference](../reference.md) for detailed method documentation
