From ce7b8c76f5985503f3f2a891b5cb1aa36ff5372f Mon Sep 17 00:00:00 2001 From: Johannes Soltwedel <38459088+jo-mueller@users.noreply.github.com> Date: Tue, 4 Aug 2026 10:13:03 +0200 Subject: [PATCH 01/51] feat: add OMEZarrScene feat: add OMEZarrScene to __init__ --- ome_zarr/__init__.py | 4 +- ome_zarr/classes/__init__.py | 2 + ome_zarr/classes/scene.py | 461 +++++++++++++++++++++++++++++++++++ pyproject.toml | 4 +- 4 files changed, 468 insertions(+), 3 deletions(-) create mode 100644 ome_zarr/classes/scene.py diff --git a/ome_zarr/__init__.py b/ome_zarr/__init__.py index 84c34696..467e0945 100644 --- a/ome_zarr/__init__.py +++ b/ome_zarr/__init__.py @@ -1,7 +1,7 @@ from dask import __version__ as dask_version from packaging.version import Version -from .classes import OMEZarrImage, OMEZarrLabels, OMEZarrMultiscale +from .classes import OMEZarrImage, OMEZarrLabels, OMEZarrMultiscale, OMEZarrScene # Expose __version__ and fallback when _version.py doesn't exist. try: @@ -12,4 +12,4 @@ # If not 2026.3.0 it must be 2025.11.0 or lower. Name indicates kwargs only contain array kwargs in the dask version. USE_DASK_ARRAY_KWARGS = Version(dask_version) >= Version("2026.3.0") -__all__ = ["OMEZarrImage", "OMEZarrLabels", "OMEZarrMultiscale", "__version__"] +__all__ = ["OMEZarrImage", "OMEZarrLabels", "OMEZarrMultiscale", "OMEZarrScene", "__version__"] diff --git a/ome_zarr/classes/__init__.py b/ome_zarr/classes/__init__.py index fb26acb1..3a8b13d1 100644 --- a/ome_zarr/classes/__init__.py +++ b/ome_zarr/classes/__init__.py @@ -1,7 +1,9 @@ from .image import OMEZarrImage, OMEZarrLabels, OMEZarrMultiscale +from .scene import OMEZarrScene __all__ = [ "OMEZarrImage", "OMEZarrLabels", "OMEZarrMultiscale", + "OMEZarrScene", ] diff --git a/ome_zarr/classes/scene.py b/ome_zarr/classes/scene.py new file mode 100644 index 00000000..a2285a93 --- /dev/null +++ b/ome_zarr/classes/scene.py @@ -0,0 +1,461 @@ +# the class for storage representation, not exposed to the user +import os +from collections.abc import Sequence +from typing import Any, cast + +import transformnd as tnd +import zarr +from ome_zarr_models.v06.coordinate_transforms import ( + AnyTransform, + CoordinateSystem, +) +from ome_zarr_models.v06.scene import SceneAttrs +from pydantic import TypeAdapter +from zarr.storage import StoreLike + +from .image import OMEZarrMultiscale + + +class OMEZarrScene: + def __init__( + self, + images: list[OMEZarrMultiscale] | dict[str, OMEZarrMultiscale], + coordinate_transformations: Sequence[AnyTransform] | list[dict[str, Any]], + coordinate_systems: ( + Sequence[CoordinateSystem] | Sequence[dict[str, Any]] | None + ) = None, + coordinates_displacements: dict[str, OMEZarrMultiscale] | None = None, + ): + """ + Parameters + ---------- + images : list[OMEZarrMultiscale] | dict[str, OMEZarrMultiscale] + Either a list of images (keyed internally by metadata.name) or a dict + mapping zarr group paths to images. The dict form gives explicit control + over the paths where images will be stored in the zarr hierarchy. + """ + # Coerce list to dict keyed by metadata.name + if isinstance(images, list): + self.images = {str(img.metadata.name): img for img in images} + else: + self.images = images + + # parse coordinate systems and transforms + self.coordinate_systems = self._parse_coordinate_systems(coordinate_systems) + self.coordinate_transformations = self._parse_transforms( + coordinate_transformations + ) + self.coordinates_displacements = coordinates_displacements + + self.metadata = SceneAttrs( + coordinateSystems=self.coordinate_systems, + coordinateTransformations=self.coordinate_transformations, + ) + + self._build_graph() + + def get_coordinate_system( + self, name: str, path: str | None = None + ) -> CoordinateSystem | None: + """ + Retrieve a coordinate system by name and optional path. + + Parameters + ---------- + name: str + The name of the coordinate system to retrieve. + path: str | None + Optional path to disambiguate coordinate systems with the same name. If None, will return the first match with the given name. + + Returns + ------- + CoordinateSystem or None + The matching CoordinateSystem object, or None if no match is found. + """ + if path is None: + # coordinate system can only be in top-level + possible_coordinate_systems: Sequence[CoordinateSystem] = ( + self.coordinate_systems or [] + ) + + return next( + (cs for cs in possible_coordinate_systems if cs.name == name), None + ) + + possible_coordinate_systems = [] + for group in self.images: + if group == path: + img = self.images[group] + for cs in img.metadata.coordinateSystems: + if cs.name == name: + return cs + return None + + def _build_graph(self): + self._graph = tnd.graph.TransformGraph() + # Add scene-level transformations (empty context = root level) + for tf in self.coordinate_transformations: + if tf.type == "sequence": + source_cs = self.get_coordinate_system(tf.input.name, tf.input.path) + target_cs = self.get_coordinate_system(tf.output.name, tf.output.path) + tnd_transform = self._ozmp_tf_to_tnd( + tf, + zarr_context="", + source_cs=source_cs, + target_cs=target_cs, + ).simplify() + else: + tnd_transform = self._ozmp_tf_to_tnd( + tf, zarr_context="", source_cs=None, target_cs=None + ) + self._graph.add_transform(tnd_transform) + + # check if input/output are defined + subgroups = [] + if tf.input.path is not None: + subgroups.append(tf.input.path) + if tf.output.path is not None: + subgroups.append(tf.output.path) + + for subgroup in subgroups: + img = self.images.get(subgroup) + if img is None: + # Image not found in scene - skip or warn + continue + if img.metadata.coordinateTransformations: + for img_tf in img.metadata.coordinateTransformations: + ind_transform = self._ozmp_tf_to_tnd( + img_tf, + zarr_context=subgroup, + source_cs=None, + target_cs=None, + ) + self._graph.add_transform(ind_transform) + + def to_ome_zarr(self, store: StoreLike, overwrite: bool = False): + """ + Write scene to OME-Zarr format. + + Parameters + ---------- + store: StoreLike + A zarr-compatible storage backend (e.g., directory path, in-memory store, etc.) + overwrite: bool + If True, overwrite all images in the store with the current state of the scene. + If False, only write new images that haven't been written before. Existing images in the store will be left unchanged. + + """ + import shutil + + import tqdm + + from ome_zarr.utils import _recursive_pop_nones + + if overwrite and os.path.exists(str(store)): + # Clear the store if it already exists and we're not doing incremental writes + shutil.rmtree(str(store)) + + # Open or create zarr group + mode = "w" if overwrite else "a" + zarr_group = zarr.open(store, mode=mode) + + # Create a subgroup for each image using its path key + for img_path, img in tqdm.tqdm(self.images.items(), desc="Writing images"): + # Skip if already written (incremental mode) + if not overwrite and img_path in zarr_group: + continue + + # Write the image + subgroup = zarr_group.create_group(img_path, overwrite=overwrite) + img.to_ome_zarr(subgroup, overwrite=True, version="0.6") + + for disp_path, disp_img in (self.coordinates_displacements or {}).items(): + # Skip if already written (incremental mode) + if not overwrite and disp_path in zarr_group: + continue + + # Write the displacement image + subgroup = zarr_group.create_group( + f"coordinateTransformations/{disp_path}", overwrite=overwrite + ) + disp_img.to_ome_zarr(subgroup, overwrite=True, version="0.6") + + # Always update scene metadata + metadata_dict = self.metadata.model_dump() + metadata_dict = _recursive_pop_nones(metadata_dict) + + zarr_group.attrs["ome"] = {"scene": metadata_dict, "version": "0.6.dev4"} + + @classmethod + def from_ome_zarr(cls, store: StoreLike): + """ + Load an existing scene from OME-Zarr format. + + Args: + path: Path to the OME-Zarr scene + + Returns: + NgffScene instance with images and metadata loaded from disk + """ + from ome_zarr_models.v06.scene import BaseSceneAttrs + + # Handle both StoreLike (string, dict, etc.) and zarr.Group objects + if isinstance(store, zarr.Group): + zarr_group = store + else: + zarr_group = zarr.open(store, mode="r") + + # load coordinateTransformations array data, if it exists + if "coordinateTransformations" in zarr_group: + coordinates_displacements = {} + for disp_path in zarr_group["coordinateTransformations"].group_keys(): + disp_group = zarr_group["coordinateTransformations"][disp_path] + disp_img = cast( + OMEZarrMultiscale, OMEZarrMultiscale.from_ome_zarr(disp_group) + ) + coordinates_displacements[disp_path] = disp_img + else: + coordinates_displacements = None + + # Load scene metadata + scene_metadata = BaseSceneAttrs.model_validate(zarr_group.attrs.get("ome", {})) + transformations = scene_metadata.scene.coordinateTransformations + coordinate_systems = scene_metadata.scene.coordinateSystems + + # Load all image subgroups, keyed by their zarr path + images = {} + for tf in transformations: + if hasattr(tf, "input"): + path = tf.input.path + if path is not None and path in zarr_group: + img_group = zarr_group[path] + img = cast( + OMEZarrMultiscale, OMEZarrMultiscale.from_ome_zarr(img_group) + ) + images[path] = img + elif path is not None and path not in zarr_group: + raise ValueError( + f"Image specified in metadata at '{path}' not found in zarr group." + ) + if hasattr(tf, "output"): + path = tf.output.path + if path is not None and path in zarr_group: + img_group = zarr_group[path] + img = cast( + OMEZarrMultiscale, OMEZarrMultiscale.from_ome_zarr(img_group) + ) + images[path] = img + elif path is not None and path not in zarr_group: + raise ValueError( + f"Image specified in metadata at '{path}' not found in zarr group." + ) + + scene = OMEZarrScene( + images=images, + coordinate_transformations=transformations, + coordinate_systems=coordinate_systems, + coordinates_displacements=coordinates_displacements, + ) + + return scene + + def __setattr__(self, name: str, value: Any) -> None: + if name == "coordinate_transformations": + # Update metadata when coordinate transformations are set + parsed_transforms = self._parse_transforms(value) + super().__setattr__(name, parsed_transforms) + # Only update metadata if it exists (not during initial construction) + if hasattr(self, "metadata") and self.metadata is not None: + self.metadata = self.metadata.model_copy( + update={"coordinateTransformations": parsed_transforms} + ) + + elif name == "coordinate_systems": + # Update metadata when coordinate systems are set + parsed_coordinate_systems = self._parse_coordinate_systems(value) + super().__setattr__(name, parsed_coordinate_systems) + # Only update metadata if it exists (not during initial construction) + if hasattr(self, "metadata") and self.metadata is not None: + self.metadata = self.metadata.model_copy( + update={"coordinateSystems": parsed_coordinate_systems} + ) + + else: + # Default behavior for all other attributes + super().__setattr__(name, value) + + @staticmethod + def _parse_transforms( + transforms: Sequence[AnyTransform] | list[dict[str, Any]], + ) -> tuple[AnyTransform, ...]: + """ + Helper method to parse a sequence of coordinate transformations that may be provided as either + AnyTransform instances or dictionaries. + This ensures that all transformations are stored as AnyTransform objects in the scene metadata. + """ + tf_adapter = TypeAdapter(AnyTransform) + parsed_transforms = [] + + for tf in transforms: + if isinstance(tf, dict): + parsed_transforms.append(tf_adapter.validate_python(tf)) + else: + parsed_transforms.append(tf) + + return tuple(parsed_transforms) + + @staticmethod + def _parse_coordinate_systems( + coordinate_systems: ( + Sequence[CoordinateSystem] | Sequence[dict[str, Any]] | None + ), + ) -> tuple[CoordinateSystem, ...] | None: + """ + Helper method to parse a sequence of coordinate systems that may be provided as either + CoordinateSystem instances or dictionaries. + This ensures that all coordinate systems are stored as CoordinateSystem objects in the scene metadata. + If coordinate_systems is None, it will be returned as None. + """ + if coordinate_systems is None: + return None + + parsed_coordinate_systems = [] + for cs in coordinate_systems: + if isinstance(cs, dict): + parsed_coordinate_systems.append(CoordinateSystem.model_validate(cs)) + elif isinstance(cs, CoordinateSystem): + parsed_coordinate_systems.append(cs) + + return tuple(parsed_coordinate_systems) + + def _ozmp_tf_to_tnd( + self, + transform: AnyTransform, + zarr_context: str = "", + source_cs: CoordinateSystem | None = None, + target_cs: CoordinateSystem | None = None, + ) -> tnd.base.Transform: + """ + Convert an OME-Zarr coordinate transformation to a transformnd Transform object. + This is a placeholder function and will need to be implemented based on the specific types of transformations you expect to encounter in OME-Zarr metadata. + """ + import numpy as np + + if transform.input is not None: + input_path = ( + transform.input.path if transform.input.path is not None else "" + ) + output_path = ( + transform.output.path if transform.output.path is not None else "" + ) + + # zarr_context prepends path with relative path from root + # to keep track of global location of coordinate systems in the zarr store + if zarr_context != "" and input_path != "": + input_path = f"{zarr_context}/{input_path}" + elif zarr_context != "": + input_path = zarr_context + + if zarr_context != "" and output_path != "": + output_path = f"{zarr_context}/{output_path}" + elif zarr_context != "": + output_path = zarr_context + + spaces = tnd.Spaces( + f"{input_path}:{transform.input.name}", + f"{output_path}:{transform.output.name}", + ) + else: + spaces = tnd.Spaces(None, None) + + tnd_transform = None + # Example for an affine transformation (this will depend on the actual structure of AnyTransform) + if transform.type == "affine": + aff = np.asarray(transform.affine) + if aff.shape[0] == aff.shape[1]: + tnd_transform = tnd.transforms.Affine( + transform.affine, + spaces=spaces, + ) + else: + aff = np.eye(max(aff.shape)) + aff[: aff.shape[0], : aff.shape[1]] = aff + tnd_transform = tnd.transforms.Affine(aff, spaces=spaces) + + elif transform.type == "displacements": + path_to_dfield = transform.path if transform.path is not None else "" + if zarr_context != "" and path_to_dfield != "": + path_to_dfield = f"{zarr_context}/{path_to_dfield}" + + if self.coordinates_displacements is not None: + dfield = self.coordinates_displacements.get( + path_to_dfield.split("/")[-1] + ) + if dfield is not None: + if dfield.images[0].scale is None: + raise ValueError( + f"Displacement field at {path_to_dfield} is missing scale information." + ) + tnd_transform = tnd.transforms.Displacements( + dfield.images[0].data, + index_transform=tnd.transforms.Scale( + list(dfield.images[0].scale.values())[1:] + ), + vector_axis=0, + spaces=spaces, + ) + elif transform.type == "mapAxis": + tnd_transform = tnd.transforms.MapAxis( + list(transform.mapAxis), + spaces=spaces, + ) + + elif transform.type == "projectAxis": + tnd_transform = tnd.transforms.ProjectAxis( + created=transform.createdOutputs, + dropped=transform.droppedInputs, + spaces=spaces, + source_ndim=len(source_cs.axes) if source_cs is not None else None, + target_ndim=len(target_cs.axes) if target_cs is not None else None, + ) + + elif transform.type == "scale": + tnd_transform = tnd.transforms.Scale(transform.scale, spaces=spaces) + + elif transform.type == "translation": + tnd_transform = tnd.transforms.Translate( + transform.translation, spaces=spaces + ) + + elif transform.type == "rotation": + tnd_transform = tnd.transforms.Affine.from_linear_map( + transform.rotation, spaces=spaces + ) + + elif transform.type == "byDimension": + sub_transformations = transform.transformations + tnd_sub_transforms = [ + tnd.transforms.by_dimension.SubTransform( + transform=self._ozmp_tf_to_tnd(sub_tf.transformation), + input_axes=sub_tf.input_axes, + output_axes=sub_tf.output_axes, + ) + for sub_tf in sub_transformations + ] + tnd_transform = tnd.transforms.ByDimension( + subtransforms=tnd_sub_transforms, + fill_identity=0, + spaces=spaces, + ) + elif transform.type == "sequence": + sub_transformations = transform.transformations + tnd_sub_transforms = [ + self._ozmp_tf_to_tnd(sub_tf, zarr_context, source_cs, target_cs) + for sub_tf in sub_transformations + ] + tnd_transform = tnd.base.TransformSequence( + tnd_sub_transforms, + spaces=spaces, + ) + + return tnd_transform diff --git a/pyproject.toml b/pyproject.toml index 5df1ad4c..549fca1a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,8 +26,10 @@ dependencies = [ "scikit-image>=0.19.0", "toolz", "rangehttpserver", - "ome-zarr-models==1.8.0rc0", + "transformnd>=0.6.0", + "ome-zarr-models==1.8.0rc1", "Deprecated", + "tqdm", ] classifiers = [ "Development Status :: 4 - Beta", From 92e22813628514ad62c01ec41c8aac81e792c2d1 Mon Sep 17 00:00:00 2001 From: Johannes Soltwedel <38459088+jo-mueller@users.noreply.github.com> Date: Tue, 4 Aug 2026 10:13:13 +0200 Subject: [PATCH 02/51] tests: add tests for OMEZarrScene --- tests/test_scene.py | 361 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 361 insertions(+) create mode 100644 tests/test_scene.py diff --git a/tests/test_scene.py b/tests/test_scene.py new file mode 100644 index 00000000..960b3039 --- /dev/null +++ b/tests/test_scene.py @@ -0,0 +1,361 @@ +import numpy as np +import pytest +import zarr + +from ome_zarr import OMEZarrImage, OMEZarrMultiscale, OMEZarrScene + +TRANSFORMS = [ + {"type": "scale", "scale": [1.0, 1.0]}, + {"type": "translation", "translation": [0.0, 0.0]}, + {"type": "rotation", "rotation": [[0.0, -1.0], [1.0, 0.0]]}, + {"type": "affine", "affine": [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]}, + {"type": "mapAxis", "mapAxis": [1, 0]}, +] + + +@pytest.fixture +def test_data_dir(tmp_path): + """Create a temporary directory with zarr v3 group for testing.""" + path = tmp_path / "data" / "v3" + root_v3 = zarr.open_group(path, mode="w", zarr_format=3) + root_v3.create_group("test") + return path + + +def create_data(shape, dtype=np.uint8, mean_val=10): + """Create dummy testing data of defined shape and type, + with a given mean value.""" + rng = np.random.default_rng(0) + return rng.poisson(mean_val, size=shape).astype(dtype) + + +@pytest.mark.parametrize("transform", TRANSFORMS) +def test_create_scene_without_coordinate_systems(test_data_dir, transform): + """ + Create a scene with two images and a single coordinate transformation + between them. The data is saved and loaded back in the test. + """ + shape = (64, 64) + img_a = OMEZarrImage( + data=create_data(shape), + name="imageA", + axes=["y", "x"], + scale={"y": 1.0, "x": 1.0}, + ) + + img_b = OMEZarrImage( + data=create_data(shape), + name="imageB", + axes=["y", "x"], + scale={"y": 1.0, "x": 1.0}, + ) + + img_a_ms = OMEZarrMultiscale(image=img_a) + img_b_ms = OMEZarrMultiscale(image=img_b) + + # avoid leaking transform mutations into other tests + transform = transform.copy() + transform["input"] = {"name": "physical", "path": "imageA"} + transform["output"] = {"name": "physical", "path": "imageB"} + + scene = OMEZarrScene( + images=[img_a_ms, img_b_ms], + coordinate_transformations=[transform], + ) + + scene.to_ome_zarr("test_scene.zarr", overwrite=True) + + # check that the graph is created correctly + assert scene._graph is not None + + # check that the graph has the correct number of nodes + # (aka coordinate systems) + assert len(scene._graph.graph.nodes) == 2 + + # traverse graph + tf = scene._graph.get_sequence(f"{img_a.name}:physical", f"{img_b.name}:physical") + + # check that the transform graph can be traversed (i.e. transform is not None) + assert tf is not None + + # write to disk and read back + scene.to_ome_zarr(str(test_data_dir / "test_scene.zarr"), overwrite=True) + scene_read = OMEZarrScene.from_ome_zarr(str(test_data_dir / "test_scene.zarr")) + + # check that the graph is created correctly on read + assert scene_read._graph is not None + assert len(scene_read._graph.graph.nodes) == 2 + + # open the zarr group and check the metadata + # and check that the correct metadata fields are present in the store + zarr_group = zarr.open_group(str(test_data_dir / "test_scene.zarr"), mode="r") + assert "ome" in zarr_group.attrs + ome_metadata = zarr_group.attrs["ome"] + assert "scene" in ome_metadata + assert "version" in ome_metadata and ome_metadata["version"] == "0.6.dev4" + + # check transforms + assert "coordinateTransformations" in ome_metadata["scene"] + assert len(ome_metadata["scene"]["coordinateTransformations"]) == 1 + + transform_md = ome_metadata["scene"]["coordinateTransformations"][0] + + # make sure that the loaded transform is the same as the original + assert transform_md == transform + + +@pytest.mark.parametrize("transform", TRANSFORMS) +def test_create_scene_with_coordinate_systems(test_data_dir, transform): + """ + Create a scene with two images and three coordinate transformations + between them. The data is saved and loaded back in the test. + """ + shape = (64, 64) + img_a = OMEZarrImage( + data=create_data(shape), + name="imageA", + axes=["y", "x"], + scale={"y": 1.0, "x": 1.0}, + ) + + img_b = OMEZarrImage( + data=create_data(shape), + name="imageB", + axes=["y", "x"], + scale={"y": 1.0, "x": 1.0}, + ) + + img_a_ms = OMEZarrMultiscale(image=img_a) + img_b_ms = OMEZarrMultiscale(image=img_b) + + world1_cs = { + "name": "world", + "axes": [ax.model_dump() for ax in img_a_ms.metadata.coordinateSystems[0].axes], + } + world2_cs = { + "name": "world2", + "axes": [ax.model_dump() for ax in img_b_ms.metadata.coordinateSystems[0].axes], + } + + transform1 = transform.copy() + transform1["input"] = {"name": "physical", "path": "imageA"} + transform1["output"] = {"name": "world"} + + transform2 = transform.copy() + transform2["input"] = {"name": "world"} + transform2["output"] = {"name": "world2"} + + transform3 = transform.copy() + transform3["input"] = {"name": "world2"} + transform3["output"] = {"name": "physical", "path": "imageB"} + + scene = OMEZarrScene( + images=[img_a_ms, img_b_ms], + coordinate_transformations=[transform1, transform2, transform3], + coordinate_systems=[world1_cs, world2_cs], + ) + + # check that the graph is created correctly + # and has the correct number of nodes (coordinate systems) + assert scene._graph is not None + assert len(scene._graph.graph.nodes) == 4 + + scene.to_ome_zarr(str(test_data_dir / "test_scene_with_cs.zarr"), overwrite=True) + scene_read = OMEZarrScene.from_ome_zarr( + str(test_data_dir / "test_scene_with_cs.zarr") + ) + + # check that the graph is created correctly on read + # and has the correct number of nodes (coordinate systems) + assert scene_read._graph is not None + assert len(scene_read._graph.graph.nodes) == 4 + + # open zarr group and check metadata + zarr_group = zarr.open_group( + str(test_data_dir / "test_scene_with_cs.zarr"), mode="r" + ) + + # make sure that the correct metadata fields are present in the store + assert "ome" in zarr_group.attrs + ome_metadata = zarr_group.attrs["ome"] + assert "scene" in ome_metadata + assert "version" in ome_metadata and ome_metadata["version"] == "0.6.dev4" + assert "coordinateSystems" in ome_metadata["scene"] + assert len(ome_metadata["scene"]["coordinateSystems"]) == 2 + + +def test_appending_scene(test_data_dir): + """ + Create a scene with two images and a single coordinate transformation + between them. The data is saved and loaded back. + We then append a third image to the scene and make sure the graph is updated. + We then save the scene again and ensure that we are writing only what's new + (new data and metadata) + """ + img_a = OMEZarrImage( + data=create_data((64, 64)), + name="imageA", + axes=["y", "x"], + scale={"y": 1.0, "x": 1.0}, + ) + img_b = OMEZarrImage( + data=create_data((64, 64)), + name="imageB", + axes=["y", "x"], + scale={"y": 1.0, "x": 1.0}, + ) + img_c = OMEZarrImage( + data=create_data((64, 64)), + name="imageC", + axes=["y", "x"], + scale={"y": 1.0, "x": 1.0}, + ) + + img_a_ms = OMEZarrMultiscale(image=img_a) + img_b_ms = OMEZarrMultiscale(image=img_b) + img_c_ms = OMEZarrMultiscale(image=img_c) + + world_cs = { + "name": "world", + "axes": [ax.model_dump() for ax in img_a_ms.metadata.coordinateSystems[0].axes], + } + + tf1 = { + "type": "scale", + "scale": [1.0, 1.0], + "input": {"name": "physical", "path": "imageA"}, + "output": {"name": "world"}, + } + + tf2 = { + "type": "scale", + "scale": [1.0, 1.0], + "input": {"name": "world"}, + "output": {"name": "physical", "path": "imageB"}, + } + + tf3 = { + "type": "scale", + "scale": [1.0, 1.0], + "input": {"name": "world"}, + "output": {"name": "physical", "path": "imageC"}, + } + + scene = OMEZarrScene( + images=[img_a_ms, img_b_ms], + coordinate_transformations=[tf1, tf2], + coordinate_systems=[world_cs], + ) + + scene.to_ome_zarr(str(test_data_dir / "test_scene_append.zarr"), overwrite=True) + + # now we load the scene and append a new image to it + scene_read = OMEZarrScene.from_ome_zarr( + str(test_data_dir / "test_scene_append.zarr") + ) + + new_scene = OMEZarrScene( + images=list(scene_read.images.values()) + [img_c_ms], + coordinate_transformations=list(scene_read.coordinate_transformations) + [tf3], + coordinate_systems=[world_cs], + ) + + new_scene.to_ome_zarr( + str(test_data_dir / "test_scene_append.zarr"), overwrite=False + ) + + # check that the graph is built correctly + assert new_scene._graph is not None + assert len(new_scene._graph.graph.nodes) == 4 + + # check that the data is written and not empty + zarr_group = zarr.open_group( + str(test_data_dir / "test_scene_append.zarr"), mode="r" + ) + assert "imageA" in zarr_group and "s0" in zarr_group["imageA"] + assert "imageB" in zarr_group and "s0" in zarr_group["imageB"] + assert "imageC" in zarr_group and "s0" in zarr_group["imageC"] + + # check that the metadata is written and correct + assert "ome" in zarr_group.attrs + ome_metadata = zarr_group.attrs["ome"] + assert "scene" in ome_metadata + scene_metadata = ome_metadata["scene"] + assert "coordinateTransformations" in scene_metadata + assert "coordinateSystems" in scene_metadata + assert len(scene_metadata["coordinateTransformations"]) == 3 + assert len(scene_metadata["coordinateSystems"]) == 1 + + +def test_scene_with_displacements(test_data_dir): + """ + Create a scene with two images and a single coordinate transformation + between them. The data is saved and loaded back in the test. + """ + shape = (64, 64) + img_a = OMEZarrImage( + data=create_data(shape), + name="imageA", + axes=["y", "x"], + scale={"y": 1.0, "x": 1.0}, + ) + + img_b = OMEZarrImage( + data=create_data(shape), + name="imageB", + axes=["y", "x"], + scale={"y": 1.0, "x": 1.0}, + ) + + vector_field = np.zeros((2, 64, 64), dtype=np.float32) + dfield_img = OMEZarrImage( + data=vector_field, + name="displacementField", + axes=["c", "y", "x"], + scale={"y": 1.0, "x": 1.0}, + axes_types={"c": "displacement", "y": "space", "x": "space"}, + ) + + img_a_ms = OMEZarrMultiscale(image=img_a) + img_b_ms = OMEZarrMultiscale(image=img_b) + dfield_img_ms = OMEZarrMultiscale(image=dfield_img) + + transform = { + "type": "displacements", + "input": {"name": "physical", "path": "imageA"}, + "output": {"name": "physical", "path": "imageB"}, + "path": "coordinateTransformations/displacementField", + } + + scene = OMEZarrScene( + images=[img_a_ms, img_b_ms], + coordinate_transformations=[transform], + coordinates_displacements={"displacementField": dfield_img_ms}, + ) + + save_grp = str(test_data_dir / "test_scene_displacement.zarr") + scene.to_ome_zarr(save_grp, overwrite=True) + + # check that all subgroups are there + group = zarr.open_group(save_grp, mode="r") + assert "coordinateTransformations" in group + assert "displacementField" in group["coordinateTransformations"] + assert "imageA" in group + assert "imageB" in group + + # check that the metadata of the displacement field is correct + dfield_attrs = group["coordinateTransformations"]["displacementField"].attrs + assert "ome" in dfield_attrs + assert dfield_attrs["ome"]["version"] == "0.6" + axes_md = dfield_attrs["ome"]["multiscales"][0]["coordinateSystems"][0]["axes"] + assert axes_md[0]["type"] == "displacement" + assert axes_md[0]["discrete"] == True + + # read displacements back in and check that the (meta)data is correct + scene_read = OMEZarrScene.from_ome_zarr(save_grp) + assert "displacementField" in scene_read.coordinates_displacements + + dfield_img = scene_read.coordinates_displacements["displacementField"] + for image in dfield_img.images: + assert image.axes_types["c"] == "displacement" + assert dfield_img.metadata.coordinateSystems[0].axes[0].discrete == True From b700a98dc695c07cd0dd7917ab64e54b80a38372 Mon Sep 17 00:00:00 2001 From: Johannes Soltwedel <38459088+jo-mueller@users.noreply.github.com> Date: Tue, 4 Aug 2026 10:13:22 +0200 Subject: [PATCH 03/51] docs; add documentation for OMEZarrScene --- docs/source/_toc.yml | 3 + .../advanced/transforms/create_scenes.ipynb | 255 ++++++++++++++++++ .../advanced/transforms/reading_scenes.ipynb | 92 +++++++ .../advanced/transforms/scene_2d_to_3d.ipynb | 201 ++++++++++++++ 4 files changed, 551 insertions(+) create mode 100644 docs/source/advanced/transforms/create_scenes.ipynb create mode 100644 docs/source/advanced/transforms/reading_scenes.ipynb create mode 100644 docs/source/advanced/transforms/scene_2d_to_3d.ipynb diff --git a/docs/source/_toc.yml b/docs/source/_toc.yml index 3782c38b..fcd90be0 100644 --- a/docs/source/_toc.yml +++ b/docs/source/_toc.yml @@ -24,6 +24,9 @@ parts: - file: advanced/transforms/index sections: - file: advanced/transforms/add_transforms_to_multiscales + - file: advanced/transforms/create_scenes + - file: advanced/transforms/reading_scenes + - file: advanced/transforms/scene_2d_to_3d - file: advanced/sharding - file: advanced/build_custom_pyramid diff --git a/docs/source/advanced/transforms/create_scenes.ipynb b/docs/source/advanced/transforms/create_scenes.ipynb new file mode 100644 index 00000000..07512e66 --- /dev/null +++ b/docs/source/advanced/transforms/create_scenes.ipynb @@ -0,0 +1,255 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "8b0be7e9", + "metadata": {}, + "source": [ + "# Create Scenes\n", + "\n", + "(advanced:create-scenes)=\n", + "\n", + "This tutorial demonstrates basic usage around writing and reading [Ngff Scenes](https://ngff.openmicroscopy.org/specifications/dev/index.html#scene-metadata) using the {py:class}`ome_zarr.classes.scene.OMEZarrScene` class." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "a49ec1c2", + "metadata": {}, + "outputs": [], + "source": [ + "from skimage import data\n", + "\n", + "from ome_zarr import OMEZarrImage, OMEZarrMultiscale, OMEZarrScene" + ] + }, + { + "cell_type": "markdown", + "id": "98bbabd7", + "metadata": {}, + "source": [ + "We create some sample data, which we will store as a tiled layout in a scene zarr group:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "e3023c95", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "c:\\Users\\johan\\Documents\\GitHub\\ome-zarr-py\\.venv\\Lib\\site-packages\\tqdm\\auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", + " from .autonotebook import tqdm as notebook_tqdm\n" + ] + }, + { + "data": { + "text/plain": [ + "(512, 512)" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "example_image = data.human_mitosis()\n", + "example_image.shape" + ] + }, + { + "cell_type": "markdown", + "id": "71e31e2a", + "metadata": {}, + "source": [ + "First, we cut the image into four tiles and convert them into instances of {py:class}`ome_zarr.classes.image.OMEZarrMultiscale`:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "017d4c59", + "metadata": {}, + "outputs": [], + "source": [ + "img1 = OMEZarrImage(data=example_image[:256, :256], axes=[\"y\", \"x\"], name=\"img1\")\n", + "img2 = OMEZarrImage(data=example_image[256:, :256], axes=[\"y\", \"x\"], name=\"img2\")\n", + "img3 = OMEZarrImage(data=example_image[:256, 256:], axes=[\"y\", \"x\"], name=\"img3\")\n", + "img4 = OMEZarrImage(data=example_image[256:, 256:], axes=[\"y\", \"x\"], name=\"img4\")\n", + "\n", + "img1_ms = OMEZarrMultiscale(img1)\n", + "img2_ms = OMEZarrMultiscale(img2)\n", + "img3_ms = OMEZarrMultiscale(img3)\n", + "img4_ms = OMEZarrMultiscale(img4)" + ] + }, + { + "cell_type": "markdown", + "id": "cf59df32", + "metadata": {}, + "source": [ + "Next, we need to define a coordinate system into which all images are projected.\n", + "This is defined in accordance with the NGFF [coordinate systems specification](https://ngff.openmicroscopy.org/specifications/dev/index.html#coordinatesystems-metadata)." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "994674c2", + "metadata": {}, + "outputs": [], + "source": [ + "coordinate_system = {\n", + " \"name\": \"world\",\n", + " \"axes\": [\n", + " {\"name\": \"y\", \"type\": \"space\"},\n", + " {\"name\": \"x\", \"type\": \"space\"},\n", + " ],\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "c1d629be", + "metadata": {}, + "source": [ + "We then define translations that move each tile into the appropriate position in the world coordinate system.\n", + "In this example, these are simple translations in the y and x dimensions:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "d6329caa", + "metadata": {}, + "outputs": [], + "source": [ + "coordinate_transformations = [\n", + " {\n", + " \"type\": \"translation\",\n", + " \"translation\": [0, 0],\n", + " \"input\": {\"path\": \"img1\", \"name\": \"physical\"},\n", + " \"output\": {\"name\": \"world\"},\n", + " },\n", + " {\n", + " \"type\": \"translation\",\n", + " \"translation\": [256, 0],\n", + " \"input\": {\"path\": \"img2\", \"name\": \"physical\"},\n", + " \"output\": {\"name\": \"world\"},\n", + " },\n", + " {\n", + " \"type\": \"translation\",\n", + " \"translation\": [0, 256],\n", + " \"input\": {\"path\": \"img3\", \"name\": \"physical\"},\n", + " \"output\": {\"name\": \"world\"},\n", + " },\n", + " {\n", + " \"type\": \"translation\",\n", + " \"translation\": [256, 256],\n", + " \"input\": {\"path\": \"img4\", \"name\": \"physical\"},\n", + " \"output\": {\"name\": \"world\"},\n", + " },\n", + "]" + ] + }, + { + "cell_type": "markdown", + "id": "4444a8d3", + "metadata": {}, + "source": [ + "We can then create and write a scene like this:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "d05be093", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Writing images: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 4/4 [00:00<00:00, 7.08it/s]\n" + ] + } + ], + "source": [ + "scene = OMEZarrScene(\n", + " images=[img1_ms, img2_ms, img3_ms, img4_ms],\n", + " coordinate_systems=[coordinate_system],\n", + " coordinate_transformations=coordinate_transformations\n", + ")\n", + "\n", + "scene.to_ome_zarr(\"test_example_scene.zarr\", overwrite=True)" + ] + }, + { + "cell_type": "markdown", + "id": "9333cd91", + "metadata": {}, + "source": [ + "....and load it back like this:" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "8b87b923", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(Translation(type='translation', input=CoordinateSystemIdentifier(name='physical', path='img1'), output=CoordinateSystemIdentifier(name='world', path=None), name=None, translation=(0.0, 0.0)),\n", + " Translation(type='translation', input=CoordinateSystemIdentifier(name='physical', path='img2'), output=CoordinateSystemIdentifier(name='world', path=None), name=None, translation=(256.0, 0.0)),\n", + " Translation(type='translation', input=CoordinateSystemIdentifier(name='physical', path='img3'), output=CoordinateSystemIdentifier(name='world', path=None), name=None, translation=(0.0, 256.0)),\n", + " Translation(type='translation', input=CoordinateSystemIdentifier(name='physical', path='img4'), output=CoordinateSystemIdentifier(name='world', path=None), name=None, translation=(256.0, 256.0)))" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "loaded_scene = OMEZarrScene.from_ome_zarr(\"test_example_scene.zarr\")\n", + "loaded_scene.coordinate_transformations" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cd5dda7c", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "ome-zarr (3.13.12)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/source/advanced/transforms/reading_scenes.ipynb b/docs/source/advanced/transforms/reading_scenes.ipynb new file mode 100644 index 00000000..c177c476 --- /dev/null +++ b/docs/source/advanced/transforms/reading_scenes.ipynb @@ -0,0 +1,92 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "28740944", + "metadata": {}, + "source": [ + "# Reading scenes" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "3222cfcb", + "metadata": {}, + "outputs": [], + "source": [ + "import networkx as nx\n", + "\n", + "from ome_zarr import OMEZarrScene" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "a9bebd85", + "metadata": {}, + "outputs": [], + "source": [ + "scene = OMEZarrScene.from_ome_zarr(\"https://radosgw.public.os.wwu.de/s2v/P2A_B6_M2.ome.zarr\")" + ] + }, + { + "cell_type": "markdown", + "id": "7e3c003e", + "metadata": {}, + "source": [ + "This step requires matplotlib" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "a9277576", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAApQAAAHzCAYAAACe1o1DAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjExLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlcelbwAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzsnQW4FNX7x8+9dEtjgKggKmIjtmInCgaoiP5sBVtRMcAOVAxE7P7bha0oIgoiNmIQEioK0h0X9v98Dr7Xc+fO7s7s7mzc+36fZ72yOzs7c+bE93zfKorFYjGjUCgUCoVCoVCkiOJUv6hQKBQKhUKhUCihVCgUCoVCoVCkDVUoFQqFQqFQKBRpQQmlQqFQKBQKhSItKKFUKBQKhUKhUKQFJZQKhUKhUCgUirSghFKhUCgUCoVCkRaUUCoUCoVCoVAo0oISSoVCoVAoFApFWlBCqVAoFAqFQqFIC0ooFQqFQqFQKBRpQQmlQqFQKBQKhSItKKFUKBQKhUKhUKQFJZQKhUKhUCgUirSghFKhUCgUCoVCkRaUUCoUCoVCoVAo0oISSoVCoVAoFApFWlBCqVAoFAqFQqFIC0ooFQqFQqFQKBRpQQmlQqFQKBQKhSItKKFUKBQKhUKhUKQFJZQKhUKhUCgUirSghFKhUCgUCoVCkRaUUCoUCoVCoVAo0oISSoVCoVAoFApFWlBCqVAoFAqFQqFIC0ooFQqFQqFQKBRpQQmlQqFQKBQKhSItKKFUKBQKhUKhUKQFJZQKhUKhUCgUirSghFKhUCgUCoVCkRaUUCoUCoVCoVAo0oISSoVCoVAoFApFWlBCqVAoFAqFQqFIC0ooFQqFQqFQKBRpQQmlQqFQKBQKhSItKKFUKBQKhUKhUKSFqul9XaFQVHYsXVlips1dalaVrDXVqxab1o3rmDo1dGpRKBSKygSd9RUKRWhMmrXYPDt2hhnx62wzY94yE3M+KzLGtGpU23Ru18yc2KmVadu8nrawQqFQVHAUxWIxdy1QKBSKuPh93jLT77XxZtTkOaZKcZFZszb+9CGf79mmibm5awfTslFtbVmFQqGooFBCqVAoAuH5cTNM/2ETTMnaWEIi6UcsqxYXmeu6tDc9OrbS1lYoFIoKCA3KUSgKGA8//LDZYostzLJlyyL9ncEjJpkrXh1vVpasDUUmAcfzPb7PeSpK+7z66qtml112MStWrDD5invuucfe/5o1a0y+4oEHHjAHH3ywWbt2bYVqg8cff9z+7uLFiwvy/AqFqeyE8oUXXrCDjNeXX37pe8zpp59uP991113jfpfX1ltvbfbZZx9z7bXXmnnz5sX9rTFjxgS6tj///LPM+RO93nzzTfudWbNmmSFDhph9993Xvv/UU0+ZdMEENHjwYNO1a1ez0047mY4dO9r/v+qqq8xPP/2UkTaJ93r22WdDX++MGTPMpZdeag444ACzww47mKOOOsrccccdZs6cOUmvtXPnzua6664zCxYsCP27PNezzz7b7L777rav9OzZ0wwfPjzpPW+77bZm7733NmeeeaZ57rnnIiUcc+fONb/++mtki7Eok3d8MDHhMWuWzjeLv3nb/P1//cyfD51tloz/yPc4zvPge19nvE9nu314ppdccok59NBDTc2aNe17zz//vL2fkSNHljv+jDPOsJ+9/vrr5T7r27ev2WqrrcyiRYtMpvHPP//Y+8+UZ9Ps2bMtAdxvv/3s/UBqEmHmzJnm8ssvN3vssYfp1KmTueiii8xff/1V5pju3bvbsZbsXPnSBmH7XlRENurzKxSmsgflzJ8/3w6yatWqmccee8zsvPPOZT6HoD355JOmqKjI1K9f3/e7fA8CsXLlSktKr7jiCkuEvv322zLfkeOXLl0a6NqaNWtWbkE59dRTzffff2++/vrrMu9vsMEGZvz48XbnfuSRR9qF67LLLvMlcWHw2WefmWOOOcY0adLEnHfeeWb77be3bQWRfPTRR83NN99sF0Ym+XTaRI73okWLFqGu9+OPPzaHHXaYJb6Qyo033theK9d59dVX27avUqWK72+z6I8ePdpceeWV5v/+7/9sG9etWzfQ7z7xxBOWDNIOPCPOxf1CavndG264Ie49l5SU2EWM3+aZ0Vb8PsQ004C0QrDr1KljovKZxMydCKtmTzOzX7zW1Gq7i6m12U5mwYjHzNoVS+Ie2/u4k80Jx3TLWJ/ORfswh0CWevfuXfpehw4dbD9477337IZCwIL/4osvWpX0rbfestfjgs8YQ975KN/AuNt///3tfHT44Yebiy++2JKaeIAkHnLIIfbY66+/3jRu3Nh8/vnn5oQTTjAjRowoPa5Ro0bm5JNPtmPqf//7nykurhg6B/MG957vz1WhyBQqHKEUMJGhHN19992lCgJ4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SeaKV8eblSVrkxJJLzie7/F9zlOR2mfJkiVmxYoVJl+xdu1ae/8UFyhUKKFUKBSRo127dqa4uLhcCUYm0ESLiEyyvFavXu37XV4LFy40JSUlCa+B45YtWxb4mpcuXVrmN+K9WKi8WLRokVm8eLHJ9IK7fPnyhMeEbRNUYyrj9OjRw57b/X66159sYQx7rUGRjJB47zFbXl/kYSWK/oEHHojsN1AW7/hgYuDj165cZl9+4DwvOEplFH06W+3zxx9/mObNm5sRI0aUIa9+42nNmjVx5wqZj6IgpjNmzLD3z3jMNLjeoP08zBzphRJKhUKRk0hvCAQTKK/jjjvO93uPP/546TFvv/12ue82a9bMtG7d2rRq1crWDYe43n333eUmTzn+1FNPDXzNJ554oj23vDbccEN7Dv6670PGwM8//2z69+9vttxyS7PeeuuZXXfd1aQLFvEbbrjBbLvttvb+GjdubOrXr2+23nprc8UVV5hff/01rTbZYIMNLJl78skny9wTr0MPPTT09U6bNs2cfvrpZv311zd169a119qxY0dz77332t9Jdq0o2ffff3/o333//ffNkUceaZVWfpN2oha5dwPj/d2WLVva391oo43MUUcdZV5++eXICCYbKmqkMxai8pnsP6zs/fph9ZzfzYJRz5o/Hz7b/D6ou/n76UvjHnv5Y++aC/v2y2ifzkX7XHnllXYMHXLIIaX9gHHctWvXcsf+3//9n+0je+21l28/i4r0RYGhQ4eaTTfd1D47Sqkyz/r5sc+bN8+cdtpptv3lRYAe5DkMlFAqFIqcRnpDAN555x1b8s8LUtnweTxgphWFi9eBBx5oLrroIjNw4MC0r/f1118vo2YRVAT4677/1ltv2fchfiyKr732ms29mS6mTJlitt9+e/P000+ba6+91vqfoh6wIFx99dU2MptFIN02gVxBXL3K66hRo0Jd799//2123nlnM2bMGNt2qD9USOIaCcp67rnnEl4rixpVevr06WPuvPPOwL+LosTCyfd+++03q8agREG2IUAoP/F+V0yMH3/8senQoYPdRBx00EFJleBU0KhRI/t755xzjokCBOCUBDBxLxj9vDFFRaZZt6tM1UYbJjx2zqfPmRG//pOxPp2L9uH50/d69+5d+l61atXMHnvsYT777LNylg/6AoTq22+/LefzzWdg3333NfmO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CoUi81BCqcgb/Pbbb7Hbb789tvPOO9sFoEaNGrEjjzwy9vTTT8cWLFgQq0yAzNEGufbnwt8VUnnSSSflJal89913bTtNnTo1st/o169fbL311rNKIORtr732iuUDuGf8/jC/f/rpp7m+nLwAavpbb70V69atm3Xb4HXsscfafpKP/VehqEhQQqnIS0ybNs06yOMjB2FAgTniiCNsIMT8+fNjFR2iio0aNSrXl2KVr+Li4tgpp5wSqRKYCmbMmGHbCRIRBVCIN91009hpp50W++WXX+xvoZ7nGuPGjYs1b948tskmm9jrquzAjebKK6+0wWw8IwIB77nnntg///yT60tTKCoNlFAq8h7Tp0+3pqvddtvNLhaoDocddljs8ccftya/iho8UFRUFHvsscdi+QBIFKQS3858IpUQvrp161plOwqMHTvW9rkPP/wwdvHFF8caN24cW7FiRSyXIGgKMz9K/qxZs2KVFUSxMwfsueee9hmhIp977rmxr776Sv1IFYocIPryEgpFmqA6yUUXXWRz/v3+++9m4MCBZuHChTZPXPPmzW2VF3LIkZS4ooB8neRYpMRlPuCEE04wTzzxhK0ccu6559pcffkAyt5ttdVWNudpFHj++edtH9t1113t/Z9yyik5TfpOLXHKnVKNasSIEaZZs2amMgERZPTo0bbsJvk1STjO8yAZOSUw77//frPjjjtqOUSFIgfQxOaKggULCFVdXn75ZTNq1ChbFYRKLyR1Puqoo0yTJk1MIWP//fc3DRs2tEnG8wWQKoj8OeecY+tY50MdY67nxx9/NF9++WXGE16zmenWrZvZZZddTM+ePW2Z0c0339xkGxD4yy+/3Nxxxx3m/PPPt1V76O+VBX///bd5+umn7cbxl19+MRtvvLElk1Sxad26da4vT6FQgFzIogpFpjFz5szY4MGDbdAEplkCSQ444IDYgw8+aKM9CxGkOdl2221j+YaHH37YmhjPP//8vDAtDhw40OaIzLQp/pNPPrH3+fnnn1uzaufOnWO5wPLly21gCS4Qd999d6yygMT6r7/+eqxLly52PBOkR8aB4cOH55XbhUKhWAcllIoKByKjhwwZYpMXC7ncb7/9Yg888EBB+ZxJSpx8IG1eDB061JKtiy66KOfXR3UfroVArkwT+latWpUGSJGEP9uYM2dObPfdd7fptIj8rwwgJdOll14aa9asmW138ozef//9FdZfWqGoKFCTt6JCY/bs2bY+L2ZxfM7YRO29997WLI4ps0WLFiZfQY1qamtTs3rDDTc0+YYhQ4aY3r17m0svvdTcfvvtOTN/T58+3Zo93377betPmwmsXr3a+uiddtppZsWKFdaXEv/dbNY3nzJliq0lPX/+fNsXMLtXVCxatMi88MIL1qT9xRdfmMaNG1sXA8za2267ba4vT6FQBIAG5SgqNAhaOOuss8yHH35o/bAefPBBU61aNeuHtsEGG1hyiS/gX3/9ZfINbdu2tX/zJTDHC4Jz7r33XuvX169fP0vWcwH8HOvWrWt++umnjJ3zo48+MnPnzrW+uE899ZQlNtkkk5AqIZDu/1ck0F8+/fRTG+gEeT/77LNLfYb//PNPc/fddyuZVCgKCEooFZUGBOkQHfr++++bWbNmmUceecTUrl3bRpCjAO65556WILGY5QM23XRTU1xcnLeEEpx33nk2QOTWW28111xzTU5IJcrolltumdFI7+eee860a9fOBuEsWLDAnHHGGSZbePXVV03nzp3t748ZM8ZsttlmpiKB8XXzzTfb4CY2dATUsSFBaX7nnXes9SCXkfQKhSJF5NrmrlDkGnPnzrX57A499FCb45JhQc7LQYMG2cTZuQRJtS+77LJYvoPAGNptwIABOfl9kq6TlzFTQTCUvezfv39s1113tcFd2QJ9juCb4447zl5HRQG5O6m6dMghh1i/5lq1asV69eplA580wEahqBhQQqlQOMDxn2o8hx9+uK3OA0miWg8BMiRYzzYOOuig2FFHHVUQz+iWW26x7XXDDTdk/bdJbE6C80wECL3yyiv2Pt544w379+WXX45FDcoCEjXP7/Xt27fCkKwffvghduGFF9qE8DKWHnroodjChQtzfWkKhSLDUEKpUMQB9cOpI07aEiGXqGCocVHWjnbRp0+fWPv27QvmGd144422nW6++eas/u7bb79tfzcTpJ8UPdttt12sd+/esRYtWtj0NVFi6dKltmY9yh3ZCQodlEblPnbaaSf7TIjWJmp7woQJub40hUIRIZRQKhQBgKJC+UHUQvLhsVCyYN52222xKVOmRNaG1CMmZUwhKVaYvWmfqMoh+gGCz2++++67aZ1n0aJF1hx7/fXXx+rXrx+76qqrYlGCNFZsUsijGVU98myA/kl+SPJE0l9J1XXEEUfEXnvttcgJuUKhyA9oUI5CEQD169e35QdJQfTPP//YoA2ii/v372+DJij3RmAKqV4yHelN2hpSBxUKrr32WnP11Vebvn37mkGDBmXlN3kWderUSTswZ9iwYWb58uU2onvx4sWRBuMQ8EP09owZM8zIkSPNYYcdZgoNBNJcd911NoCMyk5ff/21/TcplmhLouTJqqBQKCo+NA+lQpEGlixZYiNTSXVCHkTIyHbbbWeOPfZY+5LUP6li8uTJ9hykPWLBLhRg/bjqqqvMLbfcYu655x6bpilqdOzY0WyzzTa23niqOOKII2y6oJKSEpsVgGcbBYhspiY36XL4DUoJFgrY4Lz++us2Z+Tw4cMtke/evbstgUnN83wox6lQKLIPVSgVijRA/sPjjjvOEkqUyxdffNGmQ7npppvsX5Iy33jjjVaNSgUk7K5atWpepw7yA6SCNrjsssvMBRdcYO6///7If7N9+/ZpKZTz5s2zKaX22GMPM27cOJu/NAqQJJ3NARuPzz//vCDIJBuEb775xvTp08eS4OOPP95uniDv5HAlBdduu+2mZFKhqMRQQqlQZAgoNaiSVPyAXFKdZ6uttrKm8C222MJ06NDBXH/99ebnn38OfE7I5CabbFJwhFJI5W233WbzfEJEhg4dGunv0dYkN081Fyb5H9esWWMT4JOXNNMmaK6L9oCMoei99957Zr311jP5DNRacrNuv/321q2DNiIBORskVFYSvrOpUigUCg3KUSgixrJly2wdZgIWSG0Dt9hqq61snsMff/wx6ffJj0kao0IFqXwkJc7DDz8c2e+8+eab9jdSzR1K7fe9997bPiOeTSaxevXq2FlnnWWv75prrsl5/fNkKYzee+89G+1OdoOqVavGunXrZoOGuA+FQqHwgxJKhSKLIFn166+/HuvZs6eNIoZgbLnllpZkkLPPj2iQx69du3YF/Zy4L9LwkLT7sccei+Q3fvvtN9uekKGwmDlzpr22k046yabv+f333zN2XUSOk9AbYvboo4/G8hWTJ0+OXX311bGNNtrItiPpqu666y4bia5QKBTJoIRSochh9ZBhw4bZiiENGjSwizjEkVQ13333XSm5vP/++20Fn0JXh7ifs88+2xI3ksdHkbqmdu3aNgl9KumZIHzbbLONTXeTKfz555+x7bff3lbeef/992P5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YqlVvnCnrfx2uhOP5et0tk4HXWwVXXu7z1dVzrH5D19kHfYZbm233qfOd5dPf8f22ekGt9377befTZ8+3U4//XR7++23nZhE4O60005266232m9+8xv3nSeeeCIyxnHixIl24YUXuuODZZgYV9z/N910k7PiYvk944wz6oQkkOl/1lln2ciRI5tMln5TRoJSCCGE+AUEI5a51q1bO2smL/BWRUQSFjwvMhFZWDwpg/Tmm286V7svh4RVNCgyWQYQbJdddpnts88+ttN1z9vkB66y+Y9fad1O/n9Wvk6XOudi8fiHbJ0dj7SK9TdzVsr5j1zqBOb6J93hRFqLDTe3Zl02sZ8/fLGeoFwy+Xlrvk4n23vvvd3/jzvuOLdviL111lnH7cvYsWPtvffeqxWUUbz//vu222672V577eX2vXPnzk5UY/GdNm2aCxHg87Db/9lnn7WjjjrKlYRCWIriRoJSCCGE+IXzzjsvMinn3XffdQkruHCxYPLCunfjjTe6GEpviURoRZVDwho5deraxBuEJxbDVu3aW+ez/tc67neuzb37FPtx0rPWYY9T6/xui55bOTEJZS3bWdvtDrJFL95pqypnW/NOa93pbQfsZ4v+7y5bMfcLq9hgrTBctXC2rZg1xdYZfKQtr15jrcvMbcPZZ5/txCSwL4jERFx66aXuO6NHj3YJTsAx+v3vf19vWWJRqT+KW53knoEDB9qTTz4pQdkEkKAUQgghfmHMmDG1VsmGgNDq3bu3e4W55ZZbnMv7X//6l3OTv/flPBtdWWLlbTtZsw4b2oq5n9f7TovudbelWftu7u/qJQvMfhGUrfvuZj+Me8B++vCFWkHJv2nQ2LrfHvZNZZX17baO7b777i6bHdF7wAEH2ODBg2sFYiywYr7xxht28MEHx12W2MoRI0bYY4895qygxGxi0UVcYtEUxY+iZYUQQogcQmY62eTDjjjq18G4RRtbs/LX2pq177eqW7S9pFmF+1uzavmvyzRrYW367W5Lv3jTucVrVi63nz99zSp69LNm7de3ldU1bjnE3uWXX15rISX2cf/993exlbHAnY8ADSb1RIFLG6H88ssv2w8//GBffvmlc4HvuuuutRn0oriRoBRCCCFyCHGW0Lz81yF49U8LraxN+wavs+2A/W1N9Sr7+ZOxVvXZ67ZmRZW12WrPOr+DhfFPf/qTTZ482Yk+XNgk0xx22GEx10ssafv27Z3bPh7PPfecHXLIIc7qGQRhKZoGEpRCCCFEhsE6RyxlFI8++qj7u1HH1kbuM67u6sXfWcteAxv8e806bGAtNtrKJef8NPkFK61oba02G+zWz+/gug5aCslqP/zww50rm6xzn0gUBcuRvBNlyVy1alXt+igdFOTVV1+VoGxCKIZSCCFEkwArW7B2oidYHifWMsQBduzYMenfoiQR7t7XX3+93meIuyuuuMJ17mkx8x2b/dzfXKY2VsZ04PvfP3W9+3ebAftbabMK69GxlbWuKHcWSRJkyDDfbrvtnAv7o48+cm7qo48+Om5Zn7/85S/21ltvuUzua6+91rbeemuXsX7ffffZ8OHDXWLPCSec4BKU7rjjDpeshBWUmp641kkGEsWPBKUQQoiiBvFExjEJMVGQhZxomfPPP7+2zmIQip2vWLGi3vusK1jUPMiVV15pjzzyiF1wwQW2ZP5ia7PlEGu3/eFWUv5rdnlZy7ZW3mFDKyktq/NdluH90or6LRVb9v6tlbXt5NznbfrvaWWlJTZks7VliHBbjxs3zu6++24nEKlBSaY6iUKIwXitF4m1JMv9f/7nf9x2k9hD0hE1LX2WODU8Ed1PP/20jRo1ygYNGuTc4NSpJOM73vpFcVCyJp6dWwghhBAZwWd506XGJ7l8Of8n2/P2NzKyfmIo5/z1OCtbZz3r9vs73XtjR+xivbu0zcj6hYiHYiiFEEKIRmLT9drazr07OWtiuiz7erLVrKhyBc5ZH+uVmBS5QoJSCCGEaERGHtLPytMQlNU/VdqKedNs8RujnHWyTf893PpYrxC5QoJSCCGEyAEk9RA/GE766d6hlV0ztG+D1/vje09b5Qt3WPm6Xa3L4Ve5OMtrh/Z16xUiVyiGUgghhMgD7hr3pd3ycuwi48ly0V597Owh9Tv1CJFNJCiFEEKIPOGxSbPsqjFTrbpmja2uST5nlphJ3NxYJo8cGJ1dLkQ2kaAUQggh8ojZi5baZU9PsQlfLXRCMZ6w9J+TgEPMpNzcorGQoBRCCCHyEEoKjZ44y8ZNX2CzKpdaUFaSwkPRcupMHrd9D2Vzi0ZHglIIIYTIc6pWVNs3lVW2srrG9eamnSIdcITIFyQohRBCCCFEWqhskBBCCCGESAsJSiGEEEIIkRYSlEIIIYQQIi0kKIUQQgghRFpIUAohhBBCiLSQoBRCCCGEEGkhQSmEEEIIIdJCglIIIYQQQqSFBKUQQgghhEgLCUohhBBCCJEWEpRCCCGEECItJCiFEEIIIURaSFAKIYQQQoi0kKAUQgghhBBpIUEphBBCCCHSQoJSCCGEEEKkhQSlEEIIIYRICwlKIYQQQgiRFhKUQgghhBAiLSQohRBCCCFEWkhQCiGEEEKItJCgFEIIIYQQaSFBKYQQQggh0kKCUgghhBBCpIUEpRBCCCGESAsJSiGEEEIIkRYSlEIIIYQQIi0kKIUQQgghRFpIUAohhBBCiLSQoBRCCCGEEGkhQSmEEEIIIdJCglIIIYQQQqSFBKUQQgghhEgLCUohhBBCCJEWEpRCCCGEECItJCiFEEIIIURaSFAKIYQQQghLh/8PRKDerF9KyvcAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "nx.draw(scene._graph.graph, with_labels=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a2766d6c", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "ome-zarr (3.13.12)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/source/advanced/transforms/scene_2d_to_3d.ipynb b/docs/source/advanced/transforms/scene_2d_to_3d.ipynb new file mode 100644 index 00000000..a5493afc --- /dev/null +++ b/docs/source/advanced/transforms/scene_2d_to_3d.ipynb @@ -0,0 +1,201 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "002e4aac", + "metadata": {}, + "source": [ + "# Transforms with changing dimensionality\n", + "\n", + "A particular and important kind of transforms,\n", + "are transforms that change the dimensionality of the data.\n", + "Common examples for this case are:\n", + "- 2D to 3D transforms, e.g. for aligning a 2D slice to a 3D volume\n", + "- 2D + channel to 2D transforms, e.g. for aligning a 2D slice with multiple channels to a 2D slice with a single channel\n", + "\n", + "The transform that expresses this change in dimensionality is the [`ProjectAxis` transform](https://ngff.openmicroscopy.org/specifications/dev/index.html#projectaxis).\n", + "\n", + "This tutorial demonstrates its usage for the case of a 2D to 3D transform,\n", + "where a 2D slice is aligned to a 3D volume." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ac4f951d", + "metadata": {}, + "outputs": [], + "source": [ + "from skimage import data\n", + "\n", + "from ome_zarr import OMEZarrImage, OMEZarrMultiscale, OMEZarrScene" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "ede90a69", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "c:\\Users\\johan\\Documents\\GitHub\\ome-zarr-py\\.venv\\Lib\\site-packages\\tqdm\\auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", + " from .autonotebook import tqdm as notebook_tqdm\n" + ] + }, + { + "data": { + "text/plain": [ + "(2, 60, 256, 256)" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "img = data.cells3d().transpose((1, 0, 2, 3))\n", + "img.shape" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "a9445304", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(256, 256)" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "some_slice = img[0, 30, :, :]\n", + "some_slice.shape" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "454e972b", + "metadata": {}, + "outputs": [], + "source": [ + "ngff_img = OMEZarrImage(\n", + " data=img,\n", + " axes=[\"c\", \"z\", \"y\", \"x\"],\n", + " scale={\"c\": 1, \"z\": 1, \"y\": 1, \"x\": 1},\n", + " name=\"cells3d\"\n", + ")\n", + "\n", + "ngff_ms = OMEZarrMultiscale(\n", + " image=ngff_img,\n", + ")\n", + "\n", + "slice_img = OMEZarrImage(\n", + " data=some_slice,\n", + " axes=[\"y\", \"x\"],\n", + " scale={\"y\": 1, \"x\": 1},\n", + " name=\"cells3d_slice\"\n", + ")\n", + "\n", + "slice_ms = OMEZarrMultiscale(\n", + " image=slice_img,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "a7b4469f", + "metadata": {}, + "outputs": [], + "source": [ + "transform_to_3d = {\n", + " \"type\": \"sequence\",\n", + " \"input\": {\"path\": \"cells3d_slice\", \"name\": \"physical\"},\n", + " \"output\": {\"path\": \"cells3d\", \"name\": \"physical\"},\n", + " \"transformations\": [\n", + " {\n", + " \"type\": \"projectAxis\",\n", + " \"createdOutputs\": [0, 1]\n", + " },\n", + " {\n", + " \"type\": \"translation\",\n", + " \"translation\": [0, 30, 0, 0]\n", + " }\n", + " ]\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "abde22ea", + "metadata": {}, + "outputs": [], + "source": [ + "scene = OMEZarrScene(\n", + " images=[ngff_ms, slice_ms],\n", + " coordinate_transformations=[transform_to_3d]\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "8d723932", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Writing images: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 2/2 [00:01<00:00, 1.98it/s]\n" + ] + } + ], + "source": [ + "scene.to_ome_zarr(\"scene_2d_to_3d.zarr\", overwrite=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "062391f5", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "ome-zarr (3.13.12)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From af27cb16cf3f978482ba739b1f00764daf48b7c4 Mon Sep 17 00:00:00 2001 From: Johannes Soltwedel <38459088+jo-mueller@users.noreply.github.com> Date: Fri, 24 Jul 2026 10:33:50 +0200 Subject: [PATCH 04/51] feat: write 0.6, not 0.6.dev4 --- .../transforms/add_transforms_to_multiscales.ipynb | 8 ++++---- ome_zarr/classes/image.py | 10 +++++----- tests/test_writer.py | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/source/advanced/transforms/add_transforms_to_multiscales.ipynb b/docs/source/advanced/transforms/add_transforms_to_multiscales.ipynb index 6737276e..bd6ed553 100644 --- a/docs/source/advanced/transforms/add_transforms_to_multiscales.ipynb +++ b/docs/source/advanced/transforms/add_transforms_to_multiscales.ipynb @@ -78,8 +78,8 @@ "\n", "$\n", "R = \\begin{bmatrix}\n", - "\\cos(45\u00b0) & -\\sin(45\u00b0) \\\\\n", - "\\sin(45\u00b0) & \\cos(45\u00b0)\n", + "\\cos(45°) & -\\sin(45°) \\\\\n", + "\\sin(45°) & \\cos(45°)\n", "\\end{bmatrix} = \\begin{bmatrix}\n", "\\cos(\\pi/4) & -\\sin(\\pi/4) \\\\\n", "\\sin(\\pi/4) & \\cos(\\pi/4)\n", @@ -157,7 +157,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "id": "2e7d870d", "metadata": {}, "outputs": [ @@ -175,7 +175,7 @@ "source": [ "ms.to_ome_zarr(\n", " \"multiscale_with_transforms.ome.zarr\",\n", - " version=\"0.6.dev4\", # specify the desired ome-zarr version\n", + " version=\"0.6\", # specify the desired ome-zarr version\n", " overwrite=True,\n", ")" ] diff --git a/ome_zarr/classes/image.py b/ome_zarr/classes/image.py index 799e5c3b..dae08539 100644 --- a/ome_zarr/classes/image.py +++ b/ome_zarr/classes/image.py @@ -343,7 +343,7 @@ def to_ome_zarr( self, group: zarr.Group | str, storage_options: list[dict[str, Any]] | dict[str, Any] | None = None, - version: Literal["0.6.dev4", "0.5", "0.4"] = "0.5", + version: Literal["0.6", "0.5", "0.4"] = "0.5", compute: bool = True, overwrite: bool = False, ) -> list: @@ -372,7 +372,7 @@ def to_ome_zarr( shutil.rmtree(group) fmt: Format | None = None - if version in {"0.5", "0.6", "0.6.dev4"}: + if version in {"0.5", "0.6", "0.6"}: fmt = FormatV05() elif version == "0.4": fmt = FormatV04() @@ -438,7 +438,7 @@ def to_ome_zarr( group.attrs["ome"] = metadata_dict - elif version == "0.6.dev4": + elif version == "0.6": metadata_dict = { "version": version, "multiscales": [ @@ -615,7 +615,7 @@ def images(self) -> list[OMEZarrImage]: def _write_additional_meta_data( self, group: zarr.Group, - version: Literal["0.6.dev4", "0.5", "0.4"] = "0.5", + version: Literal["0.6", "0.5", "0.4"] = "0.5", storage_options: list[dict[str, Any]] | dict[str, Any] | None = None, compute: bool = True, overwrite: bool = False, @@ -840,7 +840,7 @@ def __init__( def _write_additional_meta_data( self, group: zarr.Group, - version: Literal["0.6.dev4", "0.5", "0.4"] = "0.5", + version: Literal["0.6", "0.5", "0.4"] = "0.5", storage_options: list[dict[str, Any]] | dict[str, Any] | None = None, compute: bool = True, overwrite: bool = False, diff --git a/tests/test_writer.py b/tests/test_writer.py index 18ef1ca1..98a6ca9c 100644 --- a/tests/test_writer.py +++ b/tests/test_writer.py @@ -188,7 +188,7 @@ def test_additional_transforms(self): ) ms.to_ome_zarr( zarr.open(self.path / "test_transforms.zarr", mode="w"), - version="0.6.dev4", + version="0.6", overwrite=True, ) @@ -207,7 +207,7 @@ def test_additional_transforms(self): ) @pytest.mark.parametrize( - "version", ("0.4", "0.5", "0.6.dev4"), ids=["V04", "V05", "V06"] + "version", ("0.4", "0.5", "0.6"), ids=["V04", "V05", "V06"] ) def test_image_class_versions(self, version): from ome_zarr_models.v06.multiscales import Multiscale as Multiscale_V06 From 1f36579975651ccdad4a848168c4f5772f6b0010 Mon Sep 17 00:00:00 2001 From: Johannes Soltwedel <38459088+jo-mueller@users.noreply.github.com> Date: Fri, 24 Jul 2026 10:34:01 +0200 Subject: [PATCH 05/51] feat write 0.6 --- ome_zarr/classes/image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ome_zarr/classes/image.py b/ome_zarr/classes/image.py index dae08539..2a5cedba 100644 --- a/ome_zarr/classes/image.py +++ b/ome_zarr/classes/image.py @@ -1161,7 +1161,7 @@ def image_label(self, value: Label | dict[str, Any] | None): def _write_additional_meta_data( self, group: zarr.Group, - version: Literal["0.6.dev4", "0.5", "0.4"] = "0.5", + version: Literal["0.6", "0.5", "0.4"] = "0.5", storage_options: list[dict[str, Any]] | dict[str, Any] | None = None, compute: bool = True, overwrite: bool = False, From 82e46a567d59a5ac0466c173bc7592cb9ff05812 Mon Sep 17 00:00:00 2001 From: Johannes Soltwedel <38459088+jo-mueller@users.noreply.github.com> Date: Fri, 24 Jul 2026 10:34:28 +0200 Subject: [PATCH 06/51] feat: add missing version checks in additional metadata --- ome_zarr/classes/image.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/ome_zarr/classes/image.py b/ome_zarr/classes/image.py index 2a5cedba..73733305 100644 --- a/ome_zarr/classes/image.py +++ b/ome_zarr/classes/image.py @@ -853,13 +853,17 @@ def _write_additional_meta_data( if self._omero and isinstance(self._omero, Omero): omero_dict = _recursive_pop_nones(self._omero.model_dump(by_alias=True)) + # in 0.4, omero metadata goes in group attrs if version == "0.4": group.attrs["omero"] = omero_dict - elif version == "0.5": + + # in >=0.5, omero metadata goes in the ome attr + elif version == "0.5" or version.startswith("0.6"): if "ome" not in group.attrs: raise ValueError("OME-Zarr attributes not found in group") ome = cast(dict, group.attrs["ome"]) - omero_dict["version"] = version + if version == "0.5": + omero_dict["version"] = version ome["omero"] = omero_dict group.attrs["ome"] = ome @@ -897,7 +901,7 @@ def _write_additional_meta_data( # Update labels list in metadata if version == "0.4": label_group.attrs["labels"] = list_of_labels - elif version == "0.5": + elif version == "0.5" or version.startswith("0.6"): label_group.attrs["ome"] = { "version": version, "labels": list_of_labels, @@ -1031,7 +1035,7 @@ def _read_additional_metadata( if version in ("0.1", "0.2", "0.3", "0.4") and "omero" in group.attrs: omero_dict = cast(dict[str, Any] | None, group.attrs.get("omero", None)) - elif version == "0.5": + elif version == "0.5" or version.startswith("0.6"): ome_attrs = cast(dict[str, Any], group.attrs.get("ome", {})) if "omero" in ome_attrs: omero_dict = cast(dict[str, Any] | None, ome_attrs.get("omero", None)) @@ -1050,7 +1054,7 @@ def _read_additional_metadata( list_of_labels = ( cast(list[str], labels_json) if isinstance(labels_json, list) else [] ) - elif version == "0.5" and "labels" in group: + elif (version == "0.5" or version.startswith("0.6")) and "labels" in group: labels_ome_attrs = cast( dict[str, Any], group["labels"].attrs.get("ome", {}) ) @@ -1173,7 +1177,7 @@ def _write_additional_meta_data( group.attrs["image-label"] = _recursive_pop_nones( self._image_label.model_dump(by_alias=True) ) - elif version == "0.5": + elif version == "0.5" or version.startswith("0.6"): ome = cast(dict, group.attrs.get("ome", {})) ome["image-label"] = _recursive_pop_nones( self._image_label.model_dump(by_alias=True) @@ -1198,7 +1202,7 @@ def _read_additional_metadata( image_label_dict = cast( dict[str, Any] | None, group.attrs.get("image-label", None) ) - elif version == "0.5": + elif version == "0.5" or version.startswith("0.6"): ome_attrs = cast(dict[str, Any], group.attrs.get("ome", {})) if "image-label" in ome_attrs: image_label_dict = cast( From e0e013570fc8ca0333deca23d0459617ad9c5eb0 Mon Sep 17 00:00:00 2001 From: Johannes Soltwedel <38459088+jo-mueller@users.noreply.github.com> Date: Fri, 24 Jul 2026 10:34:40 +0200 Subject: [PATCH 07/51] tests: add 0.6 to tested versions in writer --- tests/test_writer.py | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/tests/test_writer.py b/tests/test_writer.py index 98a6ca9c..ab4429ad 100644 --- a/tests/test_writer.py +++ b/tests/test_writer.py @@ -15,6 +15,7 @@ from ome_zarr_models.v04 import Well as Models04Well from ome_zarr_models.v05.hcs import HCS as Models05HCS from ome_zarr_models.v05.image import Image as Models05Image +from ome_zarr_models.v06.image import Image as Models06Image from ome_zarr_models.v05.well import Well as Models05Well from skimage.data import binary_blobs from zarr.abc.codec import BytesBytesCodec @@ -297,12 +298,15 @@ def test_image_class_bad_args(self): multiscales.to_ome_zarr(self.path / "test_bad_args.zarr", version="0.5.5") @pytest.mark.parametrize("storage_options_list", [True, False]) + @pytest.mark.parametrize( + "version", ["0.4", "0.5", "0.6"], + ids=["V04", "V05", "V06"], + ) def test_image_class_writer( - self, shape, format_version_all, array_constructor, storage_options_list + self, shape, version, array_constructor, storage_options_list ): - version = format_version_all() - if version.version == "0.5": + if version.startswith(("0.5", "0.6")): grp_path = self.path_v3 / "test" else: grp_path = self.path / "test" @@ -361,7 +365,7 @@ def test_image_class_writer( # write image and labels to disk image_multiscales.to_ome_zarr( group=str(grp_path), - version=version.version, + version=version, storage_options=storage_options, overwrite=True, ) @@ -371,15 +375,13 @@ def test_image_class_writer( node_metadata = out.attrs if "ome" in node_metadata: node_metadata = node_metadata["ome"] + + # multiscales and omero data must be present by default assert "multiscales" in node_metadata + assert "omero" in node_metadata + paths = [d["path"] for d in node_metadata["multiscales"][0]["datasets"]] node_data = [da.from_zarr(grp_path / path) for path in paths] - if version.version in ("0.1", "0.2"): - # v0.1 and v0.2 MUST be 5D - assert node_data[0].ndim == 5 - else: - assert node_data[0].shape == shape - print("node.metadata", node_metadata) # check written coordinatetransormations match relative factors between array sizes for level, nd_array in enumerate(node_data): @@ -434,7 +436,7 @@ def test_image_class_writer( # Verify labels data label_group = zarr.open(f"{grp_path}/labels", mode="r") label_group_attrs = label_group.attrs - if version.version == "0.5": + if version == "0.5" or version.startswith("0.6"): label_group_attrs = label_group_attrs["ome"] assert "labels" in label_group_attrs assert labels_name in label_group_attrs["labels"] @@ -444,12 +446,6 @@ def test_image_class_writer( assert labels_name in list(image.labels.keys()) - if version.version == "0.4": - # Validate with ome-zarr-models-py: only supports v0.4 - Models04Image.from_zarr(out) - elif version.version == "0.5": - Models05Image.from_zarr(out) - # verify omero and image-labels metadata if "c" in axes: assert image.omero is not None From 018e9421293b3dac1a6edbe10a06675890febbfa Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 24 Jul 2026 08:37:16 +0000 Subject: [PATCH 08/51] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../advanced/transforms/add_transforms_to_multiscales.ipynb | 4 ++-- ome_zarr/classes/image.py | 2 +- tests/test_writer.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/source/advanced/transforms/add_transforms_to_multiscales.ipynb b/docs/source/advanced/transforms/add_transforms_to_multiscales.ipynb index bd6ed553..f7b503fd 100644 --- a/docs/source/advanced/transforms/add_transforms_to_multiscales.ipynb +++ b/docs/source/advanced/transforms/add_transforms_to_multiscales.ipynb @@ -78,8 +78,8 @@ "\n", "$\n", "R = \\begin{bmatrix}\n", - "\\cos(45°) & -\\sin(45°) \\\\\n", - "\\sin(45°) & \\cos(45°)\n", + "\\cos(45\u00b0) & -\\sin(45\u00b0) \\\\\n", + "\\sin(45\u00b0) & \\cos(45\u00b0)\n", "\\end{bmatrix} = \\begin{bmatrix}\n", "\\cos(\\pi/4) & -\\sin(\\pi/4) \\\\\n", "\\sin(\\pi/4) & \\cos(\\pi/4)\n", diff --git a/ome_zarr/classes/image.py b/ome_zarr/classes/image.py index 73733305..2832b793 100644 --- a/ome_zarr/classes/image.py +++ b/ome_zarr/classes/image.py @@ -372,7 +372,7 @@ def to_ome_zarr( shutil.rmtree(group) fmt: Format | None = None - if version in {"0.5", "0.6", "0.6"}: + if version in {"0.5", "0.6"}: fmt = FormatV05() elif version == "0.4": fmt = FormatV04() diff --git a/tests/test_writer.py b/tests/test_writer.py index ab4429ad..693bad98 100644 --- a/tests/test_writer.py +++ b/tests/test_writer.py @@ -15,7 +15,6 @@ from ome_zarr_models.v04 import Well as Models04Well from ome_zarr_models.v05.hcs import HCS as Models05HCS from ome_zarr_models.v05.image import Image as Models05Image -from ome_zarr_models.v06.image import Image as Models06Image from ome_zarr_models.v05.well import Well as Models05Well from skimage.data import binary_blobs from zarr.abc.codec import BytesBytesCodec @@ -299,7 +298,8 @@ def test_image_class_bad_args(self): @pytest.mark.parametrize("storage_options_list", [True, False]) @pytest.mark.parametrize( - "version", ["0.4", "0.5", "0.6"], + "version", + ["0.4", "0.5", "0.6"], ids=["V04", "V05", "V06"], ) def test_image_class_writer( From 538541a1c9d9407f95a879497e6d1b486d561ed7 Mon Sep 17 00:00:00 2001 From: Johannes Soltwedel <38459088+jo-mueller@users.noreply.github.com> Date: Fri, 24 Jul 2026 10:52:35 +0200 Subject: [PATCH 09/51] tests: pull out scale transforms correctly irrespective of version --- tests/test_writer.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/tests/test_writer.py b/tests/test_writer.py index 693bad98..5347f3fd 100644 --- a/tests/test_writer.py +++ b/tests/test_writer.py @@ -385,15 +385,22 @@ def test_image_class_writer( # check written coordinatetransormations match relative factors between array sizes for level, nd_array in enumerate(node_data): + ds = node_metadata["multiscales"][0]["datasets"][level] if level == 0: # check first written scale values explicitly match those in TRANSFORMATIONS for d in axes: - assert ( - node_metadata["multiscales"][0]["datasets"][level][ - "coordinateTransformations" - ][0]["scale"][axes.index(d)] - == TRANSFORMATIONS[0][0]["scale"][axes.index(d)] - ) + if version.startswith(("0.4", "0.5")): + tf = ds["coordinateTransformations"][0] + assert ( + tf["scale"][axes.index(d)] + == TRANSFORMATIONS[0][0]["scale"][axes.index(d)] + ) + elif version.startswith("0.6"): + tf = ds["coordinateTransformations"][0]["transformations"][0] + assert ( + tf["scale"][axes.index(d)] + == TRANSFORMATIONS[0][0]["scale"][axes.index(d)] + ) continue # first calculate relative factors between this and previous level @@ -418,9 +425,10 @@ def test_image_class_writer( assert relative_factors["z"] == 1.0 # retrieve written scale factors from metadata and check they match expected - cts = node_metadata["multiscales"][0]["datasets"][level][ - "coordinateTransformations" - ] + if version.startswith(("0.4", "0.5")): + cts = ds["coordinateTransformations"] + elif version.startswith("0.6"): + cts = ds["coordinateTransformations"][0]["transformations"] assert len(cts) == 2 transf = cts[0] assert transf["type"] == "scale" From 935842c4e9a46921f1bb4c5c45b60504f1eaaf57 Mon Sep 17 00:00:00 2001 From: Johannes Soltwedel <38459088+jo-mueller@users.noreply.github.com> Date: Thu, 30 Jul 2026 13:21:12 +0200 Subject: [PATCH 10/51] feat: default to 0.6 for writing --- docs/source/basic/write_image.ipynb | 8 ++++---- ome_zarr/classes/image.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/source/basic/write_image.ipynb b/docs/source/basic/write_image.ipynb index 0327fa61..99b58b74 100644 --- a/docs/source/basic/write_image.ipynb +++ b/docs/source/basic/write_image.ipynb @@ -57,14 +57,14 @@ "As a last step, we write the multiscale image to disk using the `to_ome_zarr` method, which will create a valid OME-ZARR file that can be read by any OME-ZARR compatible viewer.\n", "\n", "```{hint}\n", - "The demonstrated writer method below defaults to writing OME-ZARR version `0.5`,\n", - "but also supports writing OME-ZARR version `0.6.dev4` and `0.4`.\n", + "The demonstrated writer method below defaults to writing OME-ZARR version `0.6`.\n", + "Writing OME-ZARR version `0.5` and `0.4` needs to be explicitly specified.\n", "```" ] }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "ce122c48", "metadata": {}, "outputs": [ @@ -95,7 +95,7 @@ " channel_colors=[\"00FFFF\", \"FF00FF\"], # optional\n", " contrast_limits=[(0, 255), (0, 255)] # optional\n", " )\n", - "multiscales.to_ome_zarr(\"test_ngff.ome.zarr\", version=\"0.5\")" + "multiscales.to_ome_zarr(\"test_ngff.ome.zarr\", version=\"0.6\")" ] }, { diff --git a/ome_zarr/classes/image.py b/ome_zarr/classes/image.py index 2832b793..0bf9ef51 100644 --- a/ome_zarr/classes/image.py +++ b/ome_zarr/classes/image.py @@ -343,7 +343,7 @@ def to_ome_zarr( self, group: zarr.Group | str, storage_options: list[dict[str, Any]] | dict[str, Any] | None = None, - version: Literal["0.6", "0.5", "0.4"] = "0.5", + version: Literal["0.6", "0.5", "0.4"] = "0.6", compute: bool = True, overwrite: bool = False, ) -> list: From a7d097c91b01bf36463ede574de1d4a76d01f58e Mon Sep 17 00:00:00 2001 From: Johannes Soltwedel <38459088+jo-mueller@users.noreply.github.com> Date: Thu, 30 Jul 2026 14:39:40 +0200 Subject: [PATCH 11/51] feat: add formatV06 class --- ome_zarr/format.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/ome_zarr/format.py b/ome_zarr/format.py index 3b2c33e7..7878a8c9 100644 --- a/ome_zarr/format.py +++ b/ome_zarr/format.py @@ -383,5 +383,17 @@ def chunk_key_encoding(self) -> dict[str, str]: # this is default for Zarr v3. Could return None? return {"name": "default", "separator": "/"} +class FormatV06(FormatV05): + """ + Changelog: added FormatV06 (June 2025): writing not supported yet + """ + + @property + def version(self) -> str: + return "0.6" + + @property + def zarr_format(self) -> int: + return 3 -CurrentFormat = FormatV05 +CurrentFormat = FormatV06 From 2a93d4f39d96c6345a94009f15915b7fc949792b Mon Sep 17 00:00:00 2001 From: Johannes Soltwedel <38459088+jo-mueller@users.noreply.github.com> Date: Thu, 30 Jul 2026 14:39:53 +0200 Subject: [PATCH 12/51] tests: add checks for what version is encountered --- tests/test_writer.py | 173 ++++++++++++++++++++++++++++++------------- 1 file changed, 123 insertions(+), 50 deletions(-) diff --git a/tests/test_writer.py b/tests/test_writer.py index 5347f3fd..c0b62826 100644 --- a/tests/test_writer.py +++ b/tests/test_writer.py @@ -16,6 +16,9 @@ from ome_zarr_models.v05.hcs import HCS as Models05HCS from ome_zarr_models.v05.image import Image as Models05Image from ome_zarr_models.v05.well import Well as Models05Well +from ome_zarr_models.v06.image import Image as Models06Image +from ome_zarr_models.v06.hcs import HCS as Models06HCS +from ome_zarr_models.v06.well import Well as Models06Well from skimage.data import binary_blobs from zarr.abc.codec import BytesBytesCodec from zarr.codecs import BloscCodec @@ -31,6 +34,7 @@ FormatV03, FormatV04, FormatV05, + FormatV06, format_from_version, ) from ome_zarr.scale import _build_pyramid @@ -58,6 +62,7 @@ FORMAT_VERSIONS = [ pytest.param(FormatV04, id="V04"), pytest.param(FormatV05, id="V05"), + pytest.param(FormatV06, id="V06") ] ARRAY_CONSTRUCTORS = [np.array, da.from_array] @@ -94,11 +99,11 @@ def _make_storage_options(fmt, shape, axes): ) options = { "chunks": (16, 16), - "shards": (32, 32) if fmt.version == "0.5" else None, + "shards": (32, 32) if fmt.version in ("0.5", "0.6") else None, "compressors": compressor, - "serializer": BytesCodec(endian="little") if fmt.version == "0.5" else None, + "serializer": BytesCodec(endian="little") if fmt.version in ("0.5", "0.6") else None, "fill_value": 0, - "dimension_names": list(axes) if fmt.version == "0.5" else None, + "dimension_names": list(axes) if fmt.version in ("0.5", "0.6") else None, "order": "C", } return {k: v for k, v in options.items() if v is not None} @@ -465,7 +470,7 @@ def test_writer( ): fmt = format_version_all() - if fmt.version == "0.5": + if fmt.version in ("0.5", "0.6"): grp_path = self.path_v3 / "test" else: grp_path = self.path / "test" @@ -497,7 +502,7 @@ def test_writer( ) storage_options = _make_storage_options(fmt, shape, axes) - if fmt.version != "0.5": + if fmt.version not in ("0.5", "0.6"): chunks = [(128, 128), (50, 50), (25, 25), (25, 25), (25, 25), (25, 25)] if storage_options_list: @@ -536,16 +541,30 @@ def test_writer( else: assert node_data[0].shape == shape - for ax in node_metadata["multiscales"][0].get("axes"): + if fmt.version == "0.4": + axes_md = node_metadata["multiscales"][0].get("axes") + elif fmt.version == "0.5": + axes_md = node_metadata["multiscales"][0].get("axes") + elif fmt.version == "0.6": + axes_md = node_metadata["multiscales"][0]["coordinateSystems"][0].get( + "axes" + ) + for ax in axes_md: if ax["name"] in axes_units: assert ax.get("unit") == axes_units[ax["name"]] print("node.metadata", node_metadata) if fmt.version not in ("0.1", "0.2", "0.3"): - cts = [ - d["coordinateTransformations"] - for d in node_metadata["multiscales"][0]["datasets"] - ] + if fmt.version in ("0.4", "0.5"): + cts = [ + d["coordinateTransformations"] + for d in node_metadata["multiscales"][0]["datasets"] + ] + elif fmt.version == "0.6": + cts = [ + d["coordinateTransformations"][0]["transformations"] + for d in node_metadata["multiscales"][0]["datasets"] + ] for transf, expected in zip(cts, transformations): assert transf == expected assert len(cts) == len(node_data) @@ -562,6 +581,8 @@ def test_writer( Models04Image.from_zarr(out) elif fmt.version == "0.5": Models05Image.from_zarr(out) + elif fmt.version == "0.6": + Models06Image.from_zarr(out) @pytest.mark.parametrize( "format_version", @@ -581,7 +602,7 @@ def test_writer_with_scale( """ version = format_version() - if version.version == "0.5": + if version.version in ("0.5", "0.6"): grp_path = self.path_v3 / "test" else: grp_path = self.path / "test" @@ -633,6 +654,8 @@ def test_writer_with_scale( Models04Image.from_zarr(out) elif version.version == "0.5": Models05Image.from_zarr(out) + elif version.version == "0.6": + Models06Image.from_zarr(out) def test_mix_zarr_formats(self): # check group zarr v2 and v3 matches fmt @@ -698,10 +721,21 @@ def test_write_image_current(self, array_constructor, zarr_format): node_metadata = out.attrs if "ome" in node_metadata: node_metadata = node_metadata["ome"] - cts = [ - d["coordinateTransformations"] - for d in node_metadata["multiscales"][0]["datasets"] - ] + version = node_metadata.get("version") + assert version in ("0.5", "0.6") + else: + version = "0.4" + + if version in ("0.4", "0.5"): + cts = [ + d["coordinateTransformations"] + for d in node_metadata["multiscales"][0]["datasets"] + ] + elif version == "0.6": + cts = [ + d["coordinateTransformations"][0]["transformations"] + for d in node_metadata["multiscales"][0]["datasets"] + ] for level, transfs in enumerate(cts): assert len(transfs) == 2 assert transfs[0]["type"] == "scale" @@ -794,15 +828,25 @@ def test_write_image_dask(self, read_from_zarr, compute, zarr_format): node_metadata = out.attrs if "ome" in node_metadata: node_metadata = node_metadata["ome"] + version = node_metadata.get("version", "0.6") + + else: + version = node_metadata.get("version", "0.4") paths = [d["path"] for d in node_metadata["multiscales"][0]["datasets"]] image_node_data = [da.from_zarr(out[path]) for path in paths] first_chunk = [c[0] for c in image_node_data[0].chunks] assert tuple(first_chunk) == _retuple(chunks, image_node_data[0].shape) - cts = [ - d["coordinateTransformations"] - for d in node_metadata["multiscales"][0]["datasets"] - ] + if version in ("0.4", "0.5"): + cts = [ + d["coordinateTransformations"] + for d in node_metadata["multiscales"][0]["datasets"] + ] + else: + cts = [ + d["coordinateTransformations"][0]["transformations"] + for d in node_metadata["multiscales"][0]["datasets"] + ] for level, transfs in enumerate(cts): assert len(transfs) == 2 assert transfs[0]["type"] == "scale" @@ -851,6 +895,8 @@ def test_write_image_dask(self, read_from_zarr, compute, zarr_format): Models04Image.from_zarr(group) elif fmt.version == "0.5": Models05Image.from_zarr(group) + elif fmt.version == "0.6": + Models06Image.from_zarr(group) def test_write_image_scalar_chunks(self): """ @@ -874,6 +920,7 @@ def test_write_image_scalar_chunks(self): ( pytest.param(FormatV04, id="V04"), pytest.param(FormatV05, id="V05"), + pytest.param(FormatV06, id="V06") ), ) def test_write_image_compressed(self, array_constructor, format_version): @@ -931,17 +978,20 @@ def test_write_image_compressed(self, array_constructor, format_version): "shuffle": Blosc.SHUFFLE, "blocksize": 0, } - assert format_version().version in ("0.4", "0.5") + assert format_version().version in ("0.4", "0.5", "0.6") if format_version().version == "0.4": Models04Image.from_zarr(group) elif format_version().version == "0.5": Models05Image.from_zarr(group) + elif format_version().version == "0.6": + Models06Image.from_zarr(group) @pytest.mark.parametrize( "format_version", ( pytest.param(FormatV04, id="V04"), pytest.param(FormatV05, id="V05"), + pytest.param(FormatV06, id="V06"), ), ) def test_default_compression(self, array_constructor, format_version): @@ -1126,19 +1176,25 @@ def initdir(self, tmpdir): self.path_v3 = self.path / "v3" self.root_v3 = zarr.open_group(self.path_v3, mode="w", zarr_format=3) - @pytest.mark.parametrize("fmt", (FormatV04(), FormatV05())) + @pytest.mark.parametrize( + "fmt", ( + FormatV04(), + FormatV05(), + FormatV06() + ) + ) def test_multi_levels_transformations(self, fmt): datasets = [] for level, transf in enumerate(TRANSFORMATIONS): datasets.append({"path": str(level), "coordinateTransformations": transf}) - if fmt.version == "0.5": + if fmt.version in ("0.5", "0.6"): path = self.path_v3 else: path = self.path write_multiscales_metadata(str(path), datasets, axes="tczyx", fmt=fmt) # we want to be sure this is zarr v2 / v3 attrs = zarr.open_group(path).attrs - if fmt.version == "0.5": + if fmt.version in ("0.5", "0.6"): attrs = attrs.get("ome") assert "version" in attrs json_text = (self.path_v3 / "zarr.json").read_text(encoding="utf-8") @@ -1160,8 +1216,10 @@ def test_multi_levels_transformations(self, fmt): out = zarr.open_group(path) if fmt.version == "0.4": Models04Image.from_zarr(out) - if fmt.version == "0.5": + elif fmt.version == "0.5": Models05Image.from_zarr(out) + elif fmt.version == "0.6": + Models06Image.from_zarr(out) @pytest.mark.parametrize( "axes", @@ -1415,9 +1473,12 @@ def test_minimal_plate(self, fmt): assert fmt.version in ("0.4", "0.5") if fmt.version == "0.4": Models04HCS.from_zarr(group) - elif fmt.version == "0.5": + elif fmt.version in ("0.5", "0.6"): # https://github.com/ome-zarr-models/ome-zarr-models-py/issues/218 - Models05HCS.from_zarr(group) + if fmt.version == "0.5": + Models05HCS.from_zarr(group) + elif fmt.version == "0.6": + Models06HCS.from_zarr(group) @pytest.mark.parametrize("fmt", (FormatV04(), FormatV05())) def test_12wells_plate(self, fmt): @@ -1480,6 +1541,8 @@ def test_12wells_plate(self, fmt): Models04HCS.from_zarr(group) elif fmt.version == "0.5": Models05HCS.from_zarr(group) + elif fmt.version == "0.6": + Models06HCS.from_zarr(group) @pytest.mark.parametrize("fmt", (FormatV04(), FormatV05())) def test_sparse_plate(self, fmt): @@ -1524,6 +1587,8 @@ def test_sparse_plate(self, fmt): Models04HCS.from_zarr(group) elif fmt.version == "0.5": Models05HCS.from_zarr(group) + elif fmt.version == "0.6": + Models06HCS.from_zarr(group) def test_plate_name(self): # We don't need to test v04 and v05 for all tests since @@ -1738,14 +1803,14 @@ def initdir(self, tmpdir): @pytest.mark.parametrize("fmt", (FormatV04(), FormatV05())) @pytest.mark.parametrize("images", (["0"], [{"path": "0"}])) def test_minimal_well(self, images, fmt): - if fmt.version == "0.5": + if fmt.version in ("0.5", "0.6"): group = self.root_v3 else: group = self.root write_well_metadata(group, images) # we want to be sure this is zarr v2 / v3, so we load json manually too attrs = group.attrs - if fmt.version == "0.5": + if fmt.version in ("0.5", "0.6"): attrs = attrs.get("ome") assert attrs["version"] == fmt.version json_text = (self.path_v3 / "zarr.json").read_text(encoding="utf-8") @@ -1761,6 +1826,8 @@ def test_minimal_well(self, images, fmt): Models04Well.from_zarr(group) elif fmt.version == "0.5": Models05Well.from_zarr(group) + elif fmt.version == "0.6": + Models06Well.from_zarr(group) @pytest.mark.parametrize( "images", @@ -1902,14 +1969,14 @@ def verify_label_data( # Verify label metadata label_root = zarr.open(f"{img_path}/labels", mode="r") label_attrs = label_root.attrs - if fmt.version == "0.5": + if fmt.version in ("0.5", "0.6"): label_attrs = label_attrs["ome"] assert "labels" in label_attrs assert label_name in label_attrs["labels"] label_group = zarr.open(f"{img_path}/labels/{label_name}", mode="r") imglabel_attrs = label_group.attrs - if fmt.version == "0.5": + if fmt.version in ("0.5", "0.6"): imglabel_attrs = imglabel_attrs["ome"] assert imglabel_attrs["version"] == fmt.version else: @@ -1932,7 +1999,7 @@ def test_write_labels( ): fmt = format_version_all() - if fmt.version == "0.5": + if fmt.version in ("0.5", "0.6"): img_path = self.path_v3 group = self.root_v3 else: @@ -2000,7 +2067,7 @@ def test_write_labels_with_storage_options( array_constructor, ): fmt = format_version_all() - if fmt.version == "0.5": + if fmt.version in ("0.5", "0.6"): group = self.root_v3 else: group = self.root @@ -2060,7 +2127,7 @@ def test_write_labels_with_storage_options( level0.chunks == expected_chunks ), f"Expected chunks {expected_chunks}, got {level0.chunks}" - if USE_DASK_ARRAY_KWARGS and fmt.version == "0.5" and hasattr(level0, "shards"): + if USE_DASK_ARRAY_KWARGS and fmt.version in ("0.5", "0.6") and hasattr(level0, "shards"): expected_shards = _retuple(storage_options["shards"], level0.shape) assert ( level0.shards == expected_shards @@ -2069,7 +2136,7 @@ def test_write_labels_with_storage_options( assert level0.fill_value == 0 if level0.compressors: - if fmt.version == "0.5": + if fmt.version in ("0.5", "0.6"): if USE_DASK_ARRAY_KWARGS: assert _codec_value(level0.compressors[0].cname) == "zstd" else: @@ -2078,7 +2145,7 @@ def test_write_labels_with_storage_options( assert level0.compressors[0].cname == "zstd" if USE_DASK_ARRAY_KWARGS: assert level0.compressors[0].clevel == 3 - if fmt.version == "0.5" and hasattr(level0, "serializer"): + if fmt.version in ("0.5", "0.6") and hasattr(level0, "serializer"): assert ( _codec_value(level0.metadata.codecs[0].index_codecs[0].endian) == "little" @@ -2086,7 +2153,7 @@ def test_write_labels_with_storage_options( else: assert level0.compressors[0].to_dict()["configuration"]["level"] == 0 - if fmt.version == "0.5" and hasattr(level0, "dimension_names"): + if fmt.version in ("0.5", "0.6") and hasattr(level0, "dimension_names"): assert level0.dimension_names == tuple(axes) if fmt.version == "0.4": @@ -2096,7 +2163,7 @@ def test_write_multiscale_labels( self, shape, format_version_all, array_constructor ): fmt = format_version_all() - if fmt.version == "0.5": + if fmt.version in ("0.5", "0.6"): img_path = self.path_v3 group = self.root_v3 else: @@ -2166,7 +2233,7 @@ def test_write_multiscale_labels_storage_options( self, shape, format_version_all, array_constructor ): fmt = format_version_all() - if fmt.version == "0.5": + if fmt.version in ("0.5", "0.6"): img_path = self.path_v3 group = self.root_v3 else: @@ -2249,7 +2316,7 @@ def test_write_multiscale_labels_storage_options( if ( USE_DASK_ARRAY_KWARGS - and fmt.version == "0.5" + and fmt.version in ("0.5", "0.6") and hasattr(level, "shards") ): expected_shards = _retuple(storage_options["shards"], level.shape) @@ -2262,7 +2329,7 @@ def test_write_multiscale_labels_storage_options( ), f"Level {level_idx}: Expected fill_value 0, got {level.fill_value}" if level.compressors: - if fmt.version == "0.5": + if fmt.version in ("0.5", "0.6"): if USE_DASK_ARRAY_KWARGS: assert _codec_value(level.compressors[0].cname) == "zstd" else: @@ -2271,7 +2338,7 @@ def test_write_multiscale_labels_storage_options( assert level.compressors[0].cname == "zstd" if USE_DASK_ARRAY_KWARGS: assert level.compressors[0].clevel == 3 - if fmt.version == "0.5" and hasattr(level, "serializer"): + if fmt.version in ("0.5", "0.6") and hasattr(level, "serializer"): assert ( _codec_value( level.metadata.codecs[0].index_codecs[0].endian @@ -2281,7 +2348,7 @@ def test_write_multiscale_labels_storage_options( else: assert level.compressors[0].to_dict()["configuration"]["level"] == 0 - if fmt.version == "0.5" and hasattr(level, "dimension_names"): + if fmt.version in ("0.5", "0.6") and hasattr(level, "dimension_names"): assert level.dimension_names == tuple( axes ), f"Level {level_idx}: Expected dimension names {tuple(axes)}, got {level.dimension_names}" @@ -2297,10 +2364,13 @@ def test_write_multiscale_labels_storage_options( @pytest.mark.parametrize( "fmt", - (pytest.param(FormatV04(), id="V04"), pytest.param(FormatV05(), id="V05")), + (pytest.param(FormatV04(), id="V04"), + pytest.param(FormatV05(), id="V05"), + pytest.param(FormatV06(), id="V06"), + ), ) def test_two_label_images(self, array_constructor, fmt): - if fmt.version == "0.5": + if fmt.version in ("0.5", "0.6"): img_path = self.path_v3 group = self.root_v3 else: @@ -2361,7 +2431,7 @@ def test_two_label_images(self, array_constructor, fmt): # Verify label metadata label_root = zarr.open(f"{img_path}/labels", mode="r") attrs = label_root.attrs - if fmt.version == "0.5": + if fmt.version in ("0.5", "0.6"): attrs = attrs["ome"] assert "labels" in attrs assert len(attrs["labels"]) == len(label_names) @@ -2369,12 +2439,15 @@ def test_two_label_images(self, array_constructor, fmt): @pytest.mark.parametrize( "fmt", - (pytest.param(FormatV04(), id="V04"), pytest.param(FormatV05(), id="V05")), + (pytest.param(FormatV04(), id="V04"), + pytest.param(FormatV05(), id="V05"), + pytest.param(FormatV06(), id="V06") + ), ) def write_labels_class_API(self, fmt): from ome_zarr import OMEZarrImage, OMEZarrMultiscale - if fmt.version == "0.5": + if fmt.version in ("0.5", "0.6"): img_path = self.path_v3 group = self.root_v3 else: @@ -2404,7 +2477,7 @@ def write_labels_class_API(self, fmt): # now check that the respective groups and metadata exist and is correct label_group = zarr.open(f"{img_path}/labels", mode="r") - if fmt.version == "0.5": + if fmt.version in ("0.5", "0.6"): label_attrs = label_group.attrs["ome"] elif fmt.version == "0.4": label_attrs = label_group.attrs @@ -2422,7 +2495,7 @@ def write_labels_class_API(self, fmt): # now check that we still have both labels in the metadata label_group = zarr.open(f"{img_path}/labels", mode="r") - if fmt.version == "0.5": + if fmt.version in ("0.5", "0.6"): label_attrs = label_group.attrs["ome"] elif fmt.version == "0.4": label_attrs = label_group.attrs @@ -2438,7 +2511,7 @@ def write_labels_class_API(self, fmt): # Now, only the third label should be present in the metadata and as a zarr group, # but the first and second labels should be gone label_group = zarr.open(f"{img_path}/labels", mode="r") - if fmt.version == "0.5": + if fmt.version in ("0.5", "0.6"): label_attrs = label_group.attrs["ome"] elif fmt.version == "0.4": label_attrs = label_group.attrs From 38a6cdd074d06c4f5f93b231810fced575819276 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 30 Jul 2026 12:40:10 +0000 Subject: [PATCH 13/51] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- ome_zarr/format.py | 2 ++ tests/test_writer.py | 42 +++++++++++++++++++++--------------------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/ome_zarr/format.py b/ome_zarr/format.py index 7878a8c9..7aecbb7b 100644 --- a/ome_zarr/format.py +++ b/ome_zarr/format.py @@ -383,6 +383,7 @@ def chunk_key_encoding(self) -> dict[str, str]: # this is default for Zarr v3. Could return None? return {"name": "default", "separator": "/"} + class FormatV06(FormatV05): """ Changelog: added FormatV06 (June 2025): writing not supported yet @@ -396,4 +397,5 @@ def version(self) -> str: def zarr_format(self) -> int: return 3 + CurrentFormat = FormatV06 diff --git a/tests/test_writer.py b/tests/test_writer.py index c0b62826..ba22dc98 100644 --- a/tests/test_writer.py +++ b/tests/test_writer.py @@ -16,8 +16,8 @@ from ome_zarr_models.v05.hcs import HCS as Models05HCS from ome_zarr_models.v05.image import Image as Models05Image from ome_zarr_models.v05.well import Well as Models05Well -from ome_zarr_models.v06.image import Image as Models06Image from ome_zarr_models.v06.hcs import HCS as Models06HCS +from ome_zarr_models.v06.image import Image as Models06Image from ome_zarr_models.v06.well import Well as Models06Well from skimage.data import binary_blobs from zarr.abc.codec import BytesBytesCodec @@ -62,7 +62,7 @@ FORMAT_VERSIONS = [ pytest.param(FormatV04, id="V04"), pytest.param(FormatV05, id="V05"), - pytest.param(FormatV06, id="V06") + pytest.param(FormatV06, id="V06"), ] ARRAY_CONSTRUCTORS = [np.array, da.from_array] @@ -101,7 +101,9 @@ def _make_storage_options(fmt, shape, axes): "chunks": (16, 16), "shards": (32, 32) if fmt.version in ("0.5", "0.6") else None, "compressors": compressor, - "serializer": BytesCodec(endian="little") if fmt.version in ("0.5", "0.6") else None, + "serializer": ( + BytesCodec(endian="little") if fmt.version in ("0.5", "0.6") else None + ), "fill_value": 0, "dimension_names": list(axes) if fmt.version in ("0.5", "0.6") else None, "order": "C", @@ -541,9 +543,7 @@ def test_writer( else: assert node_data[0].shape == shape - if fmt.version == "0.4": - axes_md = node_metadata["multiscales"][0].get("axes") - elif fmt.version == "0.5": + if fmt.version == "0.4" or fmt.version == "0.5": axes_md = node_metadata["multiscales"][0].get("axes") elif fmt.version == "0.6": axes_md = node_metadata["multiscales"][0]["coordinateSystems"][0].get( @@ -920,7 +920,7 @@ def test_write_image_scalar_chunks(self): ( pytest.param(FormatV04, id="V04"), pytest.param(FormatV05, id="V05"), - pytest.param(FormatV06, id="V06") + pytest.param(FormatV06, id="V06"), ), ) def test_write_image_compressed(self, array_constructor, format_version): @@ -1176,13 +1176,7 @@ def initdir(self, tmpdir): self.path_v3 = self.path / "v3" self.root_v3 = zarr.open_group(self.path_v3, mode="w", zarr_format=3) - @pytest.mark.parametrize( - "fmt", ( - FormatV04(), - FormatV05(), - FormatV06() - ) - ) + @pytest.mark.parametrize("fmt", (FormatV04(), FormatV05(), FormatV06())) def test_multi_levels_transformations(self, fmt): datasets = [] for level, transf in enumerate(TRANSFORMATIONS): @@ -2127,7 +2121,11 @@ def test_write_labels_with_storage_options( level0.chunks == expected_chunks ), f"Expected chunks {expected_chunks}, got {level0.chunks}" - if USE_DASK_ARRAY_KWARGS and fmt.version in ("0.5", "0.6") and hasattr(level0, "shards"): + if ( + USE_DASK_ARRAY_KWARGS + and fmt.version in ("0.5", "0.6") + and hasattr(level0, "shards") + ): expected_shards = _retuple(storage_options["shards"], level0.shape) assert ( level0.shards == expected_shards @@ -2364,9 +2362,10 @@ def test_write_multiscale_labels_storage_options( @pytest.mark.parametrize( "fmt", - (pytest.param(FormatV04(), id="V04"), - pytest.param(FormatV05(), id="V05"), - pytest.param(FormatV06(), id="V06"), + ( + pytest.param(FormatV04(), id="V04"), + pytest.param(FormatV05(), id="V05"), + pytest.param(FormatV06(), id="V06"), ), ) def test_two_label_images(self, array_constructor, fmt): @@ -2439,9 +2438,10 @@ def test_two_label_images(self, array_constructor, fmt): @pytest.mark.parametrize( "fmt", - (pytest.param(FormatV04(), id="V04"), - pytest.param(FormatV05(), id="V05"), - pytest.param(FormatV06(), id="V06") + ( + pytest.param(FormatV04(), id="V04"), + pytest.param(FormatV05(), id="V05"), + pytest.param(FormatV06(), id="V06"), ), ) def write_labels_class_API(self, fmt): From 59ad17d1cfc0e3e6f393c82ea64029d889453b64 Mon Sep 17 00:00:00 2001 From: Johannes Soltwedel <38459088+jo-mueller@users.noreply.github.com> Date: Fri, 31 Jul 2026 11:29:02 +0200 Subject: [PATCH 14/51] Revert "[pre-commit.ci] auto fixes from pre-commit.com hooks" This reverts commit e0a03cd4bd0c40813c089e031d69af47ae2fb352. --- ome_zarr/format.py | 2 -- tests/test_writer.py | 42 +++++++++++++++++++++--------------------- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/ome_zarr/format.py b/ome_zarr/format.py index 7aecbb7b..7878a8c9 100644 --- a/ome_zarr/format.py +++ b/ome_zarr/format.py @@ -383,7 +383,6 @@ def chunk_key_encoding(self) -> dict[str, str]: # this is default for Zarr v3. Could return None? return {"name": "default", "separator": "/"} - class FormatV06(FormatV05): """ Changelog: added FormatV06 (June 2025): writing not supported yet @@ -397,5 +396,4 @@ def version(self) -> str: def zarr_format(self) -> int: return 3 - CurrentFormat = FormatV06 diff --git a/tests/test_writer.py b/tests/test_writer.py index ba22dc98..c0b62826 100644 --- a/tests/test_writer.py +++ b/tests/test_writer.py @@ -16,8 +16,8 @@ from ome_zarr_models.v05.hcs import HCS as Models05HCS from ome_zarr_models.v05.image import Image as Models05Image from ome_zarr_models.v05.well import Well as Models05Well -from ome_zarr_models.v06.hcs import HCS as Models06HCS from ome_zarr_models.v06.image import Image as Models06Image +from ome_zarr_models.v06.hcs import HCS as Models06HCS from ome_zarr_models.v06.well import Well as Models06Well from skimage.data import binary_blobs from zarr.abc.codec import BytesBytesCodec @@ -62,7 +62,7 @@ FORMAT_VERSIONS = [ pytest.param(FormatV04, id="V04"), pytest.param(FormatV05, id="V05"), - pytest.param(FormatV06, id="V06"), + pytest.param(FormatV06, id="V06") ] ARRAY_CONSTRUCTORS = [np.array, da.from_array] @@ -101,9 +101,7 @@ def _make_storage_options(fmt, shape, axes): "chunks": (16, 16), "shards": (32, 32) if fmt.version in ("0.5", "0.6") else None, "compressors": compressor, - "serializer": ( - BytesCodec(endian="little") if fmt.version in ("0.5", "0.6") else None - ), + "serializer": BytesCodec(endian="little") if fmt.version in ("0.5", "0.6") else None, "fill_value": 0, "dimension_names": list(axes) if fmt.version in ("0.5", "0.6") else None, "order": "C", @@ -543,7 +541,9 @@ def test_writer( else: assert node_data[0].shape == shape - if fmt.version == "0.4" or fmt.version == "0.5": + if fmt.version == "0.4": + axes_md = node_metadata["multiscales"][0].get("axes") + elif fmt.version == "0.5": axes_md = node_metadata["multiscales"][0].get("axes") elif fmt.version == "0.6": axes_md = node_metadata["multiscales"][0]["coordinateSystems"][0].get( @@ -920,7 +920,7 @@ def test_write_image_scalar_chunks(self): ( pytest.param(FormatV04, id="V04"), pytest.param(FormatV05, id="V05"), - pytest.param(FormatV06, id="V06"), + pytest.param(FormatV06, id="V06") ), ) def test_write_image_compressed(self, array_constructor, format_version): @@ -1176,7 +1176,13 @@ def initdir(self, tmpdir): self.path_v3 = self.path / "v3" self.root_v3 = zarr.open_group(self.path_v3, mode="w", zarr_format=3) - @pytest.mark.parametrize("fmt", (FormatV04(), FormatV05(), FormatV06())) + @pytest.mark.parametrize( + "fmt", ( + FormatV04(), + FormatV05(), + FormatV06() + ) + ) def test_multi_levels_transformations(self, fmt): datasets = [] for level, transf in enumerate(TRANSFORMATIONS): @@ -2121,11 +2127,7 @@ def test_write_labels_with_storage_options( level0.chunks == expected_chunks ), f"Expected chunks {expected_chunks}, got {level0.chunks}" - if ( - USE_DASK_ARRAY_KWARGS - and fmt.version in ("0.5", "0.6") - and hasattr(level0, "shards") - ): + if USE_DASK_ARRAY_KWARGS and fmt.version in ("0.5", "0.6") and hasattr(level0, "shards"): expected_shards = _retuple(storage_options["shards"], level0.shape) assert ( level0.shards == expected_shards @@ -2362,10 +2364,9 @@ def test_write_multiscale_labels_storage_options( @pytest.mark.parametrize( "fmt", - ( - pytest.param(FormatV04(), id="V04"), - pytest.param(FormatV05(), id="V05"), - pytest.param(FormatV06(), id="V06"), + (pytest.param(FormatV04(), id="V04"), + pytest.param(FormatV05(), id="V05"), + pytest.param(FormatV06(), id="V06"), ), ) def test_two_label_images(self, array_constructor, fmt): @@ -2438,10 +2439,9 @@ def test_two_label_images(self, array_constructor, fmt): @pytest.mark.parametrize( "fmt", - ( - pytest.param(FormatV04(), id="V04"), - pytest.param(FormatV05(), id="V05"), - pytest.param(FormatV06(), id="V06"), + (pytest.param(FormatV04(), id="V04"), + pytest.param(FormatV05(), id="V05"), + pytest.param(FormatV06(), id="V06") ), ) def write_labels_class_API(self, fmt): From 53c275004326cf6d02a23d59b571c355d84f097b Mon Sep 17 00:00:00 2001 From: Johannes Soltwedel <38459088+jo-mueller@users.noreply.github.com> Date: Fri, 31 Jul 2026 11:29:06 +0200 Subject: [PATCH 15/51] Revert "tests: add checks for what version is encountered" This reverts commit 75b9402fc0bf318bb072663b068ebd25284dc789. --- tests/test_writer.py | 173 +++++++++++++------------------------------ 1 file changed, 50 insertions(+), 123 deletions(-) diff --git a/tests/test_writer.py b/tests/test_writer.py index c0b62826..5347f3fd 100644 --- a/tests/test_writer.py +++ b/tests/test_writer.py @@ -16,9 +16,6 @@ from ome_zarr_models.v05.hcs import HCS as Models05HCS from ome_zarr_models.v05.image import Image as Models05Image from ome_zarr_models.v05.well import Well as Models05Well -from ome_zarr_models.v06.image import Image as Models06Image -from ome_zarr_models.v06.hcs import HCS as Models06HCS -from ome_zarr_models.v06.well import Well as Models06Well from skimage.data import binary_blobs from zarr.abc.codec import BytesBytesCodec from zarr.codecs import BloscCodec @@ -34,7 +31,6 @@ FormatV03, FormatV04, FormatV05, - FormatV06, format_from_version, ) from ome_zarr.scale import _build_pyramid @@ -62,7 +58,6 @@ FORMAT_VERSIONS = [ pytest.param(FormatV04, id="V04"), pytest.param(FormatV05, id="V05"), - pytest.param(FormatV06, id="V06") ] ARRAY_CONSTRUCTORS = [np.array, da.from_array] @@ -99,11 +94,11 @@ def _make_storage_options(fmt, shape, axes): ) options = { "chunks": (16, 16), - "shards": (32, 32) if fmt.version in ("0.5", "0.6") else None, + "shards": (32, 32) if fmt.version == "0.5" else None, "compressors": compressor, - "serializer": BytesCodec(endian="little") if fmt.version in ("0.5", "0.6") else None, + "serializer": BytesCodec(endian="little") if fmt.version == "0.5" else None, "fill_value": 0, - "dimension_names": list(axes) if fmt.version in ("0.5", "0.6") else None, + "dimension_names": list(axes) if fmt.version == "0.5" else None, "order": "C", } return {k: v for k, v in options.items() if v is not None} @@ -470,7 +465,7 @@ def test_writer( ): fmt = format_version_all() - if fmt.version in ("0.5", "0.6"): + if fmt.version == "0.5": grp_path = self.path_v3 / "test" else: grp_path = self.path / "test" @@ -502,7 +497,7 @@ def test_writer( ) storage_options = _make_storage_options(fmt, shape, axes) - if fmt.version not in ("0.5", "0.6"): + if fmt.version != "0.5": chunks = [(128, 128), (50, 50), (25, 25), (25, 25), (25, 25), (25, 25)] if storage_options_list: @@ -541,30 +536,16 @@ def test_writer( else: assert node_data[0].shape == shape - if fmt.version == "0.4": - axes_md = node_metadata["multiscales"][0].get("axes") - elif fmt.version == "0.5": - axes_md = node_metadata["multiscales"][0].get("axes") - elif fmt.version == "0.6": - axes_md = node_metadata["multiscales"][0]["coordinateSystems"][0].get( - "axes" - ) - for ax in axes_md: + for ax in node_metadata["multiscales"][0].get("axes"): if ax["name"] in axes_units: assert ax.get("unit") == axes_units[ax["name"]] print("node.metadata", node_metadata) if fmt.version not in ("0.1", "0.2", "0.3"): - if fmt.version in ("0.4", "0.5"): - cts = [ - d["coordinateTransformations"] - for d in node_metadata["multiscales"][0]["datasets"] - ] - elif fmt.version == "0.6": - cts = [ - d["coordinateTransformations"][0]["transformations"] - for d in node_metadata["multiscales"][0]["datasets"] - ] + cts = [ + d["coordinateTransformations"] + for d in node_metadata["multiscales"][0]["datasets"] + ] for transf, expected in zip(cts, transformations): assert transf == expected assert len(cts) == len(node_data) @@ -581,8 +562,6 @@ def test_writer( Models04Image.from_zarr(out) elif fmt.version == "0.5": Models05Image.from_zarr(out) - elif fmt.version == "0.6": - Models06Image.from_zarr(out) @pytest.mark.parametrize( "format_version", @@ -602,7 +581,7 @@ def test_writer_with_scale( """ version = format_version() - if version.version in ("0.5", "0.6"): + if version.version == "0.5": grp_path = self.path_v3 / "test" else: grp_path = self.path / "test" @@ -654,8 +633,6 @@ def test_writer_with_scale( Models04Image.from_zarr(out) elif version.version == "0.5": Models05Image.from_zarr(out) - elif version.version == "0.6": - Models06Image.from_zarr(out) def test_mix_zarr_formats(self): # check group zarr v2 and v3 matches fmt @@ -721,21 +698,10 @@ def test_write_image_current(self, array_constructor, zarr_format): node_metadata = out.attrs if "ome" in node_metadata: node_metadata = node_metadata["ome"] - version = node_metadata.get("version") - assert version in ("0.5", "0.6") - else: - version = "0.4" - - if version in ("0.4", "0.5"): - cts = [ - d["coordinateTransformations"] - for d in node_metadata["multiscales"][0]["datasets"] - ] - elif version == "0.6": - cts = [ - d["coordinateTransformations"][0]["transformations"] - for d in node_metadata["multiscales"][0]["datasets"] - ] + cts = [ + d["coordinateTransformations"] + for d in node_metadata["multiscales"][0]["datasets"] + ] for level, transfs in enumerate(cts): assert len(transfs) == 2 assert transfs[0]["type"] == "scale" @@ -828,25 +794,15 @@ def test_write_image_dask(self, read_from_zarr, compute, zarr_format): node_metadata = out.attrs if "ome" in node_metadata: node_metadata = node_metadata["ome"] - version = node_metadata.get("version", "0.6") - - else: - version = node_metadata.get("version", "0.4") paths = [d["path"] for d in node_metadata["multiscales"][0]["datasets"]] image_node_data = [da.from_zarr(out[path]) for path in paths] first_chunk = [c[0] for c in image_node_data[0].chunks] assert tuple(first_chunk) == _retuple(chunks, image_node_data[0].shape) - if version in ("0.4", "0.5"): - cts = [ - d["coordinateTransformations"] - for d in node_metadata["multiscales"][0]["datasets"] - ] - else: - cts = [ - d["coordinateTransformations"][0]["transformations"] - for d in node_metadata["multiscales"][0]["datasets"] - ] + cts = [ + d["coordinateTransformations"] + for d in node_metadata["multiscales"][0]["datasets"] + ] for level, transfs in enumerate(cts): assert len(transfs) == 2 assert transfs[0]["type"] == "scale" @@ -895,8 +851,6 @@ def test_write_image_dask(self, read_from_zarr, compute, zarr_format): Models04Image.from_zarr(group) elif fmt.version == "0.5": Models05Image.from_zarr(group) - elif fmt.version == "0.6": - Models06Image.from_zarr(group) def test_write_image_scalar_chunks(self): """ @@ -920,7 +874,6 @@ def test_write_image_scalar_chunks(self): ( pytest.param(FormatV04, id="V04"), pytest.param(FormatV05, id="V05"), - pytest.param(FormatV06, id="V06") ), ) def test_write_image_compressed(self, array_constructor, format_version): @@ -978,20 +931,17 @@ def test_write_image_compressed(self, array_constructor, format_version): "shuffle": Blosc.SHUFFLE, "blocksize": 0, } - assert format_version().version in ("0.4", "0.5", "0.6") + assert format_version().version in ("0.4", "0.5") if format_version().version == "0.4": Models04Image.from_zarr(group) elif format_version().version == "0.5": Models05Image.from_zarr(group) - elif format_version().version == "0.6": - Models06Image.from_zarr(group) @pytest.mark.parametrize( "format_version", ( pytest.param(FormatV04, id="V04"), pytest.param(FormatV05, id="V05"), - pytest.param(FormatV06, id="V06"), ), ) def test_default_compression(self, array_constructor, format_version): @@ -1176,25 +1126,19 @@ def initdir(self, tmpdir): self.path_v3 = self.path / "v3" self.root_v3 = zarr.open_group(self.path_v3, mode="w", zarr_format=3) - @pytest.mark.parametrize( - "fmt", ( - FormatV04(), - FormatV05(), - FormatV06() - ) - ) + @pytest.mark.parametrize("fmt", (FormatV04(), FormatV05())) def test_multi_levels_transformations(self, fmt): datasets = [] for level, transf in enumerate(TRANSFORMATIONS): datasets.append({"path": str(level), "coordinateTransformations": transf}) - if fmt.version in ("0.5", "0.6"): + if fmt.version == "0.5": path = self.path_v3 else: path = self.path write_multiscales_metadata(str(path), datasets, axes="tczyx", fmt=fmt) # we want to be sure this is zarr v2 / v3 attrs = zarr.open_group(path).attrs - if fmt.version in ("0.5", "0.6"): + if fmt.version == "0.5": attrs = attrs.get("ome") assert "version" in attrs json_text = (self.path_v3 / "zarr.json").read_text(encoding="utf-8") @@ -1216,10 +1160,8 @@ def test_multi_levels_transformations(self, fmt): out = zarr.open_group(path) if fmt.version == "0.4": Models04Image.from_zarr(out) - elif fmt.version == "0.5": + if fmt.version == "0.5": Models05Image.from_zarr(out) - elif fmt.version == "0.6": - Models06Image.from_zarr(out) @pytest.mark.parametrize( "axes", @@ -1473,12 +1415,9 @@ def test_minimal_plate(self, fmt): assert fmt.version in ("0.4", "0.5") if fmt.version == "0.4": Models04HCS.from_zarr(group) - elif fmt.version in ("0.5", "0.6"): + elif fmt.version == "0.5": # https://github.com/ome-zarr-models/ome-zarr-models-py/issues/218 - if fmt.version == "0.5": - Models05HCS.from_zarr(group) - elif fmt.version == "0.6": - Models06HCS.from_zarr(group) + Models05HCS.from_zarr(group) @pytest.mark.parametrize("fmt", (FormatV04(), FormatV05())) def test_12wells_plate(self, fmt): @@ -1541,8 +1480,6 @@ def test_12wells_plate(self, fmt): Models04HCS.from_zarr(group) elif fmt.version == "0.5": Models05HCS.from_zarr(group) - elif fmt.version == "0.6": - Models06HCS.from_zarr(group) @pytest.mark.parametrize("fmt", (FormatV04(), FormatV05())) def test_sparse_plate(self, fmt): @@ -1587,8 +1524,6 @@ def test_sparse_plate(self, fmt): Models04HCS.from_zarr(group) elif fmt.version == "0.5": Models05HCS.from_zarr(group) - elif fmt.version == "0.6": - Models06HCS.from_zarr(group) def test_plate_name(self): # We don't need to test v04 and v05 for all tests since @@ -1803,14 +1738,14 @@ def initdir(self, tmpdir): @pytest.mark.parametrize("fmt", (FormatV04(), FormatV05())) @pytest.mark.parametrize("images", (["0"], [{"path": "0"}])) def test_minimal_well(self, images, fmt): - if fmt.version in ("0.5", "0.6"): + if fmt.version == "0.5": group = self.root_v3 else: group = self.root write_well_metadata(group, images) # we want to be sure this is zarr v2 / v3, so we load json manually too attrs = group.attrs - if fmt.version in ("0.5", "0.6"): + if fmt.version == "0.5": attrs = attrs.get("ome") assert attrs["version"] == fmt.version json_text = (self.path_v3 / "zarr.json").read_text(encoding="utf-8") @@ -1826,8 +1761,6 @@ def test_minimal_well(self, images, fmt): Models04Well.from_zarr(group) elif fmt.version == "0.5": Models05Well.from_zarr(group) - elif fmt.version == "0.6": - Models06Well.from_zarr(group) @pytest.mark.parametrize( "images", @@ -1969,14 +1902,14 @@ def verify_label_data( # Verify label metadata label_root = zarr.open(f"{img_path}/labels", mode="r") label_attrs = label_root.attrs - if fmt.version in ("0.5", "0.6"): + if fmt.version == "0.5": label_attrs = label_attrs["ome"] assert "labels" in label_attrs assert label_name in label_attrs["labels"] label_group = zarr.open(f"{img_path}/labels/{label_name}", mode="r") imglabel_attrs = label_group.attrs - if fmt.version in ("0.5", "0.6"): + if fmt.version == "0.5": imglabel_attrs = imglabel_attrs["ome"] assert imglabel_attrs["version"] == fmt.version else: @@ -1999,7 +1932,7 @@ def test_write_labels( ): fmt = format_version_all() - if fmt.version in ("0.5", "0.6"): + if fmt.version == "0.5": img_path = self.path_v3 group = self.root_v3 else: @@ -2067,7 +2000,7 @@ def test_write_labels_with_storage_options( array_constructor, ): fmt = format_version_all() - if fmt.version in ("0.5", "0.6"): + if fmt.version == "0.5": group = self.root_v3 else: group = self.root @@ -2127,7 +2060,7 @@ def test_write_labels_with_storage_options( level0.chunks == expected_chunks ), f"Expected chunks {expected_chunks}, got {level0.chunks}" - if USE_DASK_ARRAY_KWARGS and fmt.version in ("0.5", "0.6") and hasattr(level0, "shards"): + if USE_DASK_ARRAY_KWARGS and fmt.version == "0.5" and hasattr(level0, "shards"): expected_shards = _retuple(storage_options["shards"], level0.shape) assert ( level0.shards == expected_shards @@ -2136,7 +2069,7 @@ def test_write_labels_with_storage_options( assert level0.fill_value == 0 if level0.compressors: - if fmt.version in ("0.5", "0.6"): + if fmt.version == "0.5": if USE_DASK_ARRAY_KWARGS: assert _codec_value(level0.compressors[0].cname) == "zstd" else: @@ -2145,7 +2078,7 @@ def test_write_labels_with_storage_options( assert level0.compressors[0].cname == "zstd" if USE_DASK_ARRAY_KWARGS: assert level0.compressors[0].clevel == 3 - if fmt.version in ("0.5", "0.6") and hasattr(level0, "serializer"): + if fmt.version == "0.5" and hasattr(level0, "serializer"): assert ( _codec_value(level0.metadata.codecs[0].index_codecs[0].endian) == "little" @@ -2153,7 +2086,7 @@ def test_write_labels_with_storage_options( else: assert level0.compressors[0].to_dict()["configuration"]["level"] == 0 - if fmt.version in ("0.5", "0.6") and hasattr(level0, "dimension_names"): + if fmt.version == "0.5" and hasattr(level0, "dimension_names"): assert level0.dimension_names == tuple(axes) if fmt.version == "0.4": @@ -2163,7 +2096,7 @@ def test_write_multiscale_labels( self, shape, format_version_all, array_constructor ): fmt = format_version_all() - if fmt.version in ("0.5", "0.6"): + if fmt.version == "0.5": img_path = self.path_v3 group = self.root_v3 else: @@ -2233,7 +2166,7 @@ def test_write_multiscale_labels_storage_options( self, shape, format_version_all, array_constructor ): fmt = format_version_all() - if fmt.version in ("0.5", "0.6"): + if fmt.version == "0.5": img_path = self.path_v3 group = self.root_v3 else: @@ -2316,7 +2249,7 @@ def test_write_multiscale_labels_storage_options( if ( USE_DASK_ARRAY_KWARGS - and fmt.version in ("0.5", "0.6") + and fmt.version == "0.5" and hasattr(level, "shards") ): expected_shards = _retuple(storage_options["shards"], level.shape) @@ -2329,7 +2262,7 @@ def test_write_multiscale_labels_storage_options( ), f"Level {level_idx}: Expected fill_value 0, got {level.fill_value}" if level.compressors: - if fmt.version in ("0.5", "0.6"): + if fmt.version == "0.5": if USE_DASK_ARRAY_KWARGS: assert _codec_value(level.compressors[0].cname) == "zstd" else: @@ -2338,7 +2271,7 @@ def test_write_multiscale_labels_storage_options( assert level.compressors[0].cname == "zstd" if USE_DASK_ARRAY_KWARGS: assert level.compressors[0].clevel == 3 - if fmt.version in ("0.5", "0.6") and hasattr(level, "serializer"): + if fmt.version == "0.5" and hasattr(level, "serializer"): assert ( _codec_value( level.metadata.codecs[0].index_codecs[0].endian @@ -2348,7 +2281,7 @@ def test_write_multiscale_labels_storage_options( else: assert level.compressors[0].to_dict()["configuration"]["level"] == 0 - if fmt.version in ("0.5", "0.6") and hasattr(level, "dimension_names"): + if fmt.version == "0.5" and hasattr(level, "dimension_names"): assert level.dimension_names == tuple( axes ), f"Level {level_idx}: Expected dimension names {tuple(axes)}, got {level.dimension_names}" @@ -2364,13 +2297,10 @@ def test_write_multiscale_labels_storage_options( @pytest.mark.parametrize( "fmt", - (pytest.param(FormatV04(), id="V04"), - pytest.param(FormatV05(), id="V05"), - pytest.param(FormatV06(), id="V06"), - ), + (pytest.param(FormatV04(), id="V04"), pytest.param(FormatV05(), id="V05")), ) def test_two_label_images(self, array_constructor, fmt): - if fmt.version in ("0.5", "0.6"): + if fmt.version == "0.5": img_path = self.path_v3 group = self.root_v3 else: @@ -2431,7 +2361,7 @@ def test_two_label_images(self, array_constructor, fmt): # Verify label metadata label_root = zarr.open(f"{img_path}/labels", mode="r") attrs = label_root.attrs - if fmt.version in ("0.5", "0.6"): + if fmt.version == "0.5": attrs = attrs["ome"] assert "labels" in attrs assert len(attrs["labels"]) == len(label_names) @@ -2439,15 +2369,12 @@ def test_two_label_images(self, array_constructor, fmt): @pytest.mark.parametrize( "fmt", - (pytest.param(FormatV04(), id="V04"), - pytest.param(FormatV05(), id="V05"), - pytest.param(FormatV06(), id="V06") - ), + (pytest.param(FormatV04(), id="V04"), pytest.param(FormatV05(), id="V05")), ) def write_labels_class_API(self, fmt): from ome_zarr import OMEZarrImage, OMEZarrMultiscale - if fmt.version in ("0.5", "0.6"): + if fmt.version == "0.5": img_path = self.path_v3 group = self.root_v3 else: @@ -2477,7 +2404,7 @@ def write_labels_class_API(self, fmt): # now check that the respective groups and metadata exist and is correct label_group = zarr.open(f"{img_path}/labels", mode="r") - if fmt.version in ("0.5", "0.6"): + if fmt.version == "0.5": label_attrs = label_group.attrs["ome"] elif fmt.version == "0.4": label_attrs = label_group.attrs @@ -2495,7 +2422,7 @@ def write_labels_class_API(self, fmt): # now check that we still have both labels in the metadata label_group = zarr.open(f"{img_path}/labels", mode="r") - if fmt.version in ("0.5", "0.6"): + if fmt.version == "0.5": label_attrs = label_group.attrs["ome"] elif fmt.version == "0.4": label_attrs = label_group.attrs @@ -2511,7 +2438,7 @@ def write_labels_class_API(self, fmt): # Now, only the third label should be present in the metadata and as a zarr group, # but the first and second labels should be gone label_group = zarr.open(f"{img_path}/labels", mode="r") - if fmt.version in ("0.5", "0.6"): + if fmt.version == "0.5": label_attrs = label_group.attrs["ome"] elif fmt.version == "0.4": label_attrs = label_group.attrs From f543138f5548d9c39d06397bbd838a7612508d23 Mon Sep 17 00:00:00 2001 From: Johannes Soltwedel <38459088+jo-mueller@users.noreply.github.com> Date: Fri, 31 Jul 2026 11:29:10 +0200 Subject: [PATCH 16/51] Revert "feat: add formatV06 class" This reverts commit b12fb12246005651b3b546af629318831ba2902b. --- ome_zarr/format.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/ome_zarr/format.py b/ome_zarr/format.py index 7878a8c9..3b2c33e7 100644 --- a/ome_zarr/format.py +++ b/ome_zarr/format.py @@ -383,17 +383,5 @@ def chunk_key_encoding(self) -> dict[str, str]: # this is default for Zarr v3. Could return None? return {"name": "default", "separator": "/"} -class FormatV06(FormatV05): - """ - Changelog: added FormatV06 (June 2025): writing not supported yet - """ - - @property - def version(self) -> str: - return "0.6" - - @property - def zarr_format(self) -> int: - return 3 -CurrentFormat = FormatV06 +CurrentFormat = FormatV05 From a213ad48f5adc52c65e33edc0ced99233f488e60 Mon Sep 17 00:00:00 2001 From: Johannes Soltwedel <38459088+jo-mueller@users.noreply.github.com> Date: Fri, 31 Jul 2026 15:32:22 +0200 Subject: [PATCH 17/51] deps: pin to zarr-python <3.3.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 549fca1a..ef01b40a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ requires-python = ">3.11" dependencies = [ "numpy", "dask>=2025.2.0,!=2025.12.*,!=2026.1.*,!=2026.2.*", - "zarr>=3.0.0", + "zarr>=3.0.0, <3.3.0", "fsspec[s3]>=0.8,!=2021.07.0,!=2023.9.0", "aiohttp", "requests", From b1c510fabcefa765b172fbb8ad1b9a1a1d361b21 Mon Sep 17 00:00:00 2001 From: Johannes Soltwedel <38459088+jo-mueller@users.noreply.github.com> Date: Fri, 31 Jul 2026 18:52:15 +0200 Subject: [PATCH 18/51] Revert "deps: pin to zarr-python <3.3.0" This reverts commit dcd38587aa0f8ceaab06df49849b0964415c2bcf. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ef01b40a..549fca1a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ requires-python = ">3.11" dependencies = [ "numpy", "dask>=2025.2.0,!=2025.12.*,!=2026.1.*,!=2026.2.*", - "zarr>=3.0.0, <3.3.0", + "zarr>=3.0.0", "fsspec[s3]>=0.8,!=2021.07.0,!=2023.9.0", "aiohttp", "requests", From 7a5809da73621626a35d2fa5568d469d84ca4403 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 4 Aug 2026 08:36:39 +0000 Subject: [PATCH 19/51] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- ome_zarr/__init__.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ome_zarr/__init__.py b/ome_zarr/__init__.py index 467e0945..07f8a1db 100644 --- a/ome_zarr/__init__.py +++ b/ome_zarr/__init__.py @@ -12,4 +12,10 @@ # If not 2026.3.0 it must be 2025.11.0 or lower. Name indicates kwargs only contain array kwargs in the dask version. USE_DASK_ARRAY_KWARGS = Version(dask_version) >= Version("2026.3.0") -__all__ = ["OMEZarrImage", "OMEZarrLabels", "OMEZarrMultiscale", "OMEZarrScene", "__version__"] +__all__ = [ + "OMEZarrImage", + "OMEZarrLabels", + "OMEZarrMultiscale", + "OMEZarrScene", + "__version__", +] From 5394cb12c754dc270848952a9fef377f12f5c88d Mon Sep 17 00:00:00 2001 From: Johannes Soltwedel <38459088+jo-mueller@users.noreply.github.com> Date: Tue, 4 Aug 2026 14:18:20 +0200 Subject: [PATCH 20/51] deps: bump required python --- .github/workflows/build.yml | 4 ++-- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c609bc6b..da68ccd0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,12 +10,12 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.11', '3.12', '3.13'] + python-version: ['3.12', '3.13', '3.14'] os: ['windows-latest', 'macos-latest', 'ubuntu-latest'] dask-version: [ 'latest' ] include: - os: ubuntu-latest - python-version: '3.11' + python-version: '3.12' dask-version: '2025.2.0' steps: - uses: actions/checkout@v4 diff --git a/pyproject.toml b/pyproject.toml index 549fca1a..c2bced3d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ description="Implementation of images in Zarr files." license = "BSD-2-Clause" license-files = ["LICENSE"] -requires-python = ">3.11" +requires-python = ">3.12" dependencies = [ "numpy", From 982859201da1ca61327e5803b0fdf9ca37ba58af Mon Sep 17 00:00:00 2001 From: Johannes Soltwedel <38459088+jo-mueller@users.noreply.github.com> Date: Tue, 4 Aug 2026 16:34:01 +0200 Subject: [PATCH 21/51] feat: add explicit inverse --- ome_zarr/classes/scene.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ome_zarr/classes/scene.py b/ome_zarr/classes/scene.py index a2285a93..384d7e7c 100644 --- a/ome_zarr/classes/scene.py +++ b/ome_zarr/classes/scene.py @@ -109,6 +109,10 @@ def _build_graph(self): tf, zarr_context="", source_cs=None, target_cs=None ) self._graph.add_transform(tnd_transform) + # Add inverse edge if transform is invertible + inverse = tnd_transform.invert() + if inverse is not None: + self._graph.add_transform(inverse) # check if input/output are defined subgroups = [] @@ -131,6 +135,10 @@ def _build_graph(self): target_cs=None, ) self._graph.add_transform(ind_transform) + # Add inverse edge if transform is invertible + inverse = ind_transform.invert() + if inverse is not None: + self._graph.add_transform(inverse) def to_ome_zarr(self, store: StoreLike, overwrite: bool = False): """ From 4235c686ca415c22df7a03880604f707ce7767df Mon Sep 17 00:00:00 2001 From: Johannes Soltwedel <38459088+jo-mueller@users.noreply.github.com> Date: Wed, 5 Aug 2026 11:11:03 +0200 Subject: [PATCH 22/51] feat: default coordinate systems to `()` rather than None --- ome_zarr/classes/scene.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/ome_zarr/classes/scene.py b/ome_zarr/classes/scene.py index 384d7e7c..1b13a66a 100644 --- a/ome_zarr/classes/scene.py +++ b/ome_zarr/classes/scene.py @@ -21,9 +21,7 @@ def __init__( self, images: list[OMEZarrMultiscale] | dict[str, OMEZarrMultiscale], coordinate_transformations: Sequence[AnyTransform] | list[dict[str, Any]], - coordinate_systems: ( - Sequence[CoordinateSystem] | Sequence[dict[str, Any]] | None - ) = None, + coordinate_systems: Sequence[CoordinateSystem] | Sequence[dict[str, Any]] = (), coordinates_displacements: dict[str, OMEZarrMultiscale] | None = None, ): """ @@ -315,17 +313,15 @@ def _parse_transforms( @staticmethod def _parse_coordinate_systems( coordinate_systems: ( - Sequence[CoordinateSystem] | Sequence[dict[str, Any]] | None + Sequence[CoordinateSystem] | Sequence[dict[str, Any]] ), - ) -> tuple[CoordinateSystem, ...] | None: + ) -> tuple[CoordinateSystem, ...]: """ Helper method to parse a sequence of coordinate systems that may be provided as either CoordinateSystem instances or dictionaries. This ensures that all coordinate systems are stored as CoordinateSystem objects in the scene metadata. If coordinate_systems is None, it will be returned as None. """ - if coordinate_systems is None: - return None parsed_coordinate_systems = [] for cs in coordinate_systems: From ad8ea3c86975cea7b9030b82dfbd955979b164c5 Mon Sep 17 00:00:00 2001 From: Johannes Soltwedel <38459088+jo-mueller@users.noreply.github.com> Date: Wed, 5 Aug 2026 11:12:55 +0200 Subject: [PATCH 23/51] feat: use pydantic feature to pop `None`s from generated dicts [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- ome_zarr/classes/scene.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/ome_zarr/classes/scene.py b/ome_zarr/classes/scene.py index 1b13a66a..3950c8a4 100644 --- a/ome_zarr/classes/scene.py +++ b/ome_zarr/classes/scene.py @@ -187,8 +187,7 @@ def to_ome_zarr(self, store: StoreLike, overwrite: bool = False): disp_img.to_ome_zarr(subgroup, overwrite=True, version="0.6") # Always update scene metadata - metadata_dict = self.metadata.model_dump() - metadata_dict = _recursive_pop_nones(metadata_dict) + metadata_dict = self.metadata.model_dump(exclude_none=True) zarr_group.attrs["ome"] = {"scene": metadata_dict, "version": "0.6.dev4"} @@ -312,9 +311,7 @@ def _parse_transforms( @staticmethod def _parse_coordinate_systems( - coordinate_systems: ( - Sequence[CoordinateSystem] | Sequence[dict[str, Any]] - ), + coordinate_systems: Sequence[CoordinateSystem] | Sequence[dict[str, Any]], ) -> tuple[CoordinateSystem, ...]: """ Helper method to parse a sequence of coordinate systems that may be provided as either From faee5e1258ac6692b83a4f487d809a8d8510f102 Mon Sep 17 00:00:00 2001 From: Johannes Soltwedel <38459088+jo-mueller@users.noreply.github.com> Date: Wed, 5 Aug 2026 11:15:56 +0200 Subject: [PATCH 24/51] chore: type checks --- ome_zarr/classes/scene.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ome_zarr/classes/scene.py b/ome_zarr/classes/scene.py index 3950c8a4..e04dd834 100644 --- a/ome_zarr/classes/scene.py +++ b/ome_zarr/classes/scene.py @@ -230,7 +230,7 @@ def from_ome_zarr(cls, store: StoreLike): # Load all image subgroups, keyed by their zarr path images = {} for tf in transformations: - if hasattr(tf, "input"): + if tf.input is not None: path = tf.input.path if path is not None and path in zarr_group: img_group = zarr_group[path] @@ -242,7 +242,7 @@ def from_ome_zarr(cls, store: StoreLike): raise ValueError( f"Image specified in metadata at '{path}' not found in zarr group." ) - if hasattr(tf, "output"): + if tf.output is not None: path = tf.output.path if path is not None and path in zarr_group: img_group = zarr_group[path] @@ -258,7 +258,7 @@ def from_ome_zarr(cls, store: StoreLike): scene = OMEZarrScene( images=images, coordinate_transformations=transformations, - coordinate_systems=coordinate_systems, + coordinate_systems=coordinate_systems if coordinate_systems is not None else (), coordinates_displacements=coordinates_displacements, ) From 529b9cf40eac1b92d7f07eb7d298b6256665c4b9 Mon Sep 17 00:00:00 2001 From: Johannes Soltwedel <38459088+jo-mueller@users.noreply.github.com> Date: Wed, 5 Aug 2026 11:45:54 +0200 Subject: [PATCH 25/51] feat: add properties/getters/setters for the following attributes: - metadata - coordinate_systems - coordinate_transformations --- ome_zarr/classes/scene.py | 76 ++++++++++++++++++++------------------- 1 file changed, 40 insertions(+), 36 deletions(-) diff --git a/ome_zarr/classes/scene.py b/ome_zarr/classes/scene.py index e04dd834..68406bcb 100644 --- a/ome_zarr/classes/scene.py +++ b/ome_zarr/classes/scene.py @@ -38,18 +38,47 @@ def __init__( else: self.images = images - # parse coordinate systems and transforms - self.coordinate_systems = self._parse_coordinate_systems(coordinate_systems) - self.coordinate_transformations = self._parse_transforms( - coordinate_transformations - ) self.coordinates_displacements = coordinates_displacements - self.metadata = SceneAttrs( - coordinateSystems=self.coordinate_systems, - coordinateTransformations=self.coordinate_transformations, + # metadata is the single source of truth + self._metadata = SceneAttrs( + coordinateSystems=self._parse_coordinate_systems(coordinate_systems), + coordinateTransformations=self._parse_transforms( + coordinate_transformations + ), + ) + + @property + def coordinate_systems(self) -> tuple[CoordinateSystem, ...]: + return tuple(self._metadata.coordinateSystems or ()) + + @coordinate_systems.setter + def coordinate_systems( + self, value: Sequence[CoordinateSystem] | Sequence[dict[str, Any]] + ) -> None: + self._metadata = self._metadata.model_copy( + update={"coordinateSystems": self._parse_coordinate_systems(value)} ) + @property + def coordinate_transformations(self) -> tuple[AnyTransform, ...]: + return tuple(self._metadata.coordinateTransformations or ()) + + @coordinate_transformations.setter + def coordinate_transformations( + self, value: Sequence[AnyTransform] | list[dict[str, Any]] + ) -> None: + self._metadata = self._metadata.model_copy( + update={"coordinateTransformations": self._parse_transforms(value)} + ) + + @property + def metadata(self) -> SceneAttrs: + return self._metadata + + @metadata.setter + def metadata(self, value: SceneAttrs) -> None: + self._metadata = value self._build_graph() def get_coordinate_system( @@ -155,8 +184,6 @@ def to_ome_zarr(self, store: StoreLike, overwrite: bool = False): import tqdm - from ome_zarr.utils import _recursive_pop_nones - if overwrite and os.path.exists(str(store)): # Clear the store if it already exists and we're not doing incremental writes shutil.rmtree(str(store)) @@ -258,37 +285,14 @@ def from_ome_zarr(cls, store: StoreLike): scene = OMEZarrScene( images=images, coordinate_transformations=transformations, - coordinate_systems=coordinate_systems if coordinate_systems is not None else (), + coordinate_systems=( + coordinate_systems if coordinate_systems is not None else () + ), coordinates_displacements=coordinates_displacements, ) return scene - def __setattr__(self, name: str, value: Any) -> None: - if name == "coordinate_transformations": - # Update metadata when coordinate transformations are set - parsed_transforms = self._parse_transforms(value) - super().__setattr__(name, parsed_transforms) - # Only update metadata if it exists (not during initial construction) - if hasattr(self, "metadata") and self.metadata is not None: - self.metadata = self.metadata.model_copy( - update={"coordinateTransformations": parsed_transforms} - ) - - elif name == "coordinate_systems": - # Update metadata when coordinate systems are set - parsed_coordinate_systems = self._parse_coordinate_systems(value) - super().__setattr__(name, parsed_coordinate_systems) - # Only update metadata if it exists (not during initial construction) - if hasattr(self, "metadata") and self.metadata is not None: - self.metadata = self.metadata.model_copy( - update={"coordinateSystems": parsed_coordinate_systems} - ) - - else: - # Default behavior for all other attributes - super().__setattr__(name, value) - @staticmethod def _parse_transforms( transforms: Sequence[AnyTransform] | list[dict[str, Any]], From 8b3810b3f4c658657a9e2143a4b1aeb33efbb52e Mon Sep 17 00:00:00 2001 From: Johannes Soltwedel <38459088+jo-mueller@users.noreply.github.com> Date: Wed, 5 Aug 2026 11:50:21 +0200 Subject: [PATCH 26/51] docs: add docstring for metadata property --- ome_zarr/classes/scene.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ome_zarr/classes/scene.py b/ome_zarr/classes/scene.py index 68406bcb..dfc3eebd 100644 --- a/ome_zarr/classes/scene.py +++ b/ome_zarr/classes/scene.py @@ -74,6 +74,14 @@ def coordinate_transformations( @property def metadata(self) -> SceneAttrs: + """ + Get the scene metadata as a SceneAttrs object. + + Returns + ------- + SceneAttrs: ome_zarr_models.v06.scene.SceneAttrs + The scene metadata, including coordinate systems and transformations. + """ return self._metadata @metadata.setter From 0041226d1870bb133b9f97ff4751cf3c855d6feb Mon Sep 17 00:00:00 2001 From: Johannes Soltwedel <38459088+jo-mueller@users.noreply.github.com> Date: Wed, 5 Aug 2026 11:54:09 +0200 Subject: [PATCH 27/51] docs: complete docstring --- ome_zarr/classes/scene.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/ome_zarr/classes/scene.py b/ome_zarr/classes/scene.py index dfc3eebd..3b2337ec 100644 --- a/ome_zarr/classes/scene.py +++ b/ome_zarr/classes/scene.py @@ -31,6 +31,23 @@ def __init__( Either a list of images (keyed internally by metadata.name) or a dict mapping zarr group paths to images. The dict form gives explicit control over the paths where images will be stored in the zarr hierarchy. + coordinate_transformations : Sequence[ome_zarr_models.v06.coordinate_transforms.AnyTransform] | list[dict[str, Any]] + A sequence of coordinate transformations that define how to map between + different coordinate systems in the scene. + Each transformation can be provided as an AnyTransform instance + or as a dictionary that can be validated into an AnyTransform. + For more information see [ngff specification](https://ngff.openmicroscopy.org/specifications/dev/index.html#coordinatetransformations-metadata). + coordinate_systems : Sequence[ome_zarr_models.v06.coordinate_transforms.CoordinateSystem] | Sequence[dict[str, Any]], optional + A sequence of coordinate systems that define the coordinate spaces used + in the scene. + Each coordinate system can be provided as a CoordinateSystem instance + or as a dictionary that can be validated into a CoordinateSystem. + For more information see [ngff specification](https://ngff.openmicroscopy.org/specifications/dev/index.html#coordinatesystems-metadata). + coordinates_displacements : dict[str, OMEZarrMultiscale] | None, optional + A dictionary mapping zarr group paths to displacement field images + or coordinate arrays, which are referenced by the [displacements and + coordinates transformations](https://ngff.openmicroscopy.org/specifications/dev/index.html#coordinates-and-displacements) in the scene metadata. + """ # Coerce list to dict keyed by metadata.name if isinstance(images, list): From 7db011d6d335b156e99c4f8fc94ad62cb9a71455 Mon Sep 17 00:00:00 2001 From: Johannes Soltwedel <38459088+jo-mueller@users.noreply.github.com> Date: Wed, 5 Aug 2026 12:57:40 +0200 Subject: [PATCH 28/51] feat: add compute parameter to writer method --- ome_zarr/classes/scene.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/ome_zarr/classes/scene.py b/ome_zarr/classes/scene.py index 3b2337ec..0e24046b 100644 --- a/ome_zarr/classes/scene.py +++ b/ome_zarr/classes/scene.py @@ -192,7 +192,7 @@ def _build_graph(self): if inverse is not None: self._graph.add_transform(inverse) - def to_ome_zarr(self, store: StoreLike, overwrite: bool = False): + def to_ome_zarr(self, store: StoreLike, overwrite: bool = False, compute: bool = True) -> list: """ Write scene to OME-Zarr format. @@ -203,6 +203,9 @@ def to_ome_zarr(self, store: StoreLike, overwrite: bool = False): overwrite: bool If True, overwrite all images in the store with the current state of the scene. If False, only write new images that haven't been written before. Existing images in the store will be left unchanged. + compute: bool + If True, compute any lazy operations on write of each image. + Otherwise return a list of delayed operations that can be computed later. """ import shutil @@ -217,6 +220,8 @@ def to_ome_zarr(self, store: StoreLike, overwrite: bool = False): mode = "w" if overwrite else "a" zarr_group = zarr.open(store, mode=mode) + delayed = [] + # Create a subgroup for each image using its path key for img_path, img in tqdm.tqdm(self.images.items(), desc="Writing images"): # Skip if already written (incremental mode) @@ -225,7 +230,7 @@ def to_ome_zarr(self, store: StoreLike, overwrite: bool = False): # Write the image subgroup = zarr_group.create_group(img_path, overwrite=overwrite) - img.to_ome_zarr(subgroup, overwrite=True, version="0.6") + delayed += img.to_ome_zarr(subgroup, overwrite=True, version="0.6", compute=compute) for disp_path, disp_img in (self.coordinates_displacements or {}).items(): # Skip if already written (incremental mode) @@ -236,12 +241,14 @@ def to_ome_zarr(self, store: StoreLike, overwrite: bool = False): subgroup = zarr_group.create_group( f"coordinateTransformations/{disp_path}", overwrite=overwrite ) - disp_img.to_ome_zarr(subgroup, overwrite=True, version="0.6") + delayed += disp_img.to_ome_zarr(subgroup, overwrite=True, version="0.6", compute=compute) # Always update scene metadata metadata_dict = self.metadata.model_dump(exclude_none=True) - zarr_group.attrs["ome"] = {"scene": metadata_dict, "version": "0.6.dev4"} + zarr_group.attrs["ome"] = {"scene": metadata_dict, "version": "0.6"} + + return delayed @classmethod def from_ome_zarr(cls, store: StoreLike): From dd1070f5b4ef1c4b769344323d834415efb3e491 Mon Sep 17 00:00:00 2001 From: Johannes Soltwedel <38459088+jo-mueller@users.noreply.github.com> Date: Wed, 5 Aug 2026 12:58:15 +0200 Subject: [PATCH 29/51] feat: more powerful get_coordinate_system function --- ome_zarr/classes/scene.py | 56 ++++++++++++++++++++++----------------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/ome_zarr/classes/scene.py b/ome_zarr/classes/scene.py index 0e24046b..839b2731 100644 --- a/ome_zarr/classes/scene.py +++ b/ome_zarr/classes/scene.py @@ -107,41 +107,49 @@ def metadata(self, value: SceneAttrs) -> None: self._build_graph() def get_coordinate_system( - self, name: str, path: str | None = None - ) -> CoordinateSystem | None: + self, name: str | None = None, path: str | None = None + ) -> dict[str, list[CoordinateSystem]]: """ Retrieve a coordinate system by name and optional path. Parameters ---------- - name: str - The name of the coordinate system to retrieve. + name: str | None + Optional name of the coordinate system to retrieve. + If provided, only coordinate systems with this name will be returned. path: str | None - Optional path to disambiguate coordinate systems with the same name. If None, will return the first match with the given name. + Optional path to disambiguate coordinate systems by the zarr path + under which they are stored. + If provided, only coordinate systems associated with this path will be returned. Returns ------- - CoordinateSystem or None - The matching CoordinateSystem object, or None if no match is found. + dict[str, list[CoordinateSystem]] + A dictionary of matching CoordinateSystem objects keyed by their context + (e.g., "" for top-level, image key for image-level). + Empty if no match is found. """ - if path is None: - # coordinate system can only be in top-level - possible_coordinate_systems: Sequence[CoordinateSystem] = ( - self.coordinate_systems or [] + # find top-level matches + top_level_matches = {} + if len(self.coordinate_systems) > 0: + top_level_matches = {"": list(self.coordinate_systems)} + + # find image-level matches + image_matches = {} + for key, img in self.images.items(): + image_matches.update( + {key: list(img.metadata.coordinateSystems)} ) - return next( - (cs for cs in possible_coordinate_systems if cs.name == name), None - ) + matches = {**top_level_matches, **image_matches} + + if name is not None: + matches = {k: [cs for cs in v if cs.name == name] for k, v in matches.items()} + + if path is not None: + matches = {k: v for k, v in matches.items() if k == path} - possible_coordinate_systems = [] - for group in self.images: - if group == path: - img = self.images[group] - for cs in img.metadata.coordinateSystems: - if cs.name == name: - return cs - return None + return matches def _build_graph(self): self._graph = tnd.graph.TransformGraph() @@ -153,8 +161,8 @@ def _build_graph(self): tnd_transform = self._ozmp_tf_to_tnd( tf, zarr_context="", - source_cs=source_cs, - target_cs=target_cs, + source_cs=source_cs[tf.input.path or ""][0], + target_cs=target_cs[tf.output.path or ""][0], ).simplify() else: tnd_transform = self._ozmp_tf_to_tnd( From c933832b82fd0a27bc1aac230c31e8e6f8598924 Mon Sep 17 00:00:00 2001 From: Johannes Soltwedel <38459088+jo-mueller@users.noreply.github.com> Date: Wed, 5 Aug 2026 12:58:24 +0200 Subject: [PATCH 30/51] chore: type checker --- ome_zarr/classes/scene.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ome_zarr/classes/scene.py b/ome_zarr/classes/scene.py index 839b2731..5d33a628 100644 --- a/ome_zarr/classes/scene.py +++ b/ome_zarr/classes/scene.py @@ -155,6 +155,10 @@ def _build_graph(self): self._graph = tnd.graph.TransformGraph() # Add scene-level transformations (empty context = root level) for tf in self.coordinate_transformations: + if tf.input is None or tf.output is None: + raise ValueError( + f"Coordinate transformation {tf} is missing input or output information." + ) if tf.type == "sequence": source_cs = self.get_coordinate_system(tf.input.name, tf.input.path) target_cs = self.get_coordinate_system(tf.output.name, tf.output.path) From 7e3eaf810dfebd1265c410706cfad1a90ccba08f Mon Sep 17 00:00:00 2001 From: Johannes Soltwedel <38459088+jo-mueller@users.noreply.github.com> Date: Wed, 5 Aug 2026 12:58:41 +0200 Subject: [PATCH 31/51] docs: improve docstring --- ome_zarr/classes/scene.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/ome_zarr/classes/scene.py b/ome_zarr/classes/scene.py index 5d33a628..b2546f5a 100644 --- a/ome_zarr/classes/scene.py +++ b/ome_zarr/classes/scene.py @@ -48,6 +48,17 @@ def __init__( or coordinate arrays, which are referenced by the [displacements and coordinates transformations](https://ngff.openmicroscopy.org/specifications/dev/index.html#coordinates-and-displacements) in the scene metadata. + Methods + ------- + to_ome_zarr(store: StoreLike, overwrite: bool = False, compute: bool = True) + Write the scene to OME-Zarr format in the specified store. + from_ome_zarr(store: StoreLike) + Load an existing scene from OME-Zarr format in the specified store. + get_coordinate_system(name: str | None, path: str | None = None) + Retrieve a coordinate system by name or path. + If neither is + + """ # Coerce list to dict keyed by metadata.name if isinstance(images, list): From 0b1ad99f2278544b53a326d68bdc4db748be6838 Mon Sep 17 00:00:00 2001 From: Johannes Soltwedel <38459088+jo-mueller@users.noreply.github.com> Date: Thu, 6 Aug 2026 15:15:52 +0200 Subject: [PATCH 32/51] feat: posixnames and tnd querying by tuples --- ome_zarr/classes/scene.py | 35 ++++++++++++----------------------- 1 file changed, 12 insertions(+), 23 deletions(-) diff --git a/ome_zarr/classes/scene.py b/ome_zarr/classes/scene.py index b2546f5a..ce6e3929 100644 --- a/ome_zarr/classes/scene.py +++ b/ome_zarr/classes/scene.py @@ -1,5 +1,6 @@ # the class for storage representation, not exposed to the user import os +import posixpath from collections.abc import Sequence from typing import Any, cast @@ -402,28 +403,18 @@ def _ozmp_tf_to_tnd( import numpy as np if transform.input is not None: - input_path = ( - transform.input.path if transform.input.path is not None else "" - ) - output_path = ( - transform.output.path if transform.output.path is not None else "" - ) + input_path = transform.input.path or "" + output_path = transform.output.path or "" # zarr_context prepends path with relative path from root # to keep track of global location of coordinate systems in the zarr store - if zarr_context != "" and input_path != "": - input_path = f"{zarr_context}/{input_path}" - elif zarr_context != "": - input_path = zarr_context - - if zarr_context != "" and output_path != "": - output_path = f"{zarr_context}/{output_path}" - elif zarr_context != "": - output_path = zarr_context + if zarr_context: + input_path = posixpath.join(zarr_context, input_path) if input_path else zarr_context + output_path = posixpath.join(zarr_context, output_path) if output_path else zarr_context spaces = tnd.Spaces( - f"{input_path}:{transform.input.name}", - f"{output_path}:{transform.output.name}", + (input_path, transform.input.name), + (output_path, transform.output.name), ) else: spaces = tnd.Spaces(None, None) @@ -443,14 +434,12 @@ def _ozmp_tf_to_tnd( tnd_transform = tnd.transforms.Affine(aff, spaces=spaces) elif transform.type == "displacements": - path_to_dfield = transform.path if transform.path is not None else "" - if zarr_context != "" and path_to_dfield != "": - path_to_dfield = f"{zarr_context}/{path_to_dfield}" + path_to_dfield = transform.path or "" + if zarr_context and path_to_dfield: + path_to_dfield = posixpath.join(zarr_context, path_to_dfield) if self.coordinates_displacements is not None: - dfield = self.coordinates_displacements.get( - path_to_dfield.split("/")[-1] - ) + dfield = self.coordinates_displacements.get(posixpath.basename(path_to_dfield)) if dfield is not None: if dfield.images[0].scale is None: raise ValueError( From c3bc9c8248e4e918d111e09582a86168cf88074f Mon Sep 17 00:00:00 2001 From: Johannes Soltwedel <38459088+jo-mueller@users.noreply.github.com> Date: Thu, 6 Aug 2026 15:16:05 +0200 Subject: [PATCH 33/51] fix: do not forget building the graph on init :) --- ome_zarr/classes/scene.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ome_zarr/classes/scene.py b/ome_zarr/classes/scene.py index ce6e3929..e1f2dd1d 100644 --- a/ome_zarr/classes/scene.py +++ b/ome_zarr/classes/scene.py @@ -77,6 +77,8 @@ def __init__( ), ) + self._build_graph() + @property def coordinate_systems(self) -> tuple[CoordinateSystem, ...]: return tuple(self._metadata.coordinateSystems or ()) From 17cfbb98d90cc3cc77c2057378020f0206842996 Mon Sep 17 00:00:00 2001 From: Johannes Soltwedel <38459088+jo-mueller@users.noreply.github.com> Date: Thu, 6 Aug 2026 15:16:26 +0200 Subject: [PATCH 34/51] tests: fix call to test --- ome_zarr/classes/scene.py | 22 ++++++++++++++-------- tests/test_scene.py | 2 +- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/ome_zarr/classes/scene.py b/ome_zarr/classes/scene.py index e1f2dd1d..8f536a16 100644 --- a/ome_zarr/classes/scene.py +++ b/ome_zarr/classes/scene.py @@ -57,7 +57,7 @@ def __init__( Load an existing scene from OME-Zarr format in the specified store. get_coordinate_system(name: str | None, path: str | None = None) Retrieve a coordinate system by name or path. - If neither is + If neither is """ @@ -151,14 +151,14 @@ def get_coordinate_system( # find image-level matches image_matches = {} for key, img in self.images.items(): - image_matches.update( - {key: list(img.metadata.coordinateSystems)} - ) + image_matches.update({key: list(img.metadata.coordinateSystems)}) matches = {**top_level_matches, **image_matches} if name is not None: - matches = {k: [cs for cs in v if cs.name == name] for k, v in matches.items()} + matches = { + k: [cs for cs in v if cs.name == name] for k, v in matches.items() + } if path is not None: matches = {k: v for k, v in matches.items() if k == path} @@ -218,7 +218,9 @@ def _build_graph(self): if inverse is not None: self._graph.add_transform(inverse) - def to_ome_zarr(self, store: StoreLike, overwrite: bool = False, compute: bool = True) -> list: + def to_ome_zarr( + self, store: StoreLike, overwrite: bool = False, compute: bool = True + ) -> list: """ Write scene to OME-Zarr format. @@ -256,7 +258,9 @@ def to_ome_zarr(self, store: StoreLike, overwrite: bool = False, compute: bool = # Write the image subgroup = zarr_group.create_group(img_path, overwrite=overwrite) - delayed += img.to_ome_zarr(subgroup, overwrite=True, version="0.6", compute=compute) + delayed += img.to_ome_zarr( + subgroup, overwrite=True, version="0.6", compute=compute + ) for disp_path, disp_img in (self.coordinates_displacements or {}).items(): # Skip if already written (incremental mode) @@ -267,7 +271,9 @@ def to_ome_zarr(self, store: StoreLike, overwrite: bool = False, compute: bool = subgroup = zarr_group.create_group( f"coordinateTransformations/{disp_path}", overwrite=overwrite ) - delayed += disp_img.to_ome_zarr(subgroup, overwrite=True, version="0.6", compute=compute) + delayed += disp_img.to_ome_zarr( + subgroup, overwrite=True, version="0.6", compute=compute + ) # Always update scene metadata metadata_dict = self.metadata.model_dump(exclude_none=True) diff --git a/tests/test_scene.py b/tests/test_scene.py index 960b3039..a285a7ab 100644 --- a/tests/test_scene.py +++ b/tests/test_scene.py @@ -73,7 +73,7 @@ def test_create_scene_without_coordinate_systems(test_data_dir, transform): assert len(scene._graph.graph.nodes) == 2 # traverse graph - tf = scene._graph.get_sequence(f"{img_a.name}:physical", f"{img_b.name}:physical") + tf = scene._graph.get_sequence((img_a.name, "physical"), (img_b.name, "physical")) # check that the transform graph can be traversed (i.e. transform is not None) assert tf is not None From 9f851e164f4eb42148751df9a610a20a125ad20a Mon Sep 17 00:00:00 2001 From: Johannes Soltwedel <38459088+jo-mueller@users.noreply.github.com> Date: Thu, 6 Aug 2026 15:18:43 +0200 Subject: [PATCH 35/51] feat: remove tqdm --- ome_zarr/classes/scene.py | 20 ++++++++++++++------ pyproject.toml | 1 - 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/ome_zarr/classes/scene.py b/ome_zarr/classes/scene.py index 8f536a16..563c95ed 100644 --- a/ome_zarr/classes/scene.py +++ b/ome_zarr/classes/scene.py @@ -238,8 +238,6 @@ def to_ome_zarr( """ import shutil - import tqdm - if overwrite and os.path.exists(str(store)): # Clear the store if it already exists and we're not doing incremental writes shutil.rmtree(str(store)) @@ -251,7 +249,7 @@ def to_ome_zarr( delayed = [] # Create a subgroup for each image using its path key - for img_path, img in tqdm.tqdm(self.images.items(), desc="Writing images"): + for img_path, img in self.images.items(): # Skip if already written (incremental mode) if not overwrite and img_path in zarr_group: continue @@ -417,8 +415,16 @@ def _ozmp_tf_to_tnd( # zarr_context prepends path with relative path from root # to keep track of global location of coordinate systems in the zarr store if zarr_context: - input_path = posixpath.join(zarr_context, input_path) if input_path else zarr_context - output_path = posixpath.join(zarr_context, output_path) if output_path else zarr_context + input_path = ( + posixpath.join(zarr_context, input_path) + if input_path + else zarr_context + ) + output_path = ( + posixpath.join(zarr_context, output_path) + if output_path + else zarr_context + ) spaces = tnd.Spaces( (input_path, transform.input.name), @@ -447,7 +453,9 @@ def _ozmp_tf_to_tnd( path_to_dfield = posixpath.join(zarr_context, path_to_dfield) if self.coordinates_displacements is not None: - dfield = self.coordinates_displacements.get(posixpath.basename(path_to_dfield)) + dfield = self.coordinates_displacements.get( + posixpath.basename(path_to_dfield) + ) if dfield is not None: if dfield.images[0].scale is None: raise ValueError( diff --git a/pyproject.toml b/pyproject.toml index c2bced3d..31327b9d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,6 @@ dependencies = [ "transformnd>=0.6.0", "ome-zarr-models==1.8.0rc1", "Deprecated", - "tqdm", ] classifiers = [ "Development Status :: 4 - Beta", From 207cfe3457ae7da62b71bb14df190cb97dbbaba8 Mon Sep 17 00:00:00 2001 From: Johannes Soltwedel <38459088+jo-mueller@users.noreply.github.com> Date: Thu, 6 Aug 2026 15:29:45 +0200 Subject: [PATCH 36/51] test: fix version check for 0.6 --- tests/test_scene.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_scene.py b/tests/test_scene.py index a285a7ab..71553da8 100644 --- a/tests/test_scene.py +++ b/tests/test_scene.py @@ -92,7 +92,8 @@ def test_create_scene_without_coordinate_systems(test_data_dir, transform): assert "ome" in zarr_group.attrs ome_metadata = zarr_group.attrs["ome"] assert "scene" in ome_metadata - assert "version" in ome_metadata and ome_metadata["version"] == "0.6.dev4" + assert "version" in ome_metadata + assert ome_metadata["version"] == "0.6" # check transforms assert "coordinateTransformations" in ome_metadata["scene"] @@ -179,7 +180,8 @@ def test_create_scene_with_coordinate_systems(test_data_dir, transform): assert "ome" in zarr_group.attrs ome_metadata = zarr_group.attrs["ome"] assert "scene" in ome_metadata - assert "version" in ome_metadata and ome_metadata["version"] == "0.6.dev4" + assert "version" in ome_metadata + assert ome_metadata["version"] == "0.6" assert "coordinateSystems" in ome_metadata["scene"] assert len(ome_metadata["scene"]["coordinateSystems"]) == 2 From ef0a3d22d5d2c3997727f72f1c9e827634f66d0c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 6 Aug 2026 13:30:00 +0000 Subject: [PATCH 37/51] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_scene.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_scene.py b/tests/test_scene.py index 71553da8..5a30883c 100644 --- a/tests/test_scene.py +++ b/tests/test_scene.py @@ -180,7 +180,7 @@ def test_create_scene_with_coordinate_systems(test_data_dir, transform): assert "ome" in zarr_group.attrs ome_metadata = zarr_group.attrs["ome"] assert "scene" in ome_metadata - assert "version" in ome_metadata + assert "version" in ome_metadata assert ome_metadata["version"] == "0.6" assert "coordinateSystems" in ome_metadata["scene"] assert len(ome_metadata["scene"]["coordinateSystems"]) == 2 @@ -351,7 +351,7 @@ def test_scene_with_displacements(test_data_dir): assert dfield_attrs["ome"]["version"] == "0.6" axes_md = dfield_attrs["ome"]["multiscales"][0]["coordinateSystems"][0]["axes"] assert axes_md[0]["type"] == "displacement" - assert axes_md[0]["discrete"] == True + assert axes_md[0]["discrete"] # read displacements back in and check that the (meta)data is correct scene_read = OMEZarrScene.from_ome_zarr(save_grp) @@ -360,4 +360,4 @@ def test_scene_with_displacements(test_data_dir): dfield_img = scene_read.coordinates_displacements["displacementField"] for image in dfield_img.images: assert image.axes_types["c"] == "displacement" - assert dfield_img.metadata.coordinateSystems[0].axes[0].discrete == True + assert dfield_img.metadata.coordinateSystems[0].axes[0].discrete From 39c9e12711bbc1330302cd9f8e22297829d8619e Mon Sep 17 00:00:00 2001 From: Johannes Soltwedel <38459088+jo-mueller@users.noreply.github.com> Date: Fri, 7 Aug 2026 08:26:40 +0200 Subject: [PATCH 38/51] tests: more tests --- tests/test_scene.py | 124 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 119 insertions(+), 5 deletions(-) diff --git a/tests/test_scene.py b/tests/test_scene.py index 5a30883c..1d074d7e 100644 --- a/tests/test_scene.py +++ b/tests/test_scene.py @@ -10,6 +10,28 @@ {"type": "rotation", "rotation": [[0.0, -1.0], [1.0, 0.0]]}, {"type": "affine", "affine": [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]}, {"type": "mapAxis", "mapAxis": [1, 0]}, + { + "type": "sequence", + "transformations": [ + {"type": "scale", "scale": [1.0, 1.0]}, + {"type": "translation", "translation": [0.0, 0.0]}, + ], + }, + { + "type": "byDimension", + "transformations": [ + { + "input_axes": [1], + "output_axes": [1], + "transformation": {"type": "translation", "translation": [0.0]}, + }, + { + "input_axes": [0], + "output_axes": [0], + "transformation": {"type": "scale", "scale": [1.0]}, + }, + ], + }, ] @@ -33,7 +55,10 @@ def create_data(shape, dtype=np.uint8, mean_val=10): def test_create_scene_without_coordinate_systems(test_data_dir, transform): """ Create a scene with two images and a single coordinate transformation - between them. The data is saved and loaded back in the test. + between them. + One of the images has an additional coordinate system besides the + physical coordinate system. + The data is saved and loaded back in the test. """ shape = (64, 64) img_a = OMEZarrImage( @@ -50,13 +75,32 @@ def test_create_scene_without_coordinate_systems(test_data_dir, transform): scale={"y": 1.0, "x": 1.0}, ) + additional_cs = { + "name": "additional", + "axes": [ + {"name": "y", "type": "space"}, + {"name": "x", "type": "space"}, + ], + } + + additional_tf = { + "type": "rotation", + "rotation": [[0.0, -1.0], [1.0, 0.0]], + "input": {"name": "physical"}, + "output": {"name": "additional"}, + } + img_a_ms = OMEZarrMultiscale(image=img_a) - img_b_ms = OMEZarrMultiscale(image=img_b) + img_b_ms = OMEZarrMultiscale( + image=img_b, + coordinate_systems=[additional_cs], + coordinate_transformations=[additional_tf], + ) # avoid leaking transform mutations into other tests transform = transform.copy() transform["input"] = {"name": "physical", "path": "imageA"} - transform["output"] = {"name": "physical", "path": "imageB"} + transform["output"] = {"name": "additional", "path": "imageB"} scene = OMEZarrScene( images=[img_a_ms, img_b_ms], @@ -70,7 +114,7 @@ def test_create_scene_without_coordinate_systems(test_data_dir, transform): # check that the graph has the correct number of nodes # (aka coordinate systems) - assert len(scene._graph.graph.nodes) == 2 + assert len(scene._graph.graph.nodes) == 3 # traverse graph tf = scene._graph.get_sequence((img_a.name, "physical"), (img_b.name, "physical")) @@ -84,7 +128,7 @@ def test_create_scene_without_coordinate_systems(test_data_dir, transform): # check that the graph is created correctly on read assert scene_read._graph is not None - assert len(scene_read._graph.graph.nodes) == 2 + assert len(scene_read._graph.graph.nodes) == 3 # open the zarr group and check the metadata # and check that the correct metadata fields are present in the store @@ -186,6 +230,76 @@ def test_create_scene_with_coordinate_systems(test_data_dir, transform): assert len(ome_metadata["scene"]["coordinateSystems"]) == 2 +def test_coordinate_system_retrieval(test_data_dir): + """ + Create a scene with two images and three coordinate transformations + between them. The data is saved and loaded back in the test. + """ + shape = (64, 64) + img_a = OMEZarrImage( + data=create_data(shape), + name="imageA", + axes=["y", "x"], + scale={"y": 1.0, "x": 1.0}, + ) + + img_b = OMEZarrImage( + data=create_data(shape), + name="imageB", + axes=["y", "x"], + scale={"y": 1.0, "x": 1.0}, + ) + + img_a_ms = OMEZarrMultiscale(image=img_a) + img_b_ms = OMEZarrMultiscale(image=img_b) + + world1_cs = { + "name": "world", + "axes": [ax.model_dump() for ax in img_a_ms.metadata.coordinateSystems[0].axes], + } + world2_cs = { + "name": "world2", + "axes": [ax.model_dump() for ax in img_b_ms.metadata.coordinateSystems[0].axes], + } + + transform1 = {"type": "scale", "scale": [1.0, 1.0]} + transform1["input"] = {"name": "physical", "path": "imageA"} + transform1["output"] = {"name": "world"} + + transform2 = {"type": "scale", "scale": [1.0, 1.0]} + transform2["input"] = {"name": "world"} + transform2["output"] = {"name": "world2"} + + transform3 = {"type": "scale", "scale": [1.0, 1.0]} + transform3["input"] = {"name": "world2"} + transform3["output"] = {"name": "physical", "path": "imageB"} + + scene = OMEZarrScene( + images=[img_a_ms, img_b_ms], + coordinate_transformations=[transform1, transform2, transform3], + coordinate_systems=[world1_cs, world2_cs], + ) + + # check that the get_coordinate_system returns the correct + # number and instances of coordinate systems + all_cs = scene.get_coordinate_system() + + # the dict keys correspond to the names of the images + # or top-level if the name is "" + assert "" in all_cs + assert len(all_cs[""]) == 2 + assert all_cs[""][0].name == "world" + assert all_cs[""][1].name == "world2" + + assert "imageB" in all_cs + assert len(all_cs["imageB"]) == 1 + assert all_cs["imageB"][0].name == "physical" + + assert "imageA" in all_cs + assert len(all_cs["imageA"]) == 1 + assert all_cs["imageA"][0].name == "physical" + + def test_appending_scene(test_data_dir): """ Create a scene with two images and a single coordinate transformation From e24ceeb772956181fbe9b314ab85483177d2c469 Mon Sep 17 00:00:00 2001 From: Johannes Soltwedel <38459088+jo-mueller@users.noreply.github.com> Date: Fri, 7 Aug 2026 08:44:41 +0200 Subject: [PATCH 39/51] tests: expand upon coordinate system retrieval --- tests/test_scene.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/test_scene.py b/tests/test_scene.py index 1d074d7e..318ed5ba 100644 --- a/tests/test_scene.py +++ b/tests/test_scene.py @@ -299,6 +299,25 @@ def test_coordinate_system_retrieval(test_data_dir): assert len(all_cs["imageA"]) == 1 assert all_cs["imageA"][0].name == "physical" + world_cs = scene.get_coordinate_system(path="") + assert "" in world_cs + assert "imageA" not in world_cs + assert "imageB" not in world_cs + assert len(world_cs[""]) == 2 + + imageA_cs = scene.get_coordinate_system(path="imageA") + assert "imageA" in imageA_cs + assert len(imageA_cs["imageA"]) == 1 + assert "" not in imageA_cs + assert "imageB" not in imageA_cs + + all_physical_cs = scene.get_coordinate_system(name="physical") + assert "" not in all_physical_cs or all_physical_cs[""] == [] + assert "imageA" in all_physical_cs + assert len(all_physical_cs["imageA"]) == 1 + assert "imageB" in all_physical_cs + assert len(all_physical_cs["imageB"]) == 1 + def test_appending_scene(test_data_dir): """ @@ -475,3 +494,7 @@ def test_scene_with_displacements(test_data_dir): for image in dfield_img.images: assert image.axes_types["c"] == "displacement" assert dfield_img.metadata.coordinateSystems[0].axes[0].discrete + + +if __name__ == "__main__": + pytest.main([__file__]) \ No newline at end of file From df610fdde8001c0d3b7348e3c9278493d9d5a7f2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 7 Aug 2026 06:44:55 +0000 Subject: [PATCH 40/51] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_scene.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_scene.py b/tests/test_scene.py index 318ed5ba..2fa31333 100644 --- a/tests/test_scene.py +++ b/tests/test_scene.py @@ -497,4 +497,4 @@ def test_scene_with_displacements(test_data_dir): if __name__ == "__main__": - pytest.main([__file__]) \ No newline at end of file + pytest.main([__file__]) From f141081cdf1c0f6c00fedcb68e7dbd980dc2d2c3 Mon Sep 17 00:00:00 2001 From: Johannes Soltwedel <38459088+jo-mueller@users.noreply.github.com> Date: Fri, 7 Aug 2026 08:46:32 +0200 Subject: [PATCH 41/51] chore: use python 3.12 type declaration --- ome_zarr/writer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ome_zarr/writer.py b/ome_zarr/writer.py index aa3833ce..8d36c707 100644 --- a/ome_zarr/writer.py +++ b/ome_zarr/writer.py @@ -4,7 +4,7 @@ import warnings from collections.abc import Sequence from pathlib import Path -from typing import Any, TypeAlias +from typing import Any import dask.array as da import numpy as np @@ -20,7 +20,7 @@ LOGGER = logging.getLogger("ome_zarr.writer") ListOfArrayLike = list[da.Array] | list[np.ndarray] -ArrayLike: TypeAlias = da.Array | np.ndarray +type ArrayLike = da.Array | np.ndarray AxesType = str | list[str] | list[dict[str, str]] | None From 5847668ddea0618bb97053749ec7b4953c0d99be Mon Sep 17 00:00:00 2001 From: Johannes Soltwedel <38459088+jo-mueller@users.noreply.github.com> Date: Fri, 7 Aug 2026 08:51:48 +0200 Subject: [PATCH 42/51] feat: do not return empty list if no global cs found --- ome_zarr/classes/scene.py | 3 +++ tests/test_scene.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/ome_zarr/classes/scene.py b/ome_zarr/classes/scene.py index 563c95ed..c21ba3a1 100644 --- a/ome_zarr/classes/scene.py +++ b/ome_zarr/classes/scene.py @@ -163,6 +163,9 @@ def get_coordinate_system( if path is not None: matches = {k: v for k, v in matches.items() if k == path} + if "" in matches and len(matches[""]) == 0: + del matches[""] + return matches def _build_graph(self): diff --git a/tests/test_scene.py b/tests/test_scene.py index 2fa31333..8e4e8cf6 100644 --- a/tests/test_scene.py +++ b/tests/test_scene.py @@ -312,7 +312,7 @@ def test_coordinate_system_retrieval(test_data_dir): assert "imageB" not in imageA_cs all_physical_cs = scene.get_coordinate_system(name="physical") - assert "" not in all_physical_cs or all_physical_cs[""] == [] + assert "" not in all_physical_cs assert "imageA" in all_physical_cs assert len(all_physical_cs["imageA"]) == 1 assert "imageB" in all_physical_cs From e927f899460d81bf913336784f484ede3e5a7742 Mon Sep 17 00:00:00 2001 From: Johannes Soltwedel <38459088+jo-mueller@users.noreply.github.com> Date: Fri, 7 Aug 2026 09:02:49 +0200 Subject: [PATCH 43/51] feat: do not simplify on graph build --- ome_zarr/classes/scene.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ome_zarr/classes/scene.py b/ome_zarr/classes/scene.py index c21ba3a1..eb57018a 100644 --- a/ome_zarr/classes/scene.py +++ b/ome_zarr/classes/scene.py @@ -184,7 +184,7 @@ def _build_graph(self): zarr_context="", source_cs=source_cs[tf.input.path or ""][0], target_cs=target_cs[tf.output.path or ""][0], - ).simplify() + ) else: tnd_transform = self._ozmp_tf_to_tnd( tf, zarr_context="", source_cs=None, target_cs=None From 3e2c389469188e4e0497c9f0462c36c333092ffb Mon Sep 17 00:00:00 2001 From: Johannes Soltwedel <38459088+jo-mueller@users.noreply.github.com> Date: Fri, 7 Aug 2026 10:49:36 +0200 Subject: [PATCH 44/51] deps: require latest ozmp --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 31327b9d..d3d774c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ dependencies = [ "toolz", "rangehttpserver", "transformnd>=0.6.0", - "ome-zarr-models==1.8.0rc1", + "ome-zarr-models==1.8.0rc2", "Deprecated", ] classifiers = [ From 37d125a47a3c7d2488e7c0d250c515bf0de76113 Mon Sep 17 00:00:00 2001 From: Johannes Soltwedel <38459088+jo-mueller@users.noreply.github.com> Date: Fri, 7 Aug 2026 10:50:05 +0200 Subject: [PATCH 45/51] refactor: change arg order in `get_coordinate_system` function --- ome_zarr/classes/scene.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ome_zarr/classes/scene.py b/ome_zarr/classes/scene.py index eb57018a..cee4c61e 100644 --- a/ome_zarr/classes/scene.py +++ b/ome_zarr/classes/scene.py @@ -55,8 +55,8 @@ def __init__( Write the scene to OME-Zarr format in the specified store. from_ome_zarr(store: StoreLike) Load an existing scene from OME-Zarr format in the specified store. - get_coordinate_system(name: str | None, path: str | None = None) - Retrieve a coordinate system by name or path. + get_coordinate_system(path: str | None, name: str | None = None) + Retrieve a coordinate system by path or name. If neither is @@ -121,7 +121,7 @@ def metadata(self, value: SceneAttrs) -> None: self._build_graph() def get_coordinate_system( - self, name: str | None = None, path: str | None = None + self, path: str | None = None, name: str | None = None ) -> dict[str, list[CoordinateSystem]]: """ Retrieve a coordinate system by name and optional path. @@ -177,8 +177,8 @@ def _build_graph(self): f"Coordinate transformation {tf} is missing input or output information." ) if tf.type == "sequence": - source_cs = self.get_coordinate_system(tf.input.name, tf.input.path) - target_cs = self.get_coordinate_system(tf.output.name, tf.output.path) + source_cs = self.get_coordinate_system(tf.input.path, tf.input.name) + target_cs = self.get_coordinate_system(tf.output.path, tf.output.name) tnd_transform = self._ozmp_tf_to_tnd( tf, zarr_context="", From 1b5bcee13fc7bd7b7b4f756d4f94ebae2124f606 Mon Sep 17 00:00:00 2001 From: Johannes Soltwedel <38459088+jo-mueller@users.noreply.github.com> Date: Mon, 10 Aug 2026 13:00:07 +0200 Subject: [PATCH 46/51] feat: simplify tnd conversion --- ome_zarr/classes/scene.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/ome_zarr/classes/scene.py b/ome_zarr/classes/scene.py index cee4c61e..06e532bf 100644 --- a/ome_zarr/classes/scene.py +++ b/ome_zarr/classes/scene.py @@ -176,20 +176,20 @@ def _build_graph(self): raise ValueError( f"Coordinate transformation {tf} is missing input or output information." ) - if tf.type == "sequence": - source_cs = self.get_coordinate_system(tf.input.path, tf.input.name) - target_cs = self.get_coordinate_system(tf.output.path, tf.output.name) - tnd_transform = self._ozmp_tf_to_tnd( - tf, - zarr_context="", - source_cs=source_cs[tf.input.path or ""][0], - target_cs=target_cs[tf.output.path or ""][0], - ) - else: - tnd_transform = self._ozmp_tf_to_tnd( - tf, zarr_context="", source_cs=None, target_cs=None - ) + source_cs = self.get_coordinate_system(tf.input.path, tf.input.name) + source_cs = source_cs[tf.input.path or ""][0] + + # convert to transformnd transform and add to graph + target_cs = self.get_coordinate_system(tf.output.path, tf.output.name) + target_cs = target_cs[tf.output.path or ""][0] + tnd_transform = self._ozmp_tf_to_tnd( + tf, + zarr_context="", + source_cs=source_cs, + target_cs=target_cs, + ) self._graph.add_transform(tnd_transform) + # Add inverse edge if transform is invertible inverse = tnd_transform.invert() if inverse is not None: From dccfcf541ddcd7285960707a890a8d5c9fb5ef42 Mon Sep 17 00:00:00 2001 From: Johannes Soltwedel <38459088+jo-mueller@users.noreply.github.com> Date: Mon, 10 Aug 2026 13:00:36 +0200 Subject: [PATCH 47/51] feat: make sure rotation matrix is passed correctly as square affine --- ome_zarr/classes/scene.py | 6 ++++-- tests/test_scene.py | 6 ++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/ome_zarr/classes/scene.py b/ome_zarr/classes/scene.py index 06e532bf..68d38543 100644 --- a/ome_zarr/classes/scene.py +++ b/ome_zarr/classes/scene.py @@ -496,8 +496,10 @@ def _ozmp_tf_to_tnd( ) elif transform.type == "rotation": - tnd_transform = tnd.transforms.Affine.from_linear_map( - transform.rotation, spaces=spaces + affine_matrix = np.eye(len(transform.rotation) + 1) + affine_matrix[:-1, :-1] = transform.rotation + tnd_transform = tnd.transforms.Affine( + affine_matrix, spaces=spaces ) elif transform.type == "byDimension": diff --git a/tests/test_scene.py b/tests/test_scene.py index 8e4e8cf6..dd35c80b 100644 --- a/tests/test_scene.py +++ b/tests/test_scene.py @@ -119,6 +119,12 @@ def test_create_scene_without_coordinate_systems(test_data_dir, transform): # traverse graph tf = scene._graph.get_sequence((img_a.name, "physical"), (img_b.name, "physical")) + # make sure transform matrix is square + # bydimension transforms cannot easily be expressed as matrix + if transform["type"] != "byDimension": + affine = tf.simplify().to_affine().matrix + assert affine.shape[0] == affine.shape[1] + # check that the transform graph can be traversed (i.e. transform is not None) assert tf is not None From d3566567c909db78a37f778602819bb3daf08066 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 11:00:54 +0000 Subject: [PATCH 48/51] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- ome_zarr/classes/scene.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ome_zarr/classes/scene.py b/ome_zarr/classes/scene.py index 68d38543..1b1cbc97 100644 --- a/ome_zarr/classes/scene.py +++ b/ome_zarr/classes/scene.py @@ -498,9 +498,7 @@ def _ozmp_tf_to_tnd( elif transform.type == "rotation": affine_matrix = np.eye(len(transform.rotation) + 1) affine_matrix[:-1, :-1] = transform.rotation - tnd_transform = tnd.transforms.Affine( - affine_matrix, spaces=spaces - ) + tnd_transform = tnd.transforms.Affine(affine_matrix, spaces=spaces) elif transform.type == "byDimension": sub_transformations = transform.transformations From 8d34fd1d699c9c19ca2c26cc1ee1d3366b5894b4 Mon Sep 17 00:00:00 2001 From: Johannes Soltwedel <38459088+jo-mueller@users.noreply.github.com> Date: Thu, 13 Aug 2026 23:25:15 +0200 Subject: [PATCH 49/51] refactor: query coordinate system by (path, name) --- ome_zarr/classes/scene.py | 53 ++++++++++++++++----------------------- tests/test_scene.py | 44 ++++++++++++++------------------ 2 files changed, 40 insertions(+), 57 deletions(-) diff --git a/ome_zarr/classes/scene.py b/ome_zarr/classes/scene.py index 1b1cbc97..6d399e46 100644 --- a/ome_zarr/classes/scene.py +++ b/ome_zarr/classes/scene.py @@ -122,9 +122,9 @@ def metadata(self, value: SceneAttrs) -> None: def get_coordinate_system( self, path: str | None = None, name: str | None = None - ) -> dict[str, list[CoordinateSystem]]: + ) -> dict[tuple[str, str], CoordinateSystem]: """ - Retrieve a coordinate system by name and optional path. + Retrieve coordinate systems. Parameters ---------- @@ -138,34 +138,23 @@ def get_coordinate_system( Returns ------- - dict[str, list[CoordinateSystem]] - A dictionary of matching CoordinateSystem objects keyed by their context - (e.g., "" for top-level, image key for image-level). + dict[tuple[str, str], CoordinateSystem] + A dictionary of matching CoordinateSystem objects keyed by (path, name) tuples. Empty if no match is found. """ - # find top-level matches - top_level_matches = {} - if len(self.coordinate_systems) > 0: - top_level_matches = {"": list(self.coordinate_systems)} - - # find image-level matches - image_matches = {} - for key, img in self.images.items(): - image_matches.update({key: list(img.metadata.coordinateSystems)}) - - matches = {**top_level_matches, **image_matches} - - if name is not None: - matches = { - k: [cs for cs in v if cs.name == name] for k, v in matches.items() - } - - if path is not None: - matches = {k: v for k, v in matches.items() if k == path} - - if "" in matches and len(matches[""]) == 0: - del matches[""] - + matches = {} + + # Add top-level coordinate systems + for cs in self.coordinate_systems: + if (name is None or cs.name == name) and (path is None or path == ""): + matches[("", cs.name)] = cs + + # Add image-level coordinate systems + for img_path, img in self.images.items(): + for cs in img.metadata.coordinateSystems: + if (name is None or cs.name == name) and (path is None or path == img_path): + matches[(img_path, cs.name)] = cs + return matches def _build_graph(self): @@ -176,12 +165,12 @@ def _build_graph(self): raise ValueError( f"Coordinate transformation {tf} is missing input or output information." ) - source_cs = self.get_coordinate_system(tf.input.path, tf.input.name) - source_cs = source_cs[tf.input.path or ""][0] + source_cs_dict = self.get_coordinate_system(tf.input.path, tf.input.name) + source_cs = source_cs_dict[(tf.input.path or "", tf.input.name)] # convert to transformnd transform and add to graph - target_cs = self.get_coordinate_system(tf.output.path, tf.output.name) - target_cs = target_cs[tf.output.path or ""][0] + target_cs_dict = self.get_coordinate_system(tf.output.path, tf.output.name) + target_cs = target_cs_dict[(tf.output.path or "", tf.output.name)] tnd_transform = self._ozmp_tf_to_tnd( tf, zarr_context="", diff --git a/tests/test_scene.py b/tests/test_scene.py index dd35c80b..2c8388c5 100644 --- a/tests/test_scene.py +++ b/tests/test_scene.py @@ -290,39 +290,33 @@ def test_coordinate_system_retrieval(test_data_dir): # number and instances of coordinate systems all_cs = scene.get_coordinate_system() - # the dict keys correspond to the names of the images - # or top-level if the name is "" - assert "" in all_cs - assert len(all_cs[""]) == 2 - assert all_cs[""][0].name == "world" - assert all_cs[""][1].name == "world2" + # Keys are (path, name) tuples + assert ("", "world") in all_cs + assert ("", "world2") in all_cs + assert all_cs[("", "world")].name == "world" + assert all_cs[("", "world2")].name == "world2" - assert "imageB" in all_cs - assert len(all_cs["imageB"]) == 1 - assert all_cs["imageB"][0].name == "physical" + assert ("imageB", "physical") in all_cs + assert all_cs[("imageB", "physical")].name == "physical" - assert "imageA" in all_cs - assert len(all_cs["imageA"]) == 1 - assert all_cs["imageA"][0].name == "physical" + assert ("imageA", "physical") in all_cs + assert all_cs[("imageA", "physical")].name == "physical" world_cs = scene.get_coordinate_system(path="") - assert "" in world_cs - assert "imageA" not in world_cs - assert "imageB" not in world_cs - assert len(world_cs[""]) == 2 + assert ("", "world") in world_cs + assert ("", "world2") in world_cs + assert ("imageA", "physical") not in world_cs + assert ("imageB", "physical") not in world_cs imageA_cs = scene.get_coordinate_system(path="imageA") - assert "imageA" in imageA_cs - assert len(imageA_cs["imageA"]) == 1 - assert "" not in imageA_cs - assert "imageB" not in imageA_cs + assert ("imageA", "physical") in imageA_cs + assert ("", "world") not in imageA_cs + assert ("imageB", "physical") not in imageA_cs all_physical_cs = scene.get_coordinate_system(name="physical") - assert "" not in all_physical_cs - assert "imageA" in all_physical_cs - assert len(all_physical_cs["imageA"]) == 1 - assert "imageB" in all_physical_cs - assert len(all_physical_cs["imageB"]) == 1 + assert ("", "world") not in all_physical_cs + assert ("imageA", "physical") in all_physical_cs + assert ("imageB", "physical") in all_physical_cs def test_appending_scene(test_data_dir): From ed6e47ed6877c663b314dadfce1bb348326d7c54 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 13 Aug 2026 21:25:31 +0000 Subject: [PATCH 50/51] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- ome_zarr/classes/scene.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/ome_zarr/classes/scene.py b/ome_zarr/classes/scene.py index 6d399e46..bee7b3a9 100644 --- a/ome_zarr/classes/scene.py +++ b/ome_zarr/classes/scene.py @@ -143,18 +143,20 @@ def get_coordinate_system( Empty if no match is found. """ matches = {} - + # Add top-level coordinate systems for cs in self.coordinate_systems: if (name is None or cs.name == name) and (path is None or path == ""): matches[("", cs.name)] = cs - + # Add image-level coordinate systems for img_path, img in self.images.items(): for cs in img.metadata.coordinateSystems: - if (name is None or cs.name == name) and (path is None or path == img_path): + if (name is None or cs.name == name) and ( + path is None or path == img_path + ): matches[(img_path, cs.name)] = cs - + return matches def _build_graph(self): From f7181623d3af8be0495fc9752050813a0d962a40 Mon Sep 17 00:00:00 2001 From: Johannes Soltwedel <38459088+jo-mueller@users.noreply.github.com> Date: Tue, 18 Aug 2026 16:46:30 +0200 Subject: [PATCH 51/51] refactor: rename arg to `coordinate_displacements` --- ome_zarr/classes/scene.py | 6 +++--- tests/test_scene.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ome_zarr/classes/scene.py b/ome_zarr/classes/scene.py index bee7b3a9..ea7628d0 100644 --- a/ome_zarr/classes/scene.py +++ b/ome_zarr/classes/scene.py @@ -23,7 +23,7 @@ def __init__( images: list[OMEZarrMultiscale] | dict[str, OMEZarrMultiscale], coordinate_transformations: Sequence[AnyTransform] | list[dict[str, Any]], coordinate_systems: Sequence[CoordinateSystem] | Sequence[dict[str, Any]] = (), - coordinates_displacements: dict[str, OMEZarrMultiscale] | None = None, + coordinate_displacements: dict[str, OMEZarrMultiscale] | None = None, ): """ Parameters @@ -67,7 +67,7 @@ def __init__( else: self.images = images - self.coordinates_displacements = coordinates_displacements + self.coordinates_displacements = coordinate_displacements # metadata is the single source of truth self._metadata = SceneAttrs( @@ -344,7 +344,7 @@ def from_ome_zarr(cls, store: StoreLike): coordinate_systems=( coordinate_systems if coordinate_systems is not None else () ), - coordinates_displacements=coordinates_displacements, + coordinate_displacements=coordinates_displacements, ) return scene diff --git a/tests/test_scene.py b/tests/test_scene.py index 2c8388c5..45aefec8 100644 --- a/tests/test_scene.py +++ b/tests/test_scene.py @@ -465,7 +465,7 @@ def test_scene_with_displacements(test_data_dir): scene = OMEZarrScene( images=[img_a_ms, img_b_ms], coordinate_transformations=[transform], - coordinates_displacements={"displacementField": dfield_img_ms}, + coordinate_displacements={"displacementField": dfield_img_ms}, ) save_grp = str(test_data_dir / "test_scene_displacement.zarr")