ome-arrow 0.0.4__py3-none-any.whl → 0.0.6__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 +72 -7
- ome_arrow/export.py +302 -8
- ome_arrow/ingest.py +542 -124
- ome_arrow/meta.py +41 -0
- ome_arrow/transform.py +34 -0
- ome_arrow/view.py +20 -22
- {ome_arrow-0.0.4.dist-info → ome_arrow-0.0.6.dist-info}/METADATA +7 -2
- ome_arrow-0.0.6.dist-info/RECORD +14 -0
- {ome_arrow-0.0.4.dist-info → ome_arrow-0.0.6.dist-info}/WHEEL +1 -1
- ome_arrow-0.0.4.dist-info/RECORD +0 -14
- {ome_arrow-0.0.4.dist-info → ome_arrow-0.0.6.dist-info}/licenses/LICENSE +0 -0
- {ome_arrow-0.0.4.dist-info → ome_arrow-0.0.6.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.6'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 0, 6)
|
|
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,
|
|
@@ -52,6 +59,7 @@ class OMEArrow:
|
|
|
52
59
|
tcz: Tuple[int, int, int] = (0, 0, 0),
|
|
53
60
|
column_name: str = "ome_arrow",
|
|
54
61
|
row_index: int = 0,
|
|
62
|
+
image_type: str | None = None,
|
|
55
63
|
) -> None:
|
|
56
64
|
"""
|
|
57
65
|
Construct an OMEArrow from:
|
|
@@ -59,10 +67,12 @@ class OMEArrow:
|
|
|
59
67
|
- a path/URL to an OME-TIFF (.tif/.tiff)
|
|
60
68
|
- a path/URL to an OME-Zarr store (.zarr / .ome.zarr)
|
|
61
69
|
- a path/URL to an OME-Parquet file (.parquet / .pq)
|
|
70
|
+
- a path/URL to a Vortex file (.vortex)
|
|
62
71
|
- a NumPy ndarray (2D-5D; interpreted
|
|
63
72
|
with from_numpy defaults)
|
|
64
73
|
- a dict already matching the OME-Arrow schema
|
|
65
74
|
- a pa.StructScalar already typed to OME_ARROW_STRUCT
|
|
75
|
+
- optionally override/set image_type metadata on ingest
|
|
66
76
|
"""
|
|
67
77
|
|
|
68
78
|
# set the tcz for viewing
|
|
@@ -75,6 +85,7 @@ class OMEArrow:
|
|
|
75
85
|
default_dim_for_unspecified="C",
|
|
76
86
|
map_series_to="T",
|
|
77
87
|
clamp_to_uint16=True,
|
|
88
|
+
image_type=image_type,
|
|
78
89
|
)
|
|
79
90
|
|
|
80
91
|
# --- 2) String path/URL: OME-Zarr / OME-Parquet / OME-TIFF ---------------
|
|
@@ -90,6 +101,8 @@ class OMEArrow:
|
|
|
90
101
|
or (path.exists() and path.is_dir() and path.suffix.lower() == ".zarr")
|
|
91
102
|
):
|
|
92
103
|
self.data = from_ome_zarr(s)
|
|
104
|
+
if image_type is not None:
|
|
105
|
+
self.data = self._wrap_with_image_type(self.data, image_type)
|
|
93
106
|
|
|
94
107
|
# OME-Parquet
|
|
95
108
|
elif s.lower().endswith((".parquet", ".pq")) or path.suffix.lower() in {
|
|
@@ -99,12 +112,24 @@ class OMEArrow:
|
|
|
99
112
|
self.data = from_ome_parquet(
|
|
100
113
|
s, column_name=column_name, row_index=row_index
|
|
101
114
|
)
|
|
115
|
+
if image_type is not None:
|
|
116
|
+
self.data = self._wrap_with_image_type(self.data, image_type)
|
|
117
|
+
|
|
118
|
+
# Vortex
|
|
119
|
+
elif s.lower().endswith(".vortex") or path.suffix.lower() == ".vortex":
|
|
120
|
+
self.data = from_ome_vortex(
|
|
121
|
+
s, column_name=column_name, row_index=row_index
|
|
122
|
+
)
|
|
123
|
+
if image_type is not None:
|
|
124
|
+
self.data = self._wrap_with_image_type(self.data, image_type)
|
|
102
125
|
|
|
103
126
|
# TIFF
|
|
104
127
|
elif path.suffix.lower() in {".tif", ".tiff"} or s.lower().endswith(
|
|
105
128
|
(".tif", ".tiff")
|
|
106
129
|
):
|
|
107
130
|
self.data = from_tiff(s)
|
|
131
|
+
if image_type is not None:
|
|
132
|
+
self.data = self._wrap_with_image_type(self.data, image_type)
|
|
108
133
|
|
|
109
134
|
elif path.exists() and path.is_dir():
|
|
110
135
|
raise ValueError(
|
|
@@ -117,6 +142,7 @@ class OMEArrow:
|
|
|
117
142
|
" • Bio-Formats pattern string (contains '<', '>' or '*')\n"
|
|
118
143
|
" • OME-Zarr path/URL ending with '.zarr' or '.ome.zarr'\n"
|
|
119
144
|
" • OME-Parquet file ending with '.parquet' or '.pq'\n"
|
|
145
|
+
" • Vortex file ending with '.vortex'\n"
|
|
120
146
|
" • OME-TIFF path/URL ending with '.tif' or '.tiff'"
|
|
121
147
|
)
|
|
122
148
|
|
|
@@ -125,15 +151,20 @@ class OMEArrow:
|
|
|
125
151
|
# Uses from_numpy defaults: dim_order="TCZYX", clamp_to_uint16=True, etc.
|
|
126
152
|
# If the array is YX/ZYX/CYX/etc.,
|
|
127
153
|
# from_numpy will expand/reorder accordingly.
|
|
128
|
-
self.data = from_numpy(data)
|
|
154
|
+
self.data = from_numpy(data, image_type=image_type)
|
|
129
155
|
|
|
130
156
|
# --- 4) Already-typed Arrow scalar ---------------------------------------
|
|
131
157
|
elif isinstance(data, pa.StructScalar):
|
|
132
158
|
self.data = data
|
|
159
|
+
if image_type is not None:
|
|
160
|
+
self.data = self._wrap_with_image_type(self.data, image_type)
|
|
133
161
|
|
|
134
162
|
# --- 5) Plain dict matching the schema -----------------------------------
|
|
135
163
|
elif isinstance(data, dict):
|
|
136
|
-
|
|
164
|
+
record = {f.name: data.get(f.name) for f in OME_ARROW_STRUCT}
|
|
165
|
+
self.data = pa.scalar(record, type=OME_ARROW_STRUCT)
|
|
166
|
+
if image_type is not None:
|
|
167
|
+
self.data = self._wrap_with_image_type(self.data, image_type)
|
|
137
168
|
|
|
138
169
|
# --- otherwise ------------------------------------------------------------
|
|
139
170
|
else:
|
|
@@ -141,7 +172,19 @@ class OMEArrow:
|
|
|
141
172
|
"input data must be str, dict, pa.StructScalar, or numpy.ndarray"
|
|
142
173
|
)
|
|
143
174
|
|
|
144
|
-
|
|
175
|
+
@staticmethod
|
|
176
|
+
def _wrap_with_image_type(
|
|
177
|
+
data: pa.StructScalar, image_type: str
|
|
178
|
+
) -> pa.StructScalar:
|
|
179
|
+
return pa.scalar(
|
|
180
|
+
{
|
|
181
|
+
**data.as_py(),
|
|
182
|
+
"image_type": str(image_type),
|
|
183
|
+
},
|
|
184
|
+
type=OME_ARROW_STRUCT,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
def export( # noqa: PLR0911
|
|
145
188
|
self,
|
|
146
189
|
how: str = "numpy",
|
|
147
190
|
dtype: np.dtype = np.uint16,
|
|
@@ -165,6 +208,8 @@ class OMEArrow:
|
|
|
165
208
|
parquet_column_name: str = "ome_arrow",
|
|
166
209
|
parquet_compression: str | None = "zstd",
|
|
167
210
|
parquet_metadata: dict[str, str] | None = None,
|
|
211
|
+
vortex_column_name: str = "ome_arrow",
|
|
212
|
+
vortex_metadata: dict[str, str] | None = None,
|
|
168
213
|
) -> np.array | dict | pa.StructScalar | str:
|
|
169
214
|
"""
|
|
170
215
|
Export the OME-Arrow content in a chosen representation.
|
|
@@ -178,6 +223,7 @@ class OMEArrow:
|
|
|
178
223
|
"ome-tiff" → write OME-TIFF via BioIO
|
|
179
224
|
"ome-zarr" → write OME-Zarr (OME-NGFF) via BioIO
|
|
180
225
|
"parquet" → write a single-row Parquet with one struct column
|
|
226
|
+
"vortex" → write a single-row Vortex file with one struct column
|
|
181
227
|
dtype:
|
|
182
228
|
Target dtype for "numpy"/writers (default: np.uint16).
|
|
183
229
|
strict:
|
|
@@ -194,11 +240,14 @@ class OMEArrow:
|
|
|
194
240
|
compression / compression_level / tile:
|
|
195
241
|
OME-TIFF options (passed through to tifffile via BioIO).
|
|
196
242
|
chunks / zarr_compressor / zarr_level :
|
|
197
|
-
OME-Zarr options (chunk shape, compressor hint, level).
|
|
243
|
+
OME-Zarr options (chunk shape, compressor hint, level). If chunks is
|
|
244
|
+
None, a TCZYX default is chosen (1,1,<=4,<=512,<=512).
|
|
198
245
|
use_channel_colors:
|
|
199
246
|
Try to embed per-channel display colors when safe; otherwise omitted.
|
|
200
247
|
parquet_*:
|
|
201
248
|
Options for Parquet export (column name, compression, file metadata).
|
|
249
|
+
vortex_*:
|
|
250
|
+
Options for Vortex export (column name, file metadata).
|
|
202
251
|
|
|
203
252
|
Returns
|
|
204
253
|
-------
|
|
@@ -209,6 +258,7 @@ class OMEArrow:
|
|
|
209
258
|
- "ome-tiff": output path (str)
|
|
210
259
|
- "ome-zarr": output path (str)
|
|
211
260
|
- "parquet": output path (str)
|
|
261
|
+
- "vortex": output path (str)
|
|
212
262
|
|
|
213
263
|
Raises
|
|
214
264
|
------
|
|
@@ -271,6 +321,18 @@ class OMEArrow:
|
|
|
271
321
|
)
|
|
272
322
|
return out
|
|
273
323
|
|
|
324
|
+
# Vortex (single row, single struct column)
|
|
325
|
+
if mode in {"ome-vortex", "omevortex", "vortex"}:
|
|
326
|
+
if not out:
|
|
327
|
+
raise ValueError("export(how='vortex') requires 'out' path.")
|
|
328
|
+
to_ome_vortex(
|
|
329
|
+
data=self.data,
|
|
330
|
+
out_path=out,
|
|
331
|
+
column_name=vortex_column_name,
|
|
332
|
+
file_metadata=vortex_metadata,
|
|
333
|
+
)
|
|
334
|
+
return out
|
|
335
|
+
|
|
274
336
|
raise ValueError(f"Unknown export method: {how}")
|
|
275
337
|
|
|
276
338
|
def info(self) -> Dict[str, Any]:
|
|
@@ -299,7 +361,7 @@ class OMEArrow:
|
|
|
299
361
|
opacity: str | float = "sigmoid",
|
|
300
362
|
clim: tuple[float, float] | None = None,
|
|
301
363
|
show_axes: bool = True,
|
|
302
|
-
scaling_values: tuple[float, float, float] | None =
|
|
364
|
+
scaling_values: tuple[float, float, float] | None = None,
|
|
303
365
|
) -> matplotlib.figure.Figure | "pyvista.Plotter":
|
|
304
366
|
"""
|
|
305
367
|
Render an OME-Arrow record using Matplotlib or PyVista.
|
|
@@ -337,7 +399,10 @@ class OMEArrow:
|
|
|
337
399
|
clim: Contrast limits (``(low, high)``) for PyVista rendering.
|
|
338
400
|
show_axes: If ``True``, display axes in the PyVista scene.
|
|
339
401
|
scaling_values: Physical scale multipliers for the (x, y, z) axes used by
|
|
340
|
-
PyVista, typically to express anisotropy.
|
|
402
|
+
PyVista, typically to express anisotropy. If ``None``, uses metadata
|
|
403
|
+
scaling from the OME-Arrow record (pixels_meta.physical_size_x/y/z).
|
|
404
|
+
These scaling values will default to 1µm if metadata is missing in
|
|
405
|
+
source image metadata.
|
|
341
406
|
|
|
342
407
|
Returns:
|
|
343
408
|
matplotlib.figure.Figure | pyvista.Plotter:
|
ome_arrow/export.py
CHANGED
|
@@ -21,7 +21,8 @@ def to_numpy(
|
|
|
21
21
|
Convert an OME-Arrow record into a NumPy array shaped (T,C,Z,Y,X).
|
|
22
22
|
|
|
23
23
|
The OME-Arrow "planes" are flattened YX slices indexed by (z, t, c).
|
|
24
|
-
|
|
24
|
+
When chunks are present, this function reconstitutes the dense TCZYX array
|
|
25
|
+
from chunked pixels instead of planes.
|
|
25
26
|
|
|
26
27
|
Args:
|
|
27
28
|
data:
|
|
@@ -58,7 +59,7 @@ def to_numpy(
|
|
|
58
59
|
if sx <= 0 or sy <= 0 or sz <= 0 or sc <= 0 or st <= 0:
|
|
59
60
|
raise ValueError("All size_* fields must be positive integers.")
|
|
60
61
|
|
|
61
|
-
|
|
62
|
+
expected_plane_len = sx * sy
|
|
62
63
|
|
|
63
64
|
# Prepare target array (T,C,Z,Y,X), zero-filled by default.
|
|
64
65
|
out = np.zeros((st, sc, sz, sy, sx), dtype=dtype)
|
|
@@ -78,6 +79,70 @@ def to_numpy(
|
|
|
78
79
|
a = np.clip(a, lo, hi)
|
|
79
80
|
return a.astype(dtype, copy=False)
|
|
80
81
|
|
|
82
|
+
chunks = data.get("chunks") or []
|
|
83
|
+
if chunks:
|
|
84
|
+
chunk_grid = data.get("chunk_grid") or {}
|
|
85
|
+
chunk_order = str(chunk_grid.get("chunk_order") or "ZYX").upper()
|
|
86
|
+
if chunk_order != "ZYX":
|
|
87
|
+
raise ValueError("Only chunk_order='ZYX' is supported for now.")
|
|
88
|
+
|
|
89
|
+
for i, ch in enumerate(chunks):
|
|
90
|
+
# Chunk coordinates include time/channel plus spatial indices.
|
|
91
|
+
t = int(ch["t"])
|
|
92
|
+
c = int(ch["c"])
|
|
93
|
+
z = int(ch["z"])
|
|
94
|
+
y = int(ch["y"])
|
|
95
|
+
x = int(ch["x"])
|
|
96
|
+
# Chunk shape is only spatial (Z, Y, X).
|
|
97
|
+
shape_z = int(ch["shape_z"])
|
|
98
|
+
shape_y = int(ch["shape_y"])
|
|
99
|
+
shape_x = int(ch["shape_x"])
|
|
100
|
+
|
|
101
|
+
# Validate chunk indices and extents within the full 5D array.
|
|
102
|
+
if not (0 <= t < st and 0 <= c < sc and 0 <= z < sz):
|
|
103
|
+
raise ValueError(
|
|
104
|
+
f"chunks[{i}] index out of range: (t,c,z)=({t},{c},{z})"
|
|
105
|
+
)
|
|
106
|
+
if y < 0 or x < 0 or shape_z <= 0 or shape_y <= 0 or shape_x <= 0:
|
|
107
|
+
raise ValueError(f"chunks[{i}] has invalid shape or origin.")
|
|
108
|
+
if z + shape_z > sz:
|
|
109
|
+
raise ValueError(
|
|
110
|
+
f"chunks[{i}] extent out of range: z+shape_z={z + shape_z} "
|
|
111
|
+
f"> sz={sz}"
|
|
112
|
+
)
|
|
113
|
+
if y + shape_y > sy:
|
|
114
|
+
raise ValueError(
|
|
115
|
+
f"chunks[{i}] extent out of range: y+shape_y={y + shape_y} "
|
|
116
|
+
f"> sy={sy}"
|
|
117
|
+
)
|
|
118
|
+
if x + shape_x > sx:
|
|
119
|
+
raise ValueError(
|
|
120
|
+
f"chunks[{i}] extent out of range: x+shape_x={x + shape_x} "
|
|
121
|
+
f"> sx={sx}"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
pix = ch["pixels"]
|
|
125
|
+
try:
|
|
126
|
+
n = len(pix)
|
|
127
|
+
except Exception as e:
|
|
128
|
+
raise ValueError(f"chunks[{i}].pixels is not a sequence") from e
|
|
129
|
+
|
|
130
|
+
expected_len = shape_z * shape_y * shape_x
|
|
131
|
+
if n != expected_len:
|
|
132
|
+
if strict:
|
|
133
|
+
raise ValueError(
|
|
134
|
+
f"chunks[{i}].pixels length {n} != expected {expected_len}"
|
|
135
|
+
)
|
|
136
|
+
if n > expected_len:
|
|
137
|
+
pix = pix[:expected_len]
|
|
138
|
+
else:
|
|
139
|
+
pix = list(pix) + [0] * (expected_len - n)
|
|
140
|
+
|
|
141
|
+
arr3d = np.asarray(pix).reshape(shape_z, shape_y, shape_x)
|
|
142
|
+
arr3d = _cast_plane(arr3d)
|
|
143
|
+
out[t, c, z : z + shape_z, y : y + shape_y, x : x + shape_x] = arr3d
|
|
144
|
+
return out
|
|
145
|
+
|
|
81
146
|
# Fill planes.
|
|
82
147
|
for i, p in enumerate(data.get("planes", [])):
|
|
83
148
|
z = int(p["z"])
|
|
@@ -94,16 +159,17 @@ def to_numpy(
|
|
|
94
159
|
except Exception as e:
|
|
95
160
|
raise ValueError(f"planes[{i}].pixels is not a sequence") from e
|
|
96
161
|
|
|
97
|
-
if n !=
|
|
162
|
+
if n != expected_plane_len:
|
|
98
163
|
if strict:
|
|
99
164
|
raise ValueError(
|
|
100
|
-
f"planes[{i}].pixels length {n} != size_x*size_y
|
|
165
|
+
f"planes[{i}].pixels length {n} != size_x*size_y "
|
|
166
|
+
f"{expected_plane_len}"
|
|
101
167
|
)
|
|
102
168
|
# Lenient mode: fix length by truncation or zero-pad.
|
|
103
|
-
if n >
|
|
104
|
-
pix = pix[:
|
|
169
|
+
if n > expected_plane_len:
|
|
170
|
+
pix = pix[:expected_plane_len]
|
|
105
171
|
else:
|
|
106
|
-
pix = list(pix) + [0] * (
|
|
172
|
+
pix = list(pix) + [0] * (expected_plane_len - n)
|
|
107
173
|
|
|
108
174
|
# Reshape to (Y,X) and cast.
|
|
109
175
|
arr2d = np.asarray(pix).reshape(sy, sx)
|
|
@@ -113,6 +179,162 @@ def to_numpy(
|
|
|
113
179
|
return out
|
|
114
180
|
|
|
115
181
|
|
|
182
|
+
# Note: x/y are implicit because this returns the full XY plane for (t, c, z).
|
|
183
|
+
def plane_from_chunks(
|
|
184
|
+
data: Dict[str, Any] | pa.StructScalar,
|
|
185
|
+
*,
|
|
186
|
+
t: int,
|
|
187
|
+
c: int,
|
|
188
|
+
z: int,
|
|
189
|
+
dtype: np.dtype = np.uint16,
|
|
190
|
+
strict: bool = True,
|
|
191
|
+
clamp: bool = False,
|
|
192
|
+
) -> np.ndarray:
|
|
193
|
+
"""Extract a single (t, c, z) plane using chunked pixels when available.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
data: OME-Arrow data as a Python dict or a `pa.StructScalar`.
|
|
197
|
+
t: Time index for the plane.
|
|
198
|
+
c: Channel index for the plane.
|
|
199
|
+
z: Z index for the plane.
|
|
200
|
+
dtype: Output dtype (default: np.uint16).
|
|
201
|
+
strict: When True, raise if chunk pixels are malformed.
|
|
202
|
+
clamp: If True, clamp values to the valid range of the target dtype.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
np.ndarray: 2D array with shape (Y, X).
|
|
206
|
+
|
|
207
|
+
Raises:
|
|
208
|
+
KeyError: If required OME-Arrow fields are missing.
|
|
209
|
+
ValueError: If indices are out of range or pixels are malformed.
|
|
210
|
+
"""
|
|
211
|
+
# The plane spans full X/Y for the given (t, c, z); x/y are implicit.
|
|
212
|
+
if isinstance(data, pa.StructScalar):
|
|
213
|
+
data = data.as_py()
|
|
214
|
+
|
|
215
|
+
# Read pixel metadata and validate requested plane indices.
|
|
216
|
+
pm = data["pixels_meta"]
|
|
217
|
+
sx, sy = int(pm["size_x"]), int(pm["size_y"])
|
|
218
|
+
sz, sc, st = int(pm["size_z"]), int(pm["size_c"]), int(pm["size_t"])
|
|
219
|
+
if not (0 <= t < st and 0 <= c < sc and 0 <= z < sz):
|
|
220
|
+
raise ValueError(f"Requested plane (t={t}, c={c}, z={z}) out of range.")
|
|
221
|
+
|
|
222
|
+
# Prepare dtype conversion (optional clamping for integer outputs).
|
|
223
|
+
if np.issubdtype(dtype, np.integer):
|
|
224
|
+
info = np.iinfo(dtype)
|
|
225
|
+
lo, hi = info.min, info.max
|
|
226
|
+
elif np.issubdtype(dtype, np.floating):
|
|
227
|
+
lo, hi = -np.inf, np.inf
|
|
228
|
+
else:
|
|
229
|
+
lo, hi = -np.inf, np.inf
|
|
230
|
+
|
|
231
|
+
def _cast_plane(a: np.ndarray) -> np.ndarray:
|
|
232
|
+
if clamp:
|
|
233
|
+
a = np.clip(a, lo, hi)
|
|
234
|
+
return a.astype(dtype, copy=False)
|
|
235
|
+
|
|
236
|
+
# Prefer chunked pixels if present, assembling the requested Z plane.
|
|
237
|
+
chunks = data.get("chunks") or []
|
|
238
|
+
if chunks:
|
|
239
|
+
chunk_grid = data.get("chunk_grid") or {}
|
|
240
|
+
chunk_order = str(chunk_grid.get("chunk_order") or "ZYX").upper()
|
|
241
|
+
if chunk_order != "ZYX":
|
|
242
|
+
raise ValueError("Only chunk_order='ZYX' is supported for now.")
|
|
243
|
+
|
|
244
|
+
# Allocate an empty XY plane; fill in tiles from matching chunks.
|
|
245
|
+
plane = np.zeros((sy, sx), dtype=dtype)
|
|
246
|
+
any_chunk_matched = False
|
|
247
|
+
for i, ch in enumerate(chunks):
|
|
248
|
+
# Skip chunks from other (t, c) positions.
|
|
249
|
+
if int(ch["t"]) != t or int(ch["c"]) != c:
|
|
250
|
+
continue
|
|
251
|
+
z0 = int(ch["z"])
|
|
252
|
+
szc = int(ch["shape_z"])
|
|
253
|
+
# Skip chunks whose Z slab does not cover the target plane.
|
|
254
|
+
if not (z0 <= z < z0 + szc):
|
|
255
|
+
continue
|
|
256
|
+
y0 = int(ch["y"])
|
|
257
|
+
x0 = int(ch["x"])
|
|
258
|
+
syc = int(ch["shape_y"])
|
|
259
|
+
sxc = int(ch["shape_x"])
|
|
260
|
+
# Validate chunk bounds (strict mode can fail fast).
|
|
261
|
+
if z0 < 0 or y0 < 0 or x0 < 0:
|
|
262
|
+
msg = f"chunks[{i}] has negative origin: (z,y,x)=({z0},{y0},{x0})"
|
|
263
|
+
if strict:
|
|
264
|
+
raise ValueError(msg)
|
|
265
|
+
continue
|
|
266
|
+
if z0 + szc > sz:
|
|
267
|
+
msg = f"chunks[{i}] extent out of range: z+shape_z={z0 + szc} > sz={sz}"
|
|
268
|
+
if strict:
|
|
269
|
+
raise ValueError(msg)
|
|
270
|
+
continue
|
|
271
|
+
if y0 + syc > sy:
|
|
272
|
+
msg = f"chunks[{i}] extent out of range: y+shape_y={y0 + syc} > sy={sy}"
|
|
273
|
+
if strict:
|
|
274
|
+
raise ValueError(msg)
|
|
275
|
+
continue
|
|
276
|
+
if x0 + sxc > sx:
|
|
277
|
+
msg = f"chunks[{i}] extent out of range: x+shape_x={x0 + sxc} > sx={sx}"
|
|
278
|
+
if strict:
|
|
279
|
+
raise ValueError(msg)
|
|
280
|
+
continue
|
|
281
|
+
pix = ch["pixels"]
|
|
282
|
+
try:
|
|
283
|
+
n = len(pix)
|
|
284
|
+
except Exception as e:
|
|
285
|
+
raise ValueError(f"chunks[{i}].pixels is not a sequence") from e
|
|
286
|
+
expected_len = szc * syc * sxc
|
|
287
|
+
if n != expected_len:
|
|
288
|
+
if strict:
|
|
289
|
+
raise ValueError(
|
|
290
|
+
f"chunks[{i}].pixels length {n} != expected {expected_len}"
|
|
291
|
+
)
|
|
292
|
+
# Lenient mode: truncate or zero-pad to match the expected size.
|
|
293
|
+
if n > expected_len:
|
|
294
|
+
pix = pix[:expected_len]
|
|
295
|
+
else:
|
|
296
|
+
pix = list(pix) + [0] * (expected_len - n)
|
|
297
|
+
|
|
298
|
+
# Convert to a Z/Y/X slab and copy the requested Z slice into the plane.
|
|
299
|
+
slab = np.asarray(pix).reshape(szc, syc, sxc)
|
|
300
|
+
slab = _cast_plane(slab)
|
|
301
|
+
zi = z - z0
|
|
302
|
+
plane[y0 : y0 + syc, x0 : x0 + sxc] = slab[zi]
|
|
303
|
+
any_chunk_matched = True
|
|
304
|
+
|
|
305
|
+
if any_chunk_matched:
|
|
306
|
+
return plane
|
|
307
|
+
|
|
308
|
+
# Fallback to planes list if chunks are absent.
|
|
309
|
+
target = next(
|
|
310
|
+
(
|
|
311
|
+
p
|
|
312
|
+
for p in data.get("planes", [])
|
|
313
|
+
if int(p["t"]) == t and int(p["c"]) == c and int(p["z"]) == z
|
|
314
|
+
),
|
|
315
|
+
None,
|
|
316
|
+
)
|
|
317
|
+
if target is None:
|
|
318
|
+
raise ValueError(f"plane (t={t}, c={c}, z={z}) not found")
|
|
319
|
+
|
|
320
|
+
pix = target["pixels"]
|
|
321
|
+
try:
|
|
322
|
+
n = len(pix)
|
|
323
|
+
except Exception as e:
|
|
324
|
+
raise ValueError("plane pixels is not a sequence") from e
|
|
325
|
+
expected_len = sx * sy
|
|
326
|
+
if n != expected_len:
|
|
327
|
+
if strict:
|
|
328
|
+
raise ValueError(f"plane pixels length {n} != size_x*size_y {expected_len}")
|
|
329
|
+
if n > expected_len:
|
|
330
|
+
pix = pix[:expected_len]
|
|
331
|
+
else:
|
|
332
|
+
pix = list(pix) + [0] * (expected_len - n)
|
|
333
|
+
|
|
334
|
+
arr2d = np.asarray(pix).reshape(sy, sx)
|
|
335
|
+
return _cast_plane(arr2d)
|
|
336
|
+
|
|
337
|
+
|
|
116
338
|
def to_ome_tiff(
|
|
117
339
|
data: Dict[str, Any] | pa.StructScalar,
|
|
118
340
|
out_path: str,
|
|
@@ -255,6 +477,7 @@ def to_ome_zarr(
|
|
|
255
477
|
- Creates level shapes for a multiscale pyramid (if multiscale_levels>1).
|
|
256
478
|
- Chooses Blosc codec compatible with zarr_format (v2 vs v3).
|
|
257
479
|
- Populates axes names/types/units and physical pixel sizes from pixels_meta.
|
|
480
|
+
- Uses default TCZYX chunks if none are provided.
|
|
258
481
|
"""
|
|
259
482
|
# --- local import to avoid hard deps at module import time
|
|
260
483
|
# Use the class you showed
|
|
@@ -317,6 +540,15 @@ def to_ome_zarr(
|
|
|
317
540
|
def _down(a: int, f: int) -> int:
|
|
318
541
|
return max(1, a // f)
|
|
319
542
|
|
|
543
|
+
def _default_chunks_tcxyz(
|
|
544
|
+
shape: Tuple[int, int, int, int, int],
|
|
545
|
+
) -> Tuple[int, int, int, int, int]:
|
|
546
|
+
_t, _c, z, y, x = shape
|
|
547
|
+
cz = min(z, 4) if z > 1 else 1
|
|
548
|
+
cy = min(y, 512)
|
|
549
|
+
cx = min(x, 512)
|
|
550
|
+
return (1, 1, cz, cy, cx)
|
|
551
|
+
|
|
320
552
|
def _level_shapes_tcxyz(levels: int) -> List[Tuple[int, int, int, int, int]]:
|
|
321
553
|
shapes = [(st, sc, sz, sy, sx)]
|
|
322
554
|
for _ in range(levels - 1):
|
|
@@ -340,6 +572,8 @@ def to_ome_zarr(
|
|
|
340
572
|
# 5) Chunking / shards (can be single-shape or per-level;
|
|
341
573
|
# we pass single-shape if provided)
|
|
342
574
|
chunk_shape: Optional[List[Tuple[int, ...]]] = None
|
|
575
|
+
if chunks is None:
|
|
576
|
+
chunks = _default_chunks_tcxyz((st, sc, sz, sy, sx))
|
|
343
577
|
if chunks is not None:
|
|
344
578
|
chunk_shape = [tuple(int(v) for v in chunks)] * multiscale_levels
|
|
345
579
|
|
|
@@ -393,7 +627,8 @@ def to_ome_parquet(
|
|
|
393
627
|
record_dict = data.as_py()
|
|
394
628
|
else:
|
|
395
629
|
# Validate by round-tripping through a typed scalar, then back to dict.
|
|
396
|
-
record_dict =
|
|
630
|
+
record_dict = {f.name: data.get(f.name) for f in OME_ARROW_STRUCT}
|
|
631
|
+
record_dict = pa.scalar(record_dict, type=OME_ARROW_STRUCT).as_py()
|
|
397
632
|
|
|
398
633
|
# 2) Build a single-row struct array from the dict, explicitly passing the schema
|
|
399
634
|
struct_array = pa.array([record_dict], type=OME_ARROW_STRUCT) # len=1
|
|
@@ -420,3 +655,62 @@ def to_ome_parquet(
|
|
|
420
655
|
compression=compression,
|
|
421
656
|
row_group_size=row_group_size,
|
|
422
657
|
)
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
def to_ome_vortex(
|
|
661
|
+
data: Dict[str, Any] | pa.StructScalar,
|
|
662
|
+
out_path: str,
|
|
663
|
+
column_name: str = "image",
|
|
664
|
+
file_metadata: Optional[Dict[str, str]] = None,
|
|
665
|
+
) -> None:
|
|
666
|
+
"""Export an OME-Arrow record to a Vortex file.
|
|
667
|
+
|
|
668
|
+
The file is written as a single-row, single-column Arrow table where the
|
|
669
|
+
column holds a struct with the OME-Arrow schema.
|
|
670
|
+
|
|
671
|
+
Args:
|
|
672
|
+
data: OME-Arrow dict or StructScalar.
|
|
673
|
+
out_path: Output path for the Vortex file.
|
|
674
|
+
column_name: Column name to store the struct.
|
|
675
|
+
file_metadata: Optional file-level metadata to attach.
|
|
676
|
+
|
|
677
|
+
Raises:
|
|
678
|
+
ImportError: If the optional `vortex-data` dependency is missing.
|
|
679
|
+
"""
|
|
680
|
+
|
|
681
|
+
try:
|
|
682
|
+
import vortex.io as vxio
|
|
683
|
+
except ImportError as exc:
|
|
684
|
+
raise ImportError(
|
|
685
|
+
"Vortex export requires the optional 'vortex-data' dependency."
|
|
686
|
+
) from exc
|
|
687
|
+
|
|
688
|
+
# 1) Normalize to a plain Python dict (works better with pyarrow builders,
|
|
689
|
+
# especially when the struct has a `null`-typed field like "masks").
|
|
690
|
+
if isinstance(data, pa.StructScalar):
|
|
691
|
+
record_dict = data.as_py()
|
|
692
|
+
else:
|
|
693
|
+
# Validate by round-tripping through a typed scalar, then back to dict.
|
|
694
|
+
record_dict = {f.name: data.get(f.name) for f in OME_ARROW_STRUCT}
|
|
695
|
+
record_dict = pa.scalar(record_dict, type=OME_ARROW_STRUCT).as_py()
|
|
696
|
+
|
|
697
|
+
# 2) Build a single-row struct array from the dict, explicitly passing the schema
|
|
698
|
+
struct_array = pa.array([record_dict], type=OME_ARROW_STRUCT) # len=1
|
|
699
|
+
|
|
700
|
+
# 3) Wrap into a one-column table
|
|
701
|
+
table = pa.table({column_name: struct_array})
|
|
702
|
+
|
|
703
|
+
# 4) Attach optional file-level metadata
|
|
704
|
+
meta: Dict[bytes, bytes] = dict(table.schema.metadata or {})
|
|
705
|
+
try:
|
|
706
|
+
meta[b"ome.arrow.type"] = str(OME_ARROW_TAG_TYPE).encode("utf-8")
|
|
707
|
+
meta[b"ome.arrow.version"] = str(OME_ARROW_TAG_VERSION).encode("utf-8")
|
|
708
|
+
except Exception:
|
|
709
|
+
pass
|
|
710
|
+
if file_metadata:
|
|
711
|
+
for k, v in file_metadata.items():
|
|
712
|
+
meta[str(k).encode("utf-8")] = str(v).encode("utf-8")
|
|
713
|
+
table = table.replace_schema_metadata(meta)
|
|
714
|
+
|
|
715
|
+
# 5) Write Vortex (single row, single column)
|
|
716
|
+
vxio.write(table, str(out_path))
|