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.
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 to_numpy, to_ome_parquet, to_ome_tiff, to_ome_zarr
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.3'
32
- __version_tuple__ = version_tuple = (0, 0, 3)
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
@@ -5,17 +5,23 @@ Core of the ome_arrow package, used for classes and such.
5
5
  from __future__ import annotations
6
6
 
7
7
  import pathlib
8
- from typing import Any, Dict, Iterable, Optional, Tuple
8
+ from typing import TYPE_CHECKING, Any, Dict, Iterable, Optional, Tuple
9
9
 
10
10
  import matplotlib
11
11
  import numpy as np
12
12
  import pyarrow as pa
13
- import pyvista
14
13
 
15
- from ome_arrow.export import to_numpy, to_ome_parquet, to_ome_tiff, to_ome_zarr
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
+ )
16
21
  from ome_arrow.ingest import (
17
22
  from_numpy,
18
23
  from_ome_parquet,
24
+ from_ome_vortex,
19
25
  from_ome_zarr,
20
26
  from_stack_pattern_path,
21
27
  from_tiff,
@@ -25,6 +31,10 @@ from ome_arrow.transform import slice_ome_arrow
25
31
  from ome_arrow.utils import describe_ome_arrow
26
32
  from ome_arrow.view import view_matplotlib, view_pyvista
27
33
 
34
+ # if not in runtime, import pyvista for type hints
35
+ if TYPE_CHECKING:
36
+ import pyvista
37
+
28
38
 
29
39
  class OMEArrow:
30
40
  """
@@ -47,6 +57,8 @@ class OMEArrow:
47
57
  self,
48
58
  data: str | dict | pa.StructScalar | "np.ndarray",
49
59
  tcz: Tuple[int, int, int] = (0, 0, 0),
60
+ column_name: str = "ome_arrow",
61
+ row_index: int = 0,
50
62
  ) -> None:
51
63
  """
52
64
  Construct an OMEArrow from:
@@ -54,6 +66,7 @@ class OMEArrow:
54
66
  - a path/URL to an OME-TIFF (.tif/.tiff)
55
67
  - a path/URL to an OME-Zarr store (.zarr / .ome.zarr)
56
68
  - a path/URL to an OME-Parquet file (.parquet / .pq)
69
+ - a path/URL to a Vortex file (.vortex)
57
70
  - a NumPy ndarray (2D-5D; interpreted
58
71
  with from_numpy defaults)
59
72
  - a dict already matching the OME-Arrow schema
@@ -91,7 +104,15 @@ class OMEArrow:
91
104
  ".parquet",
92
105
  ".pq",
93
106
  }:
94
- self.data = from_ome_parquet(s)
107
+ self.data = from_ome_parquet(
108
+ s, column_name=column_name, row_index=row_index
109
+ )
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
+ )
95
116
 
96
117
  # TIFF
97
118
  elif path.suffix.lower() in {".tif", ".tiff"} or s.lower().endswith(
@@ -110,6 +131,7 @@ class OMEArrow:
110
131
  " • Bio-Formats pattern string (contains '<', '>' or '*')\n"
111
132
  " • OME-Zarr path/URL ending with '.zarr' or '.ome.zarr'\n"
112
133
  " • OME-Parquet file ending with '.parquet' or '.pq'\n"
134
+ " • Vortex file ending with '.vortex'\n"
113
135
  " • OME-TIFF path/URL ending with '.tif' or '.tiff'"
114
136
  )
115
137
 
@@ -134,7 +156,7 @@ class OMEArrow:
134
156
  "input data must be str, dict, pa.StructScalar, or numpy.ndarray"
135
157
  )
136
158
 
137
- def export(
159
+ def export( # noqa: PLR0911
138
160
  self,
139
161
  how: str = "numpy",
140
162
  dtype: np.dtype = np.uint16,
@@ -158,6 +180,8 @@ class OMEArrow:
158
180
  parquet_column_name: str = "ome_arrow",
159
181
  parquet_compression: str | None = "zstd",
160
182
  parquet_metadata: dict[str, str] | None = None,
183
+ vortex_column_name: str = "ome_arrow",
184
+ vortex_metadata: dict[str, str] | None = None,
161
185
  ) -> np.array | dict | pa.StructScalar | str:
162
186
  """
163
187
  Export the OME-Arrow content in a chosen representation.
@@ -171,6 +195,7 @@ class OMEArrow:
171
195
  "ome-tiff" → write OME-TIFF via BioIO
172
196
  "ome-zarr" → write OME-Zarr (OME-NGFF) via BioIO
173
197
  "parquet" → write a single-row Parquet with one struct column
198
+ "vortex" → write a single-row Vortex file with one struct column
174
199
  dtype:
175
200
  Target dtype for "numpy"/writers (default: np.uint16).
176
201
  strict:
@@ -192,6 +217,8 @@ class OMEArrow:
192
217
  Try to embed per-channel display colors when safe; otherwise omitted.
193
218
  parquet_*:
194
219
  Options for Parquet export (column name, compression, file metadata).
220
+ vortex_*:
221
+ Options for Vortex export (column name, file metadata).
195
222
 
196
223
  Returns
197
224
  -------
@@ -202,6 +229,7 @@ class OMEArrow:
202
229
  - "ome-tiff": output path (str)
203
230
  - "ome-zarr": output path (str)
204
231
  - "parquet": output path (str)
232
+ - "vortex": output path (str)
205
233
 
206
234
  Raises
207
235
  ------
@@ -264,6 +292,18 @@ class OMEArrow:
264
292
  )
265
293
  return out
266
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
+
267
307
  raise ValueError(f"Unknown export method: {how}")
268
308
 
269
309
  def info(self) -> Dict[str, Any]:
@@ -292,8 +332,8 @@ class OMEArrow:
292
332
  opacity: str | float = "sigmoid",
293
333
  clim: tuple[float, float] | None = None,
294
334
  show_axes: bool = True,
295
- scaling_values: tuple[float, float, float] | None = (1.0, 0.1, 0.1),
296
- ) -> matplotlib.figure.Figure | pyvista.Plotter:
335
+ scaling_values: tuple[float, float, float] | None = None,
336
+ ) -> matplotlib.figure.Figure | "pyvista.Plotter":
297
337
  """
298
338
  Render an OME-Arrow record using Matplotlib or PyVista.
299
339
 
@@ -330,7 +370,10 @@ class OMEArrow:
330
370
  clim: Contrast limits (``(low, high)``) for PyVista rendering.
331
371
  show_axes: If ``True``, display axes in the PyVista scene.
332
372
  scaling_values: Physical scale multipliers for the (x, y, z) axes used by
333
- PyVista, typically to express anisotropy. Defaults to ``(1.0, 0.1, 0.1)``.
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.
334
377
 
335
378
  Returns:
336
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,7 +3,9 @@ Converting to and from OME-Arrow formats.
3
3
  """
4
4
 
5
5
  import itertools
6
+ import json
6
7
  import re
8
+ import warnings
7
9
  from datetime import datetime, timezone
8
10
  from pathlib import Path
9
11
  from typing import Any, Dict, List, Optional, Sequence, Tuple
@@ -19,6 +21,228 @@ from bioio_ome_zarr import Reader as OMEZarrReader
19
21
  from ome_arrow.meta import OME_ARROW_STRUCT, OME_ARROW_TAG_TYPE, OME_ARROW_TAG_VERSION
20
22
 
21
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
+
22
246
  def to_ome_arrow(
23
247
  type_: str = OME_ARROW_TAG_TYPE,
24
248
  version: str = OME_ARROW_TAG_VERSION,
@@ -337,13 +561,8 @@ def from_tiff(
337
561
  if size_x <= 0 or size_y <= 0:
338
562
  raise ValueError("Image must have positive Y and X dims.")
339
563
 
340
- pps = getattr(img, "physical_pixel_sizes", None)
341
- try:
342
- psize_x = float(getattr(pps, "X", None) or 1.0)
343
- psize_y = float(getattr(pps, "Y", None) or 1.0)
344
- psize_z = float(getattr(pps, "Z", None) or 1.0)
345
- except Exception:
346
- 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"
347
566
 
348
567
  # --- NEW: coerce top-level strings --------------------------------
349
568
  img_id = str(image_id or p.stem)
@@ -393,7 +612,7 @@ def from_tiff(
393
612
  physical_size_x=psize_x,
394
613
  physical_size_y=psize_y,
395
614
  physical_size_z=psize_z,
396
- physical_size_unit="µm",
615
+ physical_size_unit=psize_unit,
397
616
  channels=channels,
398
617
  planes=planes,
399
618
  masks=None,
@@ -409,6 +628,20 @@ def from_stack_pattern_path(
409
628
  image_id: Optional[str] = None,
410
629
  name: Optional[str] = None,
411
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
+ """
412
645
  path = Path(pattern_path)
413
646
  folder = path.parent
414
647
  line = path.name.strip()
@@ -740,13 +973,15 @@ def from_ome_zarr(
740
973
  if size_x <= 0 or size_y <= 0:
741
974
  raise ValueError("Image must have positive Y and X dimensions.")
742
975
 
743
- pps = getattr(img, "physical_pixel_sizes", None)
744
- try:
745
- psize_x = float(getattr(pps, "X", None) or 1.0)
746
- psize_y = float(getattr(pps, "Y", None) or 1.0)
747
- psize_z = float(getattr(pps, "Z", None) or 1.0)
748
- except Exception:
749
- psize_x = psize_y = psize_z = 1.0
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
750
985
 
751
986
  img_id = str(image_id or p.stem)
752
987
  display_name = str(name or p.name)
@@ -804,7 +1039,7 @@ def from_ome_zarr(
804
1039
  physical_size_x=psize_x,
805
1040
  physical_size_y=psize_y,
806
1041
  physical_size_z=psize_z,
807
- physical_size_unit="µm",
1042
+ physical_size_unit=psize_unit,
808
1043
  channels=channels,
809
1044
  planes=planes,
810
1045
  masks=None,
@@ -818,115 +1053,72 @@ def from_ome_parquet(
818
1053
  row_index: int = 0,
819
1054
  strict_schema: bool = False,
820
1055
  ) -> pa.StructScalar:
821
- """
822
- Read an OME-Arrow record from a Parquet file and return a typed StructScalar.
823
-
824
- Expected layout (as produced by `to_ome_parquet`):
825
- - single Parquet file
826
- - a single column (default name "ome_arrow") of `OME_ARROW_STRUCT` type
827
- - one row (row_index=0)
1056
+ """Read an OME-Arrow record from a Parquet file.
828
1057
 
829
- This function is forgiving:
830
- - If `column_name` is None or not found, it will auto-detect a struct column
831
- that matches the OME-Arrow field names.
832
- - If the table has multiple rows, you can choose which record to read
833
- via `row_index`.
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.
834
1063
 
835
- Parameters
836
- ----------
837
- parquet_path : str | Path
838
- Path to the .parquet file.
839
- column_name : Optional[str], default "ome_arrow"
840
- Name of the column that stores the OME-Arrow struct. If None, auto-detect.
841
- row_index : int, default 0
842
- Which row to read if the table contains multiple rows.
843
- strict_schema : bool, default False
844
- If True, require the column's type to equal `OME_ARROW_STRUCT` exactly.
845
- If False, we only require the column to be a Struct with the same field
846
- names (order can vary).
1064
+ Returns:
1065
+ A typed OME-Arrow StructScalar.
847
1066
 
848
- Returns
849
- -------
850
- pa.StructScalar
851
- A validated OME-Arrow struct scalar.
852
-
853
- Raises
854
- ------
855
- FileNotFoundError
856
- If the file does not exist.
857
- ValueError
858
- If a suitable column/row cannot be found or schema checks fail.
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.
859
1070
  """
860
1071
  p = Path(parquet_path)
861
1072
  if not p.exists():
862
1073
  raise FileNotFoundError(f"No such file: {p}")
863
1074
 
864
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
+ )
865
1082
 
866
- if table.num_rows == 0:
867
- raise ValueError("Parquet file contains 0 rows; expected at least 1.")
868
- if not (0 <= row_index < table.num_rows):
869
- raise ValueError(f"row_index {row_index} out of range [0, {table.num_rows}).")
870
-
871
- # 1) Locate the OME-Arrow column
872
- def _struct_matches_ome_fields(t: pa.StructType) -> bool:
873
- ome_fields = {f.name for f in OME_ARROW_STRUCT}
874
- col_fields = {f.name for f in t}
875
- return ome_fields == col_fields
876
1083
 
877
- candidate_col = None
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.
878
1092
 
879
- if column_name is not None and column_name in table.column_names:
880
- arr = table[column_name]
881
- if not pa.types.is_struct(arr.type):
882
- raise ValueError(f"Column '{column_name}' is not a Struct; got {arr.type}.")
883
- if strict_schema and arr.type != OME_ARROW_STRUCT:
884
- raise ValueError(
885
- f"Column '{column_name}' schema != OME_ARROW_STRUCT.\n"
886
- f"Got: {arr.type}\n"
887
- f"Expect:{OME_ARROW_STRUCT}"
888
- )
889
- if not strict_schema and not _struct_matches_ome_fields(arr.type):
890
- raise ValueError(
891
- f"Column '{column_name}' does not have the expected OME-Arrow fields."
892
- )
893
- candidate_col = arr
894
- else:
895
- # Auto-detect a struct column that matches OME-Arrow fields
896
- for name in table.column_names:
897
- arr = table[name]
898
- if pa.types.is_struct(arr.type):
899
- if strict_schema and arr.type == OME_ARROW_STRUCT:
900
- candidate_col = arr
901
- column_name = name
902
- break
903
- if not strict_schema and _struct_matches_ome_fields(arr.type):
904
- candidate_col = arr
905
- column_name = name
906
- break
907
- if candidate_col is None:
908
- if column_name is None:
909
- hint = "no struct column with OME-Arrow fields was found."
910
- else:
911
- hint = f"column '{column_name}' not found and auto-detection failed."
912
- raise ValueError(f"Could not locate an OME-Arrow struct column: {hint}")
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.
913
1098
 
914
- # 2) Extract the row as a Python dict
915
- # (Using to_pylist() for the single element slice is simple & reliable.)
916
- record_dict: Dict[str, Any] = candidate_col.slice(row_index, 1).to_pylist()[0]
1099
+ Returns:
1100
+ A typed OME-Arrow StructScalar.
917
1101
 
918
- # 3) Reconstruct a typed StructScalar using the canonical schema
919
- # (this validates field names/types and normalizes order)
920
- scalar = pa.scalar(record_dict, type=OME_ARROW_STRUCT)
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}")
921
1110
 
922
- # Optional: soft validation via file-level metadata (if present)
923
1111
  try:
924
- meta = table.schema.metadata or {}
925
- meta.get(b"ome.arrow.type", b"").decode() == str(
926
- OME_ARROW_TAG_TYPE
927
- ) and meta.get(b"ome.arrow.version", b"").decode() == str(OME_ARROW_TAG_VERSION)
928
- # You could log/print a warning if tag_ok is False, but don't fail.
929
- except Exception:
930
- pass
931
-
932
- return scalar
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
@@ -2,16 +2,27 @@
2
2
  Viewing utilities for OME-Arrow data.
3
3
  """
4
4
 
5
+ from __future__ import annotations
6
+
5
7
  import contextlib
8
+ import warnings
9
+ from typing import TYPE_CHECKING
6
10
 
7
11
  import matplotlib.pyplot as plt
8
12
  import numpy as np
9
13
  import pyarrow as pa
10
- import pyvista as pv
11
14
  from matplotlib.axes import Axes
12
15
  from matplotlib.figure import Figure
13
16
  from matplotlib.image import AxesImage
14
17
 
18
+ try: # optional dependency
19
+ import pyvista as pv
20
+ except ImportError: # pragma: no cover - exercised when viz extra missing
21
+ pv = None # type: ignore[assignment]
22
+
23
+ if TYPE_CHECKING:
24
+ import pyvista
25
+
15
26
 
16
27
  def view_matplotlib(
17
28
  data: dict[str, object] | pa.StructScalar,
@@ -22,6 +33,23 @@ def view_matplotlib(
22
33
  cmap: str = "gray",
23
34
  show: bool = True,
24
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
+ """
25
53
  if isinstance(data, pa.StructScalar):
26
54
  data = data.as_py()
27
55
 
@@ -63,6 +91,21 @@ def view_matplotlib(
63
91
  return fig, ax, im
64
92
 
65
93
 
94
+ def _require_pyvista() -> "pyvista":
95
+ """
96
+ Ensure PyVista is available, raising a helpful error otherwise.
97
+ """
98
+ if pv is None:
99
+ msg = (
100
+ "PyVista-based visualization requires the optional 'viz' extras. "
101
+ "Install with `pip install ome-arrow[viz]` to enable 3D viewing."
102
+ )
103
+ warnings.warn(msg, RuntimeWarning)
104
+ raise RuntimeError(msg)
105
+
106
+ return pv
107
+
108
+
66
109
  def view_pyvista(
67
110
  data: dict | pa.StructScalar,
68
111
  c: int = 0,
@@ -77,16 +120,14 @@ def view_pyvista(
77
120
  percentile_clim: tuple[float, float] = (1.0, 99.9), # robust contrast
78
121
  sampling_scale: float = 0.5, # smaller = denser rays (sharper, slower)
79
122
  show: bool = True,
80
- ) -> pv.Plotter:
123
+ ) -> "pyvista.Plotter":
81
124
  """
82
125
  Jupyter-inline interactive volume view using PyVista backends.
83
126
  Tries 'trame' → 'html' → 'static' when backend='auto'.
84
127
 
85
128
  sampling_scale controls ray step via the mapper after add_volume.
86
129
  """
87
- import warnings
88
-
89
- import numpy as np
130
+ pv = _require_pyvista()
90
131
 
91
132
  # ---- unwrap OME-Arrow row
92
133
  row = data.as_py() if isinstance(data, pa.StructScalar) else data
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ome-arrow
3
- Version: 0.0.3
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
@@ -16,25 +16,29 @@ Requires-Dist: bioio-ome-tiff>=1.4
16
16
  Requires-Dist: bioio-ome-zarr>=3.0.3
17
17
  Requires-Dist: bioio-tifffile>=1.3
18
18
  Requires-Dist: fire>=0.7
19
- Requires-Dist: ipywidgets>=8.1.8
20
- Requires-Dist: jupyterlab-widgets>=3.0.16
21
19
  Requires-Dist: matplotlib>=3.10.7
22
20
  Requires-Dist: numpy>=2.2.6
23
21
  Requires-Dist: pandas>=2.2.3
24
22
  Requires-Dist: pillow>=12
25
23
  Requires-Dist: pyarrow>=22
26
- Requires-Dist: pyvista>=0.46.4
27
- Requires-Dist: trame>=3.12
28
- Requires-Dist: trame-vtk>=2.10
29
- Requires-Dist: trame-vuetify>=3.1
24
+ Provides-Extra: viz
25
+ Requires-Dist: ipywidgets>=8.1.8; extra == "viz"
26
+ Requires-Dist: jupyterlab-widgets>=3.0.16; extra == "viz"
27
+ Requires-Dist: pyvista>=0.46.4; extra == "viz"
28
+ Requires-Dist: trame>=3.12; extra == "viz"
29
+ Requires-Dist: trame-vtk>=2.10; extra == "viz"
30
+ Requires-Dist: trame-vuetify>=3.1; extra == "viz"
31
+ Provides-Extra: vortex
32
+ Requires-Dist: vortex-data>=0.56; extra == "vortex"
30
33
  Dynamic: license-file
31
34
 
32
- <img height="200" src="https://raw.githubusercontent.com/wayscience/ome-arrow/main/docs/src/_static/logo.png?raw=true">
35
+ <img width="600" src="https://raw.githubusercontent.com/wayscience/ome-arrow/main/docs/src/_static/logo.png?raw=true">
33
36
 
34
37
  ![PyPI - Version](https://img.shields.io/pypi/v/ome-arrow)
35
38
  [![Build Status](https://github.com/wayscience/ome-arrow/actions/workflows/run-tests.yml/badge.svg?branch=main)](https://github.com/wayscience/ome-arrow/actions/workflows/run-tests.yml?query=branch%3Amain)
36
39
  [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
37
40
  [![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
41
+ [![Software DOI badge](https://zenodo.org/badge/DOI/10.5281/zenodo.17664969.svg)](https://doi.org/10.5281/zenodo.17664969)
38
42
 
39
43
  # Open, interoperable, and queryable microscopy images with OME Arrow
40
44
 
@@ -52,6 +56,13 @@ OME Arrow enables image data to be stored alongside metadata or derived data suc
52
56
  Images in OME Arrow are composed of mutlilayer [structs](https://arrow.apache.org/docs/python/generated/pyarrow.struct.html) so they may be stored as values within tables.
53
57
  This means you can store, query, and build relationships on data from the same location using any system which is compatible with Apache Arrow (including Parquet) through common data interfaces (such as SQL and DuckDB).
54
58
 
59
+ ## Project focus
60
+
61
+ This package is intentionally dedicated to work at a per-image level and not large batch handling (though it may be used for those purposes by users or in other projects).
62
+
63
+ - For visualizing OME Arrow and OME Parquet data in Napari, please see the [`napari-ome-arrow`](https://github.com/WayScience/napari-ome-arrow) Napari plugin.
64
+ - For more comprehensive handling of many images and features in the context of the OME Parquet format please see the [`CytoDataFrame`](https://github.com/cytomining/CytoDataFrame) project (and relevant [example notebook](https://github.com/cytomining/CytoDataFrame/blob/main/docs/src/examples/cytodataframe_at_a_glance.ipynb)).
65
+
55
66
  ## Installation
56
67
 
57
68
  Install OME Arrow from PyPI or from source:
@@ -89,12 +100,15 @@ oa_image.info()
89
100
  oa_image.view(how="matplotlib")
90
101
 
91
102
  # Display the image with pyvista
92
- # (great for ZYX 3D images).
103
+ # (great for ZYX 3D images; install extras: `pip install 'ome-arrow[viz]'`).
93
104
  oa_image.view(how="pyvista")
94
105
 
95
106
  # Export to OME-Parquet.
96
107
  # We can also export OME-TIFF, OME-Zarr or NumPy arrays.
97
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")
98
112
  ```
99
113
 
100
114
  ## Contributing, Development, and Testing
@@ -107,5 +121,5 @@ OME Arrow is used or inspired by the following projects, check them out!
107
121
 
108
122
  - [`napari-ome-arrow`](https://github.com/WayScience/napari-ome-arrow): enables you to view OME Arrow and related images.
109
123
  - [`nViz`](https://github.com/WayScience/nViz): focuses on ingesting and visualizing various 3D image data.
110
- - [`CytoDataFrame`](https://github.com/cytomining/CytoDataFrame): provides a DataFrame-like experience for viewing feature and microscopy image data within Jupyter notebook interfaces.
124
+ - [`CytoDataFrame`](https://github.com/cytomining/CytoDataFrame): provides a DataFrame-like experience for viewing feature and microscopy image data within Jupyter notebook interfaces and creating OME Parquet files.
111
125
  - [`coSMicQC`](https://github.com/cytomining/coSMicQC): performs quality control on microscopy feature datasets, visualized using CytoDataFrames.
@@ -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,,
@@ -1,14 +0,0 @@
1
- ome_arrow/__init__.py,sha256=DfQsw8l0mx1Qt3YiiMv2SUljKETP3wS5hrD5eBbjMDM,583
2
- ome_arrow/_version.py,sha256=pBZsQt6tlL02W-ri--X_4JCubpAK7jjCSnOmUp_isjc,704
3
- ome_arrow/core.py,sha256=NUCV9KUH3yCOlpetRS5NNVG_phodutE1F2ujDBPhHgY,18351
4
- ome_arrow/export.py,sha256=CCTnEdHko4Z0i5LEHuNGFLznWSsPyAFcS42H5nHU22Q,14875
5
- ome_arrow/ingest.py,sha256=zZz94LaLOpmoxnryLeoPsaWV0EzkYkGFizYSVcbd5w8,33016
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=DT8i56uV8Rw22KkqwjPPPKWJWNtfgR9OkI8Qj1WD8Ds,9355
10
- ome_arrow-0.0.3.dist-info/licenses/LICENSE,sha256=9-2Pyhu3vTt2RJU8DorHQtHeNO_e5RLeFJTyOU4hOi4,1508
11
- ome_arrow-0.0.3.dist-info/METADATA,sha256=VrhOZ3ENlUTdd3smTk_pCN8ptbuZJbhDwuCxdLu8UDc,4910
12
- ome_arrow-0.0.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
13
- ome_arrow-0.0.3.dist-info/top_level.txt,sha256=aWOtkGXo_pfU-yy82guzGhz8Zh2h2nFl8Kc5qdzMGuE,10
14
- ome_arrow-0.0.3.dist-info/RECORD,,