From e5f46c0220253fed7339ceb9d02267825fb037b2 Mon Sep 17 00:00:00 2001 From: David Stansby Date: Sun, 1 Dec 2024 17:08:28 +0000 Subject: [PATCH 1/8] Make image_label an optional Image field --- src/ome_zarr_models/v04/image.py | 8 +++++++- src/ome_zarr_models/v04/image_label.py | 19 ------------------- 2 files changed, 7 insertions(+), 20 deletions(-) diff --git a/src/ome_zarr_models/v04/image.py b/src/ome_zarr_models/v04/image.py index 077db7d..8f0a5fd 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(..., serialization_alias="image-label") + ] + + # TODO: validate: if image_labels is present, multiscales must be present too 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..5fe9ee3 100644 --- a/src/ome_zarr_models/v04/image_label.py +++ b/src/ome_zarr_models/v04/image_label.py @@ -7,7 +7,6 @@ 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 @@ -124,21 +123,3 @@ class ImageLabel(Base): @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")] From 12c4619db92e18cdb0e0f31b4713fcd0bc41e1b4 Mon Sep 17 00:00:00 2001 From: David Stansby Date: Sun, 1 Dec 2024 17:17:29 +0000 Subject: [PATCH 2/8] Clean up image_labels --- src/ome_zarr_models/v04/image.py | 4 ++- src/ome_zarr_models/v04/image_label.py | 38 +++++++++++--------------- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/src/ome_zarr_models/v04/image.py b/src/ome_zarr_models/v04/image.py index 8f0a5fd..e906605 100644 --- a/src/ome_zarr_models/v04/image.py +++ b/src/ome_zarr_models/v04/image.py @@ -76,7 +76,9 @@ class ImageAttrs(Base): ImageLabel | None, Field(..., serialization_alias="image-label") ] - # TODO: validate: if image_labels is present, multiscales must be present too + # 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 5fe9ee3..98b1068 100644 --- a/src/ome_zarr_models/v04/image_label.py +++ b/src/ome_zarr_models/v04/image_label.py @@ -1,3 +1,7 @@ +""" +See https://ngff.openmicroscopy.org/0.4/index.html#label-md +""" + from __future__ import annotations import warnings @@ -11,10 +15,10 @@ if TYPE_CHECKING: from collections.abc import Hashable, Iterable -__all__ = ["RGBA", "Color", "ConInt", "GroupAttrs", "ImageLabel", "Property", "Source"] +__all__ = ["RGBA", "Color", "ImageLabel", "Property", "Source", "Uint8"] -ConInt = Annotated[int, Field(strict=True, ge=0, le=255)] -RGBA = tuple[ConInt, ConInt, ConInt, ConInt] +Uint8 = Annotated[int, Field(strict=True, ge=0, le=255)] +RGBA = tuple[Uint8, Uint8, Uint8, Uint8] def _duplicates(values: Iterable[Hashable]) -> dict[Hashable, int]: @@ -45,8 +49,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): @@ -60,8 +66,7 @@ class Property(Base): def _parse_colors(colors: list[Color] | None) -> list[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) @@ -77,16 +82,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 @@ -110,15 +105,14 @@ 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)] - colors: Annotated[tuple[Color, ...] | None, AfterValidator(_parse_colors)] = None + # 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: From cab55c422b6b61db5625552835bdfb3df3b8444b Mon Sep 17 00:00:00 2001 From: David Stansby Date: Sun, 1 Dec 2024 17:19:00 +0000 Subject: [PATCH 3/8] Fix default value --- src/ome_zarr_models/v04/image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ome_zarr_models/v04/image.py b/src/ome_zarr_models/v04/image.py index e906605..a823254 100644 --- a/src/ome_zarr_models/v04/image.py +++ b/src/ome_zarr_models/v04/image.py @@ -74,7 +74,7 @@ class ImageAttrs(Base): omero: Omero | None = None image_labels: Annotated[ ImageLabel | None, Field(..., serialization_alias="image-label") - ] + ] = None # TODO: validate: # "image-label groups MUST also contain multiscales metadata and the two "datasets" series From 0aa5af02bcbd0044a501d67e720c7382cbe71c54 Mon Sep 17 00:00:00 2001 From: David Stansby Date: Sun, 1 Dec 2024 17:36:49 +0000 Subject: [PATCH 4/8] Add a test --- src/ome_zarr_models/v04/image.py | 8 +++----- src/ome_zarr_models/v04/image_label.py | 8 ++++---- tests/v04/data/image_label_example.json | 27 +++++++++++++++++++++++++ tests/v04/test_image_label.py | 20 ++++++++++++++++++ 4 files changed, 54 insertions(+), 9 deletions(-) create mode 100644 tests/v04/data/image_label_example.json create mode 100644 tests/v04/test_image_label.py diff --git a/src/ome_zarr_models/v04/image.py b/src/ome_zarr_models/v04/image.py index a823254..878d738 100644 --- a/src/ome_zarr_models/v04/image.py +++ b/src/ome_zarr_models/v04/image.py @@ -72,13 +72,11 @@ class ImageAttrs(Base): min_length=1, ) omero: Omero | None = None - image_labels: Annotated[ - ImageLabel | None, Field(..., serialization_alias="image-label") - ] = 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." + # "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 98b1068..ade263d 100644 --- a/src/ome_zarr_models/v04/image_label.py +++ b/src/ome_zarr_models/v04/image_label.py @@ -40,7 +40,7 @@ class Color(Base): 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 @@ -60,10 +60,10 @@ 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`.`colors` should be a list of " @@ -109,7 +109,7 @@ class ImageLabel(Base): # TODO: validate # "All the values under the label-value (of colors) key MUST be unique." - colors: Annotated[tuple[Color] | None, AfterValidator(_parse_colors)] = None + colors: Annotated[tuple[Color, ...] | None, AfterValidator(_parse_colors)] = None properties: tuple[Property, ...] | None = None source: Source | None = None version: Literal["0.4"] | None 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", + ) From a995da027149742866b411ccccba3cd66692ca8e Mon Sep 17 00:00:00 2001 From: David Stansby Date: Sun, 1 Dec 2024 17:38:45 +0000 Subject: [PATCH 5/8] Add to docs --- docs/api/v04/other.md | 4 ++++ src/ome_zarr_models/v04/image_label.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) 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/src/ome_zarr_models/v04/image_label.py b/src/ome_zarr_models/v04/image_label.py index ade263d..5810b4a 100644 --- a/src/ome_zarr_models/v04/image_label.py +++ b/src/ome_zarr_models/v04/image_label.py @@ -1,5 +1,5 @@ """ -See https://ngff.openmicroscopy.org/0.4/index.html#label-md +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 From cc786bf4fa9d3d55e8b969929d3cd4541e665a3d Mon Sep 17 00:00:00 2001 From: David Stansby Date: Sun, 1 Dec 2024 17:40:44 +0000 Subject: [PATCH 6/8] Remove duplicated code --- src/ome_zarr_models/v04/image_label.py | 23 +++-------------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/src/ome_zarr_models/v04/image_label.py b/src/ome_zarr_models/v04/image_label.py index 5810b4a..0537b42 100644 --- a/src/ome_zarr_models/v04/image_label.py +++ b/src/ome_zarr_models/v04/image_label.py @@ -5,15 +5,12 @@ 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 - -if TYPE_CHECKING: - from collections.abc import Hashable, Iterable +from ome_zarr_models.utils import duplicates __all__ = ["RGBA", "Color", "ImageLabel", "Property", "Source", "Uint8"] @@ -21,23 +18,9 @@ RGBA = tuple[Uint8, Uint8, Uint8, 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} - - class Color(Base): """ A label value and RGBA. - - References - ---------- - https://ngff.openmicroscopy.org/0.4/#label-md """ label_value: int = Field(..., alias="label-value") @@ -71,7 +54,7 @@ def _parse_colors(colors: tuple[Color] | None) -> tuple[Color] | None: ) 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())}." From 2812f18e83f5f7a9921d46cb6048af6792ad167f Mon Sep 17 00:00:00 2001 From: David Stansby Date: Sun, 1 Dec 2024 22:02:35 +0000 Subject: [PATCH 7/8] Add note about serialising/de-serialising None --- docs/index.md | 7 +++++++ 1 file changed, 7 insertions(+) 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. From 422c3b9691450ac72d92f73f09c3bf9a6e35ffa3 Mon Sep 17 00:00:00 2001 From: David Stansby Date: Sun, 1 Dec 2024 22:05:29 +0000 Subject: [PATCH 8/8] Add missing space Co-authored-by: Davis Bennett --- src/ome_zarr_models/v04/image_label.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ome_zarr_models/v04/image_label.py b/src/ome_zarr_models/v04/image_label.py index 0537b42..901af68 100644 --- a/src/ome_zarr_models/v04/image_label.py +++ b/src/ome_zarr_models/v04/image_label.py @@ -49,7 +49,7 @@ class Property(Base): def _parse_colors(colors: tuple[Color] | None) -> tuple[Color] | None: if colors is None: msg = ( - "The field `colors` is `None`.`colors` should be a list of " + "The field `colors` is `None`. `colors` should be a list of " "label descriptors." ) warnings.warn(msg, stacklevel=1)