-
Notifications
You must be signed in to change notification settings - Fork 54
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
attempts to create a writer #69
Changes from 21 commits
447ffd6
ad261bc
2d83edd
07e6cb8
3424efa
552ea9d
212db6a
c970c82
ffa2bda
371404e
2a12507
7b9599a
d55edae
7c86ddf
fce7c96
fe5d61c
f6123c2
9731606
d5f1614
6c91ed0
64bafb1
b832df5
e51b1be
d35cc5e
0d0ebfd
f22c564
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -354,7 +354,9 @@ def __init__(self, node: Node) -> None: | |
node.metadata["visible"] = visibles | ||
node.metadata["contrast_limits"] = contrast_limits | ||
node.metadata["colormap"] = colormaps | ||
|
||
except Exception as e: | ||
raise e | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be there? The LOGGER line below will never be called There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pretty likely debugging code that crept in. Thanks, @glyg |
||
LOGGER.error(f"failed to parse metadata: {e}") | ||
|
||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
"""Image writer utility | ||
|
||
""" | ||
import logging | ||
from typing import Any, List, Tuple, Union | ||
|
||
import numpy as np | ||
import zarr | ||
|
||
from .scale import Scaler | ||
from .types import JSONDict | ||
|
||
LOGGER = logging.getLogger("ome_zarr.writer") | ||
|
||
|
||
def write_multiscale( | ||
pyramid: List, group: zarr.Group, chunks: Union[Tuple[Any, ...], int] = None, | ||
) -> None: | ||
"""Write a pyramid with multiscale metadata to disk.""" | ||
paths = [] | ||
for path, dataset in enumerate(pyramid): | ||
# TODO: chunks here could be different per layer | ||
group.create_dataset(str(path), data=pyramid[path], chunks=chunks) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
joshmoore marked this conversation as resolved.
Show resolved
Hide resolved
|
||
paths.append({"path": str(path)}) | ||
|
||
multiscales = [{"version": "0.1", "datasets": paths}] | ||
group.attrs["multiscales"] = multiscales | ||
|
||
|
||
def write_image( | ||
image: np.ndarray, | ||
group: zarr.Group, | ||
chunks: Union[Tuple[Any, ...], int] = None, | ||
byte_order: Union[str, List[str]] = "tczyx", | ||
scaler: Scaler = None, | ||
**metadata: JSONDict, | ||
) -> None: | ||
"""Writes an image to the zarr store according to ome-zarr specification | ||
|
||
Parameters | ||
---------- | ||
image: np.ndarray | ||
the image to save | ||
group: zarr.Group | ||
the group within the zarr store to store the data in | ||
chunks: int or tuple of ints, | ||
size of the saved chunks to store the image | ||
byte_order: str or list of str, default "tczyx" | ||
combination of the letters defining the order | ||
in which the dimensions are saved | ||
""" | ||
|
||
if image.ndim > 5: | ||
raise ValueError("Only images of 5D or less are supported") | ||
|
||
shape_5d: Tuple[Any, ...] = (*(1,) * (5 - image.ndim), *image.shape) | ||
image = image.reshape(shape_5d) | ||
|
||
if chunks is not None: | ||
chunks = _retuple(chunks, shape_5d) | ||
omero = metadata.get("omero", {}) | ||
# Update the size entry anyway | ||
omero["size"] = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we need to have |
||
"t": image.shape[0], | ||
"c": image.shape[1], | ||
"z": image.shape[2], | ||
"height": image.shape[3], | ||
"width": image.shape[4], | ||
} | ||
if omero.get("channels") is None: | ||
size_c = image.shape[1] | ||
if size_c == 1: | ||
omero["channels"] = [{"window": {"start": 0, "end": 1}}] | ||
omero["rdefs"] = {"model": "greyscale"} | ||
else: | ||
rng = np.random.default_rng(0) | ||
colors = rng.integers(0, high=2 ** 8, size=(image.shape[1], 3)) | ||
omero["channels"] = [ | ||
{ | ||
"color": "".join(f"{i:02x}" for i in color), | ||
"window": {"start": 0, "end": 1}, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's probably better to omit all the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @glyg : any thoughts from your initial use case? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I just tried to emulate the code I saw in the tests or elsewhere, I agree that by default metadata entries should as small as possible |
||
"active": True, | ||
} | ||
for color in colors | ||
] | ||
omero["rdefs"] = {"model": "color"} | ||
|
||
metadata["omero"] = omero | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is currently missing the |
||
|
||
if scaler is None: | ||
scaler = Scaler() | ||
|
||
pyramid = scaler.nearest(image) | ||
write_multiscale(pyramid, group, chunks=chunks) | ||
group.attrs.update(metadata) | ||
|
||
|
||
def _retuple( | ||
chunks: Union[Tuple[Any, ...], int], shape: Tuple[Any, ...] | ||
) -> Tuple[Any, ...]: | ||
|
||
_chunks: Tuple[Any, ...] | ||
if isinstance(chunks, int): | ||
_chunks = (chunks,) | ||
else: | ||
_chunks = chunks | ||
|
||
return (*shape[: (5 - len(_chunks))], *_chunks) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is all based on the assumption that we have 5D data, which is being relaxed in ome/ngff#39, should there be a default retuple to 5 or no attempt at all to reshape the data? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point, but let's handle that along with #39. Ultimately I think the rules we'll have to be laid out, the most flexible being "once the shape is defined (e.g. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok great! |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
def pytest_addoption(parser): | ||
""" | ||
add `--show-viewer` as a valid command line flag | ||
""" | ||
parser.addoption( | ||
"--show-viewer", | ||
action="store_true", | ||
default=False, | ||
help="don't show viewer during tests", | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import numpy as np | ||
import pytest | ||
import zarr | ||
|
||
from ome_zarr.io import parse_url | ||
from ome_zarr.reader import OMERO, Reader | ||
from ome_zarr.writer import write_image | ||
|
||
|
||
class TestWriter: | ||
@pytest.fixture(autouse=True) | ||
def initdir(self, tmpdir): | ||
self.path = tmpdir.mkdir("data") | ||
|
||
def create_data(self, shape, dtype, mean_val=10): | ||
rng = np.random.default_rng(0) | ||
return rng.poisson(mean_val, size=shape).astype(dtype) | ||
|
||
def test_writer(self): | ||
|
||
shape = (1, 2, 1, 256, 256) | ||
data = self.create_data(shape, np.uint8) | ||
store = zarr.DirectoryStore(self.path) | ||
root = zarr.group(store=store) | ||
grp = root.create_group("test") | ||
write_image(image=data, group=grp, chunks=(128, 128)) | ||
reader = Reader(parse_url(f"{self.path}/test")) | ||
node = list(reader())[0] | ||
assert OMERO.matches(node.zarr) | ||
assert node.data[0].shape == shape | ||
assert node.data[0].chunks == ((1,), (2,), (1,), (128, 128), (128, 128)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As this function is not used in the writer module, maybe I should revert the changes in
io.py
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was working on a simple example for Wei (his use case was "how to turn an existing zarr array into an ome-zarr"). Let's keep this until we have a simple working user example. Maybe
parse_url
can still be the starting point.previous example