Skip to content

Commit

Permalink
Merge pull request #63 from BioImageTools/image-labels
Browse files Browse the repository at this point in the history
Improve image-labels metadata
  • Loading branch information
dstansby authored Dec 1, 2024
2 parents 1c9e4e3 + 422c3b9 commit 68b6cd3
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 64 deletions.
4 changes: 4 additions & 0 deletions docs/api/v04/other.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
7 changes: 7 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
8 changes: 7 additions & 1 deletion src/ome_zarr_models/v04/image.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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]):
Expand Down
84 changes: 21 additions & 63 deletions src/ome_zarr_models/v04/image_label.py
Original file line number Diff line number Diff line change
@@ -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


Expand All @@ -46,28 +32,29 @@ 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):
"""
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())}."
Expand All @@ -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
Expand All @@ -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")]
27 changes: 27 additions & 0 deletions tests/v04/data/image_label_example.json
Original file line number Diff line number Diff line change
@@ -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": "../../"
}
}
20 changes: 20 additions & 0 deletions tests/v04/test_image_label.py
Original file line number Diff line number Diff line change
@@ -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",
)

0 comments on commit 68b6cd3

Please sign in to comment.