napari-ome-arrow 0.0.4__py3-none-any.whl → 0.0.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- napari_ome_arrow/_reader.py +206 -322
- napari_ome_arrow/_reader_infer.py +233 -0
- napari_ome_arrow/_reader_napari.py +107 -0
- napari_ome_arrow/_reader_omearrow.py +474 -0
- napari_ome_arrow/_reader_stack.py +711 -0
- napari_ome_arrow/_reader_types.py +11 -0
- napari_ome_arrow/_version.py +2 -2
- napari_ome_arrow/napari.yaml +3 -0
- {napari_ome_arrow-0.0.4.dist-info → napari_ome_arrow-0.0.5.dist-info}/METADATA +24 -5
- napari_ome_arrow-0.0.5.dist-info/RECORD +15 -0
- {napari_ome_arrow-0.0.4.dist-info → napari_ome_arrow-0.0.5.dist-info}/WHEEL +1 -1
- napari_ome_arrow-0.0.4.dist-info/RECORD +0 -10
- {napari_ome_arrow-0.0.4.dist-info → napari_ome_arrow-0.0.5.dist-info}/entry_points.txt +0 -0
- {napari_ome_arrow-0.0.4.dist-info → napari_ome_arrow-0.0.5.dist-info}/licenses/LICENSE +0 -0
- {napari_ome_arrow-0.0.4.dist-info → napari_ome_arrow-0.0.5.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""Inference helpers for determining reader mode and source support."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from ome_arrow.core import OMEArrow
|
|
10
|
+
from ome_arrow.ingest import from_stack_pattern_path
|
|
11
|
+
|
|
12
|
+
from ._reader_omearrow import _read_vortex_scalar
|
|
13
|
+
|
|
14
|
+
LOGGER = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _normalize_image_type(value: Any) -> str | None:
|
|
18
|
+
"""Normalize an image type hint to "image" or "labels".
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
value: Raw hint value from metadata or user input.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
"image", "labels", or None if it cannot be inferred.
|
|
25
|
+
"""
|
|
26
|
+
if value is None:
|
|
27
|
+
return None
|
|
28
|
+
# Normalize arbitrary input to a comparable string token.
|
|
29
|
+
text = str(value).strip().lower()
|
|
30
|
+
if not text:
|
|
31
|
+
return None
|
|
32
|
+
if text in {"image", "intensity", "raw"} or "image" in text:
|
|
33
|
+
return "image"
|
|
34
|
+
if (
|
|
35
|
+
text
|
|
36
|
+
in {
|
|
37
|
+
"labels",
|
|
38
|
+
"label",
|
|
39
|
+
"mask",
|
|
40
|
+
"masks",
|
|
41
|
+
"segmentation",
|
|
42
|
+
"seg",
|
|
43
|
+
"outlines",
|
|
44
|
+
"outline",
|
|
45
|
+
}
|
|
46
|
+
or "label" in text
|
|
47
|
+
or "mask" in text
|
|
48
|
+
or "seg" in text
|
|
49
|
+
or "outline" in text
|
|
50
|
+
):
|
|
51
|
+
return "labels"
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _infer_layer_mode_from_record(record: dict[str, Any]) -> str | None:
|
|
56
|
+
"""Infer the layer mode from a parsed OME-Arrow record.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
record: Parsed metadata record from OME-Arrow.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
"image", "labels", or None if no hint is present.
|
|
63
|
+
"""
|
|
64
|
+
# Collect candidate metadata fields that may encode image type.
|
|
65
|
+
candidates = []
|
|
66
|
+
if "image_type" in record:
|
|
67
|
+
candidates.append(record.get("image_type"))
|
|
68
|
+
pixels_meta = record.get("pixels_meta")
|
|
69
|
+
if isinstance(pixels_meta, dict) and "image_type" in pixels_meta:
|
|
70
|
+
candidates.append(pixels_meta.get("image_type"))
|
|
71
|
+
for value in candidates:
|
|
72
|
+
inferred = _normalize_image_type(value)
|
|
73
|
+
if inferred:
|
|
74
|
+
return inferred
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _infer_layer_mode_from_ome_arrow(obj: OMEArrow) -> str | None:
|
|
79
|
+
"""Infer the layer mode from an OMEArrow object.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
obj: OMEArrow instance to inspect.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
"image", "labels", or None if no hint is present.
|
|
86
|
+
"""
|
|
87
|
+
try:
|
|
88
|
+
record = obj.data.as_py()
|
|
89
|
+
except Exception:
|
|
90
|
+
return None
|
|
91
|
+
if not isinstance(record, dict):
|
|
92
|
+
return None
|
|
93
|
+
return _infer_layer_mode_from_record(record)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _infer_layer_mode_from_source(
|
|
97
|
+
src: str, *, stack_default_dim: str | None = None
|
|
98
|
+
) -> str | None:
|
|
99
|
+
"""Infer the layer mode by inspecting a data source path.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
src: Source path or stack pattern.
|
|
103
|
+
stack_default_dim: Default dimension to use for stack patterns.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
"image", "labels", or None if inference fails.
|
|
107
|
+
"""
|
|
108
|
+
# Quick path sniffing before attempting to instantiate OMEArrow.
|
|
109
|
+
s = src.lower()
|
|
110
|
+
p = Path(src)
|
|
111
|
+
looks_stack = any(c in src for c in "<>*")
|
|
112
|
+
looks_zarr = (
|
|
113
|
+
s.endswith((".ome.zarr", ".zarr"))
|
|
114
|
+
or ".zarr/" in s
|
|
115
|
+
or p.exists()
|
|
116
|
+
and p.is_dir()
|
|
117
|
+
and p.suffix.lower() == ".zarr"
|
|
118
|
+
)
|
|
119
|
+
looks_parquet = s.endswith(
|
|
120
|
+
(".ome.parquet", ".parquet", ".pq")
|
|
121
|
+
) or p.suffix.lower() in {
|
|
122
|
+
".parquet",
|
|
123
|
+
".pq",
|
|
124
|
+
}
|
|
125
|
+
looks_vortex = s.endswith(
|
|
126
|
+
(".ome.vortex", ".vortex")
|
|
127
|
+
) or p.suffix.lower() in {
|
|
128
|
+
".vortex",
|
|
129
|
+
}
|
|
130
|
+
looks_tiff = s.endswith(
|
|
131
|
+
(".ome.tif", ".ome.tiff", ".tif", ".tiff")
|
|
132
|
+
) or p.suffix.lower() in {
|
|
133
|
+
".tif",
|
|
134
|
+
".tiff",
|
|
135
|
+
}
|
|
136
|
+
if not (
|
|
137
|
+
looks_stack
|
|
138
|
+
or looks_zarr
|
|
139
|
+
or looks_parquet
|
|
140
|
+
or looks_tiff
|
|
141
|
+
or looks_vortex
|
|
142
|
+
):
|
|
143
|
+
LOGGER.debug(
|
|
144
|
+
"Skipping layer-mode inference for non-OME source: %s", src
|
|
145
|
+
)
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
scalar = None
|
|
149
|
+
try:
|
|
150
|
+
if looks_vortex:
|
|
151
|
+
scalar = _read_vortex_scalar(src)
|
|
152
|
+
if looks_stack and stack_default_dim is not None:
|
|
153
|
+
scalar = from_stack_pattern_path(
|
|
154
|
+
src,
|
|
155
|
+
default_dim_for_unspecified=stack_default_dim,
|
|
156
|
+
map_series_to="T",
|
|
157
|
+
clamp_to_uint16=True,
|
|
158
|
+
)
|
|
159
|
+
obj = OMEArrow(scalar if scalar is not None else src)
|
|
160
|
+
except Exception:
|
|
161
|
+
return None
|
|
162
|
+
return _infer_layer_mode_from_ome_arrow(obj)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _looks_like_ome_source(path_str: str) -> bool:
|
|
166
|
+
"""Check whether a path appears to be an OME-Arrow supported source.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
path_str: Path or stack pattern string.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
True if the path looks like a supported source, otherwise False.
|
|
173
|
+
"""
|
|
174
|
+
s = path_str.strip().lower()
|
|
175
|
+
p = Path(path_str)
|
|
176
|
+
|
|
177
|
+
# Bio-Formats-style stack patterns.
|
|
178
|
+
looks_stack = any(c in path_str for c in "<>*")
|
|
179
|
+
looks_zarr = (
|
|
180
|
+
s.endswith((".ome.zarr", ".zarr"))
|
|
181
|
+
or ".zarr/" in s
|
|
182
|
+
or p.exists()
|
|
183
|
+
and p.is_dir()
|
|
184
|
+
and p.suffix.lower() == ".zarr"
|
|
185
|
+
)
|
|
186
|
+
looks_parquet = s.endswith(
|
|
187
|
+
(".ome.parquet", ".parquet", ".pq")
|
|
188
|
+
) or p.suffix.lower() in {
|
|
189
|
+
".parquet",
|
|
190
|
+
".pq",
|
|
191
|
+
}
|
|
192
|
+
looks_vortex = s.endswith(
|
|
193
|
+
(".ome.vortex", ".vortex")
|
|
194
|
+
) or p.suffix.lower() in {
|
|
195
|
+
".vortex",
|
|
196
|
+
}
|
|
197
|
+
looks_tiff = s.endswith(
|
|
198
|
+
(".ome.tif", ".ome.tiff", ".tif", ".tiff")
|
|
199
|
+
) or p.suffix.lower() in {
|
|
200
|
+
".tif",
|
|
201
|
+
".tiff",
|
|
202
|
+
}
|
|
203
|
+
looks_npy = s.endswith(".npy")
|
|
204
|
+
looks_dir_stack = False
|
|
205
|
+
if p.exists() and p.is_dir() and not looks_zarr:
|
|
206
|
+
try:
|
|
207
|
+
# Heuristic: directory with multiple image-like files.
|
|
208
|
+
stack_files = [
|
|
209
|
+
f
|
|
210
|
+
for f in p.iterdir()
|
|
211
|
+
if f.is_file()
|
|
212
|
+
and f.name.lower().endswith(
|
|
213
|
+
(
|
|
214
|
+
".ome.tif",
|
|
215
|
+
".ome.tiff",
|
|
216
|
+
".tif",
|
|
217
|
+
".tiff",
|
|
218
|
+
".npy",
|
|
219
|
+
)
|
|
220
|
+
)
|
|
221
|
+
]
|
|
222
|
+
looks_dir_stack = len(stack_files) > 1
|
|
223
|
+
except OSError:
|
|
224
|
+
looks_dir_stack = False
|
|
225
|
+
return (
|
|
226
|
+
looks_stack
|
|
227
|
+
or looks_zarr
|
|
228
|
+
or looks_parquet
|
|
229
|
+
or looks_vortex
|
|
230
|
+
or looks_tiff
|
|
231
|
+
or looks_npy
|
|
232
|
+
or looks_dir_stack
|
|
233
|
+
)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Napari viewer helpers for the OME-Arrow reader."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _maybe_set_viewer_3d(arr: np.ndarray) -> None:
|
|
12
|
+
"""Switch the active napari viewer to 3D if the array has a Z-stack.
|
|
13
|
+
|
|
14
|
+
Assumes OME-Arrow's TCZYX convention or a subset, i.e. Z is always the
|
|
15
|
+
third-from-last axis. No-op if there's no active viewer.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
arr: Array to inspect for a Z dimension.
|
|
19
|
+
"""
|
|
20
|
+
# Need at least (Z, Y, X)
|
|
21
|
+
if arr.ndim < 3:
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
z_size = arr.shape[-3]
|
|
25
|
+
if z_size <= 1:
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
import napari
|
|
30
|
+
|
|
31
|
+
viewer = napari.current_viewer()
|
|
32
|
+
except Exception:
|
|
33
|
+
# no viewer / not in GUI context → silently skip
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
if viewer is not None:
|
|
37
|
+
viewer.dims.ndisplay = 3
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _as_labels(arr: np.ndarray) -> np.ndarray:
|
|
41
|
+
"""Convert an array into an integer label array.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
arr: Input array.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Array converted to an integer dtype suitable for labels.
|
|
48
|
+
"""
|
|
49
|
+
if arr.dtype.kind == "f":
|
|
50
|
+
arr = np.nan_to_num(arr, nan=0.0, posinf=0.0, neginf=0.0)
|
|
51
|
+
arr = np.round(arr).astype(np.int32, copy=False)
|
|
52
|
+
elif arr.dtype.kind not in ("i", "u"):
|
|
53
|
+
arr = arr.astype(np.int32, copy=False)
|
|
54
|
+
return arr
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _enable_grid(n_layers: int) -> None:
|
|
58
|
+
"""Switch the current viewer into grid view when possible.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
n_layers: Number of layers to arrange in the grid.
|
|
62
|
+
"""
|
|
63
|
+
if n_layers <= 1:
|
|
64
|
+
return
|
|
65
|
+
try:
|
|
66
|
+
import napari
|
|
67
|
+
|
|
68
|
+
viewer = napari.current_viewer()
|
|
69
|
+
except Exception:
|
|
70
|
+
return
|
|
71
|
+
if viewer is None:
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
cols = math.ceil(math.sqrt(n_layers))
|
|
75
|
+
rows = math.ceil(n_layers / cols)
|
|
76
|
+
try:
|
|
77
|
+
viewer.grid.enabled = True
|
|
78
|
+
viewer.grid.shape = (rows, cols)
|
|
79
|
+
except Exception:
|
|
80
|
+
# grid is best-effort; ignore if unavailable
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _strip_channel_axis(
|
|
85
|
+
arr: np.ndarray, add_kwargs: dict[str, Any]
|
|
86
|
+
) -> tuple[np.ndarray, dict[str, Any]]:
|
|
87
|
+
"""Strip the channel axis from an array if configured.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
arr: Input array.
|
|
91
|
+
add_kwargs: Layer kwargs that may include "channel_axis".
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Tuple of (array without channel axis, updated kwargs).
|
|
95
|
+
"""
|
|
96
|
+
# Only strip when channel_axis is present and valid.
|
|
97
|
+
channel_axis = add_kwargs.pop("channel_axis", None)
|
|
98
|
+
if channel_axis is None:
|
|
99
|
+
return arr, add_kwargs
|
|
100
|
+
try:
|
|
101
|
+
axis = int(channel_axis)
|
|
102
|
+
except Exception:
|
|
103
|
+
return arr, add_kwargs
|
|
104
|
+
if arr.ndim <= axis:
|
|
105
|
+
return arr, add_kwargs
|
|
106
|
+
arr = np.take(arr, 0, axis=axis)
|
|
107
|
+
return arr, add_kwargs
|