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
napari_ome_arrow/_reader.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Minimal napari reader for OME-Arrow sources (stack patterns, OME-Zarr, OME-Parquet,
|
|
3
|
-
OME-TIFF) plus a fallback .npy example.
|
|
3
|
+
OME-Vortex, OME-TIFF) plus a fallback .npy example.
|
|
4
4
|
|
|
5
5
|
Behavior:
|
|
6
6
|
* If NAPARI_OME_ARROW_LAYER_TYPE is set to "image" or "labels",
|
|
@@ -11,64 +11,91 @@ Behavior:
|
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
|
-
import math
|
|
15
14
|
import os
|
|
16
15
|
import warnings
|
|
17
|
-
from collections.abc import Sequence
|
|
16
|
+
from collections.abc import Callable, Sequence
|
|
18
17
|
from pathlib import Path
|
|
19
|
-
from typing import Any, Union
|
|
20
|
-
|
|
21
|
-
import numpy as np
|
|
22
|
-
import pyarrow as pa
|
|
23
|
-
from ome_arrow.core import OMEArrow
|
|
24
|
-
from ome_arrow.meta import OME_ARROW_STRUCT
|
|
25
|
-
|
|
26
|
-
PathLike = Union[str, Path]
|
|
27
|
-
LayerData = tuple[np.ndarray, dict[str, Any], str]
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def _maybe_set_viewer_3d(arr: np.ndarray) -> None:
|
|
31
|
-
"""
|
|
32
|
-
If the array has a Z axis with size > 1, switch the current napari viewer
|
|
33
|
-
to 3D (ndisplay = 3).
|
|
34
|
-
|
|
35
|
-
Assumes OME-Arrow's TCZYX convention or a subset, i.e., Z is always
|
|
36
|
-
the third-from-last axis. No-op if there's no active viewer.
|
|
37
|
-
"""
|
|
38
|
-
# Need at least (Z, Y, X)
|
|
39
|
-
if arr.ndim < 3:
|
|
40
|
-
return
|
|
41
|
-
|
|
42
|
-
z_size = arr.shape[-3]
|
|
43
|
-
if z_size <= 1:
|
|
44
|
-
return
|
|
45
|
-
|
|
46
|
-
try:
|
|
47
|
-
import napari
|
|
48
|
-
|
|
49
|
-
viewer = napari.current_viewer()
|
|
50
|
-
except Exception:
|
|
51
|
-
# no viewer / not in GUI context → silently skip
|
|
52
|
-
return
|
|
53
|
-
|
|
54
|
-
if viewer is not None:
|
|
55
|
-
viewer.dims.ndisplay = 3
|
|
56
18
|
|
|
19
|
+
from ._reader_infer import ( # noqa: F401
|
|
20
|
+
_infer_layer_mode_from_record,
|
|
21
|
+
_infer_layer_mode_from_source,
|
|
22
|
+
_looks_like_ome_source,
|
|
23
|
+
_normalize_image_type,
|
|
24
|
+
)
|
|
25
|
+
from ._reader_napari import ( # noqa: F401
|
|
26
|
+
_maybe_set_viewer_3d,
|
|
27
|
+
_strip_channel_axis,
|
|
28
|
+
)
|
|
29
|
+
from ._reader_omearrow import ( # noqa: F401
|
|
30
|
+
_read_one,
|
|
31
|
+
_read_parquet_rows,
|
|
32
|
+
_read_vortex_rows,
|
|
33
|
+
)
|
|
34
|
+
from ._reader_stack import ( # noqa: F401
|
|
35
|
+
_channel_names_from_pattern,
|
|
36
|
+
_collect_stack_files,
|
|
37
|
+
_infer_stack_scale_from_pattern,
|
|
38
|
+
_parse_stack_scale,
|
|
39
|
+
_prompt_stack_pattern,
|
|
40
|
+
_prompt_stack_scale,
|
|
41
|
+
_read_rgb_stack_pattern,
|
|
42
|
+
_replace_channel_placeholder,
|
|
43
|
+
_stack_default_dim_for_pattern,
|
|
44
|
+
_suggest_stack_pattern,
|
|
45
|
+
)
|
|
46
|
+
from ._reader_types import LayerData, PathLike
|
|
47
|
+
|
|
48
|
+
__all__ = [
|
|
49
|
+
"napari_get_reader",
|
|
50
|
+
"reader_function",
|
|
51
|
+
"_infer_layer_mode_from_record",
|
|
52
|
+
"_infer_layer_mode_from_source",
|
|
53
|
+
"_looks_like_ome_source",
|
|
54
|
+
"_normalize_image_type",
|
|
55
|
+
"_maybe_set_viewer_3d",
|
|
56
|
+
"_strip_channel_axis",
|
|
57
|
+
"_read_one",
|
|
58
|
+
"_read_parquet_rows",
|
|
59
|
+
"_read_vortex_rows",
|
|
60
|
+
"_channel_names_from_pattern",
|
|
61
|
+
"_collect_stack_files",
|
|
62
|
+
"_infer_stack_scale_from_pattern",
|
|
63
|
+
"_parse_stack_scale",
|
|
64
|
+
"_prompt_stack_pattern",
|
|
65
|
+
"_prompt_stack_scale",
|
|
66
|
+
"_read_rgb_stack_pattern",
|
|
67
|
+
"_replace_channel_placeholder",
|
|
68
|
+
"_stack_default_dim_for_pattern",
|
|
69
|
+
"_suggest_stack_pattern",
|
|
70
|
+
]
|
|
57
71
|
|
|
58
72
|
# --------------------------------------------------------------------- #
|
|
59
73
|
# Mode selection (env var + GUI prompt)
|
|
60
74
|
# --------------------------------------------------------------------- #
|
|
61
75
|
|
|
62
76
|
|
|
63
|
-
def _get_layer_mode(
|
|
64
|
-
|
|
65
|
-
|
|
77
|
+
def _get_layer_mode(
|
|
78
|
+
sample_path: str, *, image_type_hint: str | None = None
|
|
79
|
+
) -> str:
|
|
80
|
+
"""Decide whether to load as "image" or "labels".
|
|
66
81
|
|
|
67
82
|
Priority:
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
83
|
+
1. `NAPARI_OME_ARROW_LAYER_TYPE` env var (image/labels).
|
|
84
|
+
2. If in a Qt GUI context, show a modal dialog asking the user.
|
|
85
|
+
3. Otherwise, default to "image".
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
sample_path: Path used for prompt labeling.
|
|
89
|
+
image_type_hint: Optional inferred mode from metadata.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
The selected mode, "image" or "labels".
|
|
93
|
+
|
|
94
|
+
Raises:
|
|
95
|
+
RuntimeError: If the environment variable is invalid or the user
|
|
96
|
+
cancels the dialog.
|
|
71
97
|
"""
|
|
98
|
+
# Env var override (used in headless/batch workflows).
|
|
72
99
|
mode = os.environ.get("NAPARI_OME_ARROW_LAYER_TYPE")
|
|
73
100
|
if mode is not None:
|
|
74
101
|
mode = mode.lower()
|
|
@@ -78,6 +105,10 @@ def _get_layer_mode(sample_path: str) -> str:
|
|
|
78
105
|
f"Invalid NAPARI_OME_ARROW_LAYER_TYPE={mode!r}; expected 'image' or 'labels'."
|
|
79
106
|
)
|
|
80
107
|
|
|
108
|
+
# Metadata hint (best effort).
|
|
109
|
+
if image_type_hint in {"image", "labels"}:
|
|
110
|
+
return image_type_hint
|
|
111
|
+
|
|
81
112
|
# No env var → try to prompt in GUI context
|
|
82
113
|
try:
|
|
83
114
|
from qtpy import QtWidgets
|
|
@@ -127,180 +158,21 @@ def _get_layer_mode(sample_path: str) -> str:
|
|
|
127
158
|
raise RuntimeError("User cancelled napari-ome-arrow load dialog.")
|
|
128
159
|
|
|
129
160
|
|
|
130
|
-
# --------------------------------------------------------------------- #
|
|
131
|
-
# Helper utilities
|
|
132
|
-
# --------------------------------------------------------------------- #
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
def _as_labels(arr: np.ndarray) -> np.ndarray:
|
|
136
|
-
"""Convert any array into an integer label array."""
|
|
137
|
-
if arr.dtype.kind == "f":
|
|
138
|
-
arr = np.nan_to_num(arr, nan=0.0, posinf=0.0, neginf=0.0)
|
|
139
|
-
arr = np.round(arr).astype(np.int32, copy=False)
|
|
140
|
-
elif arr.dtype.kind not in ("i", "u"):
|
|
141
|
-
arr = arr.astype(np.int32, copy=False)
|
|
142
|
-
return arr
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
def _looks_like_ome_source(path_str: str) -> bool:
|
|
146
|
-
"""Basic extension / pattern sniffing for OME-Arrow supported formats."""
|
|
147
|
-
s = path_str.strip().lower()
|
|
148
|
-
p = Path(path_str)
|
|
149
|
-
|
|
150
|
-
looks_stack = any(c in path_str for c in "<>*")
|
|
151
|
-
looks_zarr = (
|
|
152
|
-
s.endswith((".ome.zarr", ".zarr"))
|
|
153
|
-
or ".zarr/" in s
|
|
154
|
-
or p.exists()
|
|
155
|
-
and p.is_dir()
|
|
156
|
-
and p.suffix.lower() == ".zarr"
|
|
157
|
-
)
|
|
158
|
-
looks_parquet = s.endswith(
|
|
159
|
-
(".ome.parquet", ".parquet", ".pq")
|
|
160
|
-
) or p.suffix.lower() in {
|
|
161
|
-
".parquet",
|
|
162
|
-
".pq",
|
|
163
|
-
}
|
|
164
|
-
looks_tiff = s.endswith(
|
|
165
|
-
(".ome.tif", ".ome.tiff", ".tif", ".tiff")
|
|
166
|
-
) or p.suffix.lower() in {
|
|
167
|
-
".tif",
|
|
168
|
-
".tiff",
|
|
169
|
-
}
|
|
170
|
-
looks_npy = s.endswith(".npy")
|
|
171
|
-
return (
|
|
172
|
-
looks_stack or looks_zarr or looks_parquet or looks_tiff or looks_npy
|
|
173
|
-
)
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
# --------------------------------------------------------------------- #
|
|
177
|
-
# OME-Parquet helpers (multi-row grid)
|
|
178
|
-
# --------------------------------------------------------------------- #
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
def _find_ome_parquet_columns(table: pa.Table) -> list[str]:
|
|
182
|
-
"""Return struct columns matching the OME-Arrow schema."""
|
|
183
|
-
import pyarrow as pa
|
|
184
|
-
|
|
185
|
-
expected_fields = {f.name for f in OME_ARROW_STRUCT}
|
|
186
|
-
names: list[str] = []
|
|
187
|
-
for name, col in zip(table.column_names, table.columns, strict=False):
|
|
188
|
-
if (
|
|
189
|
-
pa.types.is_struct(col.type)
|
|
190
|
-
and {f.name for f in col.type} == expected_fields
|
|
191
|
-
):
|
|
192
|
-
names.append(name)
|
|
193
|
-
return names
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
def _enable_grid(n_layers: int) -> None:
|
|
197
|
-
"""Switch current viewer into grid view when possible."""
|
|
198
|
-
if n_layers <= 1:
|
|
199
|
-
return
|
|
200
|
-
try:
|
|
201
|
-
import napari
|
|
202
|
-
|
|
203
|
-
viewer = napari.current_viewer()
|
|
204
|
-
except Exception:
|
|
205
|
-
return
|
|
206
|
-
if viewer is None:
|
|
207
|
-
return
|
|
208
|
-
|
|
209
|
-
cols = math.ceil(math.sqrt(n_layers))
|
|
210
|
-
rows = math.ceil(n_layers / cols)
|
|
211
|
-
try:
|
|
212
|
-
viewer.grid.enabled = True
|
|
213
|
-
viewer.grid.shape = (rows, cols)
|
|
214
|
-
except Exception:
|
|
215
|
-
# grid is best-effort; ignore if unavailable
|
|
216
|
-
return
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
def _read_parquet_rows(src: str, mode: str) -> list[LayerData] | None:
|
|
220
|
-
"""
|
|
221
|
-
Specialized path for multi-row OME-Parquet:
|
|
222
|
-
create one layer per row and enable napari grid view.
|
|
223
|
-
"""
|
|
224
|
-
s = src.lower()
|
|
225
|
-
if not (s.endswith((".ome.parquet", ".parquet", ".pq"))):
|
|
226
|
-
return None
|
|
227
|
-
|
|
228
|
-
try:
|
|
229
|
-
import pyarrow as pa
|
|
230
|
-
import pyarrow.parquet as pq
|
|
231
|
-
except Exception:
|
|
232
|
-
return None
|
|
233
|
-
|
|
234
|
-
table = pq.read_table(src)
|
|
235
|
-
ome_cols = _find_ome_parquet_columns(table)
|
|
236
|
-
if not ome_cols or table.num_rows <= 1:
|
|
237
|
-
return None
|
|
238
|
-
|
|
239
|
-
override = os.environ.get("NAPARI_OME_ARROW_PARQUET_COLUMN")
|
|
240
|
-
if override:
|
|
241
|
-
matched = [c for c in ome_cols if c.lower() == override.lower()]
|
|
242
|
-
selected = matched[0] if matched else ome_cols[0]
|
|
243
|
-
if not matched:
|
|
244
|
-
warnings.warn(
|
|
245
|
-
f"Column '{override}' not found in {Path(src).name}; using {selected!r}.",
|
|
246
|
-
stacklevel=2,
|
|
247
|
-
)
|
|
248
|
-
else:
|
|
249
|
-
selected = ome_cols[0]
|
|
250
|
-
|
|
251
|
-
column = table[selected]
|
|
252
|
-
layers: list[LayerData] = []
|
|
253
|
-
|
|
254
|
-
for idx in range(table.num_rows):
|
|
255
|
-
try:
|
|
256
|
-
record = column.slice(idx, 1).to_pylist()[0]
|
|
257
|
-
scalar = pa.scalar(record, type=OME_ARROW_STRUCT)
|
|
258
|
-
arr = OMEArrow(scalar).export(
|
|
259
|
-
how="numpy", dtype=np.uint16, strict=False
|
|
260
|
-
)
|
|
261
|
-
except Exception as e: # pragma: no cover - warn and skip bad rows
|
|
262
|
-
warnings.warn(
|
|
263
|
-
f"Skipping row {idx} in column '{selected}': {e}",
|
|
264
|
-
stacklevel=2,
|
|
265
|
-
)
|
|
266
|
-
continue
|
|
267
|
-
|
|
268
|
-
add_kwargs: dict[str, Any] = {
|
|
269
|
-
"name": f"{Path(src).name}[{selected}][row {idx}]"
|
|
270
|
-
}
|
|
271
|
-
if mode == "image":
|
|
272
|
-
if arr.ndim >= 5:
|
|
273
|
-
add_kwargs["channel_axis"] = 1 # TCZYX
|
|
274
|
-
elif arr.ndim == 4:
|
|
275
|
-
add_kwargs["channel_axis"] = 0 # CZYX
|
|
276
|
-
layer_type = "image"
|
|
277
|
-
else:
|
|
278
|
-
if arr.ndim == 5:
|
|
279
|
-
arr = arr[:, 0, ...]
|
|
280
|
-
elif arr.ndim == 4:
|
|
281
|
-
arr = arr[0, ...]
|
|
282
|
-
arr = _as_labels(arr)
|
|
283
|
-
add_kwargs.setdefault("opacity", 0.7)
|
|
284
|
-
layer_type = "labels"
|
|
285
|
-
|
|
286
|
-
_maybe_set_viewer_3d(arr)
|
|
287
|
-
layers.append((arr, add_kwargs, layer_type))
|
|
288
|
-
|
|
289
|
-
if layers:
|
|
290
|
-
_enable_grid(len(layers))
|
|
291
|
-
return layers or None
|
|
292
|
-
|
|
293
|
-
|
|
294
161
|
# --------------------------------------------------------------------- #
|
|
295
162
|
# napari entry point: napari_get_reader
|
|
296
163
|
# --------------------------------------------------------------------- #
|
|
297
164
|
|
|
298
165
|
|
|
299
|
-
def napari_get_reader(
|
|
300
|
-
|
|
301
|
-
|
|
166
|
+
def napari_get_reader(
|
|
167
|
+
path: PathLike | Sequence[PathLike],
|
|
168
|
+
) -> Callable[[PathLike | Sequence[PathLike]], list[LayerData]] | None:
|
|
169
|
+
"""Return a reader callable for napari if the path is supported.
|
|
302
170
|
|
|
303
|
-
|
|
171
|
+
Args:
|
|
172
|
+
path: A single path or a sequence of paths provided by napari.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Reader callable if the path is supported, otherwise None.
|
|
304
176
|
"""
|
|
305
177
|
# napari may pass a list/tuple or a single path
|
|
306
178
|
first = str(path[0] if isinstance(path, (list, tuple)) else path).strip()
|
|
@@ -315,135 +187,147 @@ def napari_get_reader(path: Union[PathLike, Sequence[PathLike]]):
|
|
|
315
187
|
# --------------------------------------------------------------------- #
|
|
316
188
|
|
|
317
189
|
|
|
318
|
-
def
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
"""
|
|
323
|
-
s = src.lower()
|
|
324
|
-
p = Path(src)
|
|
325
|
-
|
|
326
|
-
looks_stack = any(c in src for c in "<>*")
|
|
327
|
-
looks_zarr = (
|
|
328
|
-
s.endswith((".ome.zarr", ".zarr"))
|
|
329
|
-
or ".zarr/" in s
|
|
330
|
-
or p.exists()
|
|
331
|
-
and p.is_dir()
|
|
332
|
-
and p.suffix.lower() == ".zarr"
|
|
333
|
-
)
|
|
334
|
-
looks_parquet = s.endswith(
|
|
335
|
-
(".ome.parquet", ".parquet", ".pq")
|
|
336
|
-
) or p.suffix.lower() in {
|
|
337
|
-
".parquet",
|
|
338
|
-
".pq",
|
|
339
|
-
}
|
|
340
|
-
looks_tiff = s.endswith(
|
|
341
|
-
(".ome.tif", ".ome.tiff", ".tif", ".tiff")
|
|
342
|
-
) or p.suffix.lower() in {
|
|
343
|
-
".tif",
|
|
344
|
-
".tiff",
|
|
345
|
-
}
|
|
346
|
-
looks_npy = s.endswith(".npy")
|
|
347
|
-
|
|
348
|
-
add_kwargs: dict[str, Any] = {"name": p.name}
|
|
349
|
-
|
|
350
|
-
# ---- OME-Arrow-backed sources -----------------------------------
|
|
351
|
-
if looks_stack or looks_zarr or looks_parquet or looks_tiff:
|
|
352
|
-
obj = OMEArrow(src)
|
|
353
|
-
arr = obj.export(how="numpy", dtype=np.uint16) # TCZYX
|
|
354
|
-
info = obj.info() # may contain 'shape': (T, C, Z, Y, X)
|
|
355
|
-
|
|
356
|
-
# Recover from accidental 1D flatten
|
|
357
|
-
if getattr(arr, "ndim", 0) == 1:
|
|
358
|
-
T, C, Z, Y, X = info.get("shape", (1, 1, 1, 0, 0))
|
|
359
|
-
if Y and X and arr.size == Y * X:
|
|
360
|
-
arr = arr.reshape((1, 1, 1, Y, X))
|
|
361
|
-
else:
|
|
362
|
-
raise ValueError(
|
|
363
|
-
f"Flat array with unknown shape for {src}: size={arr.size}"
|
|
364
|
-
)
|
|
365
|
-
|
|
366
|
-
if mode == "image":
|
|
367
|
-
# Image: preserve channels
|
|
368
|
-
if arr.ndim >= 5:
|
|
369
|
-
add_kwargs["channel_axis"] = 1 # TCZYX
|
|
370
|
-
elif arr.ndim == 4:
|
|
371
|
-
add_kwargs["channel_axis"] = 0 # CZYX
|
|
372
|
-
layer_type = "image"
|
|
373
|
-
else:
|
|
374
|
-
# Labels: squash channels, ensure integer dtype
|
|
375
|
-
if arr.ndim == 5: # (T, C, Z, Y, X)
|
|
376
|
-
arr = arr[:, 0, ...]
|
|
377
|
-
elif arr.ndim == 4: # (C, Z, Y, X)
|
|
378
|
-
arr = arr[0, ...]
|
|
379
|
-
arr = _as_labels(arr)
|
|
380
|
-
add_kwargs.setdefault("opacity", 0.7)
|
|
381
|
-
layer_type = "labels"
|
|
382
|
-
|
|
383
|
-
# 🔹 Ask viewer to switch to 3D if there is a real Z-stack
|
|
384
|
-
_maybe_set_viewer_3d(arr)
|
|
385
|
-
|
|
386
|
-
return arr, add_kwargs, layer_type
|
|
387
|
-
|
|
388
|
-
# ---- bare .npy fallback -----------------------------------------
|
|
389
|
-
if looks_npy:
|
|
390
|
-
arr = np.load(src)
|
|
391
|
-
if arr.ndim == 1:
|
|
392
|
-
n = int(np.sqrt(arr.size))
|
|
393
|
-
if n * n == arr.size:
|
|
394
|
-
arr = arr.reshape(n, n)
|
|
395
|
-
else:
|
|
396
|
-
raise ValueError(
|
|
397
|
-
f".npy is 1D and not a square image: {arr.shape}"
|
|
398
|
-
)
|
|
399
|
-
|
|
400
|
-
if mode == "image":
|
|
401
|
-
if arr.ndim == 3 and arr.shape[0] <= 6:
|
|
402
|
-
add_kwargs["channel_axis"] = 0
|
|
403
|
-
layer_type = "image"
|
|
404
|
-
else:
|
|
405
|
-
# labels
|
|
406
|
-
if arr.ndim == 3: # treat as (C, Y, X) → first channel
|
|
407
|
-
arr = arr[0, ...]
|
|
408
|
-
arr = _as_labels(arr)
|
|
409
|
-
add_kwargs.setdefault("opacity", 0.7)
|
|
410
|
-
layer_type = "labels"
|
|
411
|
-
|
|
412
|
-
# 🔹 Same 3D toggle for npy-based data
|
|
413
|
-
_maybe_set_viewer_3d(arr)
|
|
414
|
-
|
|
415
|
-
return arr, add_kwargs, layer_type
|
|
190
|
+
def reader_function(
|
|
191
|
+
path: PathLike | Sequence[PathLike],
|
|
192
|
+
) -> list[LayerData]:
|
|
193
|
+
"""Read one or more paths into napari layer data.
|
|
416
194
|
|
|
417
|
-
|
|
195
|
+
The user may be prompted (or an env var used) to decide "image" vs
|
|
196
|
+
"labels".
|
|
418
197
|
|
|
198
|
+
Args:
|
|
199
|
+
path: A single path or a sequence of paths provided by napari.
|
|
419
200
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
) -> list[LayerData]:
|
|
423
|
-
"""
|
|
424
|
-
The actual reader callable napari will use.
|
|
201
|
+
Returns:
|
|
202
|
+
List of layer tuples for napari.
|
|
425
203
|
|
|
426
|
-
|
|
427
|
-
|
|
204
|
+
Raises:
|
|
205
|
+
ValueError: If no readable inputs are found.
|
|
428
206
|
"""
|
|
429
207
|
paths: list[str] = [
|
|
430
208
|
str(p) for p in (path if isinstance(path, (list, tuple)) else [path])
|
|
431
209
|
]
|
|
432
210
|
layers: list[LayerData] = []
|
|
433
211
|
|
|
434
|
-
#
|
|
212
|
+
# Offer a stack pattern prompt when multiple compatible files are present.
|
|
213
|
+
stack_selection = _collect_stack_files(paths)
|
|
214
|
+
stack_pattern = None
|
|
215
|
+
if stack_selection is not None:
|
|
216
|
+
stack_pattern = _prompt_stack_pattern(*stack_selection)
|
|
217
|
+
|
|
218
|
+
# Use the original first path as context for the dialog label
|
|
219
|
+
mode_hint = None
|
|
220
|
+
if stack_pattern is not None:
|
|
221
|
+
stack_default_dim = _stack_default_dim_for_pattern(stack_pattern)
|
|
222
|
+
mode_hint = _infer_layer_mode_from_source(
|
|
223
|
+
stack_pattern, stack_default_dim=stack_default_dim
|
|
224
|
+
)
|
|
225
|
+
elif paths:
|
|
226
|
+
mode_hint = _infer_layer_mode_from_source(paths[0])
|
|
435
227
|
try:
|
|
436
|
-
mode = _get_layer_mode(
|
|
228
|
+
mode = _get_layer_mode(
|
|
229
|
+
sample_path=paths[0], image_type_hint=mode_hint
|
|
230
|
+
) # 'image' or 'labels'
|
|
437
231
|
except RuntimeError as e:
|
|
438
232
|
# If user canceled the dialog, propagate a clean error for napari
|
|
439
233
|
raise ValueError(str(e)) from e
|
|
440
234
|
|
|
235
|
+
if stack_pattern is not None:
|
|
236
|
+
try:
|
|
237
|
+
channel_names = _channel_names_from_pattern(
|
|
238
|
+
stack_pattern, stack_default_dim
|
|
239
|
+
)
|
|
240
|
+
resolved_stack_scale = None
|
|
241
|
+
if mode == "image" and channel_names and len(channel_names) > 1:
|
|
242
|
+
# Split each channel into separate layers for stack patterns.
|
|
243
|
+
inferred_stack_scale = _infer_stack_scale_from_pattern(
|
|
244
|
+
stack_pattern, stack_default_dim
|
|
245
|
+
)
|
|
246
|
+
env_scale = os.environ.get("NAPARI_OME_ARROW_STACK_SCALE")
|
|
247
|
+
if env_scale:
|
|
248
|
+
try:
|
|
249
|
+
resolved_stack_scale = _parse_stack_scale(env_scale)
|
|
250
|
+
except ValueError as exc:
|
|
251
|
+
warnings.warn(
|
|
252
|
+
f"Invalid NAPARI_OME_ARROW_STACK_SCALE '{env_scale}': {exc}.",
|
|
253
|
+
stacklevel=2,
|
|
254
|
+
)
|
|
255
|
+
resolved_stack_scale = None
|
|
256
|
+
if (
|
|
257
|
+
resolved_stack_scale is None
|
|
258
|
+
and inferred_stack_scale is None
|
|
259
|
+
):
|
|
260
|
+
resolved_stack_scale = _prompt_stack_scale(
|
|
261
|
+
sample_path=stack_pattern, default_scale=None
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
for label in channel_names:
|
|
265
|
+
channel_pattern = _replace_channel_placeholder(
|
|
266
|
+
stack_pattern, label, stack_default_dim
|
|
267
|
+
)
|
|
268
|
+
channel_default_dim = _stack_default_dim_for_pattern(
|
|
269
|
+
channel_pattern
|
|
270
|
+
)
|
|
271
|
+
try:
|
|
272
|
+
arr, add_kwargs, layer_type = _read_one(
|
|
273
|
+
channel_pattern,
|
|
274
|
+
mode=mode,
|
|
275
|
+
stack_default_dim=channel_default_dim,
|
|
276
|
+
stack_scale_override=resolved_stack_scale,
|
|
277
|
+
)
|
|
278
|
+
arr, add_kwargs = _strip_channel_axis(arr, add_kwargs)
|
|
279
|
+
except Exception as exc:
|
|
280
|
+
try:
|
|
281
|
+
arr, is_rgb = _read_rgb_stack_pattern(
|
|
282
|
+
channel_pattern
|
|
283
|
+
)
|
|
284
|
+
except Exception:
|
|
285
|
+
warnings.warn(
|
|
286
|
+
f"Failed to read channel '{label}' from stack "
|
|
287
|
+
f"pattern '{stack_pattern}': {exc}",
|
|
288
|
+
stacklevel=2,
|
|
289
|
+
)
|
|
290
|
+
continue
|
|
291
|
+
add_kwargs = {"name": Path(channel_pattern).name}
|
|
292
|
+
if is_rgb:
|
|
293
|
+
add_kwargs["rgb"] = True
|
|
294
|
+
layer_type = "image"
|
|
295
|
+
|
|
296
|
+
add_kwargs["name"] = (
|
|
297
|
+
f"{add_kwargs.get('name', label)}[{label}]"
|
|
298
|
+
)
|
|
299
|
+
if not add_kwargs.get("rgb"):
|
|
300
|
+
_maybe_set_viewer_3d(arr)
|
|
301
|
+
layers.append((arr, add_kwargs, layer_type))
|
|
302
|
+
else:
|
|
303
|
+
# Single-layer stack read.
|
|
304
|
+
arr, add_kwargs, layer_type = _read_one(
|
|
305
|
+
stack_pattern,
|
|
306
|
+
mode=mode,
|
|
307
|
+
stack_default_dim=stack_default_dim,
|
|
308
|
+
)
|
|
309
|
+
layers.append((arr, add_kwargs, layer_type))
|
|
310
|
+
except Exception as e:
|
|
311
|
+
warnings.warn(
|
|
312
|
+
f"Failed to read stack pattern '{stack_pattern}': {e}. "
|
|
313
|
+
"Loading files individually instead.",
|
|
314
|
+
stacklevel=2,
|
|
315
|
+
)
|
|
316
|
+
else:
|
|
317
|
+
return layers
|
|
318
|
+
elif stack_selection is not None and len(paths) == 1:
|
|
319
|
+
paths = [str(p) for p in stack_selection[0]]
|
|
320
|
+
|
|
441
321
|
for src in paths:
|
|
442
322
|
try:
|
|
443
323
|
parquet_layers = _read_parquet_rows(src, mode)
|
|
444
324
|
if parquet_layers is not None:
|
|
445
325
|
layers.extend(parquet_layers)
|
|
446
326
|
continue
|
|
327
|
+
vortex_layers = _read_vortex_rows(src, mode)
|
|
328
|
+
if vortex_layers is not None:
|
|
329
|
+
layers.extend(vortex_layers)
|
|
330
|
+
continue
|
|
447
331
|
layers.append(_read_one(src, mode=mode))
|
|
448
332
|
except Exception as e:
|
|
449
333
|
warnings.warn(
|