Skip to content
Open
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
68b26bd
investigate dropping ome-zarr dependency
will-moore Dec 13, 2024
29fb252
Handle more channel metadata
will-moore Dec 13, 2024
076cc7c
Handle no channel_axis
will-moore Dec 16, 2024
4fc57d3
Remove ome_zarr imports and unused code from _reader.py
will-moore Jan 6, 2025
e4cda75
Read OME/METADATA.xml for bioformats2raw.layout and open all series i…
will-moore Jan 6, 2025
dd63922
Support Plates
will-moore May 22, 2025
a92c2a1
Initial support of PlateLabels
will-moore Jun 9, 2025
4f6360f
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 9, 2025
af26355
Add Labels support and fix some tests
will-moore Sep 1, 2025
db766d0
Handle reading Labels and Label -> start at parent Image
will-moore Sep 1, 2025
e338190
mypy fixes
will-moore Sep 1, 2025
c6ff3b1
tests_require = ome-zarr in setup.cfg
will-moore Sep 1, 2025
e34c881
Remove py3.9 and py3.10 from test matrix
will-moore Sep 1, 2025
89a5ccd
Try to fix ome_zarr ImportError in tests
will-moore Sep 1, 2025
b95463b
Add ome-zarr to install_requires to test
will-moore Sep 1, 2025
31d9216
contrast_limits is list instead of tuple
will-moore Sep 1, 2025
067097c
skip qt_viewer napari test
will-moore Sep 1, 2025
eb27e2f
Move ome-zarr test dep to tox.ini
will-moore Sep 1, 2025
c8c87c9
Merge remote-tracking branch 'origin/main' into investigate_ome_zarr_…
will-moore Sep 2, 2025
123fafe
Fix some tests
will-moore Sep 3, 2025
0e36eae
Fix other tests
will-moore Sep 4, 2025
4dd88d0
Handle scale and transform on 1st multiscales.dataset
will-moore Sep 22, 2025
15147a1
Ensure that label metadata doesn't include channel_axis
will-moore Sep 22, 2025
9462088
Handle KeyError for missing Wells
will-moore Sep 22, 2025
6cd62c3
Handle label colors
will-moore Sep 22, 2025
078e93f
Handle label properties
will-moore Sep 22, 2025
161aa7a
Handle axes from v0.1 - v0.3
will-moore Oct 29, 2025
4439b9f
Remove most print() statements
will-moore Oct 29, 2025
0c00f9d
Fix display of PlateLabels with correct metadata
will-moore Oct 29, 2025
ba7e50f
Fix typo pth_to_image
will-moore Oct 30, 2025
da61ab8
Merge branch 'main' into investigate_ome_zarr_py_alternative
will-moore Mar 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .isort.cfg
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
[settings]
known_third_party = napari,numpy,ome_zarr,pytest,setuptools,zarr
known_third_party = dask,napari,numpy,ome_zarr,pytest,setuptools,zarr
193 changes: 12 additions & 181 deletions napari_ome_zarr/_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,15 @@
It implements the ``napari_get_reader`` hook specification, (to create a reader plugin).
"""

import logging
import warnings
from importlib.metadata import version
from typing import Any, Dict, Iterator, List, Optional
from typing import Callable

import numpy as np
from napari.utils.colormaps import AVAILABLE_COLORMAPS, Colormap
from ome_zarr.io import parse_url
from ome_zarr.reader import Label, Node, Reader
from ome_zarr.types import LayerData, PathLike, ReaderFunction
import zarr

LOGGER = logging.getLogger("napari_ome_zarr.reader")
from .ome_zarr_reader import read_ome_zarr

METADATA_KEYS = ("name", "visible", "contrast_limits", "colormap", "metadata")

# major and minor versions as int
napari_version = tuple(map(int, list(version("napari").split(".")[:2])))


def napari_get_reader(path: PathLike) -> Optional[ReaderFunction]:
def napari_get_reader(path: str | list) -> Callable | None:
"""Returns a reader for supported paths that include IDR ID.

- URL of the form: https://uk1s3.embassy.ebi.ac.uk/idr/zarr/v0.1/ID.zarr/
Expand All @@ -31,172 +20,14 @@ def napari_get_reader(path: PathLike) -> Optional[ReaderFunction]:
if len(path) > 1:
warnings.warn("more than one path is not currently supported")
path = path[0]
zarr = parse_url(path)
if zarr:
reader = Reader(zarr)
return transform(reader())
# Ignoring this path
return None


def transform_properties(
props: Optional[Dict[str, Dict]] = None,
) -> Optional[Dict[str, List]]:
"""
Transform properties

Transform a dict of {label_id : {key: value, key2: value2}}
with a key for every LABEL
into a dict of a key for every VALUE, with a list of values for each
.. code::

{
"index": [1381342, 1381343...]
"omero:roiId": [1381342, 1381343...],
"omero:shapeId": [1682567, 1682567...]
}

"""
if props is None:
group = None
try:
group = zarr.open_group(path, mode="r")
except Exception as e:
warnings.warn(f"Failed to open Zarr group: {e}")
return None

properties: Dict[str, List] = {}

# First, create lists for all existing keys...
for label_id, props_dict in props.items():
for key in props_dict.keys():
properties[key] = []

keys = list(properties.keys())

properties["index"] = []
for label_id, props_dict in props.items():
properties["index"].append(label_id)
# ...in case some objects don't have all the keys
for key in keys:
properties[key].append(props_dict.get(key, None))
return properties


def transform_scale(
node_metadata: Dict, metadata: Dict, channel_axis: Optional[int]
) -> None:
"""
e.g. transformation is {"scale": [0.2, 0.06, 0.06]}
Get a list of these for each level in data. Just use first?
"""
if "coordinateTransformations" in node_metadata:
level_0_transforms = node_metadata["coordinateTransformations"][0]
for transf in level_0_transforms:
if "scale" in transf:
scale = transf["scale"]
if channel_axis is not None:
scale.pop(channel_axis)
metadata["scale"] = tuple(scale)
if "translation" in transf:
translate = transf["translation"]
if channel_axis is not None:
translate.pop(channel_axis)
metadata["translate"] = tuple(translate)


def _match_colors_to_available_colormap(custom_cmap: Colormap) -> Colormap:
"""Helper function to match Colormap to an existing napari Colormap.

If the colormap matches, return the specific napari Colormap, otherwise return the
the original Colormap.
"""
for available_cmap in AVAILABLE_COLORMAPS.values():
if (
np.array_equal(available_cmap.controls, custom_cmap.controls)
and np.array_equal(available_cmap.colors, custom_cmap.colors)
and available_cmap.interpolation == custom_cmap.interpolation
):
custom_cmap = available_cmap
break

return custom_cmap


def transform(nodes: Iterator[Node]) -> Optional[ReaderFunction]:
def f(*args: Any, **kwargs: Any) -> List[LayerData]:
results: List[LayerData] = list()

for node in nodes:
data: List[Any] = node.data
metadata: Dict[str, Any] = {}
if data is None or len(data) < 1:
LOGGER.debug("skipping non-data %s", node)
else:
LOGGER.debug("transforming %s", node)
LOGGER.debug("node.metadata: %s", node.metadata)

layer_type: str = "image"
channel_axis = None
try:
ch_types = [axis["type"] for axis in node.metadata["axes"]]
if "channel" in ch_types:
channel_axis = ch_types.index("channel")
except Exception:
LOGGER.error("Error reading axes: Please update ome-zarr")
raise

transform_scale(node.metadata, metadata, channel_axis)

if node.load(Label):
layer_type = "labels"
for x in METADATA_KEYS:
if x in node.metadata:
metadata[x] = node.metadata[x]
elif x == "colormap" and node.metadata["color"]:
# key changed 'color' -> 'colormap' in napari 0.5
if napari_version >= (0, 5):
metadata["colormap"] = node.metadata["color"]
else:
metadata["color"] = node.metadata["color"]
if channel_axis is not None:
data = [
np.squeeze(level, axis=channel_axis) for level in node.data
]
else:
cms = node.metadata.get("colormap", [])
for idx, cm in enumerate(cms):
if not isinstance(cm, Colormap):
cms[idx] = Colormap(cm)
# Try to match colormap to an existing napari colormap
cms[idx] = _match_colors_to_available_colormap(cms[idx])

if channel_axis is not None:
# multi-channel; Copy known metadata values
metadata["channel_axis"] = channel_axis
for x in METADATA_KEYS:
if x in node.metadata:
metadata[x] = node.metadata[x]
# overwrite 'name' if we have 'channel_names'
if "channel_names" in node.metadata:
metadata["name"] = node.metadata["channel_names"]
else:
# single channel image, so metadata just needs
# single items (not lists)
for x in METADATA_KEYS:
if x in node.metadata:
try:
metadata[x] = node.metadata[x][0]
except Exception:
pass
# overwrite 'name' if we have 'channel_names'
if "channel_names" in node.metadata:
if len(node.metadata["channel_names"]) > 0:
metadata["name"] = node.metadata["channel_names"][0]

properties = transform_properties(node.metadata.get("properties"))
if properties is not None:
metadata["properties"] = properties

rv: LayerData = (data, metadata, layer_type)
LOGGER.debug("Transformed: %s", rv)
results.append(rv)

return results

return f
if group is not None:
return read_ome_zarr(group)
return None
17 changes: 9 additions & 8 deletions napari_ome_zarr/_tests/test_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,8 @@
from ome_zarr.data import astronaut, create_zarr
from ome_zarr.writer import write_image, write_plate_metadata, write_well_metadata

from napari_ome_zarr._reader import (
_match_colors_to_available_colormap,
napari_get_reader,
)
from napari_ome_zarr._reader import napari_get_reader
from napari_ome_zarr.ome_zarr_reader import _match_colors_to_available_colormap


class TestNapari:
Expand Down Expand Up @@ -98,18 +96,21 @@ def assert_layer(self, layer_data):

@pytest.mark.parametrize("path", ["path_3d", "path_2d"])
def test_image(self, path):
layers = napari_get_reader(str(getattr(self, path)))()
pth_to_image = str(getattr(self, path))
print(f"test_image {pth_to_image}")
layers = napari_get_reader(pth_to_image)()
self.assert_layers(layers, True, False, path)

def test_labels(self):
filename = str(self.path_3d / "labels")
print(f"test_labels {filename}")
layers = napari_get_reader(filename)()
self.assert_layers(layers, False, True)
self.assert_layers(layers, True, False)

def test_label(self):
filename = str(self.path_3d / "labels" / "astronaut")
layers = napari_get_reader(filename)()
self.assert_layers(layers, False, True)
self.assert_layers(layers, True, False)


@pytest.mark.parametrize(
Expand Down Expand Up @@ -174,7 +175,7 @@ def test_read_plate(self):
)

# check plate compared with an Image
well_path = self.plate_path / self.well_paths[0]
well_path = self.plate_path / self.well_paths[0] / "0"
img_layers = napari_get_reader(str(well_path))()
assert len(img_layers) == 1
img_layer = img_layers[0]
Expand Down
Loading