Skip to content

Commit 9139e68

Browse files
authored
Merge pull request #129 from thewtex/omero
omero
2 parents 5dd03f6 + 294c503 commit 9139e68

File tree

6 files changed

+186
-2
lines changed

6 files changed

+186
-2
lines changed

ngff_zarr/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@
3232
Transform,
3333
Dataset,
3434
Metadata,
35+
Omero,
36+
OmeroChannel,
37+
OmeroWindow,
3538
)
3639

3740
__all__ = [
@@ -67,4 +70,7 @@
6770
"Transform",
6871
"Dataset",
6972
"Metadata",
73+
"Omero",
74+
"OmeroChannel",
75+
"OmeroWindow",
7076
]

ngff_zarr/from_ngff_zarr.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,15 @@
1616

1717
from .ngff_image import NgffImage
1818
from .to_multiscales import Multiscales
19-
from .v04.zarr_metadata import Axis, Dataset, Scale, Translation
19+
from .v04.zarr_metadata import (
20+
Axis,
21+
Dataset,
22+
Scale,
23+
Translation,
24+
Omero,
25+
OmeroChannel,
26+
OmeroWindow,
27+
)
2028
from .validate import validate as validate_ngff
2129

2230
zarr_version = packaging.version.parse(zarr.__version__)
@@ -145,9 +153,29 @@ def from_ngff_zarr(
145153
Axis(name="y", type="space"),
146154
Axis(name="x", type="space"),
147155
]
156+
148157
coordinateTransformations = None
149158
if "coordinateTransformations" in metadata:
150159
coordinateTransformations = metadata["coordinateTransformations"]
160+
161+
omero = None
162+
if "omero" in root.attrs:
163+
omero_data = root.attrs["omero"]
164+
omero = Omero(
165+
channels=[
166+
OmeroChannel(
167+
color=channel["color"],
168+
window=OmeroWindow(
169+
min=channel["window"]["min"],
170+
max=channel["window"]["max"],
171+
start=channel["window"]["start"],
172+
end=channel["window"]["end"],
173+
),
174+
)
175+
for channel in omero_data["channels"]
176+
]
177+
)
178+
151179
if version == "0.5":
152180
from .v05.zarr_metadata import Metadata
153181

@@ -156,6 +184,7 @@ def from_ngff_zarr(
156184
datasets=datasets,
157185
name=name,
158186
coordinateTransformations=coordinateTransformations,
187+
omero=omero,
159188
)
160189
else:
161190
from .v04.zarr_metadata import Metadata
@@ -166,6 +195,7 @@ def from_ngff_zarr(
166195
name=name,
167196
version=metadata["version"],
168197
coordinateTransformations=coordinateTransformations,
198+
omero=omero,
169199
)
170200

171201
return Multiscales(images, metadata)

ngff_zarr/to_ngff_zarr.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ def _pop_metadata_optionals(metadata_dict):
4747
if metadata_dict["coordinateTransformations"] is None:
4848
metadata_dict.pop("coordinateTransformations")
4949

50+
if metadata_dict["omero"] is None:
51+
metadata_dict.pop("omero")
52+
5053
return metadata_dict
5154

5255

@@ -231,6 +234,9 @@ def to_ngff_zarr(
231234
**format_kwargs,
232235
)
233236

237+
if "omero" in metadata_dict:
238+
root.attrs["omero"] = metadata_dict.pop("omero")
239+
234240
if version != "0.4":
235241
# RFC 2, Zarr 3
236242
root.attrs["ome"] = {"version": version, "multiscales": [metadata_dict]}

ngff_zarr/v04/zarr_metadata.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from typing import List, Optional, Union
33

44
from typing_extensions import Literal
5+
import re
56

67
SupportedDims = Union[
78
Literal["c"], Literal["x"], Literal["y"], Literal["z"], Literal["t"]
@@ -165,10 +166,34 @@ class Dataset:
165166
coordinateTransformations: List[Transform]
166167

167168

169+
@dataclass
170+
class OmeroWindow:
171+
min: float
172+
max: float
173+
start: float
174+
end: float
175+
176+
177+
@dataclass
178+
class OmeroChannel:
179+
color: str
180+
window: OmeroWindow
181+
182+
def validate_color(self):
183+
if not re.fullmatch(r"[0-9A-Fa-f]{6}", self.color):
184+
raise ValueError(f"Invalid color '{self.color}'. Must be 6 hex digits.")
185+
186+
187+
@dataclass
188+
class Omero:
189+
channels: List[OmeroChannel]
190+
191+
168192
@dataclass
169193
class Metadata:
170194
axes: List[Axis]
171195
datasets: List[Dataset]
172196
coordinateTransformations: Optional[List[Transform]]
197+
omero: Optional[Omero] = None
173198
name: str = "image"
174199
version: str = "0.4"

ngff_zarr/v05/zarr_metadata.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
from typing import List, Optional
22
from dataclasses import dataclass
33

4-
from ..v04.zarr_metadata import Axis, Transform, Dataset
4+
from ..v04.zarr_metadata import Axis, Transform, Dataset, Omero
55

66

77
@dataclass
88
class Metadata:
99
axes: List[Axis]
1010
datasets: List[Dataset]
1111
coordinateTransformations: Optional[List[Transform]]
12+
omero: Optional[Omero] = None
1213
name: str = "image"

test/test_omero.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import pytest
2+
import numpy as np
3+
from zarr.storage import MemoryStore
4+
from ngff_zarr import (
5+
Omero,
6+
OmeroChannel,
7+
OmeroWindow,
8+
from_ngff_zarr,
9+
to_ngff_image,
10+
to_multiscales,
11+
to_ngff_zarr,
12+
)
13+
14+
from ._data import test_data_dir
15+
16+
17+
def test_read_omero(input_images): # noqa: ARG001
18+
dataset_name = "13457537"
19+
store_path = test_data_dir / "input" / f"{dataset_name}.zarr"
20+
multiscales = from_ngff_zarr(store_path, validate=True)
21+
22+
omero = multiscales.metadata.omero
23+
assert omero is not None
24+
assert len(omero.channels) == 6
25+
26+
# Channel 0
27+
assert omero.channels[0].color == "FFFFFF"
28+
assert omero.channels[0].window.min == 0.0
29+
assert omero.channels[0].window.max == 65535.0
30+
assert omero.channels[0].window.start == 0.0
31+
assert omero.channels[0].window.end == 1200.0
32+
33+
# Channel 1
34+
assert omero.channels[1].color == "FFFFFF"
35+
assert omero.channels[1].window.min == 0.0
36+
assert omero.channels[1].window.max == 65535.0
37+
assert omero.channels[1].window.start == 0.0
38+
assert omero.channels[1].window.end == 1200.0
39+
40+
# Channel 2
41+
assert omero.channels[2].color == "FFFFFF"
42+
assert omero.channels[2].window.min == 0.0
43+
assert omero.channels[2].window.max == 65535.0
44+
assert omero.channels[2].window.start == 0.0
45+
assert omero.channels[2].window.end == 1200.0
46+
47+
# Channel 3
48+
assert omero.channels[3].color == "FFFFFF"
49+
assert omero.channels[3].window.min == 0.0
50+
assert omero.channels[3].window.max == 65535.0
51+
assert omero.channels[3].window.start == 0.0
52+
assert omero.channels[3].window.end == 1200.0
53+
54+
# Channel 4
55+
assert omero.channels[4].color == "0000FF"
56+
assert omero.channels[4].window.min == 0.0
57+
assert omero.channels[4].window.max == 65535.0
58+
assert omero.channels[4].window.start == 0.0
59+
assert omero.channels[4].window.end == 5000.0
60+
61+
# Channel 5
62+
assert omero.channels[5].color == "FF0000"
63+
assert omero.channels[5].window.min == 0.0
64+
assert omero.channels[5].window.max == 65535.0
65+
assert omero.channels[5].window.start == 0.0
66+
assert omero.channels[5].window.end == 100.0
67+
68+
69+
def test_write_omero():
70+
data = np.random.randint(0, 256, 262144).reshape((2, 32, 64, 64)).astype(np.uint8)
71+
image = to_ngff_image(data, dims=["c", "z", "y", "x"])
72+
multiscales = to_multiscales(image, scale_factors=[2, 4], chunks=32)
73+
74+
omero = Omero(
75+
channels=[
76+
OmeroChannel(
77+
color="008000",
78+
window=OmeroWindow(min=0.0, max=255.0, start=10.0, end=150.0),
79+
),
80+
OmeroChannel(
81+
color="0000FF",
82+
window=OmeroWindow(min=0.0, max=255.0, start=30.0, end=200.0),
83+
),
84+
]
85+
)
86+
multiscales.metadata.omero = omero
87+
88+
store = MemoryStore()
89+
version = "0.4"
90+
to_ngff_zarr(store, multiscales, version=version)
91+
92+
multiscales_read = from_ngff_zarr(store, validate=True, version=version)
93+
read_omero = multiscales_read.metadata.omero
94+
95+
assert read_omero is not None
96+
assert len(read_omero.channels) == 2
97+
assert read_omero.channels[0].color == "008000"
98+
assert read_omero.channels[0].window.start == 10.0
99+
assert read_omero.channels[0].window.end == 150.0
100+
assert read_omero.channels[1].color == "0000FF"
101+
assert read_omero.channels[1].window.start == 30.0
102+
assert read_omero.channels[1].window.end == 200.0
103+
104+
105+
def test_validate_color():
106+
valid_channel = OmeroChannel(
107+
color="1A2B3C", window=OmeroWindow(min=0.0, max=255.0, start=0.0, end=100.0)
108+
)
109+
# This should not raise an error
110+
valid_channel.validate_color()
111+
112+
invalid_channel = OmeroChannel(
113+
color="ZZZZZZ", window=OmeroWindow(min=0.0, max=255.0, start=0.0, end=100.0)
114+
)
115+
with pytest.raises(ValueError, match=r"Invalid color 'ZZZZZZ'"):
116+
invalid_channel.validate_color()

0 commit comments

Comments
 (0)