Skip to content

Commit fd18517

Browse files
committed
Merge remote-tracking branch 'origin/master' into scaler_preserve_dtype
2 parents 3d8904b + bb1c3fe commit fd18517

File tree

9 files changed

+104
-36
lines changed

9 files changed

+104
-36
lines changed

.pre-commit-config.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,18 @@ repos:
1919
- --autofix
2020

2121
- repo: https://github.com/astral-sh/ruff-pre-commit
22-
rev: v0.12.11
22+
rev: v0.13.2
2323
hooks:
2424
- id: ruff-check
2525
args: ["--fix", "--show-fixes"]
2626

2727
- repo: https://github.com/psf/black
28-
rev: 25.1.0
28+
rev: 25.9.0
2929
hooks:
3030
- id: black
3131

3232
- repo: https://github.com/pre-commit/mirrors-mypy
33-
rev: v1.17.1
33+
rev: v1.18.2
3434
hooks:
3535
- id: mypy
3636
args: [--config-file=mypy.ini]

docs/source/cli.rst

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,13 @@ Local data::
2828
view
2929
====
3030

31-
Use the `ome_zarr` command to view Zarr data in the https://ome.github.io/ome-ngff-validator::
31+
Use the `ome_zarr` command to serve local Zarr data and view in the https://ome.github.io/ome-ngff-validator::
3232

3333
ome_zarr view 6001240.zarr/
3434

35+
# Use -f or --force to open in browser even if no valid data is found
36+
ome_zarr view 6001240.zarr/ -f
37+
3538
finder
3639
======
3740

ome_zarr/cli.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def info(args: argparse.Namespace) -> None:
3535
def view(args: argparse.Namespace) -> None:
3636
"""Wrap the :func:`~ome_zarr.utils.view` method."""
3737
config_logging(logging.WARNING, args)
38-
zarr_view(args.path, args.port)
38+
zarr_view(args.path, args.port, force=args.force)
3939

4040

4141
def finder(args: argparse.Namespace) -> None:
@@ -133,6 +133,12 @@ def main(args: list[str] | None = None) -> None:
133133
parser_view.add_argument(
134134
"--port", type=int, default=8000, help="Port to serve the data (default: 8000)"
135135
)
136+
parser_view.add_argument(
137+
"--force",
138+
"-f",
139+
action="store_true",
140+
help="Force open in browser. Don't check for OME-Zarr data first.",
141+
)
136142
parser_view.set_defaults(func=view)
137143

138144
# finder (open a dir of images in BioFile Finder in a browser)

ome_zarr/data.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def astronaut() -> tuple[list, list]:
6767
pyramid = scaler.nearest(pixels)
6868

6969
shape = list(pyramid[0].shape)
70-
c, y, x = shape
70+
_c, y, x = shape
7171
label = np.zeros((y, x), dtype=np.int8)
7272
make_circle(100, 100, 1, label[200:300, 200:300])
7373
make_circle(150, 150, 2, label[250:400, 250:400])

ome_zarr/scale.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ class Scaler:
7474
max_layer: int = 4
7575
method: str = "nearest"
7676

77+
# 0: Nearest-neighbor
78+
# 1: Bi-linear (default)
79+
order: int = 1 # only used for resize
80+
7781
@staticmethod
7882
def methods() -> Iterator[str]:
7983
"""Return the name of all methods which define a downsampling.
@@ -172,7 +176,11 @@ def _resize(image: ArrayLike, out_shape: tuple, **kwargs: Any) -> ArrayLike:
172176

173177
dtype = image.dtype
174178
image = _resize(
175-
image.astype(float), out_shape, order=1, mode="reflect", anti_aliasing=False
179+
image.astype(float),
180+
out_shape,
181+
order=self.order,
182+
mode="reflect",
183+
anti_aliasing=False,
176184
)
177185
return image.astype(dtype)
178186

ome_zarr/utils.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -73,21 +73,24 @@ def info(path: str, stats: bool = False) -> Iterator[Node]:
7373
yield node
7474

7575

76-
def view(input_path: str, port: int = 8000, dry_run: bool = False) -> None:
76+
def view(
77+
input_path: str, port: int = 8000, dry_run: bool = False, force: bool = False
78+
) -> None:
7779
# serve the parent directory in a simple server with CORS. Open browser
7880
# dry_run is for testing, so we don't open the browser or start the server
7981

80-
zarrs = []
81-
if (Path(input_path) / ".zattrs").exists() or (
82-
Path(input_path) / "zarr.json"
83-
).exists():
84-
zarrs = find_multiscales(Path(input_path))
85-
if len(zarrs) == 0:
86-
print(
87-
f"No OME-Zarr images found in {input_path}. "
88-
f"Try $ ome_zarr finder {input_path}"
89-
)
90-
return
82+
if not force:
83+
zarrs = []
84+
if (Path(input_path) / ".zattrs").exists() or (
85+
Path(input_path) / "zarr.json"
86+
).exists():
87+
zarrs = find_multiscales(Path(input_path))
88+
if len(zarrs) == 0:
89+
print(
90+
f"No OME-Zarr images found in {input_path}. "
91+
f"Try $ ome_zarr finder {input_path} or use -f to force open in browser."
92+
)
93+
return
9194

9295
parent_dir, image_name = os.path.split(input_path)
9396
if len(image_name) == 0:

ome_zarr/writer.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -927,7 +927,7 @@ def write_labels(
927927
labels: np.ndarray | da.Array,
928928
group: zarr.Group,
929929
name: str,
930-
scaler: Scaler = Scaler(),
930+
scaler: Scaler = Scaler(order=0),
931931
chunks: tuple[Any, ...] | int | None = None,
932932
fmt: Format | None = None,
933933
axes: AxesType = None,
@@ -955,8 +955,11 @@ def write_labels(
955955
:param name: The name of this labels data.
956956
:type scaler: :class:`ome_zarr.scale.Scaler`
957957
:param scaler:
958-
Scaler implementation for downsampling the image argument. If None,
959-
no downsampling will be performed.
958+
Scaler implementation for downsampling the image argument.
959+
NB: Labels downsampling should avoid interpolation.
960+
If no scaler is provided, the default scaler with nearest neighbour
961+
interpolation will be used.
962+
If scaler=None, no downsampling will be performed.
960963
:type chunks: int or tuple of ints, optional
961964
:param chunks:
962965
The size of the saved chunks to store the image.

tests/test_cli.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def test_coins_info(self, capsys, fmt):
5454
args += ["--format", fmt.version]
5555
main(args)
5656
main(["info", filename])
57-
out, err = capsys.readouterr()
57+
out, _err = capsys.readouterr()
5858
print("Captured output:", out)
5959
assert os.path.join("labels", "coins") in out
6060
version = fmt.version if fmt else CurrentFormat().version
@@ -153,6 +153,10 @@ def _rotate_and_test(self, *hierarchy: Path, reverse: bool = True):
153153
self._rotate_and_test(*list(secondpass), reverse=False)
154154

155155
def test_view(self):
156+
# view empty dir for code coverage
157+
view(str(self.path), 8000, True)
158+
view(str(self.path), 8000, True, force=True)
159+
156160
filename = f"{self.path}-4"
157161
main(["create", "--method=astronaut", filename])
158162
# CLI doesn't support the dry_run option yet

tests/test_writer.py

Lines changed: 54 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import filecmp
22
import json
33
import pathlib
4+
import re
45
from typing import Any
56

67
import dask.array as da
@@ -16,6 +17,7 @@
1617
from ome_zarr_models.v05.hcs import HCS as Models05HCS
1718
from ome_zarr_models.v05.image import Image as Models05Image
1819
from ome_zarr_models.v05.well import Well as Models05Well
20+
from skimage.data import binary_blobs
1921
from zarr.abc.codec import BytesBytesCodec
2022
from zarr.codecs import BloscCodec
2123

@@ -664,7 +666,9 @@ def test_multi_levels_transformations(self, fmt):
664666
# No arrays, so this is expected:
665667
with pytest.raises(
666668
ValueError,
667-
match="Expected to find an array at /0, but no array was found there.",
669+
match=re.escape(
670+
"Expected to find an array at 0, but no array was found there."
671+
),
668672
):
669673
if fmt.version == "0.4":
670674
Models04Image.from_zarr(group)
@@ -756,7 +760,9 @@ def test_valid_transformations(self, coordinateTransformations):
756760
# No arrays, so this is expected:
757761
with pytest.raises(
758762
ValueError,
759-
match="Expected to find an array at /0, but no array was found there.",
763+
match=re.escape(
764+
"Expected to find an array at 0, but no array was found there."
765+
),
760766
):
761767
Models04Image.from_zarr(self.root)
762768

@@ -835,7 +841,8 @@ def test_omero_metadata(self, metadata: dict[str, Any] | None):
835841
datasets.append({"path": str(level), "coordinateTransformations": transf})
836842
if metadata is None:
837843
with pytest.raises(
838-
KeyError, match="If `'omero'` is present, value cannot be `None`."
844+
KeyError,
845+
match=re.escape("If `'omero'` is present, value cannot be `None`."),
839846
):
840847
write_multiscales_metadata(
841848
self.root,
@@ -856,7 +863,7 @@ def test_omero_metadata(self, metadata: dict[str, Any] | None):
856863
)
857864
if window_metadata is not None and len(window_metadata) < 4:
858865
if isinstance(window_metadata, dict):
859-
with pytest.raises(KeyError, match=".*`'window'`.*"):
866+
with pytest.raises(KeyError, match="window"):
860867
write_multiscales_metadata(
861868
self.root,
862869
datasets,
@@ -865,7 +872,7 @@ def test_omero_metadata(self, metadata: dict[str, Any] | None):
865872
fmt=FormatV04(),
866873
)
867874
elif isinstance(window_metadata, list):
868-
with pytest.raises(TypeError, match=".*`'window'`.*"):
875+
with pytest.raises(TypeError, match="window"):
869876
write_multiscales_metadata(
870877
self.root,
871878
datasets,
@@ -874,7 +881,7 @@ def test_omero_metadata(self, metadata: dict[str, Any] | None):
874881
fmt=FormatV04(),
875882
)
876883
elif color_metadata is not None and len(color_metadata) != 6:
877-
with pytest.raises(TypeError, match=".*`'color'`.*"):
884+
with pytest.raises(TypeError, match="color"):
878885
write_multiscales_metadata(
879886
self.root,
880887
datasets,
@@ -891,7 +898,9 @@ def test_omero_metadata(self, metadata: dict[str, Any] | None):
891898
# no arrays, so this is expected
892899
with pytest.raises(
893900
ValueError,
894-
match="Expected to find an array at /0, but no array was found there.",
901+
match=re.escape(
902+
"Expected to find an array at 0, but no array was found there."
903+
),
895904
):
896905
Models04Image.from_zarr(self.root)
897906

@@ -1457,6 +1466,12 @@ def verify_label_data(
14571466
name = imglabel_attrs["multiscales"][0].get("name", "")
14581467
assert label_name == name
14591468

1469+
labels_paths = [
1470+
ds["path"] for ds in imglabel_attrs["multiscales"][0]["datasets"]
1471+
]
1472+
label_data = [da.from_zarr(label_group[path]) for path in labels_paths]
1473+
return label_data
1474+
14601475
@pytest.mark.parametrize(
14611476
"format_version",
14621477
(
@@ -1468,7 +1483,9 @@ def verify_label_data(
14681483
),
14691484
)
14701485
@pytest.mark.parametrize("array_constructor", [np.array, da.from_array])
1471-
def test_write_labels(self, shape, scaler, format_version, array_constructor):
1486+
@pytest.mark.parametrize("scale_type", ["custom", "noop", "default"])
1487+
def test_write_labels(self, shape, format_version, array_constructor, scale_type):
1488+
14721489
fmt = format_version()
14731490
if fmt.version == "0.5":
14741491
img_path = self.path_v3
@@ -1485,11 +1502,23 @@ def test_write_labels(self, shape, scaler, format_version, array_constructor):
14851502
transformations.append(
14861503
[{"type": "scale", "scale": transf["scale"][-len(shape) :]}]
14871504
)
1488-
if scaler is None:
1505+
if scale_type == "noop":
14891506
break
14901507

1491-
# create the actual label data
1492-
label_data = np.random.randint(0, 1000, size=shape)
1508+
# create the actual label data: zeros with blobs
1509+
label_data = np.zeros(shape, dtype=np.uint8)
1510+
# add some blobs, corresponding to shape
1511+
blobs = binary_blobs(length=256, volume_fraction=0.1, n_dim=2).astype("int8")
1512+
# we only apply blobs to the last two dimensions of label_data
1513+
slices = [slice(None)] * (len(shape) - blobs.ndim)
1514+
slices += [slice(0, 256), slice(0, 256)]
1515+
label_data[tuple(slices)] = 2 * blobs
1516+
1517+
print("label_data.shape:", label_data.shape, shape)
1518+
assert label_data.max() == 2
1519+
assert label_data.min() == 0
1520+
assert np.unique(label_data).tolist() == [0, 2]
1521+
14931522
if fmt.version in ("0.1", "0.2"):
14941523
# v0.1 and v0.2 require 5d
14951524
expand_dims = (np.s_[None],) * (5 - len(shape))
@@ -1498,21 +1527,33 @@ def test_write_labels(self, shape, scaler, format_version, array_constructor):
14981527
label_name = "my-labels"
14991528
label_data = array_constructor(label_data)
15001529

1530+
scaler = Scaler()
1531+
if scale_type == "noop":
1532+
scaler = None
1533+
kwargs = {"scaler": scaler}
1534+
if scale_type == "default":
1535+
del kwargs["scaler"]
1536+
15011537
# create the root level image data
15021538
self.create_image_data(group, shape, scaler, fmt, axes, transformations)
15031539

15041540
write_labels(
15051541
label_data,
15061542
group,
1507-
scaler=scaler,
15081543
name=label_name,
15091544
fmt=fmt,
15101545
axes=axes,
15111546
coordinate_transformations=transformations,
1547+
**kwargs,
15121548
)
1513-
self.verify_label_data(
1549+
label_data = self.verify_label_data(
15141550
img_path, label_name, label_data, fmt, shape, transformations
15151551
)
1552+
1553+
for level in label_data:
1554+
if scale_type == "default":
1555+
assert np.unique(level.compute()).tolist() == [0, 2]
1556+
15161557
if fmt.version == "0.4":
15171558
test_root = zarr.open(self.path)
15181559
Models04Labels.from_zarr(test_root["labels"])

0 commit comments

Comments
 (0)