Metadata-Version: 2.4
Name: napari-npifile
Version: 0.1.0
Author-email: Daniel Haase <git@dhaase.de>
License-Expression: MIT
Project-URL: Repository, https://codeberg.org/dhaase-de/napari-npifile
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: OS Independent
Classifier: Framework :: napari
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE.txt
Requires-Dist: numpy>=1.13.3
Dynamic: license-file

# Napari Plugin `napari-npifile`

Allows reading and writing of `.npi` files.

`.npi` files are essentially NumPy arrays packaged together with JSON metadata that
stores **napari viewer and layer configuration**, such as axis names or colormaps.

The goal is to keep the simplicity of `.npy` files while preserving useful visualization
settings when opening arrays in napari.

## Quick Example

    import napari_npifile
    import numpy as np
    
    data = np.random.random(size=(10, 20, 30, 40))
  
    # save it without napari metadata
    np.save("quick_example.npy", data)

    # save it with napari metadata
    napari_npifile.write_npi(
        "quick_example.npi",
        data=data,
        layer_attributes={
            "name": "foo",
            "colormap": "magma",
            "contrast_limits": (-0.25, 1.25),
        },
        viewer_settings={
            "axis_labels": ["T", "Z", "Y", "X"],
            "scale_bar_properties": {"visible": True, "unit": "px", "font_size": 40.0},
        },
    )

    # and drag and drop it into napari
    #    |
    #    |
    #    v

![quick_example.gif](quick_example.gif)

## Motivation

When working on image analysis tasks in Python it is common to save temporary NumPy
arrays such as:

* image stacks
* neural network tensors
* intermediate processing results
* ...

to `.npy` files, which can easily be opened in [napari](https://napari.org/) via
drag-and-drop.

However, `.npy` files contain no visualization metadata. Each time a file is opened,
display settings (colormap, contrast limits, etc.) must be configured manually.

`.npi` files solve this by storing the arrays together with napari viewer metadata.

## Installation

    pip install --upgrade napari-npifile

**Note**: napari is **not** included as a dependency, to allow writing `.npi` files
even if napari is not installed (e.g., on headless servers).

To view `.npi` files in napari, you must install napari separately:

    pip install napari

See the
[napari installation instructions](https://napari.org/stable/tutorials/fundamentals/installation.html)
for details.

## Usage

### Reading `.npi` files in napari

Once `napari-npifile` is installed, `.npi` files can be opened in napari like any other
supported format:

1. via drag-and-drop,
2. via the `File -> Open` menu,
3. via the napari console using `viewer.open("myfile.npi", plugin="napari-npifile")`, or
4. programmatically using:
    ```
    import napari

    viewer = napari.Viewer()
    viewer.open("myfile.npi", plugin="napari-npifile")

    napari.run()
    ```

When loaded, each layer in the `.npi` file becomes a separate napari layer. Layer
settings (colormap, contrast limits, gamma, name, etc.) and viewer settings (axis
labels, camera settings, scale bar settings, etc.) are applied automatically.

### Writing `.npi` files from Python (no napari required)

`napari-npifile` provides a main writer class `NpiWriter_v1` for full control, as well
as the convenience wrappers `write_npi`, `write_multilayer_npi` into a `.npi` file.

#### When to use what:

The convenience wrapper functions (`write_npi`, `write_multilayer_npi`) are useful for
cases where all the data to be stored is available upfront. Use `write_npi` for writing
a single layer and `write_multilayer_npi` for multiple layers.

The class `NpiWriter_v1` allows to **incrementally** write layers into a single `.npi`
file over time, making it ideal for loops.

#### Convenience wrapper `write_npi` (single-layer):

    import napari_npifile
    import numpy as np
    
    data = np.random.random((10, 20, 30))
    
    napari_npifile.write_npi(
        "single.npi",
        data=data,
        layer_attributes={"name": "layer0", "colormap": "magma"},
        viewer_settings={"axis_labels": ["Z", "Y", "X"]},
    )

#### Convenience wrapper `write_multilayer_npi` (multi-layer):

    import napari_npifile
    import numpy as np
    
    data_seq = [
        np.random.random((10, 20, 30)),
        np.random.random((10, 20, 30)),
    ]
    layer_attributes_seq = [
        {"name": "first", "colormap": "gray"},
        {"name": "second", "colormap": "green"},
    ]
    
    napari_npifile.write_multilayer_npi(
        "multi.npi",
        data_seq=data_seq,
        layer_attributes_seq=layer_attributes_seq,
        viewer_settings={"axis_labels": ["Z", "Y", "X"]},
    )

#### Full general example using `NpiWriter_v1`:

    import napari_npifile
    import numpy as np
    
    # prepare writer
    writer = napari_npifile.NpiWriter_v1(
        out_path="out.npi",
        exist_ok=True,
    )
    
    # viewer settings are saved once and are used for all layers
    writer.write_viewer_settings(
        {
            "axis_labels": ["T", "K", "Z", "Y", "X"],
            "grid_properties": {"enabled": True, "shape": (1, -1), "stride": 1, "spacing": 0.1},
            "scale_bar_properties": {"visible": True, "unit": "px", "gridded": True},
        }
    )
    
    # layers are written one at a time, each with individual attributes
    data1 = np.random.random(size=(3, 10, 20, 30, 40)) 
    writer.write_layer(data=data1, attributes={"colormap": "cyan", "name": "foo"})
    
    data2 = np.random.random(size=(1, 10, 20, 30, 40))
    writer.write_layer(data=data2, attributes={"colormap": "green", "name": "bar"})

## File Format

### Overview

`.npi` files are ZIP archives containing raw NumPy array data (stored
as `.npy` files) together with napari viewer and layer metadata stored as JSON files.

### Versions

Every `.npi` file contains a root `metadata.json` with a mandatory `"npi_format"`
field. This format versioning ensures backward compatibility: the reader uses it to
select the appropriate parser for each format.

In addition, the class `NpiWriter_v1` will remain indefinitely to ensure backward
compatibility. If a new `.npi` format version is introduced, a new writer class will be
added alongside it, leaving existing code using `NpiWriter_v1` fully functional.

Version history:

| Format Version | `napari-npifile` version | Comment                                                                                                                                               |
|----------------|--------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
| `v1`           | `0.1.0+`                 | Initial version, current format. Supports single- and multi-layer `.npi` files with full napari image layer metadata and a subset of viewer settings. |

### Current Version Details (`v1`)

#### Archive Layout

    example.npi                  # this is just a ZIP archive
    ├── metadata.json            # contains {"npi_format": "v1"}
    ├── viewer_settings.json     # dictionary with global viewer settings - see below
    └── layers
        ├── 00000000             # each layer is stored under a sequential numeric key
        │   ├── data.npy         # NumPy array as saved via np.save
        │   └── attributes.json  # layer attributes (as in napari.layers.Image)
        └── 00000001
            ├── data.npy
            └── attributes.json

#### Metadata

All metadata files are JSON files which contain a dictionary.

##### File `metadata.json`

Only contains `{"npi_format": "v1"}`.
It is used by the reader to select the correct parser for the file.

##### File `viewer_settings.json`

Stores global napari viewer settings.
All fields are **optional**; if missing, the reader does not change the current napari
configuration.

| Field                     | Type        | Notes                                                                                                                                                                                                              |
|---------------------------|-------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `clear_existing_layers`   | `bool`      | If `True`, removes all existing layers before adding new ones                                                                                                                                                      |
| `hide_existing_layers`    | `bool`      | If `True`, hides all existing layers before adding new ones                                                                                                                                                        |
| `title`                   | `str`       | Napari window title                                                                                                                                                                                                |
| `ndisplay`                | `int`       | `2` or `3` (for 2D or 3D mode)                                                                                                                                                                                     |
| `axis_labels`             | `list[str]` | One label per axis (e.g., `["T", "Z", "Y", "X"]`)                                                                                                                                                                  |
| `dims_set_point`          | `dict`      | Optional subfields as in [napari's set_point](https://napari.org/stable/api/napari.components.Dims.html#napari.components.Dims.set_point), i.e. `axis: int or Sequence[int]` and `value: float or Sequence[float]` |
| `camera_properties`       | `dict`      | Optional subfields as in [napari's Camera](https://napari.org/stable/api/napari.components.Camera.html) (e.g., `{"center": (0.0, 10.0, 20.0)}`)                                                                    |
| `grid_properties`         | `dict`      | Optional subfields as in [napari's GridCanvas](https://github.com/napari/napari/blob/main/src/napari/components/grid.py) (`enabled: bool`, `shape: tuple[int, int]`, `stride: int`, `spacing: float`)              |
| `scale_bar_properties`    | `dict`      | Optional subfields as in [napari's ScaleBarOverlay](https://github.com/napari/napari/blob/main/src/napari/components/overlays/scale_bar.py) (e.g., `visible: bool`, `unit: str`, `gridded: bool`)                  |
| `text_overlay_properties` | `dict`      | Optional subfields as in [napari's TextOverlay](https://github.com/napari/napari/blob/main/src/napari/components/overlays/text.py) (e.g., `visible: bool`, `text: str`, `gridded: bool`, `font_size: float`)       |

##### File(s) `layers/*/attributes.json`

Layer-specific settings for each layer. All fields are **optional**; if missing, the
reader uses napari defaults.

[Any keyword argument accepted by napari’s Image layer](https://napari.org/stable/api/napari.layers.Image.html#napari-layers-image)
(except `data`), e.g.:

* `name: str`
* `colormap: str` (any napari colormap)
* `contrast_limits: tuple[float, float]`
* `gamma: float`
* `blending: str ("translucent", "additive", "opaque", ...)`
* ...

## Design Principles and Limitations

Key principles for the `.npi` format and `napari-npifile` are:

* **Simplicity and accessibility**: `.npi` files are standard ZIP archives containing
  NumPy arrays and JSON metadata. Thus, the files can easily be inspected, read, and
  created even without this library.
* **Minimal dependencies**: Only NumPy and napari (for viewing) are required.
* **Tight napari integration**: `.npi` stores viewer and layer settings exclusively for
  napari, and thus allows fine-grained control over how arrays are displayed.
* **Backward compatibility**: Every `.npi` includes a format version, ensuring future
  readers can correctly handle older files.
* **Headless-friendly writing**: `.npi` files can be created without napari installed,
  which is useful for server-side or automated workflows.

These design choices naturally impose some limitations:

* **No access optimization**: `.npi` uses plain `np.save`/`np.load` from ZIP
  archives and thus can't compete with optimized formats for large arrays.
* **No compression**: `.npi` stores raw arrays; large datasets may consume significant
  disk space.
* **Viewer-centric metadata**: Metadata is tailored specifically to napari; other
  viewers or tools require extra work to interpret it.

In short, `.npi` prioritizes simplicity and tight napari integration over maximal
performance. Users needing advanced storage features or highly
efficient random access may prefer formats such as [Zarr](https://zarr.dev/) or
[HDF5](https://en.wikipedia.org/wiki/Hierarchical_Data_Format), while
[OME-TIFF](https://ome-model.readthedocs.io/en/stable/) provides more generic
metadata handling.

## Known Issues

* Setting the viewer slice position (`viewer.dims.set_point`) and camera properties does
  not work when there are no pre-existing layers or when `clear_existing_layers` is
  `True`.

## Todo / Ideas

* Add support to write `.npi` files directly from napari.
* Add CLI to convert existing `.npy` files to `.npi` files (and back).

## Related Discussions / Projects

* [napari GitHub issue: Changing viewer dimension when adding a new layer #6127](https://github.com/napari/napari/issues/6127)
* [image.sc forum: Saving volumetric data with voxel size, colormap, annotations](https://forum.image.sc/t/saving-volumetric-data-with-voxel-size-colormap-annotations/85537/24)

## License

`napari-npifile` is released under the MIT License. See `LICENSE.txt` for full details.
