patchworks 0.7.0__tar.gz → 0.8.1__tar.gz

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.
Files changed (50) hide show
  1. {patchworks-0.7.0 → patchworks-0.8.1}/PKG-INFO +1 -1
  2. {patchworks-0.7.0 → patchworks-0.8.1}/docs/guide/ome_zarr_napari.md +10 -6
  3. {patchworks-0.7.0 → patchworks-0.8.1}/src/patchworks/plugins/ome_zarr.py +97 -7
  4. {patchworks-0.7.0 → patchworks-0.8.1}/tests/test_ome_zarr.py +11 -0
  5. {patchworks-0.7.0 → patchworks-0.8.1}/.github/workflows/docs.yml +0 -0
  6. {patchworks-0.7.0 → patchworks-0.8.1}/.github/workflows/lint.yml +0 -0
  7. {patchworks-0.7.0 → patchworks-0.8.1}/.github/workflows/release.yml +0 -0
  8. {patchworks-0.7.0 → patchworks-0.8.1}/.gitignore +0 -0
  9. {patchworks-0.7.0 → patchworks-0.8.1}/README.md +0 -0
  10. {patchworks-0.7.0 → patchworks-0.8.1}/cliff.toml +0 -0
  11. {patchworks-0.7.0 → patchworks-0.8.1}/docs/api/chunks.md +0 -0
  12. {patchworks-0.7.0 → patchworks-0.8.1}/docs/api/cluster.md +0 -0
  13. {patchworks-0.7.0 → patchworks-0.8.1}/docs/api/io.md +0 -0
  14. {patchworks-0.7.0 → patchworks-0.8.1}/docs/api/merge_tile_labels.md +0 -0
  15. {patchworks-0.7.0 → patchworks-0.8.1}/docs/api/plugins/cellpose.md +0 -0
  16. {patchworks-0.7.0 → patchworks-0.8.1}/docs/api/plugins/napari.md +0 -0
  17. {patchworks-0.7.0 → patchworks-0.8.1}/docs/api/plugins/ome_zarr.md +0 -0
  18. {patchworks-0.7.0 → patchworks-0.8.1}/docs/api/relabel.md +0 -0
  19. {patchworks-0.7.0 → patchworks-0.8.1}/docs/api/tile_process.md +0 -0
  20. {patchworks-0.7.0 → patchworks-0.8.1}/docs/examples/cellpose_2d.md +0 -0
  21. {patchworks-0.7.0 → patchworks-0.8.1}/docs/examples/cellpose_2d.py +0 -0
  22. {patchworks-0.7.0 → patchworks-0.8.1}/docs/examples/cellpose_3d.md +0 -0
  23. {patchworks-0.7.0 → patchworks-0.8.1}/docs/examples/cellpose_3d.py +0 -0
  24. {patchworks-0.7.0 → patchworks-0.8.1}/docs/examples/custom.md +0 -0
  25. {patchworks-0.7.0 → patchworks-0.8.1}/docs/examples/custom_method.py +0 -0
  26. {patchworks-0.7.0 → patchworks-0.8.1}/docs/examples/standalone_merge.md +0 -0
  27. {patchworks-0.7.0 → patchworks-0.8.1}/docs/examples/stardist.md +0 -0
  28. {patchworks-0.7.0 → patchworks-0.8.1}/docs/examples/stardist_2d.py +0 -0
  29. {patchworks-0.7.0 → patchworks-0.8.1}/docs/getting_started.md +0 -0
  30. {patchworks-0.7.0 → patchworks-0.8.1}/docs/guide/gpu_distributed.md +0 -0
  31. {patchworks-0.7.0 → patchworks-0.8.1}/docs/guide/merging.md +0 -0
  32. {patchworks-0.7.0 → patchworks-0.8.1}/docs/guide/performance.md +0 -0
  33. {patchworks-0.7.0 → patchworks-0.8.1}/docs/guide/pitfalls.md +0 -0
  34. {patchworks-0.7.0 → patchworks-0.8.1}/docs/guide/skip_empty.md +0 -0
  35. {patchworks-0.7.0 → patchworks-0.8.1}/docs/guide/tiling.md +0 -0
  36. {patchworks-0.7.0 → patchworks-0.8.1}/docs/index.md +0 -0
  37. {patchworks-0.7.0 → patchworks-0.8.1}/mkdocs.yml +0 -0
  38. {patchworks-0.7.0 → patchworks-0.8.1}/pyproject.toml +0 -0
  39. {patchworks-0.7.0 → patchworks-0.8.1}/src/patchworks/__init__.py +0 -0
  40. {patchworks-0.7.0 → patchworks-0.8.1}/src/patchworks/_chunks.py +0 -0
  41. {patchworks-0.7.0 → patchworks-0.8.1}/src/patchworks/_cluster.py +0 -0
  42. {patchworks-0.7.0 → patchworks-0.8.1}/src/patchworks/_core.py +0 -0
  43. {patchworks-0.7.0 → patchworks-0.8.1}/src/patchworks/_io.py +0 -0
  44. {patchworks-0.7.0 → patchworks-0.8.1}/src/patchworks/_merge.py +0 -0
  45. {patchworks-0.7.0 → patchworks-0.8.1}/src/patchworks/_relabel.py +0 -0
  46. {patchworks-0.7.0 → patchworks-0.8.1}/src/patchworks/plugins/__init__.py +0 -0
  47. {patchworks-0.7.0 → patchworks-0.8.1}/src/patchworks/plugins/cellpose.py +0 -0
  48. {patchworks-0.7.0 → patchworks-0.8.1}/src/patchworks/plugins/napari.py +0 -0
  49. {patchworks-0.7.0 → patchworks-0.8.1}/tests/test_core.py +0 -0
  50. {patchworks-0.7.0 → patchworks-0.8.1}/tests/test_napari.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: patchworks
3
- Version: 0.7.0
3
+ Version: 0.8.1
4
4
  Summary: Tiled processing of arbitrarily large images with globally consistent labels
5
5
  Project-URL: Homepage, https://github.com/imcf/patchworks
6
6
  Project-URL: Issues, https://github.com/imcf/patchworks/issues
@@ -50,12 +50,16 @@ to_ome_zarr("scan.czi", "scan.zarr", n_levels=5) # via bioio
50
50
  to_ome_zarr("scan.ims", "scan.zarr") # Imaris, native HDF5
51
51
  ```
52
52
 
53
- !!! note "Imaris pyramids are rebuilt, not reused"
54
- `.ims` files carry their own resolution pyramid, but `to_ome_zarr` reads
55
- only the **full-resolution** level and **builds a fresh NGFF pyramid** from
56
- it. This guarantees a consistent pyramid (XY-only, nearest-neighbour,
57
- calibrated) rather than inheriting Imaris's own downsampling scheme. It
58
- costs some extra compute, but the build is lazy and OOM-safe.
53
+ !!! note "Imaris pyramids: rebuild (default) or reuse"
54
+ `.ims` files carry their own resolution pyramid. By default `to_ome_zarr`
55
+ reads only the **full-resolution** level and **builds a fresh NGFF pyramid**
56
+ (XY-only, nearest-neighbour, calibrated) for consistency. Pass
57
+ `reuse_pyramid=True` to instead **copy the Imaris levels** as-is faster,
58
+ no recompute, keeping each level's native scale:
59
+
60
+ ```python
61
+ to_ome_zarr("scan.ims", "scan.zarr", reuse_pyramid=True)
62
+ ```
59
63
 
60
64
  ### Pixel calibration
61
65
 
@@ -255,8 +255,8 @@ def _open_bioio(path: str, scene: int) -> tuple[da.Array, str, PixelSize]:
255
255
  return arr, axes, pixel_size
256
256
 
257
257
 
258
- def _open_imaris(path: str) -> tuple[da.Array, str, PixelSize]:
259
- """Open an Imaris ``.ims`` file lazily → ``(array, axes, pixel_size)``."""
258
+ def _open_imaris(path: str, level: int = 0) -> tuple[da.Array, str, PixelSize]:
259
+ """Open an Imaris ``.ims`` *level* lazily → ``(array, axes, pixel_size)``."""
260
260
  try:
261
261
  from imaris_ims_file_reader.ims import ims
262
262
  except ImportError as exc:
@@ -265,11 +265,38 @@ def _open_imaris(path: str) -> tuple[da.Array, str, PixelSize]:
265
265
  "Install it with:\n pip install 'patchworks[imaris]'"
266
266
  ) from exc
267
267
 
268
- # Full-resolution level; the object is array-like and h5py-backed (lazy).
269
- reader = ims(path, ResolutionLevelLock=0)
270
- order = _DEFAULT_ORDER[len(_DEFAULT_ORDER) - reader.ndim :]
271
- arr = da.from_array(reader, chunks=_default_chunks(reader.shape, order))
272
-
268
+ # Read straight from the underlying HDF5 datasets. The reader's own
269
+ # __getitem__ squeezes singletons and pads to chunk boundaries, which both
270
+ # break da.from_array; the raw h5py datasets slice exactly and keep their
271
+ # native chunking. Imaris stores one 3-D (Z, Y, X) Data array per
272
+ # (timepoint, channel), padded to a chunk multiple — crop to the true
273
+ # extent the reader reports.
274
+ import h5py
275
+
276
+ reader = ims(path, ResolutionLevelLock=level)
277
+ n_t = int(getattr(reader, "TimePoints", 1) or 1)
278
+ n_c = int(getattr(reader, "Channels", 1) or 1)
279
+ z, y, x = (int(s) for s in reader.shape[-3:])
280
+
281
+ # Open our own h5py handle (the reader closes its own on GC). The Dataset
282
+ # objects keep this File alive for the lifetime of the dask graph, and
283
+ # da.from_array's default read lock makes the (thread-unsafe) h5py reads
284
+ # safe under the threaded scheduler.
285
+ hf = h5py.File(path, "r")
286
+ t_stack = []
287
+ for t in range(n_t):
288
+ c_stack = []
289
+ for c in range(n_c):
290
+ ds = hf[
291
+ f"DataSet/ResolutionLevel {level}/TimePoint {t}/"
292
+ f"Channel {c}/Data"
293
+ ]
294
+ c_stack.append(da.from_array(ds, chunks=ds.chunks)[:z, :y, :x])
295
+ t_stack.append(da.stack(c_stack, axis=0)) # (c, z, y, x)
296
+ arr = da.stack(t_stack, axis=0) # (t, c, z, y, x)
297
+
298
+ # Drop singleton non-spatial axes for a tidy result.
299
+ order = "tczyx"
273
300
  keep = [
274
301
  i
275
302
  for i, name in enumerate(order)
@@ -293,6 +320,46 @@ def _open_imaris(path: str) -> tuple[da.Array, str, PixelSize]:
293
320
  return arr, axes, pixel_size
294
321
 
295
322
 
323
+ def _write_imaris_pyramid(
324
+ path: str,
325
+ out: str,
326
+ *,
327
+ chunks: Union[tuple[int, ...], None],
328
+ overwrite: bool,
329
+ ) -> str:
330
+ """Copy an Imaris file's own resolution levels into an OME-ZARR.
331
+
332
+ Each Imaris ``ResolutionLevel`` is written as a pyramid level with its own
333
+ physical scale, so no downsampling is recomputed. Lazy (h5py-backed) reads
334
+ stream straight to disk.
335
+ """
336
+ from imaris_ims_file_reader.ims import ims
337
+
338
+ base = ims(path, ResolutionLevelLock=0)
339
+ n_levels = int(getattr(base, "ResolutionLevels", 1) or 1)
340
+
341
+ zarr.open_group(out, mode="w" if overwrite else "w-")
342
+ datasets: list[dict] = []
343
+ axes = ""
344
+ calibrated = False
345
+ for level in range(n_levels):
346
+ arr, axes, ps = _open_imaris(path, level=level)
347
+ scale = _base_scale(axes, ps)
348
+ calibrated = calibrated or bool(ps)
349
+ da.to_zarr(
350
+ arr.rechunk(chunks or _default_chunks(arr.shape, axes)),
351
+ out,
352
+ component=str(level),
353
+ overwrite=True,
354
+ )
355
+ datasets.append(_dataset(str(level), scale))
356
+ logger.info("imaris level %d copied: shape=%s", level, arr.shape)
357
+ _write_multiscales(
358
+ out, axes, datasets, Path(out).stem, calibrated=calibrated
359
+ )
360
+ return out
361
+
362
+
296
363
  def _to_dask(
297
364
  source: Union[da.Array, np.ndarray, str, Path],
298
365
  axes: Union[str, None],
@@ -327,6 +394,7 @@ def to_ome_zarr(
327
394
  n_levels: int = 5,
328
395
  downscale: int = 2,
329
396
  chunks: Union[tuple[int, ...], None] = None,
397
+ reuse_pyramid: bool = False,
330
398
  overwrite: bool = False,
331
399
  ) -> str:
332
400
  """Write *source* as a pyramidal, calibrated OME-ZARR store.
@@ -359,6 +427,12 @@ def to_ome_zarr(
359
427
  Per-level X/Y downsampling factor (default 2).
360
428
  chunks : tuple of int, optional
361
429
  Chunk shape for the written levels. ``None`` → a bounded default.
430
+ reuse_pyramid : bool, optional
431
+ *Imaris ``.ims`` only.* Copy the file's **own** resolution levels
432
+ instead of rebuilding the pyramid (faster, no recompute), keeping each
433
+ level's native scale. Ignored for other inputs; falls back to a
434
+ rebuild if the Imaris levels can't be read. Default ``False`` (rebuild,
435
+ for a consistent XY-only, nearest-neighbour NGFF pyramid).
362
436
  overwrite : bool, optional
363
437
  Overwrite an existing store at *out_path*.
364
438
 
@@ -378,6 +452,22 @@ def to_ome_zarr(
378
452
  if n_levels < 1:
379
453
  raise ValueError("n_levels must be >= 1")
380
454
 
455
+ # Reuse an Imaris file's own resolution pyramid instead of rebuilding it.
456
+ if (
457
+ reuse_pyramid
458
+ and isinstance(source, (str, Path))
459
+ and str(source).lower().endswith(".ims")
460
+ ):
461
+ try:
462
+ return _write_imaris_pyramid(
463
+ str(source), str(out_path), chunks=chunks, overwrite=overwrite
464
+ )
465
+ except Exception as exc:
466
+ logger.warning(
467
+ "reuse_pyramid failed (%s); rebuilding the pyramid instead.",
468
+ exc,
469
+ )
470
+
381
471
  arr, axes, detected = _to_dask(source, axes, scene)
382
472
  if len(axes) != arr.ndim:
383
473
  raise ValueError(
@@ -128,3 +128,14 @@ def test_write_labels_into_store(tmp_path):
128
128
  assert load_ome_zarr(group, channel=None, level=1).shape == (8, 4, 4)
129
129
  lg = zarr.open_group(group, mode="r")
130
130
  assert lg.attrs["image-label"]["version"]
131
+
132
+
133
+ def test_reuse_pyramid_ignored_for_arrays(tmp_path):
134
+ """reuse_pyramid only affects .ims inputs; arrays still rebuild."""
135
+ out = to_ome_zarr(
136
+ np.zeros((8, 8, 8), "uint16"),
137
+ tmp_path / "arr.zarr",
138
+ n_levels=2,
139
+ reuse_pyramid=True,
140
+ )
141
+ assert load_ome_zarr(out, channel=None, level=1).shape == (8, 4, 4)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes