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.
- ome_arrow/__init__.py +8 -1
- ome_arrow/_version.py +2 -2
- ome_arrow/core.py +40 -4
- ome_arrow/export.py +58 -0
- ome_arrow/ingest.py +306 -88
- ome_arrow/view.py +17 -0
- {ome_arrow-0.0.4.dist-info → ome_arrow-0.0.5.dist-info}/METADATA +7 -2
- ome_arrow-0.0.5.dist-info/RECORD +14 -0
- ome_arrow-0.0.4.dist-info/RECORD +0 -14
- {ome_arrow-0.0.4.dist-info → ome_arrow-0.0.5.dist-info}/WHEEL +0 -0
- {ome_arrow-0.0.4.dist-info → ome_arrow-0.0.5.dist-info}/licenses/LICENSE +0 -0
- {ome_arrow-0.0.4.dist-info → ome_arrow-0.0.5.dist-info}/top_level.txt +0 -0
ome_arrow/__init__.py
CHANGED
|
@@ -4,10 +4,17 @@ Init file for ome_arrow package.
|
|
|
4
4
|
|
|
5
5
|
from ome_arrow._version import version as ome_arrow_version
|
|
6
6
|
from ome_arrow.core import OMEArrow
|
|
7
|
-
from ome_arrow.export import
|
|
7
|
+
from ome_arrow.export import (
|
|
8
|
+
to_numpy,
|
|
9
|
+
to_ome_parquet,
|
|
10
|
+
to_ome_tiff,
|
|
11
|
+
to_ome_vortex,
|
|
12
|
+
to_ome_zarr,
|
|
13
|
+
)
|
|
8
14
|
from ome_arrow.ingest import (
|
|
9
15
|
from_numpy,
|
|
10
16
|
from_ome_parquet,
|
|
17
|
+
from_ome_vortex,
|
|
11
18
|
from_ome_zarr,
|
|
12
19
|
from_tiff,
|
|
13
20
|
to_ome_arrow,
|
ome_arrow/_version.py
CHANGED
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.0.
|
|
32
|
-
__version_tuple__ = version_tuple = (0, 0,
|
|
31
|
+
__version__ = version = '0.0.5'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 0, 5)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
ome_arrow/core.py
CHANGED
|
@@ -11,10 +11,17 @@ import matplotlib
|
|
|
11
11
|
import numpy as np
|
|
12
12
|
import pyarrow as pa
|
|
13
13
|
|
|
14
|
-
from ome_arrow.export import
|
|
14
|
+
from ome_arrow.export import (
|
|
15
|
+
to_numpy,
|
|
16
|
+
to_ome_parquet,
|
|
17
|
+
to_ome_tiff,
|
|
18
|
+
to_ome_vortex,
|
|
19
|
+
to_ome_zarr,
|
|
20
|
+
)
|
|
15
21
|
from ome_arrow.ingest import (
|
|
16
22
|
from_numpy,
|
|
17
23
|
from_ome_parquet,
|
|
24
|
+
from_ome_vortex,
|
|
18
25
|
from_ome_zarr,
|
|
19
26
|
from_stack_pattern_path,
|
|
20
27
|
from_tiff,
|
|
@@ -59,6 +66,7 @@ class OMEArrow:
|
|
|
59
66
|
- a path/URL to an OME-TIFF (.tif/.tiff)
|
|
60
67
|
- a path/URL to an OME-Zarr store (.zarr / .ome.zarr)
|
|
61
68
|
- a path/URL to an OME-Parquet file (.parquet / .pq)
|
|
69
|
+
- a path/URL to a Vortex file (.vortex)
|
|
62
70
|
- a NumPy ndarray (2D-5D; interpreted
|
|
63
71
|
with from_numpy defaults)
|
|
64
72
|
- a dict already matching the OME-Arrow schema
|
|
@@ -100,6 +108,12 @@ class OMEArrow:
|
|
|
100
108
|
s, column_name=column_name, row_index=row_index
|
|
101
109
|
)
|
|
102
110
|
|
|
111
|
+
# Vortex
|
|
112
|
+
elif s.lower().endswith(".vortex") or path.suffix.lower() == ".vortex":
|
|
113
|
+
self.data = from_ome_vortex(
|
|
114
|
+
s, column_name=column_name, row_index=row_index
|
|
115
|
+
)
|
|
116
|
+
|
|
103
117
|
# TIFF
|
|
104
118
|
elif path.suffix.lower() in {".tif", ".tiff"} or s.lower().endswith(
|
|
105
119
|
(".tif", ".tiff")
|
|
@@ -117,6 +131,7 @@ class OMEArrow:
|
|
|
117
131
|
" • Bio-Formats pattern string (contains '<', '>' or '*')\n"
|
|
118
132
|
" • OME-Zarr path/URL ending with '.zarr' or '.ome.zarr'\n"
|
|
119
133
|
" • OME-Parquet file ending with '.parquet' or '.pq'\n"
|
|
134
|
+
" • Vortex file ending with '.vortex'\n"
|
|
120
135
|
" • OME-TIFF path/URL ending with '.tif' or '.tiff'"
|
|
121
136
|
)
|
|
122
137
|
|
|
@@ -141,7 +156,7 @@ class OMEArrow:
|
|
|
141
156
|
"input data must be str, dict, pa.StructScalar, or numpy.ndarray"
|
|
142
157
|
)
|
|
143
158
|
|
|
144
|
-
def export(
|
|
159
|
+
def export( # noqa: PLR0911
|
|
145
160
|
self,
|
|
146
161
|
how: str = "numpy",
|
|
147
162
|
dtype: np.dtype = np.uint16,
|
|
@@ -165,6 +180,8 @@ class OMEArrow:
|
|
|
165
180
|
parquet_column_name: str = "ome_arrow",
|
|
166
181
|
parquet_compression: str | None = "zstd",
|
|
167
182
|
parquet_metadata: dict[str, str] | None = None,
|
|
183
|
+
vortex_column_name: str = "ome_arrow",
|
|
184
|
+
vortex_metadata: dict[str, str] | None = None,
|
|
168
185
|
) -> np.array | dict | pa.StructScalar | str:
|
|
169
186
|
"""
|
|
170
187
|
Export the OME-Arrow content in a chosen representation.
|
|
@@ -178,6 +195,7 @@ class OMEArrow:
|
|
|
178
195
|
"ome-tiff" → write OME-TIFF via BioIO
|
|
179
196
|
"ome-zarr" → write OME-Zarr (OME-NGFF) via BioIO
|
|
180
197
|
"parquet" → write a single-row Parquet with one struct column
|
|
198
|
+
"vortex" → write a single-row Vortex file with one struct column
|
|
181
199
|
dtype:
|
|
182
200
|
Target dtype for "numpy"/writers (default: np.uint16).
|
|
183
201
|
strict:
|
|
@@ -199,6 +217,8 @@ class OMEArrow:
|
|
|
199
217
|
Try to embed per-channel display colors when safe; otherwise omitted.
|
|
200
218
|
parquet_*:
|
|
201
219
|
Options for Parquet export (column name, compression, file metadata).
|
|
220
|
+
vortex_*:
|
|
221
|
+
Options for Vortex export (column name, file metadata).
|
|
202
222
|
|
|
203
223
|
Returns
|
|
204
224
|
-------
|
|
@@ -209,6 +229,7 @@ class OMEArrow:
|
|
|
209
229
|
- "ome-tiff": output path (str)
|
|
210
230
|
- "ome-zarr": output path (str)
|
|
211
231
|
- "parquet": output path (str)
|
|
232
|
+
- "vortex": output path (str)
|
|
212
233
|
|
|
213
234
|
Raises
|
|
214
235
|
------
|
|
@@ -271,6 +292,18 @@ class OMEArrow:
|
|
|
271
292
|
)
|
|
272
293
|
return out
|
|
273
294
|
|
|
295
|
+
# Vortex (single row, single struct column)
|
|
296
|
+
if mode in {"ome-vortex", "omevortex", "vortex"}:
|
|
297
|
+
if not out:
|
|
298
|
+
raise ValueError("export(how='vortex') requires 'out' path.")
|
|
299
|
+
to_ome_vortex(
|
|
300
|
+
data=self.data,
|
|
301
|
+
out_path=out,
|
|
302
|
+
column_name=vortex_column_name,
|
|
303
|
+
file_metadata=vortex_metadata,
|
|
304
|
+
)
|
|
305
|
+
return out
|
|
306
|
+
|
|
274
307
|
raise ValueError(f"Unknown export method: {how}")
|
|
275
308
|
|
|
276
309
|
def info(self) -> Dict[str, Any]:
|
|
@@ -299,7 +332,7 @@ class OMEArrow:
|
|
|
299
332
|
opacity: str | float = "sigmoid",
|
|
300
333
|
clim: tuple[float, float] | None = None,
|
|
301
334
|
show_axes: bool = True,
|
|
302
|
-
scaling_values: tuple[float, float, float] | None =
|
|
335
|
+
scaling_values: tuple[float, float, float] | None = None,
|
|
303
336
|
) -> matplotlib.figure.Figure | "pyvista.Plotter":
|
|
304
337
|
"""
|
|
305
338
|
Render an OME-Arrow record using Matplotlib or PyVista.
|
|
@@ -337,7 +370,10 @@ class OMEArrow:
|
|
|
337
370
|
clim: Contrast limits (``(low, high)``) for PyVista rendering.
|
|
338
371
|
show_axes: If ``True``, display axes in the PyVista scene.
|
|
339
372
|
scaling_values: Physical scale multipliers for the (x, y, z) axes used by
|
|
340
|
-
PyVista, typically to express anisotropy.
|
|
373
|
+
PyVista, typically to express anisotropy. If ``None``, uses metadata
|
|
374
|
+
scaling from the OME-Arrow record (pixels_meta.physical_size_x/y/z).
|
|
375
|
+
These scaling values will default to 1µm if metadata is missing in
|
|
376
|
+
source image metadata.
|
|
341
377
|
|
|
342
378
|
Returns:
|
|
343
379
|
matplotlib.figure.Figure | pyvista.Plotter:
|
ome_arrow/export.py
CHANGED
|
@@ -420,3 +420,61 @@ def to_ome_parquet(
|
|
|
420
420
|
compression=compression,
|
|
421
421
|
row_group_size=row_group_size,
|
|
422
422
|
)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def to_ome_vortex(
|
|
426
|
+
data: Dict[str, Any] | pa.StructScalar,
|
|
427
|
+
out_path: str,
|
|
428
|
+
column_name: str = "image",
|
|
429
|
+
file_metadata: Optional[Dict[str, str]] = None,
|
|
430
|
+
) -> None:
|
|
431
|
+
"""Export an OME-Arrow record to a Vortex file.
|
|
432
|
+
|
|
433
|
+
The file is written as a single-row, single-column Arrow table where the
|
|
434
|
+
column holds a struct with the OME-Arrow schema.
|
|
435
|
+
|
|
436
|
+
Args:
|
|
437
|
+
data: OME-Arrow dict or StructScalar.
|
|
438
|
+
out_path: Output path for the Vortex file.
|
|
439
|
+
column_name: Column name to store the struct.
|
|
440
|
+
file_metadata: Optional file-level metadata to attach.
|
|
441
|
+
|
|
442
|
+
Raises:
|
|
443
|
+
ImportError: If the optional `vortex-data` dependency is missing.
|
|
444
|
+
"""
|
|
445
|
+
|
|
446
|
+
try:
|
|
447
|
+
import vortex.io as vxio
|
|
448
|
+
except ImportError as exc:
|
|
449
|
+
raise ImportError(
|
|
450
|
+
"Vortex export requires the optional 'vortex-data' dependency."
|
|
451
|
+
) from exc
|
|
452
|
+
|
|
453
|
+
# 1) Normalize to a plain Python dict (works better with pyarrow builders,
|
|
454
|
+
# especially when the struct has a `null`-typed field like "masks").
|
|
455
|
+
if isinstance(data, pa.StructScalar):
|
|
456
|
+
record_dict = data.as_py()
|
|
457
|
+
else:
|
|
458
|
+
# Validate by round-tripping through a typed scalar, then back to dict.
|
|
459
|
+
record_dict = pa.scalar(data, type=OME_ARROW_STRUCT).as_py()
|
|
460
|
+
|
|
461
|
+
# 2) Build a single-row struct array from the dict, explicitly passing the schema
|
|
462
|
+
struct_array = pa.array([record_dict], type=OME_ARROW_STRUCT) # len=1
|
|
463
|
+
|
|
464
|
+
# 3) Wrap into a one-column table
|
|
465
|
+
table = pa.table({column_name: struct_array})
|
|
466
|
+
|
|
467
|
+
# 4) Attach optional file-level metadata
|
|
468
|
+
meta: Dict[bytes, bytes] = dict(table.schema.metadata or {})
|
|
469
|
+
try:
|
|
470
|
+
meta[b"ome.arrow.type"] = str(OME_ARROW_TAG_TYPE).encode("utf-8")
|
|
471
|
+
meta[b"ome.arrow.version"] = str(OME_ARROW_TAG_VERSION).encode("utf-8")
|
|
472
|
+
except Exception:
|
|
473
|
+
pass
|
|
474
|
+
if file_metadata:
|
|
475
|
+
for k, v in file_metadata.items():
|
|
476
|
+
meta[str(k).encode("utf-8")] = str(v).encode("utf-8")
|
|
477
|
+
table = table.replace_schema_metadata(meta)
|
|
478
|
+
|
|
479
|
+
# 5) Write Vortex (single row, single column)
|
|
480
|
+
vxio.write(table, str(out_path))
|
ome_arrow/ingest.py
CHANGED
|
@@ -3,6 +3,7 @@ Converting to and from OME-Arrow formats.
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import itertools
|
|
6
|
+
import json
|
|
6
7
|
import re
|
|
7
8
|
import warnings
|
|
8
9
|
from datetime import datetime, timezone
|
|
@@ -20,6 +21,228 @@ from bioio_ome_zarr import Reader as OMEZarrReader
|
|
|
20
21
|
from ome_arrow.meta import OME_ARROW_STRUCT, OME_ARROW_TAG_TYPE, OME_ARROW_TAG_VERSION
|
|
21
22
|
|
|
22
23
|
|
|
24
|
+
def _ome_arrow_from_table(
|
|
25
|
+
table: pa.Table,
|
|
26
|
+
*,
|
|
27
|
+
column_name: Optional[str],
|
|
28
|
+
row_index: int,
|
|
29
|
+
strict_schema: bool,
|
|
30
|
+
) -> pa.StructScalar:
|
|
31
|
+
"""Extract a single OME-Arrow record from an Arrow table.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
table: Source Arrow table.
|
|
35
|
+
column_name: Column to read; auto-detected when None or invalid.
|
|
36
|
+
row_index: Row index to extract.
|
|
37
|
+
strict_schema: Require the exact OME-Arrow schema if True.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
A typed OME-Arrow StructScalar.
|
|
41
|
+
|
|
42
|
+
Raises:
|
|
43
|
+
ValueError: If the row index is out of range or no suitable column exists.
|
|
44
|
+
"""
|
|
45
|
+
if table.num_rows == 0:
|
|
46
|
+
raise ValueError("Table contains 0 rows; expected at least 1.")
|
|
47
|
+
if not (0 <= row_index < table.num_rows):
|
|
48
|
+
raise ValueError(f"row_index {row_index} out of range [0, {table.num_rows}).")
|
|
49
|
+
|
|
50
|
+
# 1) Locate the OME-Arrow column
|
|
51
|
+
def _struct_matches_ome_fields(t: pa.StructType) -> bool:
|
|
52
|
+
ome_fields = {f.name for f in OME_ARROW_STRUCT}
|
|
53
|
+
col_fields = {f.name for f in t}
|
|
54
|
+
return ome_fields == col_fields
|
|
55
|
+
|
|
56
|
+
requested_name = column_name
|
|
57
|
+
candidate_col = None
|
|
58
|
+
autodetected_name = None
|
|
59
|
+
|
|
60
|
+
if column_name is not None and column_name in table.column_names:
|
|
61
|
+
arr = table[column_name]
|
|
62
|
+
if not pa.types.is_struct(arr.type):
|
|
63
|
+
raise ValueError(f"Column '{column_name}' is not a Struct; got {arr.type}.")
|
|
64
|
+
if strict_schema and arr.type != OME_ARROW_STRUCT:
|
|
65
|
+
raise ValueError(
|
|
66
|
+
f"Column '{column_name}' schema != OME_ARROW_STRUCT.\n"
|
|
67
|
+
f"Got: {arr.type}\n"
|
|
68
|
+
f"Expect:{OME_ARROW_STRUCT}"
|
|
69
|
+
)
|
|
70
|
+
if not strict_schema and not _struct_matches_ome_fields(arr.type):
|
|
71
|
+
raise ValueError(
|
|
72
|
+
f"Column '{column_name}' does not have the expected OME-Arrow fields."
|
|
73
|
+
)
|
|
74
|
+
candidate_col = arr
|
|
75
|
+
else:
|
|
76
|
+
# Auto-detect a struct column that matches OME-Arrow fields
|
|
77
|
+
for name in table.column_names:
|
|
78
|
+
arr = table[name]
|
|
79
|
+
if pa.types.is_struct(arr.type):
|
|
80
|
+
if strict_schema and arr.type == OME_ARROW_STRUCT:
|
|
81
|
+
candidate_col = arr
|
|
82
|
+
autodetected_name = name
|
|
83
|
+
column_name = name
|
|
84
|
+
break
|
|
85
|
+
if not strict_schema and _struct_matches_ome_fields(arr.type):
|
|
86
|
+
candidate_col = arr
|
|
87
|
+
autodetected_name = name
|
|
88
|
+
column_name = name
|
|
89
|
+
break
|
|
90
|
+
if candidate_col is None:
|
|
91
|
+
if column_name is None:
|
|
92
|
+
hint = "no struct column with OME-Arrow fields was found."
|
|
93
|
+
else:
|
|
94
|
+
hint = f"column '{column_name}' not found and auto-detection failed."
|
|
95
|
+
raise ValueError(f"Could not locate an OME-Arrow struct column: {hint}")
|
|
96
|
+
|
|
97
|
+
# Emit warning if auto-detection was used
|
|
98
|
+
if autodetected_name is not None and autodetected_name != requested_name:
|
|
99
|
+
warnings.warn(
|
|
100
|
+
f"Requested column '{requested_name}' was not usable or not found. "
|
|
101
|
+
f"Auto-detected OME-Arrow column '{autodetected_name}'.",
|
|
102
|
+
UserWarning,
|
|
103
|
+
stacklevel=2,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# 2) Extract the row as a Python dict
|
|
107
|
+
record_dict: Dict[str, Any] = candidate_col.slice(row_index, 1).to_pylist()[0]
|
|
108
|
+
|
|
109
|
+
# 3) Reconstruct a typed StructScalar using the canonical schema
|
|
110
|
+
scalar = pa.scalar(record_dict, type=OME_ARROW_STRUCT)
|
|
111
|
+
|
|
112
|
+
# Optional: soft validation via file-level metadata (if present)
|
|
113
|
+
try:
|
|
114
|
+
meta = table.schema.metadata or {}
|
|
115
|
+
meta.get(b"ome.arrow.type", b"").decode() == str(OME_ARROW_TAG_TYPE)
|
|
116
|
+
meta.get(b"ome.arrow.version", b"").decode() == str(OME_ARROW_TAG_VERSION)
|
|
117
|
+
except Exception:
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
return scalar
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _normalize_unit(unit: str | None) -> str | None:
|
|
124
|
+
if not unit:
|
|
125
|
+
return None
|
|
126
|
+
u = unit.strip().lower()
|
|
127
|
+
if u in {"micrometer", "micrometre", "micron", "microns", "um", "µm"}:
|
|
128
|
+
return "µm"
|
|
129
|
+
if u in {"nanometer", "nanometre", "nm"}:
|
|
130
|
+
return "nm"
|
|
131
|
+
return unit
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _read_physical_pixel_sizes(
|
|
135
|
+
img: BioImage,
|
|
136
|
+
) -> tuple[float, float, float, str | None, bool]:
|
|
137
|
+
pps = getattr(img, "physical_pixel_sizes", None)
|
|
138
|
+
if pps is None:
|
|
139
|
+
return 1.0, 1.0, 1.0, None, False
|
|
140
|
+
|
|
141
|
+
vx = getattr(pps, "X", None) or getattr(pps, "x", None)
|
|
142
|
+
vy = getattr(pps, "Y", None) or getattr(pps, "y", None)
|
|
143
|
+
vz = getattr(pps, "Z", None) or getattr(pps, "z", None)
|
|
144
|
+
|
|
145
|
+
if vx is None and vy is None and vz is None:
|
|
146
|
+
return 1.0, 1.0, 1.0, None, False
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
psize_x = float(vx or 1.0)
|
|
150
|
+
psize_y = float(vy or 1.0)
|
|
151
|
+
psize_z = float(vz or 1.0)
|
|
152
|
+
except Exception:
|
|
153
|
+
return 1.0, 1.0, 1.0, None, False
|
|
154
|
+
|
|
155
|
+
unit = getattr(pps, "unit", None) or getattr(pps, "units", None)
|
|
156
|
+
unit = _normalize_unit(str(unit)) if unit is not None else None
|
|
157
|
+
|
|
158
|
+
return psize_x, psize_y, psize_z, unit, True
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _load_zarr_attrs(zarr_path: Path) -> dict:
|
|
162
|
+
zarr_json = zarr_path / "zarr.json"
|
|
163
|
+
if zarr_json.exists():
|
|
164
|
+
try:
|
|
165
|
+
data = json.loads(zarr_json.read_text())
|
|
166
|
+
return data.get("attributes") or data.get("attrs") or {}
|
|
167
|
+
except Exception:
|
|
168
|
+
return {}
|
|
169
|
+
zattrs = zarr_path / ".zattrs"
|
|
170
|
+
if zattrs.exists():
|
|
171
|
+
try:
|
|
172
|
+
return json.loads(zattrs.read_text())
|
|
173
|
+
except Exception:
|
|
174
|
+
return {}
|
|
175
|
+
return {}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _extract_multiscales(attrs: dict) -> list[dict]:
|
|
179
|
+
if not isinstance(attrs, dict):
|
|
180
|
+
return []
|
|
181
|
+
ome = attrs.get("ome")
|
|
182
|
+
if isinstance(ome, dict) and isinstance(ome.get("multiscales"), list):
|
|
183
|
+
return ome["multiscales"]
|
|
184
|
+
if isinstance(attrs.get("multiscales"), list):
|
|
185
|
+
return attrs["multiscales"]
|
|
186
|
+
return []
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _read_ngff_scale(zarr_path: Path) -> tuple[float, float, float, str | None] | None:
|
|
190
|
+
zarr_root = zarr_path
|
|
191
|
+
for parent in [zarr_path, *list(zarr_path.parents)]:
|
|
192
|
+
if parent.suffix.lower() in {".zarr", ".ome.zarr"}:
|
|
193
|
+
zarr_root = parent
|
|
194
|
+
break
|
|
195
|
+
|
|
196
|
+
for candidate in (zarr_path, zarr_root):
|
|
197
|
+
attrs = _load_zarr_attrs(candidate)
|
|
198
|
+
multiscales = _extract_multiscales(attrs)
|
|
199
|
+
if multiscales:
|
|
200
|
+
break
|
|
201
|
+
else:
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
ms = multiscales[0]
|
|
205
|
+
axes = ms.get("axes") or []
|
|
206
|
+
datasets = ms.get("datasets") or []
|
|
207
|
+
if not axes or not datasets:
|
|
208
|
+
return None
|
|
209
|
+
|
|
210
|
+
ds = next((d for d in datasets if str(d.get("path")) == "0"), datasets[0])
|
|
211
|
+
cts = ds.get("coordinateTransformations") or []
|
|
212
|
+
scale_ct = next((ct for ct in cts if ct.get("type") == "scale"), None)
|
|
213
|
+
if not scale_ct:
|
|
214
|
+
return None
|
|
215
|
+
|
|
216
|
+
scale = scale_ct.get("scale") or []
|
|
217
|
+
if len(scale) != len(axes):
|
|
218
|
+
return None
|
|
219
|
+
|
|
220
|
+
axis_scale: dict[str, float] = {}
|
|
221
|
+
axis_unit: dict[str, str] = {}
|
|
222
|
+
for i, ax in enumerate(axes):
|
|
223
|
+
name = str(ax.get("name", "")).lower()
|
|
224
|
+
if name in {"x", "y", "z"}:
|
|
225
|
+
try:
|
|
226
|
+
axis_scale[name] = float(scale[i])
|
|
227
|
+
except Exception:
|
|
228
|
+
continue
|
|
229
|
+
unit = _normalize_unit(ax.get("unit"))
|
|
230
|
+
if unit:
|
|
231
|
+
axis_unit[name] = unit
|
|
232
|
+
|
|
233
|
+
if not axis_scale:
|
|
234
|
+
return None
|
|
235
|
+
|
|
236
|
+
psize_x = axis_scale.get("x", 1.0)
|
|
237
|
+
psize_y = axis_scale.get("y", 1.0)
|
|
238
|
+
psize_z = axis_scale.get("z", 1.0)
|
|
239
|
+
|
|
240
|
+
units = [axis_unit.get(a) for a in ("x", "y", "z") if axis_unit.get(a)]
|
|
241
|
+
unit = units[0] if units and len(set(units)) == 1 else None
|
|
242
|
+
|
|
243
|
+
return psize_x, psize_y, psize_z, unit
|
|
244
|
+
|
|
245
|
+
|
|
23
246
|
def to_ome_arrow(
|
|
24
247
|
type_: str = OME_ARROW_TAG_TYPE,
|
|
25
248
|
version: str = OME_ARROW_TAG_VERSION,
|
|
@@ -338,13 +561,8 @@ def from_tiff(
|
|
|
338
561
|
if size_x <= 0 or size_y <= 0:
|
|
339
562
|
raise ValueError("Image must have positive Y and X dims.")
|
|
340
563
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
psize_x = float(getattr(pps, "X", None) or 1.0)
|
|
344
|
-
psize_y = float(getattr(pps, "Y", None) or 1.0)
|
|
345
|
-
psize_z = float(getattr(pps, "Z", None) or 1.0)
|
|
346
|
-
except Exception:
|
|
347
|
-
psize_x = psize_y = psize_z = 1.0
|
|
564
|
+
psize_x, psize_y, psize_z, unit, _pps_valid = _read_physical_pixel_sizes(img)
|
|
565
|
+
psize_unit = unit or "µm"
|
|
348
566
|
|
|
349
567
|
# --- NEW: coerce top-level strings --------------------------------
|
|
350
568
|
img_id = str(image_id or p.stem)
|
|
@@ -394,7 +612,7 @@ def from_tiff(
|
|
|
394
612
|
physical_size_x=psize_x,
|
|
395
613
|
physical_size_y=psize_y,
|
|
396
614
|
physical_size_z=psize_z,
|
|
397
|
-
physical_size_unit=
|
|
615
|
+
physical_size_unit=psize_unit,
|
|
398
616
|
channels=channels,
|
|
399
617
|
planes=planes,
|
|
400
618
|
masks=None,
|
|
@@ -410,6 +628,20 @@ def from_stack_pattern_path(
|
|
|
410
628
|
image_id: Optional[str] = None,
|
|
411
629
|
name: Optional[str] = None,
|
|
412
630
|
) -> pa.StructScalar:
|
|
631
|
+
"""Build an OME-Arrow record from a filename pattern describing a stack.
|
|
632
|
+
|
|
633
|
+
Args:
|
|
634
|
+
pattern_path: Path or pattern string describing the stack layout.
|
|
635
|
+
default_dim_for_unspecified: Dimension to use when tokens lack a dim.
|
|
636
|
+
map_series_to: Dimension to map series tokens to (e.g., "T"), or None.
|
|
637
|
+
clamp_to_uint16: Whether to clamp pixel values to uint16.
|
|
638
|
+
channel_names: Optional list of channel names to apply.
|
|
639
|
+
image_id: Optional image identifier override.
|
|
640
|
+
name: Optional display name override.
|
|
641
|
+
|
|
642
|
+
Returns:
|
|
643
|
+
A validated OME-Arrow StructScalar describing the stack.
|
|
644
|
+
"""
|
|
413
645
|
path = Path(pattern_path)
|
|
414
646
|
folder = path.parent
|
|
415
647
|
line = path.name.strip()
|
|
@@ -741,13 +973,15 @@ def from_ome_zarr(
|
|
|
741
973
|
if size_x <= 0 or size_y <= 0:
|
|
742
974
|
raise ValueError("Image must have positive Y and X dimensions.")
|
|
743
975
|
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
976
|
+
psize_x, psize_y, psize_z, unit, pps_valid = _read_physical_pixel_sizes(img)
|
|
977
|
+
psize_unit = unit or "µm"
|
|
978
|
+
|
|
979
|
+
if not pps_valid:
|
|
980
|
+
ngff_scale = _read_ngff_scale(p)
|
|
981
|
+
if ngff_scale is not None:
|
|
982
|
+
psize_x, psize_y, psize_z, unit = ngff_scale
|
|
983
|
+
if unit:
|
|
984
|
+
psize_unit = unit
|
|
751
985
|
|
|
752
986
|
img_id = str(image_id or p.stem)
|
|
753
987
|
display_name = str(name or p.name)
|
|
@@ -805,7 +1039,7 @@ def from_ome_zarr(
|
|
|
805
1039
|
physical_size_x=psize_x,
|
|
806
1040
|
physical_size_y=psize_y,
|
|
807
1041
|
physical_size_z=psize_z,
|
|
808
|
-
physical_size_unit=
|
|
1042
|
+
physical_size_unit=psize_unit,
|
|
809
1043
|
channels=channels,
|
|
810
1044
|
planes=planes,
|
|
811
1045
|
masks=None,
|
|
@@ -819,88 +1053,72 @@ def from_ome_parquet(
|
|
|
819
1053
|
row_index: int = 0,
|
|
820
1054
|
strict_schema: bool = False,
|
|
821
1055
|
) -> pa.StructScalar:
|
|
822
|
-
"""
|
|
823
|
-
|
|
1056
|
+
"""Read an OME-Arrow record from a Parquet file.
|
|
1057
|
+
|
|
1058
|
+
Args:
|
|
1059
|
+
parquet_path: Path to the Parquet file.
|
|
1060
|
+
column_name: Column to read; auto-detected when None or invalid.
|
|
1061
|
+
row_index: Row index to extract.
|
|
1062
|
+
strict_schema: Require the exact OME-Arrow schema if True.
|
|
1063
|
+
|
|
1064
|
+
Returns:
|
|
1065
|
+
A typed OME-Arrow StructScalar.
|
|
1066
|
+
|
|
1067
|
+
Raises:
|
|
1068
|
+
FileNotFoundError: If the Parquet path does not exist.
|
|
1069
|
+
ValueError: If the row index is out of range or no suitable column exists.
|
|
824
1070
|
"""
|
|
825
1071
|
p = Path(parquet_path)
|
|
826
1072
|
if not p.exists():
|
|
827
1073
|
raise FileNotFoundError(f"No such file: {p}")
|
|
828
1074
|
|
|
829
1075
|
table = pq.read_table(p)
|
|
1076
|
+
return _ome_arrow_from_table(
|
|
1077
|
+
table,
|
|
1078
|
+
column_name=column_name,
|
|
1079
|
+
row_index=row_index,
|
|
1080
|
+
strict_schema=strict_schema,
|
|
1081
|
+
)
|
|
830
1082
|
|
|
831
|
-
if table.num_rows == 0:
|
|
832
|
-
raise ValueError("Parquet file contains 0 rows; expected at least 1.")
|
|
833
|
-
if not (0 <= row_index < table.num_rows):
|
|
834
|
-
raise ValueError(f"row_index {row_index} out of range [0, {table.num_rows}).")
|
|
835
|
-
|
|
836
|
-
# 1) Locate the OME-Arrow column
|
|
837
|
-
def _struct_matches_ome_fields(t: pa.StructType) -> bool:
|
|
838
|
-
ome_fields = {f.name for f in OME_ARROW_STRUCT}
|
|
839
|
-
col_fields = {f.name for f in t}
|
|
840
|
-
return ome_fields == col_fields
|
|
841
|
-
|
|
842
|
-
requested_name = column_name
|
|
843
|
-
candidate_col = None
|
|
844
|
-
autodetected_name = None
|
|
845
1083
|
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
f"Expect:{OME_ARROW_STRUCT}"
|
|
855
|
-
)
|
|
856
|
-
if not strict_schema and not _struct_matches_ome_fields(arr.type):
|
|
857
|
-
raise ValueError(
|
|
858
|
-
f"Column '{column_name}' does not have the expected OME-Arrow fields."
|
|
859
|
-
)
|
|
860
|
-
candidate_col = arr
|
|
861
|
-
else:
|
|
862
|
-
# Auto-detect a struct column that matches OME-Arrow fields
|
|
863
|
-
for name in table.column_names:
|
|
864
|
-
arr = table[name]
|
|
865
|
-
if pa.types.is_struct(arr.type):
|
|
866
|
-
if strict_schema and arr.type == OME_ARROW_STRUCT:
|
|
867
|
-
candidate_col = arr
|
|
868
|
-
autodetected_name = name
|
|
869
|
-
column_name = name
|
|
870
|
-
break
|
|
871
|
-
if not strict_schema and _struct_matches_ome_fields(arr.type):
|
|
872
|
-
candidate_col = arr
|
|
873
|
-
autodetected_name = name
|
|
874
|
-
column_name = name
|
|
875
|
-
break
|
|
876
|
-
if candidate_col is None:
|
|
877
|
-
if column_name is None:
|
|
878
|
-
hint = "no struct column with OME-Arrow fields was found."
|
|
879
|
-
else:
|
|
880
|
-
hint = f"column '{column_name}' not found and auto-detection failed."
|
|
881
|
-
raise ValueError(f"Could not locate an OME-Arrow struct column: {hint}")
|
|
1084
|
+
def from_ome_vortex(
|
|
1085
|
+
vortex_path: str | Path,
|
|
1086
|
+
*,
|
|
1087
|
+
column_name: Optional[str] = "ome_arrow",
|
|
1088
|
+
row_index: int = 0,
|
|
1089
|
+
strict_schema: bool = False,
|
|
1090
|
+
) -> pa.StructScalar:
|
|
1091
|
+
"""Read an OME-Arrow record from a Vortex file.
|
|
882
1092
|
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
UserWarning,
|
|
889
|
-
stacklevel=2,
|
|
890
|
-
)
|
|
1093
|
+
Args:
|
|
1094
|
+
vortex_path: Path to the Vortex file.
|
|
1095
|
+
column_name: Column to read; auto-detected when None or invalid.
|
|
1096
|
+
row_index: Row index to extract.
|
|
1097
|
+
strict_schema: Require the exact OME-Arrow schema if True.
|
|
891
1098
|
|
|
892
|
-
|
|
893
|
-
|
|
1099
|
+
Returns:
|
|
1100
|
+
A typed OME-Arrow StructScalar.
|
|
894
1101
|
|
|
895
|
-
|
|
896
|
-
|
|
1102
|
+
Raises:
|
|
1103
|
+
FileNotFoundError: If the Vortex path does not exist.
|
|
1104
|
+
ImportError: If the optional `vortex-data` dependency is missing.
|
|
1105
|
+
ValueError: If the row index is out of range or no suitable column exists.
|
|
1106
|
+
"""
|
|
1107
|
+
p = Path(vortex_path)
|
|
1108
|
+
if not p.exists():
|
|
1109
|
+
raise FileNotFoundError(f"No such file: {p}")
|
|
897
1110
|
|
|
898
|
-
# Optional: soft validation via file-level metadata (if present)
|
|
899
1111
|
try:
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
1112
|
+
import vortex
|
|
1113
|
+
except ImportError as exc:
|
|
1114
|
+
raise ImportError(
|
|
1115
|
+
"Vortex support requires the optional 'vortex-data' dependency."
|
|
1116
|
+
) from exc
|
|
1117
|
+
|
|
1118
|
+
table = vortex.open(str(p)).to_arrow().read_all()
|
|
1119
|
+
return _ome_arrow_from_table(
|
|
1120
|
+
table,
|
|
1121
|
+
column_name=column_name,
|
|
1122
|
+
row_index=row_index,
|
|
1123
|
+
strict_schema=strict_schema,
|
|
1124
|
+
)
|
ome_arrow/view.py
CHANGED
|
@@ -33,6 +33,23 @@ def view_matplotlib(
|
|
|
33
33
|
cmap: str = "gray",
|
|
34
34
|
show: bool = True,
|
|
35
35
|
) -> tuple[Figure, Axes, AxesImage]:
|
|
36
|
+
"""Render a single (t, c, z) plane with Matplotlib.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
data: OME-Arrow row or dict containing pixels_meta and planes.
|
|
40
|
+
tcz: (t, c, z) indices of the plane to render.
|
|
41
|
+
autoscale: If True, infer vmin/vmax from the image data.
|
|
42
|
+
vmin: Explicit lower display limit for intensity scaling.
|
|
43
|
+
vmax: Explicit upper display limit for intensity scaling.
|
|
44
|
+
cmap: Matplotlib colormap name.
|
|
45
|
+
show: Whether to display the plot immediately.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
A tuple of (figure, axes, image) from Matplotlib.
|
|
49
|
+
|
|
50
|
+
Raises:
|
|
51
|
+
ValueError: If the requested plane is missing or pixel sizes mismatch.
|
|
52
|
+
"""
|
|
36
53
|
if isinstance(data, pa.StructScalar):
|
|
37
54
|
data = data.as_py()
|
|
38
55
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ome-arrow
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.5
|
|
4
4
|
Summary: Using OME specifications with Apache Arrow for fast, queryable, and language agnostic bioimage data.
|
|
5
5
|
Author: Dave Bunten
|
|
6
6
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
@@ -28,9 +28,11 @@ Requires-Dist: pyvista>=0.46.4; extra == "viz"
|
|
|
28
28
|
Requires-Dist: trame>=3.12; extra == "viz"
|
|
29
29
|
Requires-Dist: trame-vtk>=2.10; extra == "viz"
|
|
30
30
|
Requires-Dist: trame-vuetify>=3.1; extra == "viz"
|
|
31
|
+
Provides-Extra: vortex
|
|
32
|
+
Requires-Dist: vortex-data>=0.56; extra == "vortex"
|
|
31
33
|
Dynamic: license-file
|
|
32
34
|
|
|
33
|
-
<img
|
|
35
|
+
<img width="600" src="https://raw.githubusercontent.com/wayscience/ome-arrow/main/docs/src/_static/logo.png?raw=true">
|
|
34
36
|
|
|
35
37
|

|
|
36
38
|
[](https://github.com/wayscience/ome-arrow/actions/workflows/run-tests.yml?query=branch%3Amain)
|
|
@@ -104,6 +106,9 @@ oa_image.view(how="pyvista")
|
|
|
104
106
|
# Export to OME-Parquet.
|
|
105
107
|
# We can also export OME-TIFF, OME-Zarr or NumPy arrays.
|
|
106
108
|
oa_image.export(how="ome-parquet", out="your_image.ome.parquet")
|
|
109
|
+
|
|
110
|
+
# Export to Vortex (install extras: `pip install 'ome-arrow[vortex]'`).
|
|
111
|
+
oa_image.export(how="vortex", out="your_image.vortex")
|
|
107
112
|
```
|
|
108
113
|
|
|
109
114
|
## Contributing, Development, and Testing
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
ome_arrow/__init__.py,sha256=WWenJP9XxLZNGQPVOEFBDlDM1kSvj_QdHssrET6UuNQ,644
|
|
2
|
+
ome_arrow/_version.py,sha256=YRV1ohn6CdKEhsUOmFFMmr5UTjMv4Ydw3WJGxF2BHBs,704
|
|
3
|
+
ome_arrow/core.py,sha256=fgEFOwckYi3asosEUhGB8UL9Q93hO56H6qw9fUczFO8,19946
|
|
4
|
+
ome_arrow/export.py,sha256=e9Nx25bD2K51gQng-4rUXM4v1l8-K1YkxGjWKImFrJ4,16972
|
|
5
|
+
ome_arrow/ingest.py,sha256=Vt9hljI718vR-qpJXH4jk4Shs1OtFPfIVhmsILkbNxQ,38714
|
|
6
|
+
ome_arrow/meta.py,sha256=qeD0e_ItAQyZDT7ypkBU0rBh9oHIu2ziz9MCfPpPp9g,4199
|
|
7
|
+
ome_arrow/transform.py,sha256=0275_Mn1mlGXSWJ86llch8JoJyvqEOfvG-ub1dUWFNI,5997
|
|
8
|
+
ome_arrow/utils.py,sha256=XHovcqmjqoiBpKvXY47-_yUwf07f8zVE_F9BR_VKaPU,2383
|
|
9
|
+
ome_arrow/view.py,sha256=B2ZEE8LWlYzTBk0Fa19GHC1seEN_IdgOkfmJXcLRG2U,10691
|
|
10
|
+
ome_arrow-0.0.5.dist-info/licenses/LICENSE,sha256=9-2Pyhu3vTt2RJU8DorHQtHeNO_e5RLeFJTyOU4hOi4,1508
|
|
11
|
+
ome_arrow-0.0.5.dist-info/METADATA,sha256=l_CqAdgv7NFsNT48eDaC3s2uvKpg9g2FDgA3TAtricI,6110
|
|
12
|
+
ome_arrow-0.0.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
13
|
+
ome_arrow-0.0.5.dist-info/top_level.txt,sha256=aWOtkGXo_pfU-yy82guzGhz8Zh2h2nFl8Kc5qdzMGuE,10
|
|
14
|
+
ome_arrow-0.0.5.dist-info/RECORD,,
|
ome_arrow-0.0.4.dist-info/RECORD
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
ome_arrow/__init__.py,sha256=DfQsw8l0mx1Qt3YiiMv2SUljKETP3wS5hrD5eBbjMDM,583
|
|
2
|
-
ome_arrow/_version.py,sha256=QlXZ5JTjE_pgpDaeHk0GTExkc75xUZFmd0hA7kGYCJ0,704
|
|
3
|
-
ome_arrow/core.py,sha256=pTKmzKFXF5d4nSJpXQxlS71UA23P7TLHrX2eA0IxgUg,18594
|
|
4
|
-
ome_arrow/export.py,sha256=CCTnEdHko4Z0i5LEHuNGFLznWSsPyAFcS42H5nHU22Q,14875
|
|
5
|
-
ome_arrow/ingest.py,sha256=7N-M_NYOskHjs59Yhd4j8QXnKC5ykBXM7cT9Ap2chz8,32032
|
|
6
|
-
ome_arrow/meta.py,sha256=qeD0e_ItAQyZDT7ypkBU0rBh9oHIu2ziz9MCfPpPp9g,4199
|
|
7
|
-
ome_arrow/transform.py,sha256=0275_Mn1mlGXSWJ86llch8JoJyvqEOfvG-ub1dUWFNI,5997
|
|
8
|
-
ome_arrow/utils.py,sha256=XHovcqmjqoiBpKvXY47-_yUwf07f8zVE_F9BR_VKaPU,2383
|
|
9
|
-
ome_arrow/view.py,sha256=O4sIF8CZ4if-tV-rr2-yyG8WB5jHN9jq8HKvS-Ielnw,10034
|
|
10
|
-
ome_arrow-0.0.4.dist-info/licenses/LICENSE,sha256=9-2Pyhu3vTt2RJU8DorHQtHeNO_e5RLeFJTyOU4hOi4,1508
|
|
11
|
-
ome_arrow-0.0.4.dist-info/METADATA,sha256=2al9lOlUBjuximxy-1L9TsbVkFC46o0_zR3T1EnFRtU,5908
|
|
12
|
-
ome_arrow-0.0.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
13
|
-
ome_arrow-0.0.4.dist-info/top_level.txt,sha256=aWOtkGXo_pfU-yy82guzGhz8Zh2h2nFl8Kc5qdzMGuE,10
|
|
14
|
-
ome_arrow-0.0.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|