ome-arrow 0.0.5__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/_version.py +2 -2
- ome_arrow/core.py +32 -3
- ome_arrow/export.py +245 -9
- ome_arrow/ingest.py +237 -37
- ome_arrow/meta.py +41 -0
- ome_arrow/transform.py +34 -0
- ome_arrow/view.py +3 -22
- {ome_arrow-0.0.5.dist-info → ome_arrow-0.0.6.dist-info}/METADATA +1 -1
- ome_arrow-0.0.6.dist-info/RECORD +14 -0
- {ome_arrow-0.0.5.dist-info → ome_arrow-0.0.6.dist-info}/WHEEL +1 -1
- ome_arrow-0.0.5.dist-info/RECORD +0 -14
- {ome_arrow-0.0.5.dist-info → ome_arrow-0.0.6.dist-info}/licenses/LICENSE +0 -0
- {ome_arrow-0.0.5.dist-info → ome_arrow-0.0.6.dist-info}/top_level.txt +0 -0
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
|
@@ -59,6 +59,7 @@ class OMEArrow:
|
|
|
59
59
|
tcz: Tuple[int, int, int] = (0, 0, 0),
|
|
60
60
|
column_name: str = "ome_arrow",
|
|
61
61
|
row_index: int = 0,
|
|
62
|
+
image_type: str | None = None,
|
|
62
63
|
) -> None:
|
|
63
64
|
"""
|
|
64
65
|
Construct an OMEArrow from:
|
|
@@ -71,6 +72,7 @@ class OMEArrow:
|
|
|
71
72
|
with from_numpy defaults)
|
|
72
73
|
- a dict already matching the OME-Arrow schema
|
|
73
74
|
- a pa.StructScalar already typed to OME_ARROW_STRUCT
|
|
75
|
+
- optionally override/set image_type metadata on ingest
|
|
74
76
|
"""
|
|
75
77
|
|
|
76
78
|
# set the tcz for viewing
|
|
@@ -83,6 +85,7 @@ class OMEArrow:
|
|
|
83
85
|
default_dim_for_unspecified="C",
|
|
84
86
|
map_series_to="T",
|
|
85
87
|
clamp_to_uint16=True,
|
|
88
|
+
image_type=image_type,
|
|
86
89
|
)
|
|
87
90
|
|
|
88
91
|
# --- 2) String path/URL: OME-Zarr / OME-Parquet / OME-TIFF ---------------
|
|
@@ -98,6 +101,8 @@ class OMEArrow:
|
|
|
98
101
|
or (path.exists() and path.is_dir() and path.suffix.lower() == ".zarr")
|
|
99
102
|
):
|
|
100
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)
|
|
101
106
|
|
|
102
107
|
# OME-Parquet
|
|
103
108
|
elif s.lower().endswith((".parquet", ".pq")) or path.suffix.lower() in {
|
|
@@ -107,18 +112,24 @@ class OMEArrow:
|
|
|
107
112
|
self.data = from_ome_parquet(
|
|
108
113
|
s, column_name=column_name, row_index=row_index
|
|
109
114
|
)
|
|
115
|
+
if image_type is not None:
|
|
116
|
+
self.data = self._wrap_with_image_type(self.data, image_type)
|
|
110
117
|
|
|
111
118
|
# Vortex
|
|
112
119
|
elif s.lower().endswith(".vortex") or path.suffix.lower() == ".vortex":
|
|
113
120
|
self.data = from_ome_vortex(
|
|
114
121
|
s, column_name=column_name, row_index=row_index
|
|
115
122
|
)
|
|
123
|
+
if image_type is not None:
|
|
124
|
+
self.data = self._wrap_with_image_type(self.data, image_type)
|
|
116
125
|
|
|
117
126
|
# TIFF
|
|
118
127
|
elif path.suffix.lower() in {".tif", ".tiff"} or s.lower().endswith(
|
|
119
128
|
(".tif", ".tiff")
|
|
120
129
|
):
|
|
121
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)
|
|
122
133
|
|
|
123
134
|
elif path.exists() and path.is_dir():
|
|
124
135
|
raise ValueError(
|
|
@@ -140,15 +151,20 @@ class OMEArrow:
|
|
|
140
151
|
# Uses from_numpy defaults: dim_order="TCZYX", clamp_to_uint16=True, etc.
|
|
141
152
|
# If the array is YX/ZYX/CYX/etc.,
|
|
142
153
|
# from_numpy will expand/reorder accordingly.
|
|
143
|
-
self.data = from_numpy(data)
|
|
154
|
+
self.data = from_numpy(data, image_type=image_type)
|
|
144
155
|
|
|
145
156
|
# --- 4) Already-typed Arrow scalar ---------------------------------------
|
|
146
157
|
elif isinstance(data, pa.StructScalar):
|
|
147
158
|
self.data = data
|
|
159
|
+
if image_type is not None:
|
|
160
|
+
self.data = self._wrap_with_image_type(self.data, image_type)
|
|
148
161
|
|
|
149
162
|
# --- 5) Plain dict matching the schema -----------------------------------
|
|
150
163
|
elif isinstance(data, dict):
|
|
151
|
-
|
|
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)
|
|
152
168
|
|
|
153
169
|
# --- otherwise ------------------------------------------------------------
|
|
154
170
|
else:
|
|
@@ -156,6 +172,18 @@ class OMEArrow:
|
|
|
156
172
|
"input data must be str, dict, pa.StructScalar, or numpy.ndarray"
|
|
157
173
|
)
|
|
158
174
|
|
|
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
|
+
|
|
159
187
|
def export( # noqa: PLR0911
|
|
160
188
|
self,
|
|
161
189
|
how: str = "numpy",
|
|
@@ -212,7 +240,8 @@ class OMEArrow:
|
|
|
212
240
|
compression / compression_level / tile:
|
|
213
241
|
OME-TIFF options (passed through to tifffile via BioIO).
|
|
214
242
|
chunks / zarr_compressor / zarr_level :
|
|
215
|
-
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).
|
|
216
245
|
use_channel_colors:
|
|
217
246
|
Try to embed per-channel display colors when safe; otherwise omitted.
|
|
218
247
|
parquet_*:
|
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
|
|
@@ -456,7 +691,8 @@ def to_ome_vortex(
|
|
|
456
691
|
record_dict = data.as_py()
|
|
457
692
|
else:
|
|
458
693
|
# Validate by round-tripping through a typed scalar, then back to dict.
|
|
459
|
-
record_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()
|
|
460
696
|
|
|
461
697
|
# 2) Build a single-row struct array from the dict, explicitly passing the schema
|
|
462
698
|
struct_array = pa.array([record_dict], type=OME_ARROW_STRUCT) # len=1
|
ome_arrow/ingest.py
CHANGED
|
@@ -50,8 +50,9 @@ def _ome_arrow_from_table(
|
|
|
50
50
|
# 1) Locate the OME-Arrow column
|
|
51
51
|
def _struct_matches_ome_fields(t: pa.StructType) -> bool:
|
|
52
52
|
ome_fields = {f.name for f in OME_ARROW_STRUCT}
|
|
53
|
+
required_fields = ome_fields - {"image_type", "chunk_grid", "chunks"}
|
|
53
54
|
col_fields = {f.name for f in t}
|
|
54
|
-
return
|
|
55
|
+
return required_fields.issubset(col_fields)
|
|
55
56
|
|
|
56
57
|
requested_name = column_name
|
|
57
58
|
candidate_col = None
|
|
@@ -105,6 +106,11 @@ def _ome_arrow_from_table(
|
|
|
105
106
|
|
|
106
107
|
# 2) Extract the row as a Python dict
|
|
107
108
|
record_dict: Dict[str, Any] = candidate_col.slice(row_index, 1).to_pylist()[0]
|
|
109
|
+
# Back-compat: older files won't include image_type; default to None.
|
|
110
|
+
if "image_type" not in record_dict:
|
|
111
|
+
record_dict["image_type"] = None
|
|
112
|
+
# Drop unexpected fields before casting to the canonical schema.
|
|
113
|
+
record_dict = {f.name: record_dict.get(f.name) for f in OME_ARROW_STRUCT}
|
|
108
114
|
|
|
109
115
|
# 3) Reconstruct a typed StructScalar using the canonical schema
|
|
110
116
|
scalar = pa.scalar(record_dict, type=OME_ARROW_STRUCT)
|
|
@@ -243,11 +249,123 @@ def _read_ngff_scale(zarr_path: Path) -> tuple[float, float, float, str | None]
|
|
|
243
249
|
return psize_x, psize_y, psize_z, unit
|
|
244
250
|
|
|
245
251
|
|
|
252
|
+
def _normalize_chunk_shape(
|
|
253
|
+
chunk_shape: Optional[Tuple[int, int, int]],
|
|
254
|
+
size_z: int,
|
|
255
|
+
size_y: int,
|
|
256
|
+
size_x: int,
|
|
257
|
+
) -> Tuple[int, int, int]:
|
|
258
|
+
"""Normalize a chunk shape against image bounds.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
chunk_shape: Desired chunk shape as (Z, Y, X), or None.
|
|
262
|
+
size_z: Total Z size of the image.
|
|
263
|
+
size_y: Total Y size of the image.
|
|
264
|
+
size_x: Total X size of the image.
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
Tuple[int, int, int]: Normalized (Z, Y, X) chunk shape.
|
|
268
|
+
"""
|
|
269
|
+
if chunk_shape is None:
|
|
270
|
+
chunk_shape = (1, 512, 512)
|
|
271
|
+
if not isinstance(chunk_shape, (list, tuple)) or len(chunk_shape) != 3:
|
|
272
|
+
raise ValueError("chunk_shape must be a sequence of three integers (z,y,x)")
|
|
273
|
+
try:
|
|
274
|
+
cz_raw, cy_raw, cx_raw = (int(v) for v in chunk_shape)
|
|
275
|
+
except Exception as exc:
|
|
276
|
+
raise ValueError(
|
|
277
|
+
"chunk_shape must be a sequence of three integers (z,y,x)"
|
|
278
|
+
) from exc
|
|
279
|
+
if cz_raw <= 0 or cy_raw <= 0 or cx_raw <= 0:
|
|
280
|
+
raise ValueError("chunk_shape values must be positive integers")
|
|
281
|
+
cz = max(1, min(cz_raw, int(size_z)))
|
|
282
|
+
cy = max(1, min(cy_raw, int(size_y)))
|
|
283
|
+
cx = max(1, min(cx_raw, int(size_x)))
|
|
284
|
+
return cz, cy, cx
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _build_chunks_from_planes(
|
|
288
|
+
*,
|
|
289
|
+
planes: List[Dict[str, Any]],
|
|
290
|
+
size_t: int,
|
|
291
|
+
size_c: int,
|
|
292
|
+
size_z: int,
|
|
293
|
+
size_y: int,
|
|
294
|
+
size_x: int,
|
|
295
|
+
chunk_shape: Optional[Tuple[int, int, int]],
|
|
296
|
+
chunk_order: str = "ZYX",
|
|
297
|
+
) -> List[Dict[str, Any]]:
|
|
298
|
+
"""Build chunked pixels from a list of flattened planes.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
planes: List of plane dicts with keys z, t, c, and pixels.
|
|
302
|
+
size_t: Total T size of the image.
|
|
303
|
+
size_c: Total C size of the image.
|
|
304
|
+
size_z: Total Z size of the image.
|
|
305
|
+
size_y: Total Y size of the image.
|
|
306
|
+
size_x: Total X size of the image.
|
|
307
|
+
chunk_shape: Desired chunk shape as (Z, Y, X).
|
|
308
|
+
chunk_order: Flattening order for chunk pixels (default "ZYX").
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
List[Dict[str, Any]]: Chunk list with pixels stored as flat lists.
|
|
312
|
+
|
|
313
|
+
Raises:
|
|
314
|
+
ValueError: If an unsupported chunk_order is requested.
|
|
315
|
+
"""
|
|
316
|
+
if str(chunk_order).upper() != "ZYX":
|
|
317
|
+
raise ValueError("Only chunk_order='ZYX' is supported for now.")
|
|
318
|
+
|
|
319
|
+
cz, cy, cx = _normalize_chunk_shape(chunk_shape, size_z, size_y, size_x)
|
|
320
|
+
|
|
321
|
+
plane_map: Dict[Tuple[int, int, int], np.ndarray] = {}
|
|
322
|
+
for p in planes:
|
|
323
|
+
z = int(p["z"])
|
|
324
|
+
t = int(p["t"])
|
|
325
|
+
c = int(p["c"])
|
|
326
|
+
pix = p["pixels"]
|
|
327
|
+
arr2d = np.asarray(pix).reshape(size_y, size_x)
|
|
328
|
+
plane_map[(t, c, z)] = arr2d
|
|
329
|
+
|
|
330
|
+
dtype = next(iter(plane_map.values())).dtype if plane_map else np.uint16
|
|
331
|
+
|
|
332
|
+
chunks: List[Dict[str, Any]] = []
|
|
333
|
+
for t in range(size_t):
|
|
334
|
+
for c in range(size_c):
|
|
335
|
+
for z0 in range(0, size_z, cz):
|
|
336
|
+
sz = min(cz, size_z - z0)
|
|
337
|
+
for y0 in range(0, size_y, cy):
|
|
338
|
+
sy = min(cy, size_y - y0)
|
|
339
|
+
for x0 in range(0, size_x, cx):
|
|
340
|
+
sx = min(cx, size_x - x0)
|
|
341
|
+
slab = np.zeros((sz, sy, sx), dtype=dtype)
|
|
342
|
+
for zi in range(sz):
|
|
343
|
+
plane = plane_map.get((t, c, z0 + zi))
|
|
344
|
+
if plane is None:
|
|
345
|
+
continue
|
|
346
|
+
slab[zi] = plane[y0 : y0 + sy, x0 : x0 + sx]
|
|
347
|
+
chunks.append(
|
|
348
|
+
{
|
|
349
|
+
"t": t,
|
|
350
|
+
"c": c,
|
|
351
|
+
"z": z0,
|
|
352
|
+
"y": y0,
|
|
353
|
+
"x": x0,
|
|
354
|
+
"shape_z": sz,
|
|
355
|
+
"shape_y": sy,
|
|
356
|
+
"shape_x": sx,
|
|
357
|
+
"pixels": slab.reshape(-1).tolist(),
|
|
358
|
+
}
|
|
359
|
+
)
|
|
360
|
+
return chunks
|
|
361
|
+
|
|
362
|
+
|
|
246
363
|
def to_ome_arrow(
|
|
247
364
|
type_: str = OME_ARROW_TAG_TYPE,
|
|
248
365
|
version: str = OME_ARROW_TAG_VERSION,
|
|
249
366
|
image_id: str = "unnamed",
|
|
250
367
|
name: str = "unknown",
|
|
368
|
+
image_type: str | None = "image",
|
|
251
369
|
acquisition_datetime: Optional[datetime] = None,
|
|
252
370
|
dimension_order: str = "XYZCT",
|
|
253
371
|
dtype: str = "uint16",
|
|
@@ -262,6 +380,10 @@ def to_ome_arrow(
|
|
|
262
380
|
physical_size_unit: str = "µm",
|
|
263
381
|
channels: Optional[List[Dict[str, Any]]] = None,
|
|
264
382
|
planes: Optional[List[Dict[str, Any]]] = None,
|
|
383
|
+
chunks: Optional[List[Dict[str, Any]]] = None,
|
|
384
|
+
chunk_shape: Optional[Tuple[int, int, int]] = (1, 512, 512), # (Z, Y, X)
|
|
385
|
+
chunk_order: str = "ZYX",
|
|
386
|
+
build_chunks: bool = True,
|
|
265
387
|
masks: Any = None,
|
|
266
388
|
) -> pa.StructScalar:
|
|
267
389
|
"""
|
|
@@ -276,6 +398,9 @@ def to_ome_arrow(
|
|
|
276
398
|
version: Specification version string.
|
|
277
399
|
image_id: Unique image identifier.
|
|
278
400
|
name: Human-friendly name.
|
|
401
|
+
image_type: Open-ended image kind (e.g., "image", "label"). Note that
|
|
402
|
+
from_* helpers pass image_type=None by default to preserve
|
|
403
|
+
"unspecified" vs explicitly set ("image").
|
|
279
404
|
acquisition_datetime: Datetime of acquisition (defaults to now).
|
|
280
405
|
dimension_order: Dimension order ("XYZCT" or "XYCT").
|
|
281
406
|
dtype: Pixel data type string (e.g., "uint16").
|
|
@@ -284,6 +409,12 @@ def to_ome_arrow(
|
|
|
284
409
|
physical_size_unit: Unit string, default "µm".
|
|
285
410
|
channels: List of channel dicts. Autogenerates one if None.
|
|
286
411
|
planes: List of plane dicts. Empty if None.
|
|
412
|
+
chunks: Optional list of chunk dicts. If None and build_chunks is True,
|
|
413
|
+
chunks are derived from planes using chunk_shape.
|
|
414
|
+
chunk_shape: Chunk shape as (Z, Y, X). Defaults to (1, 512, 512).
|
|
415
|
+
chunk_order: Flattening order for chunk pixels (default "ZYX").
|
|
416
|
+
build_chunks: If True, build chunked pixels from planes when chunks
|
|
417
|
+
is None.
|
|
287
418
|
masks: Optional placeholder for future annotations.
|
|
288
419
|
|
|
289
420
|
Returns:
|
|
@@ -299,6 +430,7 @@ def to_ome_arrow(
|
|
|
299
430
|
version = str(version)
|
|
300
431
|
image_id = str(image_id)
|
|
301
432
|
name = str(name)
|
|
433
|
+
image_type = None if image_type is None else str(image_type)
|
|
302
434
|
dimension_order = str(dimension_order)
|
|
303
435
|
dtype = str(dtype)
|
|
304
436
|
physical_size_unit = str(physical_size_unit)
|
|
@@ -328,11 +460,62 @@ def to_ome_arrow(
|
|
|
328
460
|
if planes is None:
|
|
329
461
|
planes = [{"z": 0, "t": 0, "c": 0, "pixels": [0] * (size_x * size_y)}]
|
|
330
462
|
|
|
463
|
+
if chunks is None and build_chunks:
|
|
464
|
+
chunks = _build_chunks_from_planes(
|
|
465
|
+
planes=planes,
|
|
466
|
+
size_t=size_t,
|
|
467
|
+
size_c=size_c,
|
|
468
|
+
size_z=size_z,
|
|
469
|
+
size_y=size_y,
|
|
470
|
+
size_x=size_x,
|
|
471
|
+
chunk_shape=chunk_shape,
|
|
472
|
+
chunk_order=chunk_order,
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
chunk_grid = None
|
|
476
|
+
if chunks is not None:
|
|
477
|
+
chunk_order = str(chunk_order).upper()
|
|
478
|
+
if chunk_order != "ZYX":
|
|
479
|
+
raise ValueError("Only chunk_order='ZYX' is supported for now.")
|
|
480
|
+
if len(chunks) == 0:
|
|
481
|
+
raise ValueError("chunks must not be an empty list")
|
|
482
|
+
first = chunks[0]
|
|
483
|
+
try:
|
|
484
|
+
derived_shape = (
|
|
485
|
+
int(first["shape_z"]),
|
|
486
|
+
int(first["shape_y"]),
|
|
487
|
+
int(first["shape_x"]),
|
|
488
|
+
)
|
|
489
|
+
except Exception as exc:
|
|
490
|
+
raise ValueError(
|
|
491
|
+
"chunks entries must include shape_z/shape_y/shape_x"
|
|
492
|
+
) from exc
|
|
493
|
+
if derived_shape[0] <= 0 or derived_shape[1] <= 0 or derived_shape[2] <= 0:
|
|
494
|
+
raise ValueError("chunk shapes must be positive integers")
|
|
495
|
+
if chunk_shape is not None:
|
|
496
|
+
norm_shape = _normalize_chunk_shape(chunk_shape, size_z, size_y, size_x)
|
|
497
|
+
if norm_shape != derived_shape:
|
|
498
|
+
raise ValueError(
|
|
499
|
+
"chunk_shape does not match provided chunks "
|
|
500
|
+
f"(chunk_shape={norm_shape}, chunks_shape={derived_shape})"
|
|
501
|
+
)
|
|
502
|
+
cz, cy, cx = _normalize_chunk_shape(derived_shape, size_z, size_y, size_x)
|
|
503
|
+
chunk_grid = {
|
|
504
|
+
"order": "TCZYX",
|
|
505
|
+
"chunk_t": 1,
|
|
506
|
+
"chunk_c": 1,
|
|
507
|
+
"chunk_z": cz,
|
|
508
|
+
"chunk_y": cy,
|
|
509
|
+
"chunk_x": cx,
|
|
510
|
+
"chunk_order": str(chunk_order),
|
|
511
|
+
}
|
|
512
|
+
|
|
331
513
|
record = {
|
|
332
514
|
"type": type_,
|
|
333
515
|
"version": version,
|
|
334
516
|
"id": image_id,
|
|
335
517
|
"name": name,
|
|
518
|
+
"image_type": image_type,
|
|
336
519
|
"acquisition_datetime": acquisition_datetime or datetime.now(timezone.utc),
|
|
337
520
|
"pixels_meta": {
|
|
338
521
|
"dimension_order": dimension_order,
|
|
@@ -350,6 +533,8 @@ def to_ome_arrow(
|
|
|
350
533
|
"physical_size_z_unit": physical_size_unit,
|
|
351
534
|
"channels": channels,
|
|
352
535
|
},
|
|
536
|
+
"chunk_grid": chunk_grid,
|
|
537
|
+
"chunks": chunks,
|
|
353
538
|
"planes": planes,
|
|
354
539
|
"masks": masks,
|
|
355
540
|
}
|
|
@@ -363,9 +548,13 @@ def from_numpy(
|
|
|
363
548
|
dim_order: str = "TCZYX",
|
|
364
549
|
image_id: Optional[str] = None,
|
|
365
550
|
name: Optional[str] = None,
|
|
551
|
+
image_type: Optional[str] = None,
|
|
366
552
|
channel_names: Optional[Sequence[str]] = None,
|
|
367
553
|
acquisition_datetime: Optional[datetime] = None,
|
|
368
554
|
clamp_to_uint16: bool = True,
|
|
555
|
+
chunk_shape: Optional[Tuple[int, int, int]] = (1, 512, 512),
|
|
556
|
+
chunk_order: str = "ZYX",
|
|
557
|
+
build_chunks: bool = True,
|
|
369
558
|
# meta
|
|
370
559
|
physical_size_x: float = 1.0,
|
|
371
560
|
physical_size_y: float = 1.0,
|
|
@@ -373,42 +562,39 @@ def from_numpy(
|
|
|
373
562
|
physical_size_unit: str = "µm",
|
|
374
563
|
dtype_meta: Optional[str] = None, # if None, inferred from output dtype
|
|
375
564
|
) -> pa.StructScalar:
|
|
376
|
-
"""
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
- If Z is not in `dim_order`, `size_z` will be 1 and the meta
|
|
410
|
-
dimension_order becomes "XYCT"; otherwise "XYZCT".
|
|
411
|
-
- If T/C are absent in `dim_order`, they default to size 1.
|
|
565
|
+
"""Build an OME-Arrow StructScalar from a NumPy array.
|
|
566
|
+
|
|
567
|
+
Args:
|
|
568
|
+
arr: Image data with axes described by `dim_order`.
|
|
569
|
+
dim_order: Axis labels for `arr`. Must include "Y" and "X".
|
|
570
|
+
Supported examples: "YX", "ZYX", "CYX", "CZYX", "TYX", "TCYX", "TCZYX".
|
|
571
|
+
image_id: Optional stable image identifier.
|
|
572
|
+
name: Optional human label.
|
|
573
|
+
image_type: Open-ended image kind (e.g., "image", "label").
|
|
574
|
+
channel_names: Names for channels; defaults to C0..C{n-1}.
|
|
575
|
+
acquisition_datetime: Defaults to now (UTC) if None.
|
|
576
|
+
clamp_to_uint16: If True, clamp/cast planes to uint16 before serialization.
|
|
577
|
+
chunk_shape: Chunk shape as (Z, Y, X). Defaults to (1, 512, 512).
|
|
578
|
+
chunk_order: Flattening order for chunk pixels (default "ZYX").
|
|
579
|
+
build_chunks: If True, build chunked pixels from planes.
|
|
580
|
+
physical_size_x: Spatial pixel size (µm) for X.
|
|
581
|
+
physical_size_y: Spatial pixel size (µm) for Y.
|
|
582
|
+
physical_size_z: Spatial pixel size (µm) for Z when present.
|
|
583
|
+
physical_size_unit: Unit string for spatial axes (default "µm").
|
|
584
|
+
dtype_meta: Pixel dtype string to place in metadata; if None, inferred
|
|
585
|
+
from the (possibly cast) array's dtype.
|
|
586
|
+
|
|
587
|
+
Returns:
|
|
588
|
+
pa.StructScalar: Typed OME-Arrow record (schema = OME_ARROW_STRUCT).
|
|
589
|
+
|
|
590
|
+
Raises:
|
|
591
|
+
TypeError: If `arr` is not a NumPy ndarray.
|
|
592
|
+
ValueError: If `dim_order` is invalid or dimensions are non-positive.
|
|
593
|
+
|
|
594
|
+
Notes:
|
|
595
|
+
- If Z is not in `dim_order`, `size_z` will be 1 and the meta
|
|
596
|
+
dimension_order becomes "XYCT"; otherwise "XYZCT".
|
|
597
|
+
- If T/C are absent in `dim_order`, they default to size 1.
|
|
412
598
|
"""
|
|
413
599
|
|
|
414
600
|
if not isinstance(arr, np.ndarray):
|
|
@@ -496,6 +682,7 @@ def from_numpy(
|
|
|
496
682
|
return to_ome_arrow(
|
|
497
683
|
image_id=str(image_id or "unnamed"),
|
|
498
684
|
name=str(name or "unknown"),
|
|
685
|
+
image_type=image_type,
|
|
499
686
|
acquisition_datetime=acquisition_datetime or datetime.now(timezone.utc),
|
|
500
687
|
dimension_order=meta_dim_order,
|
|
501
688
|
dtype=dtype_str,
|
|
@@ -510,6 +697,9 @@ def from_numpy(
|
|
|
510
697
|
physical_size_unit=str(physical_size_unit),
|
|
511
698
|
channels=channels,
|
|
512
699
|
planes=planes,
|
|
700
|
+
chunk_shape=chunk_shape,
|
|
701
|
+
chunk_order=chunk_order,
|
|
702
|
+
build_chunks=build_chunks,
|
|
513
703
|
masks=None,
|
|
514
704
|
)
|
|
515
705
|
|
|
@@ -518,6 +708,7 @@ def from_tiff(
|
|
|
518
708
|
tiff_path: str | Path,
|
|
519
709
|
image_id: Optional[str] = None,
|
|
520
710
|
name: Optional[str] = None,
|
|
711
|
+
image_type: Optional[str] = None,
|
|
521
712
|
channel_names: Optional[Sequence[str]] = None,
|
|
522
713
|
acquisition_datetime: Optional[datetime] = None,
|
|
523
714
|
clamp_to_uint16: bool = True,
|
|
@@ -532,6 +723,7 @@ def from_tiff(
|
|
|
532
723
|
tiff_path: Path to a TIFF readable by bioio.
|
|
533
724
|
image_id: Optional stable image identifier (defaults to stem).
|
|
534
725
|
name: Optional human label (defaults to file name).
|
|
726
|
+
image_type: Optional image kind (e.g., "image", "label").
|
|
535
727
|
channel_names: Optional channel names; defaults to C0..C{n-1}.
|
|
536
728
|
acquisition_datetime: Optional acquisition time (UTC now if None).
|
|
537
729
|
clamp_to_uint16: If True, clamp/cast planes to uint16.
|
|
@@ -601,6 +793,7 @@ def from_tiff(
|
|
|
601
793
|
return to_ome_arrow(
|
|
602
794
|
image_id=img_id,
|
|
603
795
|
name=display_name,
|
|
796
|
+
image_type=image_type,
|
|
604
797
|
acquisition_datetime=acquisition_datetime or datetime.now(timezone.utc),
|
|
605
798
|
dimension_order=dim_order,
|
|
606
799
|
dtype="uint16",
|
|
@@ -627,6 +820,7 @@ def from_stack_pattern_path(
|
|
|
627
820
|
channel_names: Optional[List[str]] = None,
|
|
628
821
|
image_id: Optional[str] = None,
|
|
629
822
|
name: Optional[str] = None,
|
|
823
|
+
image_type: Optional[str] = None,
|
|
630
824
|
) -> pa.StructScalar:
|
|
631
825
|
"""Build an OME-Arrow record from a filename pattern describing a stack.
|
|
632
826
|
|
|
@@ -638,6 +832,7 @@ def from_stack_pattern_path(
|
|
|
638
832
|
channel_names: Optional list of channel names to apply.
|
|
639
833
|
image_id: Optional image identifier override.
|
|
640
834
|
name: Optional display name override.
|
|
835
|
+
image_type: Optional image kind (e.g., "image", "label").
|
|
641
836
|
|
|
642
837
|
Returns:
|
|
643
838
|
A validated OME-Arrow StructScalar describing the stack.
|
|
@@ -907,6 +1102,7 @@ def from_stack_pattern_path(
|
|
|
907
1102
|
return to_ome_arrow(
|
|
908
1103
|
image_id=str(img_id),
|
|
909
1104
|
name=str(display_name),
|
|
1105
|
+
image_type=image_type,
|
|
910
1106
|
acquisition_datetime=None,
|
|
911
1107
|
dimension_order=dim_order,
|
|
912
1108
|
dtype="uint16",
|
|
@@ -929,6 +1125,7 @@ def from_ome_zarr(
|
|
|
929
1125
|
zarr_path: str | Path,
|
|
930
1126
|
image_id: Optional[str] = None,
|
|
931
1127
|
name: Optional[str] = None,
|
|
1128
|
+
image_type: Optional[str] = None,
|
|
932
1129
|
channel_names: Optional[Sequence[str]] = None,
|
|
933
1130
|
acquisition_datetime: Optional[datetime] = None,
|
|
934
1131
|
clamp_to_uint16: bool = True,
|
|
@@ -947,6 +1144,8 @@ def from_ome_zarr(
|
|
|
947
1144
|
Optional stable image identifier (defaults to directory stem).
|
|
948
1145
|
name:
|
|
949
1146
|
Optional display name (defaults to directory name).
|
|
1147
|
+
image_type:
|
|
1148
|
+
Optional image kind (e.g., "image", "label").
|
|
950
1149
|
channel_names:
|
|
951
1150
|
Optional list of channel names. Defaults to C0, C1, ...
|
|
952
1151
|
acquisition_datetime:
|
|
@@ -1028,6 +1227,7 @@ def from_ome_zarr(
|
|
|
1028
1227
|
return to_ome_arrow(
|
|
1029
1228
|
image_id=img_id,
|
|
1030
1229
|
name=display_name,
|
|
1230
|
+
image_type=image_type,
|
|
1031
1231
|
acquisition_datetime=acquisition_datetime or datetime.now(timezone.utc),
|
|
1032
1232
|
dimension_order=dim_order,
|
|
1033
1233
|
dtype="uint16",
|
ome_arrow/meta.py
CHANGED
|
@@ -12,8 +12,10 @@ OME_ARROW_TAG_VERSION = ome_arrow_version
|
|
|
12
12
|
# OME_ARROW_STRUCT: ome-arrow record (describes one image/value).
|
|
13
13
|
# - type/version: quick identity & evolution.
|
|
14
14
|
# - id/name/acquisition_datetime: identity & provenance.
|
|
15
|
+
# - image_type: open-ended image kind (e.g., "image", "label").
|
|
15
16
|
# - pixels_meta: pixels struct (sizes, units, channels).
|
|
16
17
|
# - planes: list of planes struct entries, one per (t,c,z).
|
|
18
|
+
# - chunk_grid/chunks: optional chunked pixels (TCZYX-aware), stored as Arrow lists.
|
|
17
19
|
# - masks: reserved for future labels/ROIs (placeholder).
|
|
18
20
|
OME_ARROW_STRUCT: pa.StructType = pa.struct(
|
|
19
21
|
[
|
|
@@ -21,6 +23,7 @@ OME_ARROW_STRUCT: pa.StructType = pa.struct(
|
|
|
21
23
|
pa.field("version", pa.string()), # e.g., "1.0.0"
|
|
22
24
|
pa.field("id", pa.string()), # stable image identifier
|
|
23
25
|
pa.field("name", pa.string()), # human label
|
|
26
|
+
pa.field("image_type", pa.string()), # open-ended (e.g., "image", "label")
|
|
24
27
|
pa.field("acquisition_datetime", pa.timestamp("us")),
|
|
25
28
|
# PIXELS: OME-like "Pixels" header summarizing shape & scale.
|
|
26
29
|
# - dimension_order: hint like "XYZCT" (or "XYCT" when Z==1).
|
|
@@ -68,6 +71,44 @@ OME_ARROW_STRUCT: pa.StructType = pa.struct(
|
|
|
68
71
|
]
|
|
69
72
|
),
|
|
70
73
|
),
|
|
74
|
+
# CHUNK GRID: optional chunking metadata for random access.
|
|
75
|
+
# - order: axis order for the full array, e.g., "TCZYX".
|
|
76
|
+
# - chunk_*: chunk sizes for each axis (defaults to 1 for T/C).
|
|
77
|
+
# - chunk_order: order used to flatten chunk pixels (default "ZYX").
|
|
78
|
+
pa.field(
|
|
79
|
+
"chunk_grid",
|
|
80
|
+
pa.struct(
|
|
81
|
+
[
|
|
82
|
+
pa.field("order", pa.string()),
|
|
83
|
+
pa.field("chunk_t", pa.int32()),
|
|
84
|
+
pa.field("chunk_c", pa.int16()),
|
|
85
|
+
pa.field("chunk_z", pa.int32()),
|
|
86
|
+
pa.field("chunk_y", pa.int32()),
|
|
87
|
+
pa.field("chunk_x", pa.int32()),
|
|
88
|
+
pa.field("chunk_order", pa.string()),
|
|
89
|
+
]
|
|
90
|
+
),
|
|
91
|
+
),
|
|
92
|
+
# CHUNKS: list of chunk entries (Arrow-native, no binary payloads).
|
|
93
|
+
# - pixels flattened in chunk_order (default "ZYX").
|
|
94
|
+
pa.field(
|
|
95
|
+
"chunks",
|
|
96
|
+
pa.list_(
|
|
97
|
+
pa.struct(
|
|
98
|
+
[
|
|
99
|
+
pa.field("t", pa.int32()),
|
|
100
|
+
pa.field("c", pa.int16()),
|
|
101
|
+
pa.field("z", pa.int32()),
|
|
102
|
+
pa.field("y", pa.int32()),
|
|
103
|
+
pa.field("x", pa.int32()),
|
|
104
|
+
pa.field("shape_z", pa.int32()),
|
|
105
|
+
pa.field("shape_y", pa.int32()),
|
|
106
|
+
pa.field("shape_x", pa.int32()),
|
|
107
|
+
pa.field("pixels", pa.list_(pa.uint16())),
|
|
108
|
+
]
|
|
109
|
+
)
|
|
110
|
+
),
|
|
111
|
+
),
|
|
71
112
|
# PLANES: one 2D image plane for a specific (t, c, z).
|
|
72
113
|
# - pixels: flattened numeric list (Y*X) for analysis-ready computation.
|
|
73
114
|
pa.field(
|
ome_arrow/transform.py
CHANGED
|
@@ -8,6 +8,7 @@ from typing import Any, Dict, Iterable, List, Optional, Tuple
|
|
|
8
8
|
import numpy as np
|
|
9
9
|
import pyarrow as pa
|
|
10
10
|
|
|
11
|
+
from ome_arrow.ingest import _build_chunks_from_planes, _normalize_chunk_shape
|
|
11
12
|
from ome_arrow.meta import OME_ARROW_STRUCT
|
|
12
13
|
|
|
13
14
|
|
|
@@ -179,4 +180,37 @@ def slice_ome_arrow(
|
|
|
179
180
|
rec_out["pixels_meta"] = pm_out
|
|
180
181
|
rec_out["planes"] = planes_out
|
|
181
182
|
|
|
183
|
+
chunk_grid_in = row.get("chunk_grid") or {}
|
|
184
|
+
if chunk_grid_in or row.get("chunks"):
|
|
185
|
+
chunk_shape = (
|
|
186
|
+
int(chunk_grid_in.get("chunk_z", 1)),
|
|
187
|
+
int(chunk_grid_in.get("chunk_y", 512)),
|
|
188
|
+
int(chunk_grid_in.get("chunk_x", 512)),
|
|
189
|
+
)
|
|
190
|
+
chunk_order = str(chunk_grid_in.get("chunk_order") or "ZYX")
|
|
191
|
+
chunks_out = _build_chunks_from_planes(
|
|
192
|
+
planes=planes_out,
|
|
193
|
+
size_t=new_st,
|
|
194
|
+
size_c=new_sc,
|
|
195
|
+
size_z=new_sz,
|
|
196
|
+
size_y=new_sy,
|
|
197
|
+
size_x=new_sx,
|
|
198
|
+
chunk_shape=chunk_shape,
|
|
199
|
+
chunk_order=chunk_order,
|
|
200
|
+
)
|
|
201
|
+
cz, cy, cx = _normalize_chunk_shape(chunk_shape, new_sz, new_sy, new_sx)
|
|
202
|
+
rec_out["chunk_grid"] = {
|
|
203
|
+
"order": "TCZYX",
|
|
204
|
+
"chunk_t": 1,
|
|
205
|
+
"chunk_c": 1,
|
|
206
|
+
"chunk_z": cz,
|
|
207
|
+
"chunk_y": cy,
|
|
208
|
+
"chunk_x": cx,
|
|
209
|
+
"chunk_order": chunk_order,
|
|
210
|
+
}
|
|
211
|
+
rec_out["chunks"] = chunks_out
|
|
212
|
+
else:
|
|
213
|
+
rec_out["chunk_grid"] = row.get("chunk_grid")
|
|
214
|
+
rec_out["chunks"] = row.get("chunks")
|
|
215
|
+
|
|
182
216
|
return pa.scalar(rec_out, type=OME_ARROW_STRUCT)
|
ome_arrow/view.py
CHANGED
|
@@ -23,6 +23,8 @@ except ImportError: # pragma: no cover - exercised when viz extra missing
|
|
|
23
23
|
if TYPE_CHECKING:
|
|
24
24
|
import pyvista
|
|
25
25
|
|
|
26
|
+
from ome_arrow.export import plane_from_chunks
|
|
27
|
+
|
|
26
28
|
|
|
27
29
|
def view_matplotlib(
|
|
28
30
|
data: dict[str, object] | pa.StructScalar,
|
|
@@ -50,29 +52,8 @@ def view_matplotlib(
|
|
|
50
52
|
Raises:
|
|
51
53
|
ValueError: If the requested plane is missing or pixel sizes mismatch.
|
|
52
54
|
"""
|
|
53
|
-
if isinstance(data, pa.StructScalar):
|
|
54
|
-
data = data.as_py()
|
|
55
|
-
|
|
56
|
-
pm = data["pixels_meta"]
|
|
57
|
-
sx, sy = int(pm["size_x"]), int(pm["size_y"])
|
|
58
55
|
t, c, z = (int(x) for x in tcz)
|
|
59
|
-
|
|
60
|
-
plane = next(
|
|
61
|
-
(
|
|
62
|
-
p
|
|
63
|
-
for p in data["planes"]
|
|
64
|
-
if int(p["t"]) == t and int(p["c"]) == c and int(p["z"]) == z
|
|
65
|
-
),
|
|
66
|
-
None,
|
|
67
|
-
)
|
|
68
|
-
if plane is None:
|
|
69
|
-
raise ValueError(f"plane (t={t}, c={c}, z={z}) not found")
|
|
70
|
-
|
|
71
|
-
pix = plane["pixels"]
|
|
72
|
-
if len(pix) != sx * sy:
|
|
73
|
-
raise ValueError(f"pixels len {len(pix)} != size_x*size_y ({sx * sy})")
|
|
74
|
-
|
|
75
|
-
img = np.asarray(pix, dtype=np.uint16).reshape(sy, sx).copy()
|
|
56
|
+
img = plane_from_chunks(data, t=t, c=c, z=z, dtype=np.uint16).copy()
|
|
76
57
|
|
|
77
58
|
if (vmin is None or vmax is None) and autoscale:
|
|
78
59
|
lo, hi = int(img.min()), int(img.max())
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
ome_arrow/__init__.py,sha256=WWenJP9XxLZNGQPVOEFBDlDM1kSvj_QdHssrET6UuNQ,644
|
|
2
|
+
ome_arrow/_version.py,sha256=7MyqQ3iPP2mJruPfRYGCNCq1z7_Nk7c-eyYecYITxsY,704
|
|
3
|
+
ome_arrow/core.py,sha256=K1-jfojhqYkn3h-0AdmBQYFza0wtFqduJs6K7aw4Kuc,21303
|
|
4
|
+
ome_arrow/export.py,sha256=XbKVIeMLbPshgo_OGO3e0PRqtZ3vEx_JAGyF9oUmZJw,26024
|
|
5
|
+
ome_arrow/ingest.py,sha256=CptjVXXL8YzfHKlKqyoA-ani5SLbFlQSUVPqOXlPhXA,46931
|
|
6
|
+
ome_arrow/meta.py,sha256=peIx6NLriaFpGBx9Y3NyTHLfGAg91v9YQpoBzrveKFQ,6031
|
|
7
|
+
ome_arrow/transform.py,sha256=X3gKZkgTDNQSBcU3_YmJRav8JIBrDdYJci3PbFZ4t38,7200
|
|
8
|
+
ome_arrow/utils.py,sha256=XHovcqmjqoiBpKvXY47-_yUwf07f8zVE_F9BR_VKaPU,2383
|
|
9
|
+
ome_arrow/view.py,sha256=j9dpmSnVbiukwH6asFhCJ_WVRxwyCAdTeLiUnv_xvcE,10187
|
|
10
|
+
ome_arrow-0.0.6.dist-info/licenses/LICENSE,sha256=9-2Pyhu3vTt2RJU8DorHQtHeNO_e5RLeFJTyOU4hOi4,1508
|
|
11
|
+
ome_arrow-0.0.6.dist-info/METADATA,sha256=VWirz2ueYrPLOrsNj1EC2BGcsPtLpJU6Jay_NW_irZ8,6110
|
|
12
|
+
ome_arrow-0.0.6.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
13
|
+
ome_arrow-0.0.6.dist-info/top_level.txt,sha256=aWOtkGXo_pfU-yy82guzGhz8Zh2h2nFl8Kc5qdzMGuE,10
|
|
14
|
+
ome_arrow-0.0.6.dist-info/RECORD,,
|
ome_arrow-0.0.5.dist-info/RECORD
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
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,,
|
|
File without changes
|
|
File without changes
|