Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add omero metadata to writer #261

Merged
merged 21 commits into from
Nov 24, 2023
Merged
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
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
Loading