diff --git a/src/bfio/backends.py b/src/bfio/backends.py index 9c7b227..ae7094b 100644 --- a/src/bfio/backends.py +++ b/src/bfio/backends.py @@ -1661,6 +1661,35 @@ def __init__(self, frontend): self._axes_list = [] + def _get_axis_info(self): + shape_len = len(self._rdr.shape) + if shape_len == 5: + self._axes_list = ["t", "c", "z", "y", "x"] + else: + try: + # OME-NGFF 0.5: multiscales is under attrs["ome"]["multiscales"] + ome_meta = self._root.attrs["ome"] + axes_metadata = ome_meta["multiscales"][0]["axes"] + for axes in axes_metadata: + self._axes_list.append(axes["name"]) + except (AttributeError, KeyError): + # Fall back to v2 location (attrs["multiscales"]) + try: + axes_metadata = self._root.attrs["multiscales"][0]["axes"] + for axes in axes_metadata: + self._axes_list.append(axes["name"]) + except (AttributeError, KeyError): + self.logger.warning( + "Unable to find multiscales metadata. Z, C and T " + + "dimensions might be incorrect." + ) + if shape_len == 4: + self._axes_list = ["c", "z", "y", "x"] + elif shape_len == 3: + self._axes_list = ["z", "y", "x"] + elif shape_len == 2: + self._axes_list = ["y", "x"] + def read_metadata(self): self.logger.debug("read_metadata(): Reading metadata (v3)...") # First try OME metadata (same as v2) @@ -1739,14 +1768,23 @@ def _init_writer(self): with open(metadata_path, "w") as fw: fw.write(str(self.frontend._metadata.to_xml())) - self._root.attrs["multiscales"] = [ - { - "version": "0.1", - "name": self.frontend._file_path.name, - "datasets": [{"path": "0"}], - "metadata": {"method": "mean"}, - } - ] + self._root.attrs["ome"] = { + "version": "0.5", + "multiscales": [ + { + "name": self.frontend._file_path.name, + "axes": [ + {"name": "t", "type": "time"}, + {"name": "c", "type": "channel"}, + {"name": "z", "type": "space"}, + {"name": "y", "type": "space"}, + {"name": "x", "type": "space"}, + ], + "datasets": [{"path": "0"}], + "metadata": {"method": "mean"}, + } + ], + } # Check for existing arrays when appending if self.frontend.append is True: diff --git a/tests/test_zarr_v3.py b/tests/test_zarr_v3.py index 7a6fc70..18a4f66 100644 --- a/tests/test_zarr_v3.py +++ b/tests/test_zarr_v3.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- """Tests for zarr v3 support in bfio using unittest.""" + import json import tempfile import unittest @@ -97,6 +98,28 @@ def test_write_v3_ome_metadata(self): metadata_path = out_path / "OME" / "METADATA.ome.xml" self.assertTrue(metadata_path.exists()) + def test_write_v3_multiscales_under_ome_key(self): + """Verify v3 writer stores multiscales under 'ome' key per NGFF 0.5.""" + from bfio import BioWriter + + with tempfile.TemporaryDirectory() as tmp: + out_path = Path(tmp) / "output.zarr" + data = numpy.random.randint(0, 255, (128, 128), dtype=numpy.uint8) + + bw = BioWriter(out_path, backend="zarr3", X=128, Y=128, dtype=numpy.uint8) + bw[:128, :128, 0, 0, 0] = data + bw.close() + + with open(out_path / "zarr.json") as f: + meta = json.load(f) + attrs = meta.get("attributes", {}) + self.assertIn("ome", attrs) + self.assertIn("multiscales", attrs["ome"]) + self.assertEqual(attrs["ome"]["version"], "0.5") + axes = attrs["ome"]["multiscales"][0]["axes"] + axis_names = [a["name"] for a in axes] + self.assertEqual(axis_names, ["t", "c", "z", "y", "x"]) + class TestZarr3Reader(unittest.TestCase): """Test Zarr3Reader reads v3 format stores.""" @@ -147,6 +170,40 @@ def test_read_v3_dimensions(self): self.assertEqual(br.T, 1) br.close() + def test_read_v3_axis_info_from_ome_key(self): + """Verify Zarr3Reader reads axis info from ome.multiscales per NGFF 0.5.""" + from bfio import BioReader, BioWriter + + with tempfile.TemporaryDirectory() as tmp: + out_path = Path(tmp) / "axis_test.zarr" + bw = BioWriter( + out_path, + backend="zarr3", + X=64, + Y=64, + Z=2, + C=3, + T=1, + dtype=numpy.uint8, + ) + data = numpy.zeros((64, 64, 2, 3, 1), dtype=numpy.uint8) + bw.write(data) + bw.close() + + # Verify the zarr.json has ome.multiscales with axes + with open(out_path / "zarr.json") as f: + meta = json.load(f) + axes = meta["attributes"]["ome"]["multiscales"][0]["axes"] + axis_names = [a["name"] for a in axes] + self.assertEqual(axis_names, ["t", "c", "z", "y", "x"]) + + # Verify reader correctly parses dimensions from ome key + br = BioReader(out_path, backend="zarr3") + self.assertEqual(br.Z, 2) + self.assertEqual(br.C, 3) + self.assertEqual(br.T, 1) + br.close() + class TestZarrAutoBackendSelection(unittest.TestCase): """Test auto-detection picks correct backend."""