🐍 Python Interface¶
NGFF-Zarr is a Python library that provides a simple, natural interface for working with OME-Zarr data structures, creating chunked, multiscale OME-Zarr image pyramids, and reading and writing OME-Zarr multiscale image files.
NGFF-Zarr’s interface, which reflects the OME-Zarr data model, is built on Python’s built-in dataclasses and Dask arrays. It is designed to be simple, flexible, and easy to use.
Array to NGFF Image¶
NGFF-Zarr supports conversion of any NumPy array-like object that follows the
Python Array API Standard into the OME-Zarr data model. This includes such
objects as NumPy ndarrays, Dask Arrays, PyTorch Tensors, CuPy arrays, Zarr
arrays, etc.
NumPy arrays with explicit byte order, including big-endian arrays, are accepted and normalized during writes so values round-trip correctly.
Convert the array to an NgffImage, which is a standard Python dataclass
that represents an OME-Zarr image for a single scale.
When creating the image from the array, you can specify
names of the
dimsfrom{‘t’, ‘z’, ‘y’, ‘x’, ‘c’}the
scale, the pixel spacing for the spatial dimsthe
translation, the origin or offset of the center of the first pixela
namefor the imageand
axes_unitswith UDUNITS-2 identifiers
>>> # Load an image as a NumPy array
>>> from imageio.v3 import imread
>>> data = imread('cthead1.png')
>>> print(type(data))
<class 'numpy.ndarray'>
Specify optional additional metadata with to_ngff_image.
>>> import ngff_zarr as nz
>>> image = nz.to_ngff_image(data,
dims=['y', 'x'],
scale={'y': 1.0, 'x': 1.0},
translation={'y': 0.0, 'x': 0.0})
>>> print(image)
NgffImage(
data=dask.array<array, shape=(256, 256),
dtype=uint8,
chunksize=(256, 256), chunktype=numpy.ndarray>,
dims=['y', 'x'],
scale={'y': 1.0, 'x': 1.0},
translation={'y': 0.0, 'x': 0.0},
name='image',
axes_units=None,
computed_callbacks=[]
)
The image data is nested in a lazy dask.Array and chucked.
If dims, scale, or translation are not specified, NumPy-compatible
defaults are used.
Generate Multiscales¶
OME-Zarr represents images in a chunked, multiscale data structure. Use
to_multiscales to build a task graph that will produce a chunked, multiscale
image pyramid. to_multiscales has optional scale_factors and chunks
parameters. An antialiasing method can also be prescribed.
>>> multiscales = nz.to_multiscales(image,
scale_factors=[2,4],
chunks=64)
>>> print(multiscales)
NgffMultiscales(
images=[
NgffImage(
data=dask.array<rechunk-merge, shape=(256, 256), dtype=uint8,chunksize=(64, 64), chunktype=numpy.ndarray>,
dims=['y', 'x'],
scale={'y': 1.0, 'x': 1.0},
translation={'y': 0.0, 'x': 0.0},
name='image',
axes_units=None,
computed_callbacks=[]
),
NgffImage(
data=dask.array<rechunk-merge, shape=(128, 128), dtype=uint8,
chunksize=(64, 64), chunktype=numpy.ndarray>,
dims=['y', 'x'],
scale={'x': 2.0, 'y': 2.0},
translation={'x': 0.5, 'y': 0.5},
name='image',
axes_units=None,
computed_callbacks=[]
),
NgffImage(
data=dask.array<rechunk-merge, shape=(64, 64), dtype=uint8,
chunksize=(64, 64), chunktype=numpy.ndarray>,
dims=['y', 'x'],
scale={'x': 4.0, 'y': 4.0},
translation={'x': 1.5, 'y': 1.5},
name='image',
axes_units=None,
computed_callbacks=[]
)
],
metadata=Metadata(
axes=[
Axis(name='y', type='space', unit=None),
Axis(name='x', type='space', unit=None)
],
datasets=[
Dataset(
path='scale0/image',
coordinateTransformations=[
Scale(scale=[1.0, 1.0], type='scale'),
Translation(
translation=[0.0, 0.0],
type='translation'
)
]
),
Dataset(
path='scale1/image',
coordinateTransformations=[
Scale(scale=[2.0, 2.0], type='scale'),
Translation(
translation=[0.5, 0.5],
type='translation'
)
]
),
Dataset(
path='scale2/image',
coordinateTransformations=[
Scale(scale=[4.0, 4.0], type='scale'),
Translation(
translation=[1.5, 1.5],
type='translation'
)
]
)
],
coordinateTransformations=None,
name='image',
version='0.4'
),
scale_factors=[2, 4],
method=<Methods.ITKWASM_GAUSSIAN: 'itkwasm_gaussian'>,
chunks={'y': 64, 'x': 64}
)
The NgffMultiscales dataclass stores all the images and their metadata for each
scale according the OME-Zarr data model. Note that the correct scale and
translation for each scale are automatically computed.
Displacement and coordinate fields (v0.6)¶
OME-Zarr v0.6 (RFC-5) stores displacement and coordinate fields as ordinary
multiscale images. The only difference from a regular image is that the
vector-component axis, in the channel position, carries type="displacement"
(or type="coordinate") instead of type="channel". Like a channel/component
axis it is discrete (it indexes vector components rather than a continuous
coordinate). Set the type with the axes_types argument of NgffImage, which
maps a dimension name to its axis type:
import dask.array as da
import numpy as np
import ngff_zarr as nz
# A 2-component displacement field over a yx image: the "c" dimension holds the
# (y, x) displacement vector, one component per output spatial axis.
data = da.from_array(np.zeros((2, 256, 256), dtype=np.float32))
field = nz.NgffImage(
data=data,
dims=("c", "y", "x"),
scale={"c": 1.0, "y": 1.0, "x": 1.0},
translation={"c": 0.0, "y": 0.0, "x": 0.0},
axes_types={"c": "displacement"},
)
multiscales = nz.to_multiscales(field)
nz.to_ome_zarr("displacement.ome.zarr", multiscales, version="0.6")
The axis type round-trips through reading and writing. It can also be set after
the fact by editing the metadata directly, for example
multiscales.metadata.intrinsic_coordinate_system.axes[0].type = "displacement".
OME-Zarr v0.6 also models the displacements and coordinates coordinate
transformations (ngff_zarr.v06.zarr_metadata.Displacements and Coordinates)
that reference such a field from a registered image. Like the other v0.6
transforms, they round-trip through reading and writing.
See RFC-5: Coordinate Systems and Transformations for the full transformation model, including how to write an image and its transformation (an affine, or a displacement/coordinate field) into a single store.
Read an OME-Zarr¶
To read an OME-Zarr file, use from_ngff_zarr, which returns the
NgffMultiscales dataclass.
>>> multiscales = nz.from_ngff_zarr('cthead1.ome.zarr')
OME-Zarr version 0.1 to 0.6 is supported. Version 0.6 adds RFC-5 coordinate systems and transformations.
OME-Zarr Zip (.ozx) files¶
RFC-9 introduces support for OME-Zarr Zip (.ozx) files, which package an entire OME-Zarr hierarchy into a single ZIP archive. This format provides several benefits:
Single-file distribution: Share complete multiscale datasets as one portable file
Version metadata: OME-Zarr version embedded in ZIP comment for automatic detection
Reading local .ozx files¶
>>> multiscales = nz.from_ngff_zarr('cthead1.ozx')
The .ozx extension is automatically detected and handled appropriately.
Writing .ozx files¶
To write an OME-Zarr dataset as a .ozx file, simply use the .ozx extension:
>>> nz.to_ngff_zarr('cthead1.ozx', multiscales, version='0.5')
All RFC-9 recommendations are followed. By default, .ozx files are written using OME-Zarr version 0.5 (Zarr v3 format), which is recommended for the ZIP-based format.
The OME-Zarr version is automatically embedded in the ZIP file comment for proper detection when reading.
Converting existing OME-Zarr stores to .ozx¶
You can easily convert an existing OME-Zarr directory store to a portable .ozx file:
>>> # Read from directory store
>>> multiscales = nz.from_ngff_zarr('cthead1.ome.zarr')
>>>
>>> # Write as .ozx file
>>> nz.to_ngff_zarr('cthead1.ozx', multiscales)
This creates a single-file archive containing the entire multiscale pyramid, making it easy to share or distribute datasets.
For direct store-to-ZIP conversion without reprocessing the data, use write_store_to_zip:
>>> from ngff_zarr.rfc9_zip import write_store_to_zip
>>> from zarr.storage import LocalStore
>>>
>>> # Direct conversion of existing store to .ozx
>>> source_store = LocalStore('cthead1.ome.zarr')
>>> write_store_to_zip(source_store, 'cthead1.ozx', version='0.5')
This is more efficient for large datasets as it copies the store contents directly without recomputing arrays.
Validate OME-Zarr metadata¶
To validate that an OME-Zarr’s metadata following the specification’s data
model, which is used by all the programming languages in the community, use the
validate optional dependency and kwarg to from_ngff_zarr.
pip install "ngff-zarr[validate]"
>>> multiscales = nz.from_ngff_zarr('cthead1.ome.zarr', validate=True)
If the metadata does not follow the data model, an error will be raised.
Metadata validation is supported for OME-Zarr version 0.1 to 0.5.
Write an OME-Zarr¶
To write the multiscales to OME-Zarr, use to_ngff_zarr.
nz.to_ngff_zarr('cthead1.ome.zarr', multiscales)
Use the .ome.zarr extension for local directory stores by convention.
Any other Zarr store type can also be used.
The multiscales will be computed and written out-of-core, limiting memory usage.
Writing with Tensorstore¶
To write with tensorstore, which may provide better performance, use the
tensorstore optional dependency.
pip install "ngff-zarr[tensorstore]"
nz.to_ngff_zarr('cthead1.ome.zarr', multiscales, use_tensorstore=True)
The TensorStore backend uses the same dtype canonicalization, including for big-endian NumPy arrays written to Zarr v3 stores.
Write a sharded OME-Zarr store¶
Sharded Zarr stores save multiple compressed chunks in a single file or blob. This can be useful for large datasets, as it can reduce the number of files in a directory.
To generate a sharded OME-Zarr store, pass the chunks_per_shard kwarg to
to_ngff_zarr. Sharding requires OME-Zarr version 0.5, which uses the Zarr
Format Specification 3.
This can be a single integer,
version = '0.5'
nz.to_ngff_zarr('lightsheet.ome.zarr',
multiscales,
chunks_per_shard=2,
version=version)
This will use 2 chunks per shard for all dimensions.
Or, specify a tuple of integers for each dimension.
nz.to_ngff_zarr('lightsheet.ome.zarr',
multiscales,
chunks_per_shard=(2, 2, 4),
version=version)
Or, specify a dictionary of integers for each dimension.
nz.to_ngff_zarr('lightsheet.ome.zarr',
multiscales,
chunks_per_shard={'z':4, 'y':2, 'x':2},
version=version)
The resulting shard shape will be the product of the chunk shape and the
chunks_per_shard shape. In this case the shard shape will be (256, 128, 128)
for a chunk shape of (64, 64, 64).
Tensorstore can also be used with sharded OME-Zarr stores.
nz.to_ngff_zarr('lightsheet.ome.zarr',
multiscales,
chunks_per_shard={'z':4, 'y':2, 'x':2},
use_tensorstore=True,
version=version)
TIFF and OME-TIFF Files¶
NGFF-Zarr provides support for converting TIFF files, including multi-series OME-TIFF files, to OME-Zarr. When reading OME-TIFF files, physical size metadata (PhysicalSizeX/Y/Z and units) is automatically extracted and applied.
Convert all series from a TIFF file¶
from ngff_zarr import tiff_file_to_ngff_images, to_multiscales, to_ngff_zarr
# Convert all series from a multi-series TIFF
images = tiff_file_to_ngff_images("multi_series.ome.tiff")
for name, ngff_image in images:
print(f"Series: {name}")
print(f" Shape: {ngff_image.data.shape}")
print(f" Scale: {ngff_image.scale}")
print(f" Units: {ngff_image.axes_units}")
# Generate multiscales and write to OME-Zarr
# Note: series names are automatically sanitized for filesystem safety
multiscales = to_multiscales(ngff_image, scale_factors=[2, 4])
to_ngff_zarr(f"{name}.ome.zarr", multiscales)
Select specific series¶
from ngff_zarr import tiff_file_to_ngff_images
# By index
images = tiff_file_to_ngff_images("multi_series.ome.tiff", series=0)
# By glob pattern
images = tiff_file_to_ngff_images("multi_series.ome.tiff", series="*GFP*")
# Multiple selections
images = tiff_file_to_ngff_images("multi_series.ome.tiff", series=[0, 2, "*Red*"])
OME metadata extraction¶
When reading OME-TIFF files, physical sizes and units are automatically extracted:
from ngff_zarr import tiff_file_to_ngff_images
images = tiff_file_to_ngff_images("sample.ome.tiff")
name, ngff_image = images[0]
# Physical sizes from OME-XML (e.g., {'x': 0.5, 'y': 0.5, 'z': 2.0})
print(f"Scale: {ngff_image.scale}")
# Units normalized to NGFF format (e.g., {'x': 'micrometer', ...})
print(f"Units: {ngff_image.axes_units}")
For more details on TIFF conversion including unit mapping and pyramidal TIFF handling, see the TIFF documentation.
High Content Screening (HCS)¶
NGFF-Zarr provides full support for High Content Screening data, implementing the plate and well metadata structures defined in the OME-Zarr specification. This enables working with multi-well plate data commonly used in drug discovery and high-throughput imaging.
Reading HCS Data¶
Use from_hcs_zarr to load HCS plate data:
# Load an HCS plate
plate = nz.from_hcs_zarr('screening_plate.ome.zarr')
print(f"Plate: {plate.metadata.name}")
print(f"Wells: {len(plate.metadata.wells)}")
# Access a specific well
well = plate.get_well("A", "1") # Row A, Column 1
if well:
print(f"Well A/1 has {len(well.images)} field(s)")
# Get the first field image
image = well.get_image(0)
if image:
print(f"Image shape: {image.images[0].data.shape}")
Working with Multi-field Wells¶
Each well can contain multiple fields of view:
well = plate.get_well("B", "2")
for field_idx in range(len(well.images)):
image = well.get_image(field_idx)
if image:
# Each field is a standard multiscale image
ngff_image = image.images[0] # First scale level
print(f"Field {field_idx}: {ngff_image.data.shape}")
Time Series and Acquisitions¶
For plates with multiple acquisitions (time points or conditions):
if plate.metadata.acquisitions:
for acq in plate.metadata.acquisitions:
print(f"Acquisition {acq.id}: {acq.name}")
# Get image from specific acquisition
well = plate.get_well("A", "1")
image = well.get_image_by_acquisition(acquisition_id=0, field_index=0)
HCS Validation¶
Validate HCS metadata during loading:
# Validate against HCS schema
plate = nz.from_hcs_zarr('plate.ome.zarr', validate=True)
For more detailed examples and advanced usage, see the HCS documentation.
Convert OME-Zarr versions¶
To convert from OME-Zarr version 0.4, which uses the Zarr Format Specification 2, to 0.5, which uses the Zarr Format Specification 3, or vice version, specify the desired version when writing.
# Convert from 0.4 to 0.5
multiscales = from_ngff_zarr('cthead1.ome.zarr')
to_ngff_zarr('cthead1_zarr3.ome.zarr', multiscales, version='0.5')
# Convert from 0.5 to 0.4
multiscales = from_ngff_zarr('cthead1.ome.zarr')
to_ngff_zarr('cthead1_zarr2.ome.zarr', multiscales, version='0.4')
Upgrade OME-Zarr versions¶
The conversion above re-reads and re-writes the entire pyramid. When you only
need to change the recorded specification version of an existing store,
upgrade_ome_zarr does it directly in one of two modes.
In-place, metadata-only. When no output is given (or output resolves to
the same store as input) and the source and target share the same underlying
Zarr format – for example 0.5 to 0.6, both Zarr v3 – only the root group’s
zarr.json metadata is rewritten. Every array chunk on disk is left
byte-for-byte untouched, avoiding the data loss of a naive “read, erase,
re-write” upgrade. In-place upgrades that cross the Zarr v2/v3 boundary in the
upgrade direction (0.4 to 0.5 or 0.4 to 0.6) are also metadata-only: each
array’s Zarr v3 zarr.json is given a v2 chunk-key encoding so the existing
chunk binaries resolve unchanged. The obsolete Zarr v2 sidecars (.zarray,
.zgroup, .zattrs) are then removed, so the upgraded store is a valid Zarr v3
/ OME-Zarr 0.5 (or 0.6) store only – it is not simultaneously a valid Zarr v2
/ OME-Zarr 0.4 store; only the chunk data binaries are reused, resolved through
the v2 chunk-key encoding. The reverse – an in-place downgrade across that
boundary (0.5/0.6 to 0.4) – cannot preserve chunk keys and raises a
ValueError; pass an output store instead.
Write-to-new-store. When an output store distinct from input is given,
the source is read lazily and re-written to output at the requested version
through the standard write pipeline. Every supported transition (0.4, 0.5, 0.6,
in either direction) works in this mode, and the source store is never erased.
Upgrade a 0.5 store to 0.6 in place, keeping every array chunk:
import numpy as np
import ngff_zarr as nz
# Synthesize a tiny store written at OME-Zarr 0.5 (Zarr v3).
image = nz.to_ngff_image(np.zeros((4, 32, 32), dtype=np.uint8),
dims=['z', 'y', 'x'])
multiscales = nz.to_multiscales(image, scale_factors=[2])
nz.to_ome_zarr('image.ome.zarr', multiscales, version='0.5')
# Rewrite only the root metadata to 0.6; array chunks are left untouched.
nz.upgrade_ome_zarr('image.ome.zarr', version='0.6')
Write an upgraded 0.6 copy from a 0.4 source, leaving the source intact:
import numpy as np
import ngff_zarr as nz
# Synthesize a tiny store written at OME-Zarr 0.4 (Zarr v2).
image = nz.to_ngff_image(np.zeros((4, 32, 32), dtype=np.uint8),
dims=['z', 'y', 'x'])
multiscales = nz.to_multiscales(image, scale_factors=[2])
nz.to_ome_zarr('image_v04.ome.zarr', multiscales, version='0.4')
# Write a new 0.6 store; 'image_v04.ome.zarr' is read lazily and never erased.
nz.upgrade_ome_zarr('image_v04.ome.zarr', 'image_v06.ome.zarr', version='0.6')
Pass validate=True to validate the source metadata against the NGFF schema
while reading, and overwrite=False (write-to-new-store only) to refuse to
overwrite pre-existing data at output. A runnable version of both examples,
which also proves the in-place upgrade leaves the array chunks byte-for-byte
identical, is in
py/examples/upgrade_ome_zarr_example.py.
Note that upgrade_ome_zarr upgrades a single image. To upgrade an HCS plate or
a bioformats2raw container, point at each contained image by its own path (for
example plate.ome.zarr/A/1/0).