Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,7 @@ jobs:
python-version: ${{ matrix.python_version }}
- name: Install the latest version of uv
uses: astral-sh/setup-uv@v7
- name: Sync dependencies (with viz extra)
run: uv sync --frozen --extra viz
- name: Run pytest
run: uv run --frozen pytest
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ oa_image.view(how="pyvista")
# Export to OME-Parquet.
# We can also export OME-TIFF, OME-Zarr or NumPy arrays.
oa_image.export(how="ome-parquet", out="your_image.ome.parquet")

# Export to Vortex (install extras: `pip install 'ome-arrow[vortex]'`).
oa_image.export(how="vortex", out="your_image.vortex")
```

## Contributing, Development, and Testing
Expand Down
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,15 @@ optional-dependencies.viz = [
"trame-vtk>=2.10",
"trame-vuetify>=3.1",
]
optional-dependencies.vortex = [
"vortex-data>=0.56",
]

[dependency-groups]
dev = [
"poethepoet>=0.34",
"pytest>=8.3.5",
"vortex-data>=0.56",
]
docs = [
"myst-nb>=1.2",
Expand Down
9 changes: 8 additions & 1 deletion src/ome_arrow/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,17 @@

from ome_arrow._version import version as ome_arrow_version
from ome_arrow.core import OMEArrow
from ome_arrow.export import to_numpy, to_ome_parquet, to_ome_tiff, to_ome_zarr
from ome_arrow.export import (
to_numpy,
to_ome_parquet,
to_ome_tiff,
to_ome_vortex,
to_ome_zarr,
)
from ome_arrow.ingest import (
from_numpy,
from_ome_parquet,
from_ome_vortex,
from_ome_zarr,
from_tiff,
to_ome_arrow,
Expand Down
37 changes: 35 additions & 2 deletions src/ome_arrow/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,17 @@
import numpy as np
import pyarrow as pa

from ome_arrow.export import to_numpy, to_ome_parquet, to_ome_tiff, to_ome_zarr
from ome_arrow.export import (
to_numpy,
to_ome_parquet,
to_ome_tiff,
to_ome_vortex,
to_ome_zarr,
)
from ome_arrow.ingest import (
from_numpy,
from_ome_parquet,
from_ome_vortex,
from_ome_zarr,
from_stack_pattern_path,
from_tiff,
Expand Down Expand Up @@ -59,6 +66,7 @@ def __init__(
- a path/URL to an OME-TIFF (.tif/.tiff)
- a path/URL to an OME-Zarr store (.zarr / .ome.zarr)
- a path/URL to an OME-Parquet file (.parquet / .pq)
- a path/URL to a Vortex file (.vortex)
- a NumPy ndarray (2D-5D; interpreted
with from_numpy defaults)
- a dict already matching the OME-Arrow schema
Expand Down Expand Up @@ -100,6 +108,12 @@ def __init__(
s, column_name=column_name, row_index=row_index
)

# Vortex
elif s.lower().endswith(".vortex") or path.suffix.lower() == ".vortex":
self.data = from_ome_vortex(
s, column_name=column_name, row_index=row_index
)

# TIFF
elif path.suffix.lower() in {".tif", ".tiff"} or s.lower().endswith(
(".tif", ".tiff")
Expand All @@ -117,6 +131,7 @@ def __init__(
" • Bio-Formats pattern string (contains '<', '>' or '*')\n"
" • OME-Zarr path/URL ending with '.zarr' or '.ome.zarr'\n"
" • OME-Parquet file ending with '.parquet' or '.pq'\n"
" • Vortex file ending with '.vortex'\n"
" • OME-TIFF path/URL ending with '.tif' or '.tiff'"
)

Expand All @@ -141,7 +156,7 @@ def __init__(
"input data must be str, dict, pa.StructScalar, or numpy.ndarray"
)

def export(
def export( # noqa: PLR0911
self,
how: str = "numpy",
dtype: np.dtype = np.uint16,
Expand All @@ -165,6 +180,8 @@ def export(
parquet_column_name: str = "ome_arrow",
parquet_compression: str | None = "zstd",
parquet_metadata: dict[str, str] | None = None,
vortex_column_name: str = "ome_arrow",
vortex_metadata: dict[str, str] | None = None,
) -> np.array | dict | pa.StructScalar | str:
"""
Export the OME-Arrow content in a chosen representation.
Expand All @@ -178,6 +195,7 @@ def export(
"ome-tiff" → write OME-TIFF via BioIO
"ome-zarr" → write OME-Zarr (OME-NGFF) via BioIO
"parquet" → write a single-row Parquet with one struct column
"vortex" → write a single-row Vortex file with one struct column
dtype:
Target dtype for "numpy"/writers (default: np.uint16).
strict:
Expand All @@ -199,6 +217,8 @@ def export(
Try to embed per-channel display colors when safe; otherwise omitted.
parquet_*:
Options for Parquet export (column name, compression, file metadata).
vortex_*:
Options for Vortex export (column name, file metadata).

Returns
-------
Expand All @@ -209,6 +229,7 @@ def export(
- "ome-tiff": output path (str)
- "ome-zarr": output path (str)
- "parquet": output path (str)
- "vortex": output path (str)

Raises
------
Expand Down Expand Up @@ -271,6 +292,18 @@ def export(
)
return out

# Vortex (single row, single struct column)
if mode in {"ome-vortex", "omevortex", "vortex"}:
if not out:
raise ValueError("export(how='vortex') requires 'out' path.")
to_ome_vortex(
data=self.data,
out_path=out,
column_name=vortex_column_name,
file_metadata=vortex_metadata,
)
return out

raise ValueError(f"Unknown export method: {how}")

def info(self) -> Dict[str, Any]:
Expand Down
58 changes: 58 additions & 0 deletions src/ome_arrow/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,3 +420,61 @@ def to_ome_parquet(
compression=compression,
row_group_size=row_group_size,
)


def to_ome_vortex(
data: Dict[str, Any] | pa.StructScalar,
out_path: str,
column_name: str = "image",
file_metadata: Optional[Dict[str, str]] = None,
) -> None:
"""Export an OME-Arrow record to a Vortex file.

The file is written as a single-row, single-column Arrow table where the
column holds a struct with the OME-Arrow schema.

Args:
data: OME-Arrow dict or StructScalar.
out_path: Output path for the Vortex file.
column_name: Column name to store the struct.
file_metadata: Optional file-level metadata to attach.

Raises:
ImportError: If the optional `vortex-data` dependency is missing.
"""

try:
import vortex.io as vxio
except ImportError as exc:
raise ImportError(
"Vortex export requires the optional 'vortex-data' dependency."
) from exc

# 1) Normalize to a plain Python dict (works better with pyarrow builders,
# especially when the struct has a `null`-typed field like "masks").
if isinstance(data, pa.StructScalar):
record_dict = data.as_py()
else:
# Validate by round-tripping through a typed scalar, then back to dict.
record_dict = pa.scalar(data, type=OME_ARROW_STRUCT).as_py()

# 2) Build a single-row struct array from the dict, explicitly passing the schema
struct_array = pa.array([record_dict], type=OME_ARROW_STRUCT) # len=1

# 3) Wrap into a one-column table
table = pa.table({column_name: struct_array})

# 4) Attach optional file-level metadata
meta: Dict[bytes, bytes] = dict(table.schema.metadata or {})
try:
meta[b"ome.arrow.type"] = str(OME_ARROW_TAG_TYPE).encode("utf-8")
meta[b"ome.arrow.version"] = str(OME_ARROW_TAG_VERSION).encode("utf-8")
except Exception:
pass
if file_metadata:
for k, v in file_metadata.items():
meta[str(k).encode("utf-8")] = str(v).encode("utf-8")
table = table.replace_schema_metadata(meta)

# 5) Write Vortex (single row, single column)
vxio.write(table, str(out_path))
Loading