diff --git a/docs/api/v04/other.md b/docs/api/v04/other.md index ad7efdb..6dc609c 100644 --- a/docs/api/v04/other.md +++ b/docs/api/v04/other.md @@ -13,3 +13,7 @@ These are models that live further down the hierarchy. ## multiscales ::: ome_zarr_models.v04.multiscales + +## multiscales + +::: ome_zarr_models.v04.multiscales diff --git a/docs/index.md b/docs/index.md index 0a547d1..f51385b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -32,3 +32,10 @@ This package is designed with the following guiding principles: - Array reading and writing operations are out of scope We are trying to make this as usable and useful as possible while still complying with the OME-zarr specification. + +## Known issues + +- Because of the way this package is structured, it can't currently distinguish + between values that are present but set to `null` in saved metadata, and + fields that are not present. Any fields set to `None` in the Python objects + are currently not written when they are saved back to the JSON metadata using this package. diff --git a/src/ome_zarr_models/v04/image.py b/src/ome_zarr_models/v04/image.py index 077db7d..878d738 100644 --- a/src/ome_zarr_models/v04/image.py +++ b/src/ome_zarr_models/v04/image.py @@ -1,12 +1,13 @@ from __future__ import annotations -from typing import Self +from typing import Annotated, Self import zarr.errors from pydantic import Field, model_validator from pydantic_zarr.v2 import ArraySpec, GroupSpec from ome_zarr_models.base import Base +from ome_zarr_models.v04.image_label import ImageLabel from ome_zarr_models.v04.multiscales import Multiscales from ome_zarr_models.v04.omero import Omero from ome_zarr_models.zarr_utils import get_path @@ -71,6 +72,11 @@ class ImageAttrs(Base): min_length=1, ) omero: Omero | None = None + image_labels: Annotated[ImageLabel | None, Field(..., alias="image-label")] = None + + # TODO: validate: + # "image-label groups MUST also contain multiscales metadata and the two "datasets" + # series MUST have the same number of entries." class Image(GroupSpec[ImageAttrs, ArraySpec | GroupSpec]): diff --git a/src/ome_zarr_models/v04/image_label.py b/src/ome_zarr_models/v04/image_label.py index e0cb92e..901af68 100644 --- a/src/ome_zarr_models/v04/image_label.py +++ b/src/ome_zarr_models/v04/image_label.py @@ -1,43 +1,29 @@ +""" +For reference, see the [image label section of the OME-zarr specification](https://ngff.openmicroscopy.org/0.4/index.html#label-md). +""" + from __future__ import annotations import warnings -from collections import Counter -from typing import TYPE_CHECKING, Annotated, Literal +from typing import Annotated, Literal from pydantic import AfterValidator, Field, model_validator from ome_zarr_models.base import Base -from ome_zarr_models.v04.image import ImageAttrs - -if TYPE_CHECKING: - from collections.abc import Hashable, Iterable - -__all__ = ["RGBA", "Color", "ConInt", "GroupAttrs", "ImageLabel", "Property", "Source"] +from ome_zarr_models.utils import duplicates -ConInt = Annotated[int, Field(strict=True, ge=0, le=255)] -RGBA = tuple[ConInt, ConInt, ConInt, ConInt] +__all__ = ["RGBA", "Color", "ImageLabel", "Property", "Source", "Uint8"] - -def _duplicates(values: Iterable[Hashable]) -> dict[Hashable, int]: - """ - Takes a sequence of hashable elements and returns a dict where the keys are the - elements of the input that occurred at least once, and the values are the - frequencies of those elements. - """ - counts = Counter(values) - return {k: v for k, v in counts.items() if v > 1} +Uint8 = Annotated[int, Field(strict=True, ge=0, le=255)] +RGBA = tuple[Uint8, Uint8, Uint8, Uint8] class Color(Base): """ A label value and RGBA. - - References - ---------- - https://ngff.openmicroscopy.org/0.4/#label-md """ - label_value: int = Field(..., serialization_alias="label-value") + label_value: int = Field(..., alias="label-value") rgba: RGBA | None @@ -46,8 +32,10 @@ class Source(Base): Source data for the labels. """ - # TODO: add validation that this path resolves to something - image: str | None = "../../" + # TODO: add validation that this path resolves to a zarr image group + image: str | None = Field( + default="../../", description="Relative path to a Zarr group of a key image." + ) class Property(Base): @@ -55,19 +43,18 @@ class Property(Base): A single property. """ - label_value: int = Field(..., serialization_alias="label-value") + label_value: int = Field(..., alias="label-value") -def _parse_colors(colors: list[Color] | None) -> list[Color] | None: +def _parse_colors(colors: tuple[Color] | None) -> tuple[Color] | None: if colors is None: msg = ( - "The field `colors` is `None`. Version 0.4 of" - "the OME-NGFF spec states that `colors` should be a list of " + "The field `colors` is `None`. `colors` should be a list of " "label descriptors." ) warnings.warn(msg, stacklevel=1) else: - dupes = _duplicates(x.label_value for x in colors) + dupes = duplicates(x.label_value for x in colors) if len(dupes) > 0: msg = ( f"Duplicated label-value: {tuple(dupes.keys())}." @@ -78,16 +65,6 @@ def _parse_colors(colors: list[Color] | None) -> list[Color] | None: return colors -def _parse_version(version: Literal["0.4"] | None) -> Literal["0.4"] | None: - if version is None: - _ = ( - "The `version` attribute is `None`. Version 0.4 of " - "the OME-NGFF spec states that `version` should either be unset or " - "the string 0.4" - ) - return version - - def _parse_imagelabel(model: ImageLabel) -> ImageLabel: """ check that label_values are consistent across properties and colors @@ -111,34 +88,15 @@ def _parse_imagelabel(model: ImageLabel) -> ImageLabel: class ImageLabel(Base): """ image-label metadata. - See https://ngff.openmicroscopy.org/0.4/#label-md """ - _version: Literal["0.4"] - - version: Annotated[Literal["0.4"] | None, AfterValidator(_parse_version)] + # TODO: validate + # "All the values under the label-value (of colors) key MUST be unique." colors: Annotated[tuple[Color, ...] | None, AfterValidator(_parse_colors)] = None properties: tuple[Property, ...] | None = None source: Source | None = None + version: Literal["0.4"] | None @model_validator(mode="after") def _parse_model(self) -> ImageLabel: return _parse_imagelabel(self) - - -class GroupAttrs(ImageAttrs): - """ - Attributes for a Zarr group that contains `image-label` metadata. - Inherits from `v04.multiscales.MultiscaleAttrs`. - - See https://ngff.openmicroscopy.org/0.4/#label-md - - Attributes - ---------- - image_label: `ImageLabel` - Image label metadata. - multiscales: tuple[v04.multiscales.Multiscales] - Multiscale image metadata. - """ - - image_label: Annotated[ImageLabel, Field(..., serialization_alias="image-label")] diff --git a/tests/v04/data/image_label_example.json b/tests/v04/data/image_label_example.json new file mode 100644 index 0000000..420e4c0 --- /dev/null +++ b/tests/v04/data/image_label_example.json @@ -0,0 +1,27 @@ +{ + "version": "0.4", + "colors": [ + { + "label-value": 1, + "rgba": [255, 255, 255, 255] + }, + { + "label-value": 4, + "rgba": [0, 255, 255, 128] + } + ], + "properties": [ + { + "label-value": 1, + "area": 1200, + "cls": "foo" + }, + { + "label-value": 4, + "area": 1650 + } + ], + "source": { + "image": "../../" + } +} diff --git a/tests/v04/test_image_label.py b/tests/v04/test_image_label.py new file mode 100644 index 0000000..d4c96cd --- /dev/null +++ b/tests/v04/test_image_label.py @@ -0,0 +1,20 @@ +from tests.v04.conftest import read_in_json + +from ome_zarr_models.v04.image_label import Color, ImageLabel, Property, Source + + +def test_image_label_example_json() -> None: + model = read_in_json(json_fname="image_label_example.json", model_cls=ImageLabel) + + assert model == ImageLabel( + colors=( + Color(label_value=1, rgba=(255, 255, 255, 255)), + Color(label_value=4, rgba=(0, 255, 255, 128)), + ), + properties=( + Property(label_value=1, area=1200, cls="foo"), + Property(label_value=4, area=1650), + ), + source=Source(image="../../"), + version="0.4", + )