RFC-5: Coordinate Systems and TransformationsΒΆ
RFC-5 extends OME-NGFF with named coordinate systems and a richer set of
coordinate transformations β identity, scale, translation, rotation,
affine, transformation sequences, and array-backed displacement and
coordinate fields. This is the OME-Zarr v0.6 data model. ngff-zarr reads and
writes it in both the Python and TypeScript packages.
OverviewΒΆ
In v0.4/v0.5 each dataset carries only a scale and translation. RFC-5 (v0.6)
generalizes this: a multiscales document declares one or more
coordinateSystems (the intrinsic pixel system plus any output systems), and
maps between them with coordinateTransformations. Transformations may live on
a dataset (the per-scale scale/translation) or at the top level of the
multiscales (an affine, a displacement field, a sequence, β¦), mapping the
image into another coordinate system.
Two of these transformations reference their parameters as an array stored in the Zarr hierarchy rather than inline:
displacementsβ a vector field added to each coordinate.coordinatesβ a vector field of absolute output coordinates.
The field is itself an ordinary OME-Zarr image, so it can be written into the same store as the image it transforms. That end-to-end pattern is the focus of the Write an image and its transformation into one store section below.
Displacement and coordinate fieldsΒΆ
A displacement (or coordinate) field is a multiscale image whose
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, mapping
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, scale_factors=[])
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".
TransformationsΒΆ
The transformation data classes live in ngff_zarr.v06.zarr_metadata:
Class |
|
Parameters |
|---|---|---|
|
|
β |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Every transform has an input and output, each a
CoordinateSystemIdentifier naming a coordinate system (name=) or referencing
a dataset (path=). Displacements/Coordinates additionally carry a path
pointing at the field array within the store.
Inline transforms (affine, rotation, scale, β¦) are stored directly in the
multiscales metadata, so a single to_ome_zarr call writes both the image
pixel data and the transformation:
import numpy as np
import ngff_zarr as nz
from ngff_zarr.v06.zarr_metadata import (
Affine,
Axis,
CoordinateSystem,
CoordinateSystemIdentifier,
)
array = np.random.random((64, 64, 64)).astype(np.float32)
image = nz.to_ngff_image(array, dims=["z", "y", "x"])
multiscales = nz.to_multiscales(image, scale_factors=[])
# An affine that maps the intrinsic pixel system to an "output" system.
output_cs = CoordinateSystem(
name="output",
axes=[Axis(name=d, type="space") for d in ("z", "y", "x")],
)
affine = Affine(
affine=[
[1.0, 0.0, 0.0, 5.0],
[0.0, 1.0, 0.0, 10.0],
[0.0, 0.0, 1.0, 15.0],
],
input=CoordinateSystemIdentifier(
name=multiscales.metadata.intrinsic_coordinate_system.name
),
output=CoordinateSystemIdentifier(name=output_cs.name),
name="to_output",
)
multiscales.metadata.coordinateSystems.append(output_cs)
multiscales.metadata.coordinateTransformations = [affine]
nz.to_ome_zarr("affine.ome.zarr", multiscales, version="0.6")
Write an image and its transformation into one storeΒΆ
A displacements or coordinates transform points at a field array by path.
To keep the image and the field it references together, write the field as an
OME-Zarr image into a subgroup of the same store, then reference it from the
imageβs transform.
Because the store metadata is consolidated after the last write, write the
field subgroup first, then the image at the store root with overwrite=False.
The final write consolidates metadata for the whole store, so the field is
discoverable under its path.
import dask.array as da
import numpy as np
import ngff_zarr as nz
from ngff_zarr.v06.zarr_metadata import (
Axis,
CoordinateSystem,
CoordinateSystemIdentifier,
Displacements,
)
store = "warped.ome.zarr"
field_path = "displacement_field"
# The primary image.
image = nz.to_ngff_image(
np.random.random((256, 256)).astype(np.float32), dims=["y", "x"]
)
multiscales = nz.to_multiscales(image, scale_factors=[])
# The displacement field, an image with a displacement component axis.
field = nz.NgffImage(
data=da.from_array(np.zeros((2, 256, 256), dtype=np.float32)),
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"},
)
field_multiscales = nz.to_multiscales(field, scale_factors=[])
# A displacements transform whose `path` resolves to the field subgroup.
output_cs = CoordinateSystem(
name="output",
axes=[Axis(name="y", type="space"), Axis(name="x", type="space")],
)
transform = Displacements(path=field_path, interpolation="linear")
transform.input = CoordinateSystemIdentifier(
name=multiscales.metadata.intrinsic_coordinate_system.name
)
transform.output = CoordinateSystemIdentifier(name=output_cs.name)
transform.name = "warp"
multiscales.metadata.coordinateSystems.append(output_cs)
multiscales.metadata.coordinateTransformations = [transform]
# Write the field subgroup first, then the image at the store root.
nz.to_ome_zarr(f"{store}/{field_path}", field_multiscales, version="0.6")
nz.to_ome_zarr(store, multiscales, version="0.6", overwrite=False)
Reading back resolves both the image and the field from the one store:
imported = nz.from_ome_zarr(store)
transform = imported.metadata.coordinateTransformations[0]
assert transform.path == field_path
field = nz.from_ome_zarr(f"{store}/{transform.path}")
axes = field.metadata.intrinsic_coordinate_system.axes
assert [a.type for a in axes] == ["displacement", "space", "space"]
Use Coordinates instead of Displacements (and axes_types={"c": "coordinate"} on the field) for an absolute coordinate field; the store layout
is identical.
TypeScriptΒΆ
The TypeScript package (@fideus-labs/ngff-zarr) mirrors the Python API. Field
axis types are set with the axesTypes option, and the same field-first,
overwrite: false ordering writes an image and its field into one store:
import {
createAxis,
createCoordinateSystem,
fromOmeZarr,
toMultiscales,
toNgffImage,
toOmeZarr,
type V06Transform,
} from "@fideus-labs/ngff-zarr";
const store = "warped.ome.zarr";
const fieldPath = "displacement_field";
const image = await toNgffImage(new Float32Array(256 * 256), {
dims: ["y", "x"],
shape: [256, 256],
});
const multiscales = await toMultiscales(image, { scaleFactors: [] });
const field = await toNgffImage(new Float32Array(2 * 256 * 256), {
dims: ["c", "y", "x"],
shape: [2, 256, 256],
axesTypes: { c: "displacement" },
});
const fieldMultiscales = await toMultiscales(field, { scaleFactors: [] });
const intrinsic = multiscales.metadata.coordinateSystems![0];
const outputCs = createCoordinateSystem("output", [
createAxis("y", "space"),
createAxis("x", "space"),
]);
multiscales.metadata.coordinateSystems!.push(outputCs);
multiscales.metadata.coordinateTransformations = [{
type: "displacements",
path: fieldPath,
interpolation: "linear",
input: { name: intrinsic.name },
output: { name: outputCs.name },
name: "warp",
} as V06Transform];
// Write the field subgroup first, then the image at the store root.
await toOmeZarr(`${store}/${fieldPath}`, fieldMultiscales, { version: "0.6" });
await toOmeZarr(store, multiscales, { version: "0.6", overwrite: false });
const imported = await fromOmeZarr(store, { version: "0.6" });
const transform = imported.metadata.coordinateTransformations![0];
const field2 = await fromOmeZarr(`${store}/${(transform as { path: string }).path}`, {
version: "0.6",
});
CompatibilityΒΆ
The v0.6 data model requires a Zarr v3 store, so writing version="0.6" needs
zarr-python >= 3.0.0b1. Reading v0.1βv0.5 stores is unchanged; on read, older
metadata is converted into the v0.6 data model (a single intrinsic coordinate
system with per-dataset scale/translation sequences).