Skip to content

Commit

Permalink
Merge pull request #261 from giovp/writer/omero
Browse files Browse the repository at this point in the history
add omero metadata to writer
  • Loading branch information
joshmoore authored Nov 24, 2023
2 parents 18fe215 + c7f4716 commit 371f7f5
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 8 deletions.
4 changes: 3 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,10 @@ repos:
# default black line length is 88
"--max-line-length=88",
# Conflicts with black: E203 whitespace before ':'
# Conflicts with PEP8 and black:
# W503 line break before binary operator
# Does not recognize deprecated directive in docstrings
"--ignore=E203,RST303",
"--ignore=E203,RST303, W503",
"--rst-roles=class,func,ref,mod,meth,const",
]

Expand Down
22 changes: 16 additions & 6 deletions ome_zarr/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,38 +136,48 @@ def create_zarr(
chunks[zct] = 1

storage_options = dict(chunks=tuple(chunks))
write_multiscale(pyramid, grp, axes=axes, storage_options=storage_options)

if size_c == 1:
image_data = {
"channels": [{"window": {"start": 0, "end": 255}, "color": "FF0000"}],
"channels": [
{
"window": {"start": 0, "end": 255, "min": 0, "max": 255},
"color": "FF0000",
}
],
"rdefs": {"model": "greyscale"},
}
else:
image_data = {
"channels": [
{
"color": "FF0000",
"window": {"start": 0, "end": 255},
"window": {"start": 0, "end": 255, "min": 0, "max": 255},
"label": "Red",
"active": True,
},
{
"color": "00FF00",
"window": {"start": 0, "end": 255},
"window": {"start": 0, "end": 255, "min": 0, "max": 255},
"label": "Green",
"active": True,
},
{
"color": "0000FF",
"window": {"start": 0, "end": 255},
"window": {"start": 0, "end": 255, "min": 0, "max": 255},
"label": "Blue",
"active": True,
},
],
"rdefs": {"model": "color"},
}
grp.attrs["omero"] = image_data
write_multiscale(
pyramid,
grp,
axes=axes,
storage_options=storage_options,
metadata={"omero": image_data},
)

if labels:
labels_grp = grp.create_group("labels")
Expand Down
2 changes: 1 addition & 1 deletion ome_zarr/reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,7 @@ def __init__(self, node: Node) -> None:
elif contrast_limits is not None:
contrast_limits[idx] = [start, end]

node.metadata["name"] = names
node.metadata["channel_names"] = names
node.metadata["visible"] = visibles
node.metadata["contrast_limits"] = contrast_limits
node.metadata["colormap"] = colormaps
Expand Down
23 changes: 23 additions & 0 deletions ome_zarr/writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,29 @@ def write_multiscales_metadata(
axes = _get_valid_axes(axes=axes, fmt=fmt)
if axes is not None:
ndim = len(axes)
if (
isinstance(metadata, dict)
and metadata.get("metadata")
and isinstance(metadata["metadata"], dict)
and "omero" in metadata["metadata"]
):
omero_metadata = metadata["metadata"].get("omero")
if omero_metadata is None:
raise KeyError("If `'omero'` is present, value cannot be `None`.")
for c in omero_metadata["channels"]:
if "color" in c:
if not isinstance(c["color"], str) or len(c["color"]) != 6:
raise TypeError("`'color'` must be a hex code string.")
if "window" in c:
if not isinstance(c["window"], dict):
raise TypeError("`'window'` must be a dict.")
for p in ["min", "max", "start", "end"]:
if p not in c["window"]:
raise KeyError(f"`'{p}'` not found in `'window'`.")
if not isinstance(c["window"][p], (int, float)):
raise TypeError(f"`'{p}'` must be an int or float.")

group.attrs["omero"] = omero_metadata

# note: we construct the multiscale metadata via dict(), rather than {}
# to avoid duplication of protected keys like 'version' in **metadata
Expand Down
8 changes: 8 additions & 0 deletions tests/test_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ def test_label(self):
reader = Reader(parse_url(filename))
assert len(list(reader())) == 3

def test_omero(self):
reader = Reader(parse_url(str(self.path)))()
image_node = list(reader)[0]
omero = image_node.zarr.root_attrs.get("omero")
assert "channels" in omero
assert isinstance(omero["channels"], list)
assert len(omero["channels"]) == 1


class TestInvalid:
@pytest.fixture(autouse=True)
Expand Down
75 changes: 75 additions & 0 deletions tests/test_writer.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import filecmp
import pathlib
from tempfile import TemporaryDirectory
from typing import Any, Dict, Optional

import dask.array as da
import numpy as np
Expand Down Expand Up @@ -557,6 +558,80 @@ def test_invalid_transformations(self, coordinateTransformations):
with pytest.raises(ValueError):
write_multiscales_metadata(self.root, datasets, axes=axes)

@pytest.mark.parametrize(
"metadata",
[
{
"channels": [
{
"color": "FF0000",
"window": {"start": 0, "end": 255, "min": 0, "max": 255},
}
]
},
{"channels": [{"color": "FF0000"}]},
{"channels": [{"color": "FF000"}]}, # test wrong metadata
{"channels": [{"window": []}]}, # test wrong metadata
{
"channels": [ # test wrong metadata
{"color": "FF0000", "window": {"start": 0, "end": 255, "min": 0}},
]
},
None,
],
)
def test_omero_metadata(self, metadata: Optional[Dict[str, Any]]):
datasets = []
for level, transf in enumerate(TRANSFORMATIONS):
datasets.append({"path": str(level), "coordinateTransformations": transf})
if metadata is None:
with pytest.raises(
KeyError, match="If `'omero'` is present, value cannot be `None`."
):
write_multiscales_metadata(
self.root, datasets, axes="tczyx", metadata={"omero": metadata}
)
else:
window_metadata = (
metadata["channels"][0].get("window")
if "window" in metadata["channels"][0]
else None
)
color_metadata = (
metadata["channels"][0].get("color")
if "color" in metadata["channels"][0]
else None
)
if window_metadata is not None and len(window_metadata) < 4:
if isinstance(window_metadata, dict):
with pytest.raises(KeyError, match=".*`'window'`.*"):
write_multiscales_metadata(
self.root,
datasets,
axes="tczyx",
metadata={"omero": metadata},
)
elif isinstance(window_metadata, list):
with pytest.raises(TypeError, match=".*`'window'`.*"):
write_multiscales_metadata(
self.root,
datasets,
axes="tczyx",
metadata={"omero": metadata},
)
elif color_metadata is not None and len(color_metadata) != 6:
with pytest.raises(TypeError, match=".*`'color'`.*"):
write_multiscales_metadata(
self.root,
datasets,
axes="tczyx",
metadata={"omero": metadata},
)
else:
write_multiscales_metadata(
self.root, datasets, axes="tczyx", metadata={"omero": metadata}
)


class TestPlateMetadata:
@pytest.fixture(autouse=True)
Expand Down

0 comments on commit 371f7f5

Please sign in to comment.