spacr.ome_zarr

OME-Zarr (OME-NGFF) — read and write, with the axes and the units taken seriously.

OME-NGFF is where large bioimaging data is going, and the two properties that put it there are the two this module is built around.

Chunked. The array is stored as a grid of independently compressed blocks, so a 100 GB plate is readable a tile at a time. Nothing here ever reads an array to answer a question about it: read_ome_zarr() opens a handful of small JSON files and returns the levels, their shapes, their voxel sizes and their chunk grids without touching a single chunk, and OmeZarrImage.read() with a region= decodes only the chunks that region intersects. Every byte of chunk data in the pure-Python path passes through one function, _read_chunk_bytes(), precisely so that “it is lazy” is a countable claim rather than a sentence in a docstring — tests/test_ome_zarr.py counts its calls.

Multiscale. The group holds a resolution pyramid, so drawing a 200 px plate overview does not decode full resolution. OmeZarrImage.level_for_size() picks the level, and the per-level scale transformation is what makes the picked level land in the same world coordinates as level 0.

What spaCR implements itself, and what the extra is for

The metadata — multiscales, axes, coordinateTransformations, omero — is parsed and written here, in pure Python, because that layout is the thing worth getting right and a library would only hide it behind a call that returns “an array” with the voxel size quietly dropped.

The chunk codec is where the compiled code lives, and so it is where the optional extra lives. This module decodes and encodes chunks compressed with zlib, gzip, bz2, lzma or stored uncompressed using nothing but the standard library, which is why a spaCR-written OME-Zarr round-trips on a plain pip install spacr. A file compressed with blosc, zstd or lz4 — the common defaults of other tools — raises ZarrExtraMissing, whose message names the codec that was needed and the one command that fixes it, instead of a ModuleNotFoundError five frames down.

If zarr is importable, array access is delegated to it wholesale (see prefer_zarr= on the readers). That is the intended path: zarr handles v3 sharding, filter pipelines and every codec, and it is maintained by people who do nothing else. The pure-Python reader is a fallback for the common case, not a reimplementation of zarr, and it says so where it gives up.

Axes and units, which is the decision this module exists to get right

NGFF names its axes and gives each one a UDUNITS-2 unit ("micrometer", "nanometer", "second"). spacr.layers.Spacing names its axes and carries one short unit token ("um", "px") that spaCR compares by name when layers are stacked — that comparison, at spacr/layers.py’s LayerStack._check_units, is the whole reason Spacing has a units field at all. Mapping one onto the other has three decisions in it, and all three are made here rather than left to the caller.

1. The Spacing is built from the SPACE axes only. A 5-D NGFF image has axes (t, c, z, y, x): the time axis is in seconds, the channel axis has no unit at all, and z/y/x are in micrometers. A single Spacing over all five would have to answer “what unit is this?” with one string, and whatever it answered would be wrong for three of the axes — a stack whose spacing claims "um" while one of its axes steps in seconds passes the very check layers.py exists to make, and then draws a plausible picture of the wrong thing. So OmeZarrImage.spacing covers z/y/x, and t and c are reported alongside as Axis records through OmeZarrImage.other_axes, OmeZarrImage.time_axis and OmeZarrImage.channel_axis. Nothing is lost; the reader does not treat seconds and micrometers as the same kind of number.

2. Units are translated through an explicit table, and an unknown one is refused. NGFF_UNIT_TO_SPACR is the mapping, written out. A unit that is not in it raises OmeZarrError. It would be one line to default to "px" instead, and that one line is how a 0.65 µm pixel becomes a 0.65 px pixel and every downstream area comes out wrong by a factor of 10^6 — in a column named cell_area that still looks entirely reasonable. The error distinguishes the two cases that need different fixes: a unit NGFF has never heard of (the file is wrong), and a legal NGFF unit spaCR has no short token for (convert it, e.g. to micrometer).

3. No unit is pixels, said out loud. An NGFF file whose space axes carry no unit is legal and common — it is what every tool that never knew the pixel size writes. That reads back as units="px" with OmeZarrImage.units_declared False and a OmeZarrImage.describe() line that says the file declared none. It is never silently upgraded to µm, and "px" is never written out as a unit string, because NGFF has no pixel unit: writing a px spacing emits axes with no unit, which is the same convention read back.

Space axes that disagree with each other — y in micrometers and x in nanometers — are refused by name. A single Spacing has one units, so there is no honest way to represent that; the numbers would have to be converted, and converting the caller’s data behind their back is not this module’s decision to make.

Versions

0.4 is implemented properly — it is what essentially all real data is in, and it is zarr-format 2. 0.5 (zarr-format 3, metadata under an ome key in zarr.json) is tolerated: its metadata is parsed in full, and its chunks are decoded for the codec chains the standard library can do (bytes/transpose/gzip/crc32c). Anything else routes through the same require_codec() refusal as v2 does. .zgroup / .zarray / zarr.json and the zarr_format field are read rather than assumed, and an unsupported zarr_format is refused by number.

Typical use:

from spacr.ome_zarr import read_ome_zarr, write_ome_zarr
from spacr.layers import Spacing

img = read_ome_zarr("/data/plate.zarr/A/1/0")
print(img.describe())
print(img.spacing.describe())            # z 2, y 0.65, x 0.65 um

level = img.level_for_size(512)          # coarse enough to draw, no coarser
tile = img.read(level, world_region={"y": (0.0, 100.0),
                                     "x": (0.0, 100.0)})

write_ome_zarr(
    "/data/out.zarr", stack,
    spacing=Spacing.from_map({"z": 2.0, "y": 0.65, "x": 0.65}, units="um"),
    levels=4, channel_names=("DAPI", "GFP"),
)

Downsampling, and the transformation bug it is easy to write

The writer builds a real pyramid by 2x block mean over the two fastest-varying space axes (see write_ome_zarr()). It is a box filter and not a Gaussian pyramid: there is no pre-filter beyond the box, so it aliases where a properly filtered pyramid would not. It is chosen anyway because it is separable, exact, dependency-free, and the same local_mean the rest of the NGFF ecosystem writes — matching what other viewers produce matters more here than a marginally better kernel. Striding (downsample="stride") is offered and is mandatory for label/mask arrays: the mean of labels 3 and 5 is 4, an object that does not exist.

Level k’s scale is level 0’s scale multiplied by the cumulative factor 2^k on each downsampled axis, and leaving it at level 0’s value is the single most common NGFF bug — it renders as a pyramid whose coarse levels are a quarter the size of the fine ones and drift off the top-left corner as you zoom. The translation moves too, and by a different rule per method: a block mean puts the first output element at the centre of the block it averaged, half a level-0 pixel in from the edge, so level k is translated by scale_0 * (2^k - 1) / 2; a strided level samples element 0 exactly and is not translated at all. Both are written, and asserted in the tests with the numbers spelled out by hand.

Attributes

Exceptions

OmeZarrError

An OME-Zarr that cannot mean what it says, or a request that cannot be met.

ZarrExtraMissing

The optional zarr extra is needed here and is not installed.

Classes

Axis

One NGFF axis: what it is called, what kind it is, and how big a step is.

Level

One resolution level of a multiscale image — its metadata, not its data.

OmeZarrImage

An opened OME-Zarr multiscale image: metadata now, chunks on demand.

Functions

axes_from_spacing(→ Tuple[Axis, ...])

Build the full NGFF axis list for an array from a spacing.

ngff_unit_to_spacr(→ str)

Translate an NGFF unit name into the token Spacing.units carries.

read_ome_zarr(→ OmeZarrImage)

Open an OME-Zarr group and read its metadata. No chunk is touched.

read_ome_zarr_array(→ numpy.ndarray)

Open an OME-Zarr and read one level (or a region of it) in one call.

require_codec(→ Callable[[bytes], bytes])

Return a bytes -> bytes decoder for a zarr compressor id.

require_zarr()

Import and return zarr, or raise a message worth reading.

spacing_from_axes(→ spacr.layers.Spacing)

Build a spacr.layers.Spacing from the SPACE axes only.

spacr_unit_to_ngff(→ Optional[str])

Translate a spacr.layers.Spacing.units token into an NGFF unit.

write_ome_zarr(→ OmeZarrImage)

Write an array as an OME-NGFF 0.4 multiscale image.

Module Contents

exception spacr.ome_zarr.OmeZarrError[source]

Bases: ValueError

An OME-Zarr that cannot mean what it says, or a request that cannot be met.

Raised rather than repaired, for the reason spacr.layers.LayerError is: every case this covers — an unknown unit, space axes in two different units, a multiscales block that is not there — has a “helpful” fallback that produces a plausible-looking image with the wrong scale on it, and a wrong scale reaches a figure without ever looking wrong.

Initialize self. See help(type(self)) for accurate signature.

exception spacr.ome_zarr.ZarrExtraMissing[source]

Bases: OmeZarrError, ImportError

The optional zarr extra is needed here and is not installed.

Both parents are deliberate. It is an ImportError because a caller guarding an optional feature writes except ImportError; it is an OmeZarrError because callers experience this as another file that could not be read, and code that wraps a whole read in except OmeZarrError should not miss it.

Initialize self. See help(type(self)) for accurate signature.

class spacr.ome_zarr.Axis[source]

One NGFF axis: what it is called, what kind it is, and how big a step is.

A faithful record of the file, not an interpretation of it: unit holds the NGFF name as written, even in the cases spaCR would not write itself (a channel axis with a unit, say). The interpretation happens in spacr_units() and in spacing_from_axes(), where it can refuse.

Parameters:
  • name – the axis name — "t", "c", "z", "y", "x".

  • type – "space", "time", "channel" or, for a file that uses something else, whatever it says. Only "space" axes reach a spacr.layers.Spacing.

  • unit – the UDUNITS-2 unit name, or None when the file declares none. None on a space axis means pixels, explicitly.

  • scale – world size of one element along this axis at level 0. The per-level values live on Level; this one is the reference the spacr.layers.Spacing is built from.

  • translate – world coordinate of element 0 at level 0.

__post_init__() → None[source]

Normalise the axis and reject a step that cannot address anything.

Raises:

OmeZarrError – if the axis has no name, which NGFF requires; if the scale is zero or non-finite – a zero step collapses the axis, so every world coordinate on it resolves to element 0 and the image is drawn out of register with nothing to show for it; or if the translation is non-finite.

classmethod channel(name: str = 'c') → Axis[source]

A channel axis. No unit, ever: a channel index measures nothing.

describe() → str[source]

One clause for a status line: x: space, 0.65 micrometer/step.

A channel axis gets no step, because a channel index is not a measurement and c: channel, 1/step reads like one.

classmethod space(name: str, scale: float = 1.0, unit: str | None = None, translate: float = 0.0) → Axis[source]

A spatial axis — the kind that goes into a spacr.layers.Spacing.

Parameters:
  • name – the axis name, stripped. The type is forced to space whatever the name says, so Axis.space("t") puts a t axis inside the spacing; the name-based inference that applies when reading a file does not apply here.

  • scale – world size of one element at level 0, and what every pyramid level’s scale is derived from. Zero or non-finite is refused; negative is allowed and reaches the file unchanged. It is the second positional argument here but the fourth on Axis, so Axis.space("x", "um") is a float("um").

  • unit – the NGFF (UDUNITS-2) name, stored verbatim and not checked here — unit="furlong" builds, and only raises once a spacing is made from it, which for write_ome_zarr() is after the chunks are already on disk. None (the default) and "" both mean pixels and write an axis carrying no unit key.

  • translate – world coordinate of element 0 at level 0; non-finite is refused. Seeds every level’s translation in the written file.

Raises:

OmeZarrError – from Axis itself, on a blank name, a zero or non-finite scale, or a non-finite translation.

spacr_units() → str[source]

This axis’s unit as a spaCR token.

Returns:

the token, or PIXEL_UNITS when the file declared no unit.

Raises:

OmeZarrError – on a unit spaCR will not translate.

classmethod time(name: str = 't', scale: float = 1.0, unit: str | None = 'second', translate: float = 0.0) → Axis[source]

A time axis. Kept out of the spacing; reported beside it.

Parameters:
  • name – defaults to "t". The type is forced to time whatever the name says, so Axis.time("x") is an axis called x that spacing_from_axes() leaves out.

  • scale – world time per element at level 0. It never reaches a spacr.layers.Spacing, but write_ome_zarr() still writes it into every level’s scale — unhalved, since only space axes are downsampled. Zero or non-finite is refused.

  • unit – defaults to "second", not to None as on space(). Nothing validates it on the way in or out: reading and writing only check space-axis units, so an untranslatable one round-trips and surfaces only at spacr_units(). None writes no unit, which then reads back as pixels.

  • translate – world time of element 0 at level 0; non-finite is refused. Written into every level’s translation.

Raises:

OmeZarrError – from Axis itself, on a blank name, a zero or non-finite scale, or a non-finite translation.

to_ngff() → Dict[str, Any][source]

This axis as the axes entry NGFF wants.

Returns:

{"name": ..., "type": ...} plus "unit" when there is one. A channel axis never gets a unit, whatever it was read with — the spec forbids it and a viewer that trusts it would be misled.

property is_space: bool[source]

Whether this axis is spatial, and so part of the spacing.

class spacr.ome_zarr.Level[source]

One resolution level of a multiscale image — its metadata, not its data.

Everything here comes out of one small JSON file. Building the whole pyramid’s worth of these costs a handful of stats and no chunk reads, which is what makes “list the levels and their shapes” free on a 100 GB plate.

Parameters:
  • path – the dataset path inside the group, e.g. "0".

  • shape – array shape, in array order.

  • chunks – chunk shape — the unit of I/O, and therefore the smallest region that can be read.

  • dtype – the numpy dtype string as stored, e.g. "<u2". Kept as written, byte order included.

  • scale – per-axis world size of one element AT THIS LEVEL, already composed with any group-level transformation.

  • translation – per-axis world coordinate of element 0 at this level, likewise composed.

  • zarr_format – 2 or 3.

  • compressor – the codec id, or None when chunks are stored uncompressed.

__post_init__() → None[source]

Coerce the level’s tuples and check every rank against the array’s.

Raises:

OmeZarrError – if the chunks, the scale, or the translation do not have one entry per array axis. NGFF requires one each, and a mismatch means the transformation cannot be applied at all.

describe() → str[source]

One line: 0: (2, 12, 2048, 2048) uint16, chunks (1, 16, 256, 256), zlib.

property n_chunks: int[source]

How many chunks the level is stored in.

property nbytes: int[source]

Uncompressed size of the whole level, in bytes.

property ndim: int[source]

Number of axes.

class spacr.ome_zarr.OmeZarrImage[source]

An opened OME-Zarr multiscale image: metadata now, chunks on demand.

Returned by read_ome_zarr() after reading only the group’s JSON, so holding one of these says nothing about how much data has been read — the answer is none until read() is called, and then only the chunks the region touches.

Parameters:
  • path – the group directory.

  • axes – every axis, in array order, with level-0 scale and translation on each.

  • levels – the resolution pyramid, level 0 first.

  • ngff_version – what the file says it is.

  • name – the multiscales name, if any.

  • channel_names – from the omero block when present — spaCR has channel names to fill in, so they are read and written rather than dropped.

  • omero – the raw omero block, read-only, for the rendering settings this module does not interpret (colours, windows, rdefs).

  • units_declared – False when the space axes carried no unit, so spacing is in pixels because the file said nothing, not because it said pixels. Never quietly upgraded to micrometers.

  • multiscale – the raw multiscales entry, for anything here does not model.

An image opened from cloud storage also keeps the opened location, with the credentials it was opened with, so that read() reaches the chunks the same way.

__post_init__() → None[source]

Freeze the image’s members and check the levels against the axes.

Raises:

OmeZarrError – if the multiscales block lists no datasets – then there is no image here – or if a level’s rank differs from the number of declared axes, which NGFF requires to match.

describe() → str[source]

A short report: version, axes, spacing and every level.

The spacing line says pixel units (file declares none) when that is what happened, because “0.65” with no unit beside it has been read as micrometers before.

level(level: int | str = 0) → Level[source]

Resolve a level index or dataset path to a Level.

Parameters:

level – an index into levels, or a dataset path.

Returns:

the level.

Raises:

OmeZarrError – when there is no such level.

level_for_size(size: int, axes: Sequence[str] = ('y', 'x')) → int[source]

Index of the coarsest level still at least size along axes.

This is the multiscale payoff: drawing a 200 px thumbnail of a 40k x 40k plate should decode a 300 x 300 level, not decimate the full one. The rule is “coarsest that does not need upsampling” — never return a level smaller than asked for while a bigger one exists, because upsampling to fill the request shows a blur where there is detail. If every level is smaller than size (a small image, a big request), the finest is returned.

Parameters:
  • size – the wanted output size in pixels along axes.

  • axes – which axes the size refers to. Defaults to y and x.

Returns:

an index into levels.

Raises:

OmeZarrError – when axes names an axis the image lacks.

read(level: int | str = 0, region: Any = None, *, world_region: Mapping[str, Sequence[float]] | None = None, prefer_zarr: bool = True) → numpy.ndarray[source]

Read one level, or a region of it, as a numpy array.

Only the chunks the region intersects are opened. On a 100 GB plate that is the difference between a tile and the plate.

Parameters:
  • level – level index or dataset path. Level 0 is full resolution.

  • region – see resolve_region().

  • world_region – a {axis: (low, high)} box in world units, resolved through the level’s own spacing.

  • prefer_zarr – use zarr when it is installed. That is the intended path — zarr handles sharding, filters and every codec — and the pure-Python reader is the fallback for the common case. Pass False to force the fallback, which is what the tests do.

Returns:

the region, in the file’s own dtype and byte order.

Raises:

OmeZarrError – on an impossible region, and ZarrExtraMissing on a codec that needs the extra.

resolve_region(level: int | str = 0, region: Any = None, world_region: Mapping[str, Sequence[float]] | None = None) → Tuple[Tuple[int, int], ...][source]

Turn a region request into a half-open index box, one per axis.

Parameters:
  • level – which level the box is for. World boxes are resolved through that level’s spacing, so the same world region names the matching pixels at every resolution.

  • region – None for everything, a mapping of axis name to slice / (start, stop) / int, or a sequence of those with one entry per axis. Index regions are refused when they fall outside the array — that is a typo, not a view of an edge.

  • world_region – a mapping of SPACE axis name to (low, high) in world units, resolved with spacr.layers.Spacing.to_data() and clamped to the array. Clamping is right here and refusing is right above: a world box legitimately extends past the edge of a tile, an index box does not.

Returns:

((start, stop), ...), one per axis, in array order.

Raises:

OmeZarrError – on an unknown axis, a reversed or out-of-range index box, or the same axis constrained both ways.

spacing_at(level: int | str = 0) → spacr.layers.Spacing[source]

The voxel size of one level, over the space axes only.

This is what makes a world coordinate mean the same thing at every level: the same world box resolves to the corresponding pixels of the overview and of full resolution.

Parameters:

level – level index or dataset path.

Returns:

the spacing at that level.

property axis_names: Tuple[str, ...][source]

Axis names in array order, e.g. ("t", "c", "z", "y", "x").

property channel_axis: Axis | None[source]

The channel axis, or None.

property dtype: numpy.dtype[source]

Level 0’s dtype, byte order included.

property other_axes: Tuple[Axis, ...][source]

The non-spatial axes, reported beside the spacing rather than in it.

property shape: Tuple[int, ...][source]

Level 0’s shape.

property space_axes: Tuple[Axis, ...][source]

The spatial axes — the ones spacing is built from.

property spacing: spacr.layers.Spacing[source]

Level 0’s voxel size, over the SPACE axes only.

Raises:

OmeZarrError – when the space axes disagree about units or one of them cannot be translated. See spacing_from_axes().

property time_axis: Axis | None[source]

The time axis, or None. Its unit is seconds-like, never the spacing’s.

spacr.ome_zarr.axes_from_spacing(spacing: spacr.layers.Spacing, ndim: int | None = None, names: Sequence[str] | None = None) → Tuple[Axis, ...][source]

Build the full NGFF axis list for an array from a spacing.

Parameters:
  • spacing – the spatial spacing. Its axis names become the space axes, and its units become theirs.

  • ndim – the array’s dimensionality. When it exceeds the spacing’s, the leading axes are taken from CANONICAL_AXIS_ORDER — the only order NGFF 0.4 permits — so a (t, c, z, y, x) array written with a (z, y, x) spacing needs no extra argument.

  • names – explicit names for every axis, overriding the derivation.

Returns:

the axes, outermost first.

Raises:

OmeZarrError – when the counts cannot be reconciled, or a unit has no NGFF name.

spacr.ome_zarr.ngff_unit_to_spacr(unit: str | None, *, axis: str = '?') → str[source]

Translate an NGFF unit name into the token Spacing.units carries.

Parameters:
  • unit – the unit field of an NGFF axis, or None/"" when the file declares none.

  • axis – the axis name, used only to make the error message point at the offending axis.

Returns:

the spaCR token ("um", "nm", "s", …), or PIXEL_UNITS when no unit was declared.

Raises:

OmeZarrError – on a unit spaCR will not translate. Two distinct messages: one for a name NGFF does not define, one for a legal NGFF unit spaCR has no short token for — the second tells the user to convert rather than implying their file is broken.

spacr.ome_zarr.read_ome_zarr(path: str | os.PathLike, *, multiscale_index: int = 0) → OmeZarrImage[source]

Open an OME-Zarr group and read its metadata. No chunk is touched.

Everything this reads is small JSON: the group’s .zattrs (or the attributes of its zarr.json) and one .zarray per level. So asking a 100 GB plate what its levels, shapes, voxel size and channels are costs a few kilobytes, which is the property the whole format exists for.

Parameters:
  • path – the group directory — the one holding multiscales. For a plate that is <plate>.zarr/<row>/<column>/<field>. An s3://, gs://, az:// or https:// address is read from cloud storage, fetching only the metadata files.

  • multiscale_index – which multiscales entry to read. NGFF permits several; spaCR reads the first by default and says so here rather than pretending there can only be one.

Returns:

the opened OmeZarrImage.

Raises:

OmeZarrError – when the directory is not a zarr group, has no multiscales, declares a zarr format spaCR does not read, or carries axis units spaCR will not translate.

spacr.ome_zarr.read_ome_zarr_array(path: str | os.PathLike, level: int | str = 0, region: Any = None, *, world_region: Mapping[str, Sequence[float]] | None = None, prefer_zarr: bool = True) → numpy.ndarray[source]

Open an OME-Zarr and read one level (or a region of it) in one call.

The convenience form of read_ome_zarr(path).read(...), for when the metadata is not wanted afterwards.

Parameters:
  • path – the group directory.

  • level – level index or dataset path.

  • region – an index box — see OmeZarrImage.resolve_region().

  • world_region – a world-coordinate box, ditto.

  • prefer_zarr – delegate to zarr when installed.

Returns:

the array, in the file’s own dtype.

spacr.ome_zarr.require_codec(codec_id: str, config: Mapping[str, Any] | None = None) → Callable[[bytes], bytes][source]

Return a bytes -> bytes decoder for a zarr compressor id.

The standard library is tried first, then numcodecs. Only when both fail is anything raised, and what is raised names the codec.

Parameters:
  • codec_id – the id from a zarr v2 compressor block, or the name of a zarr v3 codec — "zlib", "blosc", "zstd".

  • config – the rest of that block, passed to numcodecs when it is needed. Ignored by the standard-library codecs, whose decompressors read their parameters out of the stream.

Returns:

a callable taking the stored chunk bytes and returning the raw bytes.

Raises:
  • ZarrExtraMissing – when neither the standard library nor an installed numcodecs provides the codec.

  • OmeZarrError – when codec_id is empty or not a string.

spacr.ome_zarr.require_zarr()[source]

Import and return zarr, or raise a message worth reading.

Returns:

the imported zarr module.

Raises:

ZarrExtraMissing – when the extra is not installed, with the pip install "spacr[zarr]" line and a note that most files do not need it.

spacr.ome_zarr.spacing_from_axes(axes: Sequence[Axis]) → spacr.layers.Spacing[source]

Build a spacr.layers.Spacing from the SPACE axes only.

The exclusion is the point. A spacing has one units string and spacr.layers.LayerStack compares it by name; a spacing that mixed a time axis in seconds with a y axis in micrometers would answer that comparison with a string that is wrong for one of them, and pass.

Parameters:

axes – every axis of the image, in array order.

Returns:

a spacing over the spatial axes, in the same relative order, with spacr.layers.Spacing.units set from their common unit.

Raises:

OmeZarrError – when there are no spatial axes; when they carry different units from each other (named); or when a unit does not translate.

spacr.ome_zarr.spacr_unit_to_ngff(units: str) → str | None[source]

Translate a spacr.layers.Spacing.units token into an NGFF unit.

Parameters:

units – the spaCR token, e.g. "um" or "px".

Returns:

the UDUNITS-2 name to write into axes, or None for pixels — NGFF has no pixel unit, and an axis with no unit is the spec’s way of saying the same thing.

Raises:

OmeZarrError – on a token with no NGFF name, naming what is available.

spacr.ome_zarr.write_ome_zarr(path: str | os.PathLike, array: Any, *, spacing: spacr.layers.Spacing | None = None, axes: Sequence[str | Axis] | None = None, name: str | None = None, levels: int = 1, downsample: str = 'mean', downsample_axes: Sequence[str] | None = None, chunks: Sequence[int] | None = None, compressor: str | None = 'zlib', compression_level: int = 5, order: str = 'C', dimension_separator: str = '/', fill_value: Any = 0, write_empty_chunks: bool = False, channel_names: Sequence[str] | None = None, channel_colors: Sequence[str] | None = None, overwrite: bool = False, ngff_version: str = '0.4') → OmeZarrImage[source]

Write an array as an OME-NGFF 0.4 multiscale image.

The defaults are chosen so the result is readable by anything: zarr-format 2, zlib chunks (which need no extra), / separators, and the axes/coordinateTransformations metadata filled in from the spacing rather than left at 1.0.

The pyramid. levels=1 writes level 0 alone; more builds each level by halving the two fastest-varying space axes of the one above with a 2x block mean (downsample="mean") or by striding (downsample="stride"). Neither is a Gaussian pyramid — there is no pre-filter beyond the box — and the block mean is the default because striding aliases: a one-pixel-wide bright structure survives or vanishes with the parity of its coordinate, so the same object appears and disappears as a user zooms. Label and mask arrays must use downsample="stride": the mean of labels 3 and 5 is 4, which is a different object, and averaging a boolean mask invents half-membership. Levels shrink by ceil(n / 2), so nothing is cropped off an odd edge.

The transformations. Level k’s scale is level 0’s multiplied by 2^k on each downsampled axis — writing level 0’s scale on every level is the most common NGFF bug there is, and it renders as coarse levels that are a quarter the size of the fine ones and slide off the corner as you zoom. The translation moves too, and differently per method: a block mean centres its first output element half a level-0 pixel inside the edge, so level k gets translate_0 + scale_0 * (2^k - 1) / 2, while a strided level samples element 0 exactly and keeps translate_0.

The chunks. Default is 1 along t and c (a viewer draws one channel of one timepoint), DEFAULT_TILE on y and x, and DEFAULT_Z_CHUNK on any other space axis — about 2 MiB per chunk for uint16. Pass chunks= to override.

Parameters:
  • path – the group directory to create.

  • array – the image data, 2 to 5 dimensional.

  • spacing – the voxel size, over the SPACE axes. Defaults to an isotropic pixel grid, which writes axes with no unit — legal NGFF and honestly what is known.

  • axes – axis names, or Axis objects for full control of the non-spatial axes (a time step, say). Derived from spacing and the array’s rank when omitted.

  • name – the multiscales name. Defaults to the directory name.

  • levels – how many resolution levels to write.

  • downsample – "mean" or "stride".

  • downsample_axes – which axes to halve. Defaults to the two fastest-varying space axes — halving z as well ruins an anisotropic stack, where 12 planes at 2 µm become 2 planes at 16 µm by level 3.

  • chunks – chunk shape, one per axis.

  • compressor – "zlib" (default), "gzip", "bz2", "lzma", None for stored, or any codec numcodecs provides. The default needs no extra by design.

  • compression_level – for the codecs that take one.

  • order – "C" or "F" chunk memory order.

  • dimension_separator – "/" (nested directories, the default, and much kinder to filesystems at 10^5 chunks) or "." (flat).

  • fill_value – what an unwritten chunk reads back as.

  • write_empty_chunks – write chunks that are entirely fill_value. False (the default) omits them, which is how a sparse mask stops costing what a dense one does.

  • channel_names – channel labels, written into the omero block along with per-channel display windows measured from the data.

  • channel_colors – hex RGB per channel; defaults to DEFAULT_CHANNEL_COLORS.

  • overwrite – replace an existing group. Without it, an existing group is refused rather than merged — a pyramid written over another pyramid’s levels leaves the leftover deeper levels of the old one in place, and they read back as part of the new image.

  • ngff_version – the version to declare. 0.4 is what this writes.

Returns:

the group, reopened with read_ome_zarr() — so the return value is a read of what was actually written, not a description of what was intended.

Raises:

OmeZarrError – on an unwritable axis layout, a unit with no NGFF name, or an existing group without overwrite=True.

spacr.ome_zarr.CODEC_MISSING_MESSAGE = Multiline-String[source]
Show Value
"""This OME-Zarr stores its chunks with the {codec!r} codec, which spaCR cannot
decode with the standard library alone, and the optional `zarr` extra that
provides it is not installed (missing module: {module}).

Install it with:

    python -m pip install "spacr[zarr]"

Codecs that need no extra: {stdlib}, or uncompressed. spaCR-written OME-Zarr
uses one of those by default, so it always round-trips on a plain
`pip install spacr`."""
spacr.ome_zarr.ZARR_MISSING_MESSAGE = Multiline-String[source]
Show Value
"""This needs the optional `zarr` extra, which is not installed in this
environment (missing module: {module}).

Install it with:

    python -m pip install "spacr[zarr]"

spaCR reads and writes OME-Zarr metadata, and stored/zlib/gzip/bz2/lzma
chunks, without it — the extra is only needed for third-party chunk codecs
and for zarr-format 3 features such as sharding."""

Nested helpers

_compose._pair(transforms: Sequence[Mapping[str, Any]]) → Tuple[List[float], List[float]]

Reduce one captured-dimension NGFF transform sequence.

Parameters:

transforms – identity, scale, and translation mappings; a falsey sequence represents the identity transform.

Returns:

mutable scale and translation lists of captured length ndim. Repeated scales multiply componentwise and repeated translations add componentwise.

Raises:

OmeZarrError – when an entry is not a mapping, a scale or translation has the wrong arity, or the transform type is not supported; messages include the captured source location.

spacr/ome_zarr.py:1761