Skip to content
Merged

omero #129

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions ngff_zarr/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
Transform,
Dataset,
Metadata,
Omero,
OmeroChannel,
OmeroWindow,
)

__all__ = [
Expand Down Expand Up @@ -67,4 +70,7 @@
"Transform",
"Dataset",
"Metadata",
"Omero",
"OmeroChannel",
"OmeroWindow",
]
32 changes: 31 additions & 1 deletion ngff_zarr/from_ngff_zarr.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,15 @@

from .ngff_image import NgffImage
from .to_multiscales import Multiscales
from .v04.zarr_metadata import Axis, Dataset, Scale, Translation
from .v04.zarr_metadata import (
Axis,
Dataset,
Scale,
Translation,
Omero,
OmeroChannel,
OmeroWindow,
)
from .validate import validate as validate_ngff

zarr_version = packaging.version.parse(zarr.__version__)
Expand Down Expand Up @@ -145,9 +153,29 @@ def from_ngff_zarr(
Axis(name="y", type="space"),
Axis(name="x", type="space"),
]

coordinateTransformations = None
if "coordinateTransformations" in metadata:
coordinateTransformations = metadata["coordinateTransformations"]

omero = None
if "omero" in root.attrs:
omero_data = root.attrs["omero"]
omero = Omero(
channels=[
OmeroChannel(
color=channel["color"],
window=OmeroWindow(
min=channel["window"]["min"],
max=channel["window"]["max"],
start=channel["window"]["start"],
end=channel["window"]["end"],
),
)
for channel in omero_data["channels"]
]
)

if version == "0.5":
from .v05.zarr_metadata import Metadata

Expand All @@ -156,6 +184,7 @@ def from_ngff_zarr(
datasets=datasets,
name=name,
coordinateTransformations=coordinateTransformations,
omero=omero,
)
else:
from .v04.zarr_metadata import Metadata
Expand All @@ -166,6 +195,7 @@ def from_ngff_zarr(
name=name,
version=metadata["version"],
coordinateTransformations=coordinateTransformations,
omero=omero,
)

return Multiscales(images, metadata)
6 changes: 6 additions & 0 deletions ngff_zarr/to_ngff_zarr.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ def _pop_metadata_optionals(metadata_dict):
if metadata_dict["coordinateTransformations"] is None:
metadata_dict.pop("coordinateTransformations")

if metadata_dict["omero"] is None:
metadata_dict.pop("omero")

return metadata_dict


Expand Down Expand Up @@ -231,6 +234,9 @@ def to_ngff_zarr(
**format_kwargs,
)

if "omero" in metadata_dict:
root.attrs["omero"] = metadata_dict.pop("omero")

if version != "0.4":
# RFC 2, Zarr 3
root.attrs["ome"] = {"version": version, "multiscales": [metadata_dict]}
Expand Down
25 changes: 25 additions & 0 deletions ngff_zarr/v04/zarr_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from typing import List, Optional, Union

from typing_extensions import Literal
import re

SupportedDims = Union[
Literal["c"], Literal["x"], Literal["y"], Literal["z"], Literal["t"]
Expand Down Expand Up @@ -165,10 +166,34 @@ class Dataset:
coordinateTransformations: List[Transform]


@dataclass
class OmeroWindow:
min: float
max: float
start: float
end: float


@dataclass
class OmeroChannel:
color: str
window: OmeroWindow

def validate_color(self):
if not re.fullmatch(r"[0-9A-Fa-f]{6}", self.color):
raise ValueError(f"Invalid color '{self.color}'. Must be 6 hex digits.")


@dataclass
class Omero:
channels: List[OmeroChannel]


@dataclass
class Metadata:
axes: List[Axis]
datasets: List[Dataset]
coordinateTransformations: Optional[List[Transform]]
omero: Optional[Omero] = None
name: str = "image"
version: str = "0.4"
3 changes: 2 additions & 1 deletion ngff_zarr/v05/zarr_metadata.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
from typing import List, Optional
from dataclasses import dataclass

from ..v04.zarr_metadata import Axis, Transform, Dataset
from ..v04.zarr_metadata import Axis, Transform, Dataset, Omero


@dataclass
class Metadata:
axes: List[Axis]
datasets: List[Dataset]
coordinateTransformations: Optional[List[Transform]]
omero: Optional[Omero] = None
name: str = "image"
116 changes: 116 additions & 0 deletions test/test_omero.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import pytest
import numpy as np
from zarr.storage import MemoryStore
from ngff_zarr import (
Omero,
OmeroChannel,
OmeroWindow,
from_ngff_zarr,
to_ngff_image,
to_multiscales,
to_ngff_zarr,
)

from ._data import test_data_dir


def test_read_omero(input_images): # noqa: ARG001
dataset_name = "13457537"
store_path = test_data_dir / "input" / f"{dataset_name}.zarr"
multiscales = from_ngff_zarr(store_path, validate=True)

omero = multiscales.metadata.omero
assert omero is not None
assert len(omero.channels) == 6

# Channel 0
assert omero.channels[0].color == "FFFFFF"
assert omero.channels[0].window.min == 0.0
assert omero.channels[0].window.max == 65535.0
assert omero.channels[0].window.start == 0.0
assert omero.channels[0].window.end == 1200.0

# Channel 1
assert omero.channels[1].color == "FFFFFF"
assert omero.channels[1].window.min == 0.0
assert omero.channels[1].window.max == 65535.0
assert omero.channels[1].window.start == 0.0
assert omero.channels[1].window.end == 1200.0

# Channel 2
assert omero.channels[2].color == "FFFFFF"
assert omero.channels[2].window.min == 0.0
assert omero.channels[2].window.max == 65535.0
assert omero.channels[2].window.start == 0.0
assert omero.channels[2].window.end == 1200.0

# Channel 3
assert omero.channels[3].color == "FFFFFF"
assert omero.channels[3].window.min == 0.0
assert omero.channels[3].window.max == 65535.0
assert omero.channels[3].window.start == 0.0
assert omero.channels[3].window.end == 1200.0

# Channel 4
assert omero.channels[4].color == "0000FF"
assert omero.channels[4].window.min == 0.0
assert omero.channels[4].window.max == 65535.0
assert omero.channels[4].window.start == 0.0
assert omero.channels[4].window.end == 5000.0

# Channel 5
assert omero.channels[5].color == "FF0000"
assert omero.channels[5].window.min == 0.0
assert omero.channels[5].window.max == 65535.0
assert omero.channels[5].window.start == 0.0
assert omero.channels[5].window.end == 100.0


def test_write_omero():
data = np.random.randint(0, 256, 262144).reshape((2, 32, 64, 64)).astype(np.uint8)
image = to_ngff_image(data, dims=["c", "z", "y", "x"])
multiscales = to_multiscales(image, scale_factors=[2, 4], chunks=32)

omero = Omero(
channels=[
OmeroChannel(
color="008000",
window=OmeroWindow(min=0.0, max=255.0, start=10.0, end=150.0),
),
OmeroChannel(
color="0000FF",
window=OmeroWindow(min=0.0, max=255.0, start=30.0, end=200.0),
),
]
)
multiscales.metadata.omero = omero

store = MemoryStore()
version = "0.4"
to_ngff_zarr(store, multiscales, version=version)

multiscales_read = from_ngff_zarr(store, validate=True, version=version)
read_omero = multiscales_read.metadata.omero

assert read_omero is not None
assert len(read_omero.channels) == 2
assert read_omero.channels[0].color == "008000"
assert read_omero.channels[0].window.start == 10.0
assert read_omero.channels[0].window.end == 150.0
assert read_omero.channels[1].color == "0000FF"
assert read_omero.channels[1].window.start == 30.0
assert read_omero.channels[1].window.end == 200.0


def test_validate_color():
valid_channel = OmeroChannel(
color="1A2B3C", window=OmeroWindow(min=0.0, max=255.0, start=0.0, end=100.0)
)
# This should not raise an error
valid_channel.validate_color()

invalid_channel = OmeroChannel(
color="ZZZZZZ", window=OmeroWindow(min=0.0, max=255.0, start=0.0, end=100.0)
)
with pytest.raises(ValueError, match=r"Invalid color 'ZZZZZZ'"):
invalid_channel.validate_color()
Loading