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,474 @@
|
|
|
1
|
+
"""OME-Arrow backed reading helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import warnings
|
|
7
|
+
from collections.abc import Sequence
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
import pyarrow as pa
|
|
13
|
+
from ome_arrow.core import OMEArrow
|
|
14
|
+
from ome_arrow.ingest import from_stack_pattern_path
|
|
15
|
+
from ome_arrow.meta import OME_ARROW_STRUCT
|
|
16
|
+
|
|
17
|
+
from ._reader_napari import _as_labels, _enable_grid, _maybe_set_viewer_3d
|
|
18
|
+
from ._reader_stack import (
|
|
19
|
+
_parse_stack_scale,
|
|
20
|
+
_prompt_stack_scale,
|
|
21
|
+
_scale_for_array,
|
|
22
|
+
_scale_from_ome_arrow,
|
|
23
|
+
)
|
|
24
|
+
from ._reader_types import LayerData
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _find_ome_parquet_columns(table: pa.Table) -> list[str]:
|
|
28
|
+
"""Find Parquet columns matching the OME-Arrow schema.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
table: Parquet table to scan.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Column names that match the OME-Arrow struct schema.
|
|
35
|
+
"""
|
|
36
|
+
import pyarrow as pa
|
|
37
|
+
|
|
38
|
+
# Match struct columns that exactly mirror the OME-Arrow schema.
|
|
39
|
+
expected_fields = {f.name for f in OME_ARROW_STRUCT}
|
|
40
|
+
names: list[str] = []
|
|
41
|
+
for name, col in zip(table.column_names, table.columns, strict=False):
|
|
42
|
+
if (
|
|
43
|
+
pa.types.is_struct(col.type)
|
|
44
|
+
and {f.name for f in col.type} == expected_fields
|
|
45
|
+
):
|
|
46
|
+
names.append(name)
|
|
47
|
+
return names
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _read_vortex_scalar(src: str) -> pa.StructScalar:
|
|
51
|
+
"""Read a single OME-Vortex row as a struct scalar.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
src: OME-Vortex file path.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Struct scalar for the selected row and column.
|
|
58
|
+
|
|
59
|
+
Raises:
|
|
60
|
+
ImportError: If OME-Vortex support is not available.
|
|
61
|
+
"""
|
|
62
|
+
# Delegate Vortex parsing to ome-arrow, which handles the file format details.
|
|
63
|
+
try:
|
|
64
|
+
from ome_arrow.ingest import from_ome_vortex
|
|
65
|
+
except Exception as exc:
|
|
66
|
+
raise ImportError(
|
|
67
|
+
"OME-Vortex support requires ome-arrow with vortex support and the "
|
|
68
|
+
"optional 'vortex' extra (vortex-data). Install with "
|
|
69
|
+
"'pip install \"napari-ome-arrow[vortex]\"'."
|
|
70
|
+
) from exc
|
|
71
|
+
|
|
72
|
+
# Allow column override for non-default schema names.
|
|
73
|
+
override = os.environ.get(
|
|
74
|
+
"NAPARI_OME_ARROW_VORTEX_COLUMN"
|
|
75
|
+
) or os.environ.get("NAPARI_OME_ARROW_PARQUET_COLUMN")
|
|
76
|
+
# Use a single row from the requested/auto-detected column for OMEArrow.
|
|
77
|
+
return from_ome_vortex(
|
|
78
|
+
src,
|
|
79
|
+
column_name=override or "ome_arrow",
|
|
80
|
+
row_index=0,
|
|
81
|
+
strict_schema=False,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _vortex_row_out_of_range(exc: Exception) -> bool:
|
|
86
|
+
"""Check whether a Vortex row exception indicates an out-of-range index.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
exc: Exception raised by the Vortex reader.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
True if the error indicates an out-of-range row, otherwise False.
|
|
93
|
+
"""
|
|
94
|
+
if isinstance(exc, IndexError):
|
|
95
|
+
return True
|
|
96
|
+
msg = str(exc).lower()
|
|
97
|
+
return "out of range" in msg or ("row_index" in msg and "range" in msg)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _read_vortex_rows(src: str, mode: str) -> list[LayerData] | None:
|
|
101
|
+
"""Read multi-row OME-Vortex data as a layer grid.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
src: OME-Vortex file path.
|
|
105
|
+
mode: "image" or "labels".
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
A list of layers if multi-row data is detected, otherwise None.
|
|
109
|
+
"""
|
|
110
|
+
s = src.lower()
|
|
111
|
+
if not (s.endswith((".ome.vortex", ".vortex"))):
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
from ome_arrow.ingest import from_ome_vortex
|
|
116
|
+
except Exception:
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
# Reuse the parquet override env var for consistency.
|
|
120
|
+
override = os.environ.get(
|
|
121
|
+
"NAPARI_OME_ARROW_VORTEX_COLUMN"
|
|
122
|
+
) or os.environ.get("NAPARI_OME_ARROW_PARQUET_COLUMN")
|
|
123
|
+
selected = override or "ome_arrow"
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
first = from_ome_vortex(
|
|
127
|
+
src,
|
|
128
|
+
column_name=selected,
|
|
129
|
+
row_index=0,
|
|
130
|
+
strict_schema=False,
|
|
131
|
+
)
|
|
132
|
+
except Exception:
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
second = from_ome_vortex(
|
|
137
|
+
src,
|
|
138
|
+
column_name=selected,
|
|
139
|
+
row_index=1,
|
|
140
|
+
strict_schema=False,
|
|
141
|
+
)
|
|
142
|
+
except Exception:
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
layers: list[LayerData] = []
|
|
146
|
+
|
|
147
|
+
def _append_layer(idx: int, scalar: pa.StructScalar) -> None:
|
|
148
|
+
# Per-row conversion with best-effort error handling.
|
|
149
|
+
try:
|
|
150
|
+
arr = OMEArrow(scalar).export(
|
|
151
|
+
how="numpy", dtype=np.uint16, strict=False
|
|
152
|
+
)
|
|
153
|
+
except Exception as e: # pragma: no cover - warn and skip bad rows
|
|
154
|
+
warnings.warn(
|
|
155
|
+
f"Skipping row {idx} in column '{selected}': {e}",
|
|
156
|
+
stacklevel=2,
|
|
157
|
+
)
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
# Layer metadata uses row index for unique naming.
|
|
161
|
+
add_kwargs: dict[str, Any] = {
|
|
162
|
+
"name": f"{Path(src).name}[{selected}][row {idx}]"
|
|
163
|
+
}
|
|
164
|
+
if mode == "image":
|
|
165
|
+
if arr.ndim >= 5:
|
|
166
|
+
add_kwargs["channel_axis"] = 1 # TCZYX
|
|
167
|
+
elif arr.ndim == 4:
|
|
168
|
+
add_kwargs["channel_axis"] = 0 # CZYX
|
|
169
|
+
layer_type = "image"
|
|
170
|
+
else:
|
|
171
|
+
if arr.ndim == 5:
|
|
172
|
+
arr = arr[:, 0, ...]
|
|
173
|
+
elif arr.ndim == 4:
|
|
174
|
+
arr = arr[0, ...]
|
|
175
|
+
arr = _as_labels(arr)
|
|
176
|
+
add_kwargs.setdefault("opacity", 0.7)
|
|
177
|
+
layer_type = "labels"
|
|
178
|
+
|
|
179
|
+
_maybe_set_viewer_3d(arr)
|
|
180
|
+
layers.append((arr, add_kwargs, layer_type))
|
|
181
|
+
|
|
182
|
+
_append_layer(0, first)
|
|
183
|
+
_append_layer(1, second)
|
|
184
|
+
|
|
185
|
+
max_rows = 10000
|
|
186
|
+
for idx in range(2, max_rows):
|
|
187
|
+
try:
|
|
188
|
+
scalar = from_ome_vortex(
|
|
189
|
+
src,
|
|
190
|
+
column_name=selected,
|
|
191
|
+
row_index=idx,
|
|
192
|
+
strict_schema=False,
|
|
193
|
+
)
|
|
194
|
+
except Exception as e:
|
|
195
|
+
if _vortex_row_out_of_range(e):
|
|
196
|
+
break
|
|
197
|
+
warnings.warn(
|
|
198
|
+
f"Skipping row {idx} in column '{selected}': {e}",
|
|
199
|
+
stacklevel=2,
|
|
200
|
+
)
|
|
201
|
+
continue
|
|
202
|
+
_append_layer(idx, scalar)
|
|
203
|
+
|
|
204
|
+
if layers:
|
|
205
|
+
_enable_grid(len(layers))
|
|
206
|
+
return layers or None
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _read_parquet_rows(src: str, mode: str) -> list[LayerData] | None:
|
|
210
|
+
"""Read multi-row OME-Parquet data as a layer grid.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
src: OME-Parquet file path.
|
|
214
|
+
mode: "image" or "labels".
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
A list of layers if multi-row data is detected, otherwise None.
|
|
218
|
+
"""
|
|
219
|
+
s = src.lower()
|
|
220
|
+
if not (s.endswith((".ome.parquet", ".parquet", ".pq"))):
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
try:
|
|
224
|
+
import pyarrow as pa
|
|
225
|
+
import pyarrow.parquet as pq
|
|
226
|
+
except Exception:
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
# Read all rows; per-row layers are assembled below.
|
|
230
|
+
table = pq.read_table(src)
|
|
231
|
+
ome_cols = _find_ome_parquet_columns(table)
|
|
232
|
+
if not ome_cols or table.num_rows <= 1:
|
|
233
|
+
return None
|
|
234
|
+
|
|
235
|
+
# Column override for multi-struct tables.
|
|
236
|
+
override = os.environ.get("NAPARI_OME_ARROW_PARQUET_COLUMN")
|
|
237
|
+
if override:
|
|
238
|
+
matched = [c for c in ome_cols if c.lower() == override.lower()]
|
|
239
|
+
selected = matched[0] if matched else ome_cols[0]
|
|
240
|
+
if not matched:
|
|
241
|
+
warnings.warn(
|
|
242
|
+
f"Column '{override}' not found in {Path(src).name}; using {selected!r}.",
|
|
243
|
+
stacklevel=2,
|
|
244
|
+
)
|
|
245
|
+
else:
|
|
246
|
+
selected = ome_cols[0]
|
|
247
|
+
|
|
248
|
+
column = table[selected]
|
|
249
|
+
layers: list[LayerData] = []
|
|
250
|
+
|
|
251
|
+
for idx in range(table.num_rows):
|
|
252
|
+
try:
|
|
253
|
+
record = column.slice(idx, 1).to_pylist()[0]
|
|
254
|
+
scalar = pa.scalar(record, type=OME_ARROW_STRUCT)
|
|
255
|
+
arr = OMEArrow(scalar).export(
|
|
256
|
+
how="numpy", dtype=np.uint16, strict=False
|
|
257
|
+
)
|
|
258
|
+
except Exception as e: # pragma: no cover - warn and skip bad rows
|
|
259
|
+
warnings.warn(
|
|
260
|
+
f"Skipping row {idx} in column '{selected}': {e}",
|
|
261
|
+
stacklevel=2,
|
|
262
|
+
)
|
|
263
|
+
continue
|
|
264
|
+
|
|
265
|
+
add_kwargs: dict[str, Any] = {
|
|
266
|
+
"name": f"{Path(src).name}[{selected}][row {idx}]"
|
|
267
|
+
}
|
|
268
|
+
if mode == "image":
|
|
269
|
+
if arr.ndim >= 5:
|
|
270
|
+
add_kwargs["channel_axis"] = 1 # TCZYX
|
|
271
|
+
elif arr.ndim == 4:
|
|
272
|
+
add_kwargs["channel_axis"] = 0 # CZYX
|
|
273
|
+
layer_type = "image"
|
|
274
|
+
else:
|
|
275
|
+
if arr.ndim == 5:
|
|
276
|
+
arr = arr[:, 0, ...]
|
|
277
|
+
elif arr.ndim == 4:
|
|
278
|
+
arr = arr[0, ...]
|
|
279
|
+
arr = _as_labels(arr)
|
|
280
|
+
add_kwargs.setdefault("opacity", 0.7)
|
|
281
|
+
layer_type = "labels"
|
|
282
|
+
|
|
283
|
+
_maybe_set_viewer_3d(arr)
|
|
284
|
+
layers.append((arr, add_kwargs, layer_type))
|
|
285
|
+
|
|
286
|
+
if layers:
|
|
287
|
+
_enable_grid(len(layers))
|
|
288
|
+
return layers or None
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _read_one(
|
|
292
|
+
src: str,
|
|
293
|
+
mode: str,
|
|
294
|
+
*,
|
|
295
|
+
stack_default_dim: str | None = None,
|
|
296
|
+
stack_scale_override: Sequence[float] | None = None,
|
|
297
|
+
) -> LayerData:
|
|
298
|
+
"""Read a single source into a napari layer tuple.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
src: Source path or stack pattern.
|
|
302
|
+
mode: "image" or "labels".
|
|
303
|
+
stack_default_dim: Default dimension to use for stack patterns.
|
|
304
|
+
stack_scale_override: Optional scale override for stack patterns.
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
Tuple of (data, add_kwargs, layer_type).
|
|
308
|
+
|
|
309
|
+
Raises:
|
|
310
|
+
ValueError: If the source cannot be parsed or inferred.
|
|
311
|
+
ImportError: If optional dependencies are required but missing.
|
|
312
|
+
FileNotFoundError: If referenced stack files are missing.
|
|
313
|
+
"""
|
|
314
|
+
s = src.lower()
|
|
315
|
+
p = Path(src)
|
|
316
|
+
|
|
317
|
+
looks_stack = any(c in src for c in "<>*")
|
|
318
|
+
looks_zarr = (
|
|
319
|
+
s.endswith((".ome.zarr", ".zarr"))
|
|
320
|
+
or ".zarr/" in s
|
|
321
|
+
or p.exists()
|
|
322
|
+
and p.is_dir()
|
|
323
|
+
and p.suffix.lower() == ".zarr"
|
|
324
|
+
)
|
|
325
|
+
looks_parquet = s.endswith(
|
|
326
|
+
(".ome.parquet", ".parquet", ".pq")
|
|
327
|
+
) or p.suffix.lower() in {
|
|
328
|
+
".parquet",
|
|
329
|
+
".pq",
|
|
330
|
+
}
|
|
331
|
+
looks_vortex = s.endswith(
|
|
332
|
+
(".ome.vortex", ".vortex")
|
|
333
|
+
) or p.suffix.lower() in {
|
|
334
|
+
".vortex",
|
|
335
|
+
}
|
|
336
|
+
looks_tiff = s.endswith(
|
|
337
|
+
(".ome.tif", ".ome.tiff", ".tif", ".tiff")
|
|
338
|
+
) or p.suffix.lower() in {
|
|
339
|
+
".tif",
|
|
340
|
+
".tiff",
|
|
341
|
+
}
|
|
342
|
+
looks_npy = s.endswith(".npy")
|
|
343
|
+
|
|
344
|
+
add_kwargs: dict[str, Any] = {"name": p.name}
|
|
345
|
+
|
|
346
|
+
# ---- OME-Arrow-backed sources -----------------------------------
|
|
347
|
+
if (
|
|
348
|
+
looks_stack
|
|
349
|
+
or looks_zarr
|
|
350
|
+
or looks_parquet
|
|
351
|
+
or looks_tiff
|
|
352
|
+
or looks_vortex
|
|
353
|
+
):
|
|
354
|
+
scalar = None
|
|
355
|
+
if looks_vortex:
|
|
356
|
+
# Vortex needs ome-arrow's ingest helper to produce a typed scalar.
|
|
357
|
+
scalar = _read_vortex_scalar(src)
|
|
358
|
+
if looks_stack and stack_default_dim is not None:
|
|
359
|
+
scalar = from_stack_pattern_path(
|
|
360
|
+
src,
|
|
361
|
+
default_dim_for_unspecified=stack_default_dim,
|
|
362
|
+
map_series_to="T",
|
|
363
|
+
clamp_to_uint16=True,
|
|
364
|
+
)
|
|
365
|
+
obj = OMEArrow(scalar if scalar is not None else src)
|
|
366
|
+
arr = obj.export(how="numpy", dtype=np.uint16) # TCZYX
|
|
367
|
+
info = obj.info() # may contain 'shape': (T, C, Z, Y, X)
|
|
368
|
+
scale_override: tuple[float, ...] | None = None
|
|
369
|
+
inferred_scale = _scale_from_ome_arrow(obj)
|
|
370
|
+
if looks_stack:
|
|
371
|
+
# Stack scales are optional and can be provided via env or prompt.
|
|
372
|
+
if stack_scale_override is not None:
|
|
373
|
+
scale_override = tuple(stack_scale_override)
|
|
374
|
+
else:
|
|
375
|
+
env_scale = os.environ.get("NAPARI_OME_ARROW_STACK_SCALE")
|
|
376
|
+
if env_scale:
|
|
377
|
+
try:
|
|
378
|
+
scale_override = _parse_stack_scale(env_scale)
|
|
379
|
+
except ValueError as exc:
|
|
380
|
+
warnings.warn(
|
|
381
|
+
f"Invalid NAPARI_OME_ARROW_STACK_SCALE '{env_scale}': {exc}.",
|
|
382
|
+
stacklevel=2,
|
|
383
|
+
)
|
|
384
|
+
scale_override = None
|
|
385
|
+
|
|
386
|
+
if scale_override is None and inferred_scale is None:
|
|
387
|
+
scale_override = _prompt_stack_scale(
|
|
388
|
+
sample_path=src, default_scale=None
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
# Recover from accidental 1D flatten by using metadata shape.
|
|
392
|
+
if getattr(arr, "ndim", 0) == 1:
|
|
393
|
+
T, C, Z, Y, X = info.get("shape", (1, 1, 1, 0, 0))
|
|
394
|
+
if Y and X and arr.size == Y * X:
|
|
395
|
+
arr = arr.reshape((1, 1, 1, Y, X))
|
|
396
|
+
else:
|
|
397
|
+
raise ValueError(
|
|
398
|
+
f"Flat array with unknown shape for {src}: size={arr.size}"
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
if mode == "image":
|
|
402
|
+
# Image: preserve channels
|
|
403
|
+
if arr.ndim >= 5:
|
|
404
|
+
add_kwargs["channel_axis"] = 1 # TCZYX
|
|
405
|
+
elif arr.ndim == 4:
|
|
406
|
+
add_kwargs["channel_axis"] = 0 # CZYX
|
|
407
|
+
layer_type = "image"
|
|
408
|
+
else:
|
|
409
|
+
# Labels: squash channels, ensure integer dtype
|
|
410
|
+
if arr.ndim == 5: # (T, C, Z, Y, X)
|
|
411
|
+
arr = arr[:, 0, ...]
|
|
412
|
+
elif arr.ndim == 4: # (C, Z, Y, X)
|
|
413
|
+
arr = arr[0, ...]
|
|
414
|
+
arr = _as_labels(arr)
|
|
415
|
+
add_kwargs.setdefault("opacity", 0.7)
|
|
416
|
+
layer_type = "labels"
|
|
417
|
+
|
|
418
|
+
# 🔹 Ask viewer to switch to 3D if there is a real Z-stack
|
|
419
|
+
_maybe_set_viewer_3d(arr)
|
|
420
|
+
|
|
421
|
+
if looks_stack:
|
|
422
|
+
# Stack scale can be inferred or user-provided.
|
|
423
|
+
scale_override = (
|
|
424
|
+
scale_override
|
|
425
|
+
if scale_override is not None
|
|
426
|
+
else inferred_scale
|
|
427
|
+
)
|
|
428
|
+
scale = (
|
|
429
|
+
_scale_for_array(arr, mode, add_kwargs, scale_override)
|
|
430
|
+
if scale_override is not None
|
|
431
|
+
else None
|
|
432
|
+
)
|
|
433
|
+
else:
|
|
434
|
+
scale = (
|
|
435
|
+
_scale_for_array(arr, mode, add_kwargs, inferred_scale)
|
|
436
|
+
if inferred_scale is not None
|
|
437
|
+
else None
|
|
438
|
+
)
|
|
439
|
+
if scale is not None:
|
|
440
|
+
add_kwargs["scale"] = scale
|
|
441
|
+
|
|
442
|
+
return arr, add_kwargs, layer_type
|
|
443
|
+
|
|
444
|
+
# ---- bare .npy fallback -----------------------------------------
|
|
445
|
+
if looks_npy:
|
|
446
|
+
# Minimal fallback for plain numpy arrays.
|
|
447
|
+
arr = np.load(src)
|
|
448
|
+
if arr.ndim == 1:
|
|
449
|
+
n = int(np.sqrt(arr.size))
|
|
450
|
+
if n * n == arr.size:
|
|
451
|
+
arr = arr.reshape(n, n)
|
|
452
|
+
else:
|
|
453
|
+
raise ValueError(
|
|
454
|
+
f".npy is 1D and not a square image: {arr.shape}"
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
if mode == "image":
|
|
458
|
+
if arr.ndim == 3 and arr.shape[0] <= 6:
|
|
459
|
+
add_kwargs["channel_axis"] = 0
|
|
460
|
+
layer_type = "image"
|
|
461
|
+
else:
|
|
462
|
+
# labels
|
|
463
|
+
if arr.ndim == 3: # treat as (C, Y, X) → first channel
|
|
464
|
+
arr = arr[0, ...]
|
|
465
|
+
arr = _as_labels(arr)
|
|
466
|
+
add_kwargs.setdefault("opacity", 0.7)
|
|
467
|
+
layer_type = "labels"
|
|
468
|
+
|
|
469
|
+
# 🔹 Same 3D toggle for npy-based data
|
|
470
|
+
_maybe_set_viewer_3d(arr)
|
|
471
|
+
|
|
472
|
+
return arr, add_kwargs, layer_type
|
|
473
|
+
|
|
474
|
+
raise ValueError(f"Unrecognized path for napari-ome-arrow reader: {src}")
|