napari-ome-arrow 0.0.3__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.
@@ -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