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 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.4'
32
- __version_tuple__ = version_tuple = (0, 0, 4)
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 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
+ )
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 = (1.0, 0.1, 0.1),
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. 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.
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
- pps = getattr(img, "physical_pixel_sizes", None)
342
- try:
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="µm",
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
- pps = getattr(img, "physical_pixel_sizes", None)
745
- try:
746
- psize_x = float(getattr(pps, "X", None) or 1.0)
747
- psize_y = float(getattr(pps, "Y", None) or 1.0)
748
- psize_z = float(getattr(pps, "Z", None) or 1.0)
749
- except Exception:
750
- 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
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="µm",
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
- Read an OME-Arrow record from a Parquet file and return a typed StructScalar.
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
- if column_name is not None and column_name in table.column_names:
847
- arr = table[column_name]
848
- if not pa.types.is_struct(arr.type):
849
- raise ValueError(f"Column '{column_name}' is not a Struct; got {arr.type}.")
850
- if strict_schema and arr.type != OME_ARROW_STRUCT:
851
- raise ValueError(
852
- f"Column '{column_name}' schema != OME_ARROW_STRUCT.\n"
853
- f"Got: {arr.type}\n"
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
- # Emit warning if auto-detection was used
884
- if autodetected_name is not None and autodetected_name != requested_name:
885
- warnings.warn(
886
- f"Requested column '{requested_name}' was not usable or not found. "
887
- f"Auto-detected OME-Arrow column '{autodetected_name}'.",
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
- # 2) Extract the row as a Python dict
893
- record_dict: Dict[str, Any] = candidate_col.slice(row_index, 1).to_pylist()[0]
1099
+ Returns:
1100
+ A typed OME-Arrow StructScalar.
894
1101
 
895
- # 3) Reconstruct a typed StructScalar using the canonical schema
896
- 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}")
897
1110
 
898
- # Optional: soft validation via file-level metadata (if present)
899
1111
  try:
900
- meta = table.schema.metadata or {}
901
- meta.get(b"ome.arrow.type", b"").decode() == str(OME_ARROW_TAG_TYPE)
902
- meta.get(b"ome.arrow.version", b"").decode() == str(OME_ARROW_TAG_VERSION)
903
- except Exception:
904
- pass
905
-
906
- 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
@@ -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.4
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 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">
34
36
 
35
37
  ![PyPI - Version](https://img.shields.io/pypi/v/ome-arrow)
36
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)
@@ -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,,
@@ -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,,