-
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 all 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 |
---|---|---|
@@ -0,0 +1,86 @@ | ||
"""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=dataset, chunks=chunks) | ||
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 = Scaler(), | ||
**metadata: JSONDict, | ||
) -> None: | ||
"""Writes an image to the zarr store according to ome-zarr specification | ||
|
||
Parameters | ||
---------- | ||
image: np.ndarray | ||
the image data to save. A downsampling of the data will be computed | ||
if the scaler argument is non-None. | ||
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 | ||
scaler: Scaler | ||
Scaler implementation for downsampling the image argument. If None, | ||
no downsampling will be performed. | ||
""" | ||
|
||
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) | ||
|
||
if scaler is not None: | ||
image = scaler.nearest(image) | ||
else: | ||
LOGGER.debug("disabling pyramid") | ||
image = [image] | ||
|
||
write_multiscale(image, 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,44 @@ | ||
import numpy as np | ||
import pytest | ||
import zarr | ||
|
||
from ome_zarr.io import parse_url | ||
from ome_zarr.reader import Multiscales, Reader | ||
from ome_zarr.scale import Scaler | ||
from ome_zarr.writer import write_image | ||
|
||
|
||
class TestWriter: | ||
@pytest.fixture(autouse=True) | ||
def initdir(self, tmpdir): | ||
self.path = tmpdir.mkdir("data") | ||
self.store = zarr.DirectoryStore(self.path) | ||
self.root = zarr.group(store=self.store) | ||
self.group = self.root.create_group("test") | ||
|
||
def create_data(self, shape, dtype=np.uint8, mean_val=10): | ||
rng = np.random.default_rng(0) | ||
return rng.poisson(mean_val, size=shape).astype(dtype) | ||
|
||
@pytest.fixture(params=((1, 2, 1, 256, 256),)) | ||
def shape(self, request): | ||
return request.param | ||
|
||
@pytest.fixture(params=[True, False], ids=["scale", "noop"]) | ||
def scaler(self, request): | ||
if request.param: | ||
return Scaler() | ||
else: | ||
return None | ||
|
||
def test_writer(self, shape, scaler): | ||
|
||
data = self.create_data(shape) | ||
write_image(image=data, group=self.group, chunks=(128, 128), scaler=scaler) | ||
|
||
# Verify | ||
reader = Reader(parse_url(f"{self.path}/test")) | ||
node = list(reader())[0] | ||
assert Multiscales.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