From 0257c80b59d3e2cffb6eb15629e22a7d975237c7 Mon Sep 17 00:00:00 2001 From: stanbot8 Date: Fri, 24 Jul 2026 14:21:45 -0700 Subject: [PATCH] fix: write OME channel labels as strings --- src/spatialdata/_io/_utils.py | 15 ++++++++++----- src/spatialdata/_io/io_raster.py | 5 +++++ tests/io/test_metadata.py | 19 +++++++++++++++++++ 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/spatialdata/_io/_utils.py b/src/spatialdata/_io/_utils.py index fa5af1dd7..ec5a73c24 100644 --- a/src/spatialdata/_io/_utils.py +++ b/src/spatialdata/_io/_utils.py @@ -153,15 +153,20 @@ def overwrite_channel_names(group: zarr.Group, element: DataArray | DataTree) -> else: channel_names = element["scale0"]["image"].coords["c"].data.tolist() - channel_metadata = [{"label": name} for name in channel_names] + attrs = group.attrs.asdict() + channel_metadata = [{"label": str(name)} for name in channel_names] + spatialdata_attrs = attrs.get("spatialdata_attrs", {}) + spatialdata_attrs["channel_names"] = channel_names + attrs["spatialdata_attrs"] = spatialdata_attrs # We don't use the ome-zarr load node API, and ome-zarr-py >= 0.18 emits no `omero` block, so default to empty. - omero_meta = group.attrs.get("omero") or group.attrs.get("ome", {}).get("omero") or {} + omero_meta = attrs.get("omero") or attrs.get("ome", {}).get("omero") or {} omero_meta["channels"] = channel_metadata - if ome_meta := group.attrs.get("ome", None): + if ome_meta := attrs.get("ome", None): ome_meta["omero"] = omero_meta - group.attrs["ome"] = ome_meta + attrs["ome"] = ome_meta else: - group.attrs["omero"] = omero_meta + attrs["omero"] = omero_meta + group.attrs.put(attrs) def _write_metadata( diff --git a/src/spatialdata/_io/io_raster.py b/src/spatialdata/_io/io_raster.py index 276f016bd..ab7480224 100644 --- a/src/spatialdata/_io/io_raster.py +++ b/src/spatialdata/_io/io_raster.py @@ -168,6 +168,9 @@ def _read_multiscale( nodes: list[Node] = [] image_loc = ZarrLocation(store, fmt=reader_format) if exists := image_loc.exists(): + spatialdata_channels = ( + zarr.open_group(store=image_loc.store, mode="r").attrs.get(ATTRS_KEY, {}).get("channel_names") + ) image_reader = Reader(image_loc)() image_nodes = list(image_reader) nodes = _get_multiscale_nodes(image_nodes, nodes) @@ -213,6 +216,8 @@ def _read_multiscale( channels = [d["label"] for d in legacy_channels_metadata["channels"]] if omero_metadata is not None: channels = [d["label"] for d in omero_metadata["channels"]] + if spatialdata_channels is not None: + channels = spatialdata_channels axes = [i["name"] for i in node.metadata["axes"]] if len(datasets) > 1: arrays = [node.load(Multiscales).array(resolution=d) for d in datasets] diff --git a/tests/io/test_metadata.py b/tests/io/test_metadata.py index c07f026c0..777f4b870 100644 --- a/tests/io/test_metadata.py +++ b/tests/io/test_metadata.py @@ -5,6 +5,7 @@ import tempfile import pytest +import zarr from spatialdata import SpatialData, read_zarr from spatialdata._io._utils import _is_element_self_contained @@ -117,6 +118,24 @@ def test_save_transformations_incremental(element_name, full_sdata, caplog): # test io for channel names +@pytest.mark.parametrize("expected", [[0, 1, 2], ["0", "1", "2"]], ids=["integers", "numeric-strings"]) +def test_channel_names_store_string_labels_without_changing_coordinates( + images: SpatialData, tmp_path, expected: list[int] | list[str] +) -> None: + path = tmp_path / "sdata.zarr" + image = images["image2d_xarray"].assign_coords(c=expected) + SpatialData(images={"image": image}).write(path) + root = zarr.open_group(path, mode="r") + attrs = root["images"]["image"].attrs + omero = attrs.get("omero") or attrs["ome"]["omero"] + labels = [channel["label"] for channel in omero["channels"]] + channel_names = attrs["spatialdata_attrs"]["channel_names"] + root.store.close() + assert labels == [str(value) for value in expected] + assert channel_names == expected + assert get_channel_names(SpatialData.read(path)["image"]) == expected + + @pytest.mark.parametrize("write", ["overwrite", "write", "no"]) def test_save_channel_names_incremental(images: SpatialData, write: str) -> None: old_channels2d = get_channel_names(images["image2d"])