ome-arrow 0.0.2__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 ADDED
@@ -0,0 +1,19 @@
1
+ """
2
+ Init file for ome_arrow package.
3
+ """
4
+
5
+ from ome_arrow._version import version as ome_arrow_version
6
+ from ome_arrow.core import OMEArrow
7
+ from ome_arrow.export import to_numpy, to_ome_parquet, to_ome_tiff, to_ome_zarr
8
+ from ome_arrow.ingest import (
9
+ from_numpy,
10
+ from_ome_parquet,
11
+ from_ome_zarr,
12
+ from_tiff,
13
+ to_ome_arrow,
14
+ )
15
+ from ome_arrow.meta import OME_ARROW_STRUCT, OME_ARROW_TAG_TYPE, OME_ARROW_TAG_VERSION
16
+ from ome_arrow.utils import describe_ome_arrow, verify_ome_arrow
17
+ from ome_arrow.view import view_matplotlib, view_pyvista
18
+
19
+ __version__ = ome_arrow_version
ome_arrow/_version.py ADDED
@@ -0,0 +1,34 @@
1
+ # file generated by setuptools-scm
2
+ # don't change, don't track in version control
3
+
4
+ __all__ = [
5
+ "__version__",
6
+ "__version_tuple__",
7
+ "version",
8
+ "version_tuple",
9
+ "__commit_id__",
10
+ "commit_id",
11
+ ]
12
+
13
+ TYPE_CHECKING = False
14
+ if TYPE_CHECKING:
15
+ from typing import Tuple
16
+ from typing import Union
17
+
18
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
19
+ COMMIT_ID = Union[str, None]
20
+ else:
21
+ VERSION_TUPLE = object
22
+ COMMIT_ID = object
23
+
24
+ version: str
25
+ __version__: str
26
+ __version_tuple__: VERSION_TUPLE
27
+ version_tuple: VERSION_TUPLE
28
+ commit_id: COMMIT_ID
29
+ __commit_id__: COMMIT_ID
30
+
31
+ __version__ = version = '0.0.2'
32
+ __version_tuple__ = version_tuple = (0, 0, 2)
33
+
34
+ __commit_id__ = commit_id = None
ome_arrow/core.py ADDED
@@ -0,0 +1,492 @@
1
+ """
2
+ Core of the ome_arrow package, used for classes and such.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import pathlib
8
+ from typing import Any, Dict, Iterable, Optional, Tuple
9
+
10
+ import matplotlib
11
+ import numpy as np
12
+ import pyarrow as pa
13
+ import pyvista
14
+
15
+ from ome_arrow.export import to_numpy, to_ome_parquet, to_ome_tiff, to_ome_zarr
16
+ from ome_arrow.ingest import (
17
+ from_numpy,
18
+ from_ome_parquet,
19
+ from_ome_zarr,
20
+ from_stack_pattern_path,
21
+ from_tiff,
22
+ )
23
+ from ome_arrow.meta import OME_ARROW_STRUCT
24
+ from ome_arrow.transform import slice_ome_arrow
25
+ from ome_arrow.utils import describe_ome_arrow
26
+ from ome_arrow.view import view_matplotlib, view_pyvista
27
+
28
+
29
+ class OMEArrow:
30
+ """
31
+ Small convenience toolkit for working with ome-arrow data.
32
+
33
+ If `input` is a TIFF path, this loads it via `tiff_to_ome_arrow`.
34
+ If `input` is a dict, it will be converted using `to_struct_scalar`.
35
+ If `input` is already a `pa.StructScalar`, it is used as-is.
36
+
37
+ In Jupyter, evaluating the instance will render the first plane using
38
+ matplotlib (via `_repr_html_`). Call `view_matplotlib()` to select a
39
+ specific (z, t, c) plane.
40
+
41
+ Args:
42
+ input: TIFF path, nested dict, or `pa.StructScalar`.
43
+ struct: Expected Arrow StructType (e.g., OME_ARROW_STRUCT).
44
+ """
45
+
46
+ def __init__(
47
+ self,
48
+ data: str | dict | pa.StructScalar | "np.ndarray",
49
+ tcz: Tuple[int, int, int] = (0, 0, 0),
50
+ ) -> None:
51
+ """
52
+ Construct an OMEArrow from:
53
+ - a Bio-Formats-style stack pattern string (contains '<', '>', or '*')
54
+ - a path/URL to an OME-TIFF (.tif/.tiff)
55
+ - a path/URL to an OME-Zarr store (.zarr / .ome.zarr)
56
+ - a path/URL to an OME-Parquet file (.parquet / .pq)
57
+ - a NumPy ndarray (2D-5D; interpreted
58
+ with from_numpy defaults)
59
+ - a dict already matching the OME-Arrow schema
60
+ - a pa.StructScalar already typed to OME_ARROW_STRUCT
61
+ """
62
+
63
+ # set the tcz for viewing
64
+ self.tcz = tcz
65
+
66
+ # --- 1) Stack pattern (Bio-Formats-style) --------------------------------
67
+ if isinstance(data, str) and any(c in data for c in "<>*"):
68
+ self.data = from_stack_pattern_path(
69
+ data,
70
+ default_dim_for_unspecified="C",
71
+ map_series_to="T",
72
+ clamp_to_uint16=True,
73
+ )
74
+
75
+ # --- 2) String path/URL: OME-Zarr / OME-Parquet / OME-TIFF ---------------
76
+ elif isinstance(data, str):
77
+ s = data.strip()
78
+ path = pathlib.Path(s)
79
+
80
+ # Zarr detection
81
+ if (
82
+ s.lower().endswith(".zarr")
83
+ or s.lower().endswith(".ome.zarr")
84
+ or ".zarr/" in s.lower()
85
+ or (path.exists() and path.is_dir() and path.suffix.lower() == ".zarr")
86
+ ):
87
+ self.data = from_ome_zarr(s)
88
+
89
+ # OME-Parquet
90
+ elif s.lower().endswith((".parquet", ".pq")) or path.suffix.lower() in {
91
+ ".parquet",
92
+ ".pq",
93
+ }:
94
+ self.data = from_ome_parquet(s)
95
+
96
+ # TIFF
97
+ elif path.suffix.lower() in {".tif", ".tiff"} or s.lower().endswith(
98
+ (".tif", ".tiff")
99
+ ):
100
+ self.data = from_tiff(s)
101
+
102
+ elif path.exists() and path.is_dir():
103
+ raise ValueError(
104
+ f"Directory '{s}' exists but does not look like an OME-Zarr store "
105
+ "(expected suffix '.zarr' or '.ome.zarr')."
106
+ )
107
+ else:
108
+ raise ValueError(
109
+ "String input must be one of:\n"
110
+ " • Bio-Formats pattern string (contains '<', '>' or '*')\n"
111
+ " • OME-Zarr path/URL ending with '.zarr' or '.ome.zarr'\n"
112
+ " • OME-Parquet file ending with '.parquet' or '.pq'\n"
113
+ " • OME-TIFF path/URL ending with '.tif' or '.tiff'"
114
+ )
115
+
116
+ # --- 3) NumPy ndarray ----------------------------------------------------
117
+ elif isinstance(data, np.ndarray):
118
+ # Uses from_numpy defaults: dim_order="TCZYX", clamp_to_uint16=True, etc.
119
+ # If the array is YX/ZYX/CYX/etc.,
120
+ # from_numpy will expand/reorder accordingly.
121
+ self.data = from_numpy(data)
122
+
123
+ # --- 4) Already-typed Arrow scalar ---------------------------------------
124
+ elif isinstance(data, pa.StructScalar):
125
+ self.data = data
126
+
127
+ # --- 5) Plain dict matching the schema -----------------------------------
128
+ elif isinstance(data, dict):
129
+ self.data = pa.scalar(data, type=OME_ARROW_STRUCT)
130
+
131
+ # --- otherwise ------------------------------------------------------------
132
+ else:
133
+ raise TypeError(
134
+ "input data must be str, dict, pa.StructScalar, or numpy.ndarray"
135
+ )
136
+
137
+ def export(
138
+ self,
139
+ how: str = "numpy",
140
+ dtype: np.dtype = np.uint16,
141
+ strict: bool = True,
142
+ clamp: bool = False,
143
+ *,
144
+ # common writer args
145
+ out: str | None = None,
146
+ dim_order: str = "TCZYX",
147
+ # OME-TIFF args
148
+ compression: str | None = "zlib",
149
+ compression_level: int = 6,
150
+ tile: tuple[int, int] | None = None,
151
+ # OME-Zarr args
152
+ chunks: tuple[int, int, int, int, int] | None = None, # (T,C,Z,Y,X)
153
+ zarr_compressor: str | None = "zstd",
154
+ zarr_level: int = 7,
155
+ # optional display metadata (both paths guard/ignore if unsafe)
156
+ use_channel_colors: bool = False,
157
+ # Parquet args
158
+ parquet_column_name: str = "ome_arrow",
159
+ parquet_compression: str | None = "zstd",
160
+ parquet_metadata: dict[str, str] | None = None,
161
+ ) -> np.array | dict | pa.StructScalar | str:
162
+ """
163
+ Export the OME-Arrow content in a chosen representation.
164
+
165
+ Args
166
+ ----
167
+ how:
168
+ "numpy" → TCZYX np.ndarray
169
+ "dict" → plain Python dict
170
+ "scalar" → pa.StructScalar (as-is)
171
+ "ome-tiff" → write OME-TIFF via BioIO
172
+ "ome-zarr" → write OME-Zarr (OME-NGFF) via BioIO
173
+ "parquet" → write a single-row Parquet with one struct column
174
+ dtype:
175
+ Target dtype for "numpy"/writers (default: np.uint16).
176
+ strict:
177
+ For "numpy": raise if a plane has wrong pixel length.
178
+ clamp:
179
+ For "numpy"/writers: clamp values into dtype range before cast.
180
+
181
+ Keyword-only (writer specific)
182
+ ------------------------------
183
+ out:
184
+ Output path (required for 'ome-tiff', 'ome-zarr', and 'parquet').
185
+ dim_order:
186
+ Axes string for BioIO writers; default "TCZYX".
187
+ compression / compression_level / tile:
188
+ OME-TIFF options (passed through to tifffile via BioIO).
189
+ chunks / zarr_compressor / zarr_level :
190
+ OME-Zarr options (chunk shape, compressor hint, level).
191
+ use_channel_colors:
192
+ Try to embed per-channel display colors when safe; otherwise omitted.
193
+ parquet_*:
194
+ Options for Parquet export (column name, compression, file metadata).
195
+
196
+ Returns
197
+ -------
198
+ Any
199
+ - "numpy": np.ndarray (T, C, Z, Y, X)
200
+ - "dict": dict
201
+ - "scalar": pa.StructScalar
202
+ - "ome-tiff": output path (str)
203
+ - "ome-zarr": output path (str)
204
+ - "parquet": output path (str)
205
+
206
+ Raises
207
+ ------
208
+ ValueError:
209
+ Unknown 'how' or missing required params.
210
+ """
211
+ # existing modes
212
+ if how == "numpy":
213
+ return to_numpy(self.data, dtype=dtype, strict=strict, clamp=clamp)
214
+ if how == "dict":
215
+ return self.data.as_py()
216
+ if how == "scalar":
217
+ return self.data
218
+
219
+ mode = how.lower().replace("_", "-")
220
+
221
+ # OME-TIFF via BioIO
222
+ if mode in {"ome-tiff", "ometiff", "tiff"}:
223
+ if not out:
224
+ raise ValueError("export(how='ome-tiff') requires 'out' path.")
225
+ to_ome_tiff(
226
+ self.data,
227
+ out,
228
+ dtype=dtype,
229
+ clamp=clamp,
230
+ dim_order=dim_order,
231
+ compression=compression,
232
+ compression_level=int(compression_level),
233
+ tile=tile,
234
+ use_channel_colors=use_channel_colors,
235
+ )
236
+ return out
237
+
238
+ # OME-Zarr via BioIO
239
+ if mode in {"ome-zarr", "omezarr", "zarr"}:
240
+ if not out:
241
+ raise ValueError("export(how='ome-zarr') requires 'out' path.")
242
+ to_ome_zarr(
243
+ self.data,
244
+ out,
245
+ dtype=dtype,
246
+ clamp=clamp,
247
+ dim_order=dim_order,
248
+ chunks=chunks,
249
+ compressor=zarr_compressor,
250
+ compressor_level=int(zarr_level),
251
+ )
252
+ return out
253
+
254
+ # Parquet (single row, single struct column)
255
+ if mode in {"ome-parquet", "omeparquet", "parquet"}:
256
+ if not out:
257
+ raise ValueError("export(how='parquet') requires 'out' path.")
258
+ to_ome_parquet(
259
+ data=self.data,
260
+ out_path=out,
261
+ column_name=parquet_column_name,
262
+ compression=parquet_compression, # default 'zstd'
263
+ file_metadata=parquet_metadata,
264
+ )
265
+ return out
266
+
267
+ raise ValueError(f"Unknown export method: {how}")
268
+
269
+ def info(self) -> Dict[str, Any]:
270
+ """
271
+ Describe the OME-Arrow data structure.
272
+
273
+ Returns:
274
+ dict with keys:
275
+ - shape: (T, C, Z, Y, X)
276
+ - type: classification string
277
+ - summary: human-readable text
278
+ """
279
+ return describe_ome_arrow(self.data)
280
+
281
+ def view(
282
+ self,
283
+ how: str = "matplotlib",
284
+ tcz: tuple[int, int, int] = (0, 0, 0),
285
+ autoscale: bool = True,
286
+ vmin: int | None = None,
287
+ vmax: int | None = None,
288
+ cmap: str = "gray",
289
+ show: bool = True,
290
+ c: int | None = None,
291
+ downsample: int = 1,
292
+ opacity: str | float = "sigmoid",
293
+ clim: tuple[float, float] | None = None,
294
+ show_axes: bool = True,
295
+ scaling_values: tuple[float, float, float] | None = (1.0, 0.1, 0.1),
296
+ ) -> matplotlib.figure.Figure | pyvista.Plotter:
297
+ """
298
+ Render an OME-Arrow record using Matplotlib or PyVista.
299
+
300
+ This convenience method supports two rendering backends:
301
+
302
+ * ``how="matplotlib"`` — renders a single (t, c, z) plane as a 2D image.
303
+ Returns a Matplotlib :class:`~matplotlib.figure.Figure` (or whatever
304
+ :func:`view_matplotlib` returns) and optionally displays it with
305
+ ``plt.show()`` when ``show=True``.
306
+
307
+ * ``how="pyvista"`` — creates an interactive 3D PyVista visualization in
308
+ Jupyter. When ``show=True``, displays the widget. Independently, a static
309
+ PNG snapshot is embedded in the notebook (inside a collapsed
310
+ ``<details>`` block) for non-interactive renderers (e.g., GitHub).
311
+
312
+ Args:
313
+ how: Rendering backend. One of ``"matplotlib"`` or ``"pyvista"``.
314
+ tcz: The (t, c, z) indices of the plane to display when using Matplotlib.
315
+ Defaults to ``(0, 0, 0)``.
316
+ autoscale: If ``True`` and ``vmin``/``vmax`` are not provided, infer
317
+ display limits from the image data range (Matplotlib path only).
318
+ vmin: Lower display limit for intensity scaling (Matplotlib path only).
319
+ vmax: Upper display limit for intensity scaling (Matplotlib path only).
320
+ cmap: Matplotlib colormap name for single-channel display (Matplotlib only).
321
+ show: Whether to display the plot immediately. For Matplotlib, calls
322
+ ``plt.show()``. For PyVista, calls ``plotter.show()``.
323
+ c: Channel index override for the PyVista view. If ``None``, uses
324
+ ``tcz[1]`` (the ``c`` from ``tcz``).
325
+ downsample: Integer downsampling factor for the PyVista volume or slices.
326
+ Must be ``>= 1``.
327
+ opacity: Opacity specification for PyVista. Either a float in ``[0, 1]``
328
+ or the string ``"sigmoid"`` (backend interprets as a preset transfer
329
+ function).
330
+ clim: Contrast limits (``(low, high)``) for PyVista rendering.
331
+ show_axes: If ``True``, display axes in the PyVista scene.
332
+ scaling_values: Physical scale multipliers for the (x, y, z) axes used by
333
+ PyVista, typically to express anisotropy. Defaults to ``(1.0, 0.1, 0.1)``.
334
+
335
+ Returns:
336
+ matplotlib.figure.Figure | pyvista.Plotter:
337
+ * If ``how="matplotlib"``, returns the figure created by
338
+ :func:`view_matplotlib` (often a :class:`~matplotlib.figure.Figure`).
339
+ * If ``how="pyvista"``, returns the created :class:`pyvista.Plotter`.
340
+
341
+ Raises:
342
+ ValueError: If a requested plane (``t,c,z``) is not found or if pixel
343
+ array dimensions are inconsistent (propagated from
344
+ :func:`view_matplotlib`).
345
+ TypeError: If parameter types are invalid (e.g., negative ``downsample``).
346
+
347
+ Notes:
348
+ * The PyVista path embeds a static PNG snapshot via Pillow (``PIL``). If
349
+ Pillow is unavailable, the method logs a warning and skips the snapshot,
350
+ but the interactive viewer is still returned.
351
+ * When ``show=False`` and ``how="pyvista"``, no interactive window is
352
+ opened, but the returned :class:`pyvista.Plotter` can be shown later.
353
+
354
+ Examples:
355
+ Display a single plane with Matplotlib:
356
+
357
+ >>> fig = obj.view(how="matplotlib", tcz=(0, 1, 5), cmap="magma")
358
+
359
+ Create an interactive PyVista scene in a Jupyter notebook:
360
+
361
+ >>> plotter = obj.view(how="pyvista", c=0, downsample=2, show=True)
362
+
363
+ Configure PyVista contrast limits and keep axes hidden:
364
+
365
+ >>> plotter = obj.view(how="pyvista", clim=(100, 2000), show_axes=False)
366
+ """
367
+ if how == "matplotlib":
368
+ return view_matplotlib(
369
+ self.data,
370
+ tcz=tcz,
371
+ autoscale=autoscale,
372
+ vmin=vmin,
373
+ vmax=vmax,
374
+ cmap=cmap,
375
+ show=show,
376
+ )
377
+
378
+ if how == "pyvista":
379
+ import base64
380
+ import io
381
+
382
+ from IPython.display import HTML, display
383
+
384
+ c_idx = int(tcz[1] if c is None else c)
385
+ plotter = view_pyvista(
386
+ data=self.data,
387
+ c=c_idx,
388
+ downsample=downsample,
389
+ opacity=opacity,
390
+ clim=clim,
391
+ show_axes=show_axes,
392
+ scaling_values=scaling_values,
393
+ show=False,
394
+ )
395
+
396
+ # 1) show the interactive widget for live work
397
+ if show:
398
+ plotter.show()
399
+
400
+ # 2) capture a PNG and embed it in a collapsed details block
401
+ try:
402
+ img = plotter.screenshot(return_img=True) # ndarray
403
+ if img is not None:
404
+ buf = io.BytesIO()
405
+ # use matplotlib-free writer: PyVista returns RGB(A) uint8
406
+ from PIL import (
407
+ Image as PILImage,
408
+ ) # pillow is a light dep most envs have
409
+
410
+ PILImage.fromarray(img).save(buf, format="PNG")
411
+ b64 = base64.b64encode(buf.getvalue()).decode("ascii")
412
+ display(
413
+ HTML(
414
+ f"""
415
+ <details>
416
+ <summary>Static snapshot (for non-interactive view)</summary>
417
+ <img src="data:image/png;base64,{b64}" />
418
+ </details>
419
+ """
420
+ )
421
+ )
422
+ except Exception as e:
423
+ print(f"Warning: could not save PyVista snapshot: {e}")
424
+
425
+ return plotter
426
+
427
+ def slice(
428
+ self,
429
+ x_min: int,
430
+ x_max: int,
431
+ y_min: int,
432
+ y_max: int,
433
+ t_indices: Optional[Iterable[int]] = None,
434
+ c_indices: Optional[Iterable[int]] = None,
435
+ z_indices: Optional[Iterable[int]] = None,
436
+ fill_missing: bool = True,
437
+ ) -> OMEArrow:
438
+ """
439
+ Create a cropped copy of an OME-Arrow record.
440
+
441
+ Crops spatially to [y_min:y_max, x_min:x_max] (half-open) and, if provided,
442
+ filters/reindexes T/C/Z to the given index sets.
443
+
444
+ Parameters
445
+ ----------
446
+ x_min, x_max, y_min, y_max : int
447
+ Half-open crop bounds in pixels (0-based).
448
+ t_indices, c_indices, z_indices : Iterable[int] | None
449
+ Optional explicit indices to keep for T, C, Z. If None, keep all.
450
+ Selected indices are reindexed to 0..len-1 in the output.
451
+ fill_missing : bool
452
+ If True, any missing (t,c,z) planes in the selection are zero-filled.
453
+
454
+ Returns
455
+ -------
456
+ OMEArrow object
457
+ New OME-Arrow record with updated sizes and planes.
458
+ """
459
+
460
+ return OMEArrow(
461
+ data=slice_ome_arrow(
462
+ data=self.data,
463
+ x_min=x_min,
464
+ x_max=x_max,
465
+ y_min=y_min,
466
+ y_max=y_max,
467
+ t_indices=t_indices,
468
+ c_indices=c_indices,
469
+ z_indices=z_indices,
470
+ fill_missing=fill_missing,
471
+ )
472
+ )
473
+
474
+ def _repr_html_(self) -> str:
475
+ """
476
+ Auto-render a plane as inline PNG in Jupyter.
477
+ """
478
+ try:
479
+ view_matplotlib(
480
+ data=self.data,
481
+ tcz=self.tcz,
482
+ autoscale=True,
483
+ vmin=None,
484
+ vmax=None,
485
+ cmap="gray",
486
+ show=False,
487
+ )
488
+ # return blank string to avoid showing class representation below image
489
+ return self.info()["summary"]
490
+ except Exception as e:
491
+ # Fallback to a tiny text status if rendering fails.
492
+ return f"<pre>OMEArrowKit: render failed: {e}</pre>"