patchworks 0.11.11__tar.gz → 0.11.13__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 (70) hide show
  1. {patchworks-0.11.11 → patchworks-0.11.13}/PKG-INFO +1 -1
  2. {patchworks-0.11.11 → patchworks-0.11.13}/src/patchworks/_distributed.py +4 -1
  3. {patchworks-0.11.11 → patchworks-0.11.13}/src/patchworks/_merge.py +66 -13
  4. {patchworks-0.11.11 → patchworks-0.11.13}/src/patchworks/plugins/napari.py +19 -4
  5. {patchworks-0.11.11 → patchworks-0.11.13}/tests/test_distributed.py +30 -0
  6. {patchworks-0.11.11 → patchworks-0.11.13}/.github/workflows/docs.yml +0 -0
  7. {patchworks-0.11.11 → patchworks-0.11.13}/.github/workflows/lint.yml +0 -0
  8. {patchworks-0.11.11 → patchworks-0.11.13}/.github/workflows/release.yml +0 -0
  9. {patchworks-0.11.11 → patchworks-0.11.13}/.gitignore +0 -0
  10. {patchworks-0.11.11 → patchworks-0.11.13}/.markdownlint-cli2.yaml +0 -0
  11. {patchworks-0.11.11 → patchworks-0.11.13}/README.md +0 -0
  12. {patchworks-0.11.11 → patchworks-0.11.13}/cliff.toml +0 -0
  13. {patchworks-0.11.11 → patchworks-0.11.13}/docs/api/chunks.md +0 -0
  14. {patchworks-0.11.11 → patchworks-0.11.13}/docs/api/cluster.md +0 -0
  15. {patchworks-0.11.11 → patchworks-0.11.13}/docs/api/io.md +0 -0
  16. {patchworks-0.11.11 → patchworks-0.11.13}/docs/api/merge_tile_labels.md +0 -0
  17. {patchworks-0.11.11 → patchworks-0.11.13}/docs/api/plugins/cellpose.md +0 -0
  18. {patchworks-0.11.11 → patchworks-0.11.13}/docs/api/plugins/napari.md +0 -0
  19. {patchworks-0.11.11 → patchworks-0.11.13}/docs/api/plugins/ome_zarr.md +0 -0
  20. {patchworks-0.11.11 → patchworks-0.11.13}/docs/api/relabel.md +0 -0
  21. {patchworks-0.11.11 → patchworks-0.11.13}/docs/api/tile_process.md +0 -0
  22. {patchworks-0.11.11 → patchworks-0.11.13}/docs/assets/logo.png +0 -0
  23. {patchworks-0.11.11 → patchworks-0.11.13}/docs/examples/cellpose_2d.md +0 -0
  24. {patchworks-0.11.11 → patchworks-0.11.13}/docs/examples/cellpose_2d.py +0 -0
  25. {patchworks-0.11.11 → patchworks-0.11.13}/docs/examples/cellpose_3d.md +0 -0
  26. {patchworks-0.11.11 → patchworks-0.11.13}/docs/examples/cellpose_3d.py +0 -0
  27. {patchworks-0.11.11 → patchworks-0.11.13}/docs/examples/custom.md +0 -0
  28. {patchworks-0.11.11 → patchworks-0.11.13}/docs/examples/custom_method.py +0 -0
  29. {patchworks-0.11.11 → patchworks-0.11.13}/docs/examples/standalone_merge.md +0 -0
  30. {patchworks-0.11.11 → patchworks-0.11.13}/docs/examples/stardist.md +0 -0
  31. {patchworks-0.11.11 → patchworks-0.11.13}/docs/examples/stardist_2d.py +0 -0
  32. {patchworks-0.11.11 → patchworks-0.11.13}/docs/getting_started.md +0 -0
  33. {patchworks-0.11.11 → patchworks-0.11.13}/docs/guide/gpu_distributed.md +0 -0
  34. {patchworks-0.11.11 → patchworks-0.11.13}/docs/guide/merging.md +0 -0
  35. {patchworks-0.11.11 → patchworks-0.11.13}/docs/guide/ome_zarr_napari.md +0 -0
  36. {patchworks-0.11.11 → patchworks-0.11.13}/docs/guide/performance.md +0 -0
  37. {patchworks-0.11.11 → patchworks-0.11.13}/docs/guide/pitfalls.md +0 -0
  38. {patchworks-0.11.11 → patchworks-0.11.13}/docs/guide/skip_empty.md +0 -0
  39. {patchworks-0.11.11 → patchworks-0.11.13}/docs/guide/snakemake.md +0 -0
  40. {patchworks-0.11.11 → patchworks-0.11.13}/docs/guide/tiling.md +0 -0
  41. {patchworks-0.11.11 → patchworks-0.11.13}/docs/index.md +0 -0
  42. {patchworks-0.11.11 → patchworks-0.11.13}/mkdocs.yml +0 -0
  43. {patchworks-0.11.11 → patchworks-0.11.13}/pyproject.toml +0 -0
  44. {patchworks-0.11.11 → patchworks-0.11.13}/src/patchworks/__init__.py +0 -0
  45. {patchworks-0.11.11 → patchworks-0.11.13}/src/patchworks/_chunks.py +0 -0
  46. {patchworks-0.11.11 → patchworks-0.11.13}/src/patchworks/_cluster.py +0 -0
  47. {patchworks-0.11.11 → patchworks-0.11.13}/src/patchworks/_core.py +0 -0
  48. {patchworks-0.11.11 → patchworks-0.11.13}/src/patchworks/_io.py +0 -0
  49. {patchworks-0.11.11 → patchworks-0.11.13}/src/patchworks/_relabel.py +0 -0
  50. {patchworks-0.11.11 → patchworks-0.11.13}/src/patchworks/plugins/__init__.py +0 -0
  51. {patchworks-0.11.11 → patchworks-0.11.13}/src/patchworks/plugins/cellpose.py +0 -0
  52. {patchworks-0.11.11 → patchworks-0.11.13}/src/patchworks/plugins/ome_zarr.py +0 -0
  53. {patchworks-0.11.11 → patchworks-0.11.13}/tests/test_core.py +0 -0
  54. {patchworks-0.11.11 → patchworks-0.11.13}/tests/test_napari.py +0 -0
  55. {patchworks-0.11.11 → patchworks-0.11.13}/tests/test_ome_zarr.py +0 -0
  56. {patchworks-0.11.11 → patchworks-0.11.13}/workflow/README.md +0 -0
  57. {patchworks-0.11.11 → patchworks-0.11.13}/workflow/Snakefile +0 -0
  58. {patchworks-0.11.11 → patchworks-0.11.13}/workflow/config/config.yaml +0 -0
  59. {patchworks-0.11.11 → patchworks-0.11.13}/workflow/pixi.toml +0 -0
  60. {patchworks-0.11.11 → patchworks-0.11.13}/workflow/profile/slurm/config.yaml +0 -0
  61. {patchworks-0.11.11 → patchworks-0.11.13}/workflow/rules/common.smk +0 -0
  62. {patchworks-0.11.11 → patchworks-0.11.13}/workflow/rules/convert.smk +0 -0
  63. {patchworks-0.11.11 → patchworks-0.11.13}/workflow/rules/merge.smk +0 -0
  64. {patchworks-0.11.11 → patchworks-0.11.13}/workflow/rules/segment.smk +0 -0
  65. {patchworks-0.11.11 → patchworks-0.11.13}/workflow/scripts/_pw.py +0 -0
  66. {patchworks-0.11.11 → patchworks-0.11.13}/workflow/scripts/convert.py +0 -0
  67. {patchworks-0.11.11 → patchworks-0.11.13}/workflow/scripts/fetch_model.py +0 -0
  68. {patchworks-0.11.11 → patchworks-0.11.13}/workflow/scripts/merge.py +0 -0
  69. {patchworks-0.11.11 → patchworks-0.11.13}/workflow/scripts/prepare_tiles.py +0 -0
  70. {patchworks-0.11.11 → patchworks-0.11.13}/workflow/scripts/segment_tile.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: patchworks
3
- Version: 0.11.11
3
+ Version: 0.11.13
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
@@ -68,7 +68,8 @@ def create_stage(
68
68
  component : str, optional
69
69
  Array name inside the store (default ``"staged"``).
70
70
  dtype : data-type, optional
71
- Label dtype (default ``int32``).
71
+ Label dtype (default ``int32``). Tiles write local labels; the merge's
72
+ first pass renumbers them to a compact global range that fits int32.
72
73
 
73
74
  Returns
74
75
  -------
@@ -135,6 +136,8 @@ def stage_tile(
135
136
  slice(left, out.shape[i] - right)
136
137
  for i, (left, right) in enumerate(trims)
137
138
  )
139
+ # Local labels (1..N) are fine here — they collide across tiles, but the
140
+ # merge's first pass makes them globally unique before stitching.
138
141
  dst = zarr.open_group(str(stage_path), mode="r+")[component]
139
142
  dst[sl] = out[sel].astype(dst.dtype)
140
143
  return index
@@ -103,7 +103,7 @@ def _relabel_chunk_worker(chunk_slice: tuple) -> None:
103
103
  block = np.asarray(src[chunk_slice], dtype=np.int64)
104
104
  max_b = int(block.max())
105
105
  if max_b == 0:
106
- dst[chunk_slice] = block.astype(np.int32)
106
+ dst[chunk_slice] = block.astype(dst.dtype)
107
107
  return
108
108
  lut = _merge_lut
109
109
  if max_b < len(lut):
@@ -111,7 +111,7 @@ def _relabel_chunk_worker(chunk_slice: tuple) -> None:
111
111
  else:
112
112
  ext = np.arange(len(lut), max_b + 1, dtype=np.int64)
113
113
  out = np.concatenate([lut, ext])[block]
114
- dst[chunk_slice] = out.astype(np.int32)
114
+ dst[chunk_slice] = out.astype(dst.dtype)
115
115
 
116
116
 
117
117
  def _boundary_face_specs(
@@ -238,9 +238,9 @@ def _build_relabel_lut(pairs: np.ndarray, max_label: int) -> np.ndarray:
238
238
 
239
239
 
240
240
  def _create_zarr_label_array(
241
- group: zarr.Group, name: str, shape: tuple, chunks: tuple
241
+ group: zarr.Group, name: str, shape: tuple, chunks: tuple, dtype=np.int32
242
242
  ) -> zarr.Array:
243
- """Create (replacing any existing) an int32 label array in *group*.
243
+ """Create (replacing any existing) a label array in *group*.
244
244
 
245
245
  Parameters
246
246
  ----------
@@ -252,6 +252,9 @@ def _create_zarr_label_array(
252
252
  Array shape.
253
253
  chunks : tuple
254
254
  Chunk shape.
255
+ dtype : data-type, optional
256
+ Label dtype (default ``int32``). Matched to the staged store so the
257
+ merge keeps wide (block-encoded) ids intact before they are compacted.
255
258
 
256
259
  Returns
257
260
  -------
@@ -261,14 +264,56 @@ def _create_zarr_label_array(
261
264
  if name in group:
262
265
  del group[name]
263
266
  if _ZARR_V3:
264
- return group.create_array(
265
- name, shape=shape, chunks=chunks, dtype=np.int32
266
- )
267
+ return group.create_array(name, shape=shape, chunks=chunks, dtype=dtype)
267
268
  return group.zeros(
268
- name, shape=shape, chunks=chunks, dtype=np.int32, overwrite=True
269
+ name, shape=shape, chunks=chunks, dtype=dtype, overwrite=True
269
270
  )
270
271
 
271
272
 
273
+ def _make_globally_unique(arr, shape: tuple, chunk_shape: tuple) -> int:
274
+ """Renumber per-chunk local labels to a globally unique, compact range.
275
+
276
+ Each tile writes local labels (``1..N``) that repeat across tiles, so the
277
+ same value means different objects in different tiles. This streams the
278
+ chunks in row-major order and remaps every chunk's non-zero labels to a
279
+ fresh contiguous block ``[base+1, …]``, leaving the store globally unique
280
+ with ``max_label == total objects`` (compact, so the relabel LUT stays
281
+ ``O(n_objects)``). Background (0) stays 0. In place, one chunk in RAM.
282
+
283
+ Parameters
284
+ ----------
285
+ arr : zarr.Array
286
+ Writable staged label array.
287
+ shape : tuple
288
+ Array shape.
289
+ chunk_shape : tuple
290
+ Chunk (= tile) shape.
291
+
292
+ Returns
293
+ -------
294
+ int
295
+ New maximum label (number of objects across all tiles, before the
296
+ cross-boundary merge fuses touching pairs).
297
+ """
298
+ n_per_dim = [(s + c - 1) // c for s, c in zip(shape, chunk_shape)]
299
+ base = 0
300
+ for idx in _iproduct(*[range(n) for n in n_per_dim]):
301
+ sl = tuple(
302
+ slice(i * c, min((i + 1) * c, s))
303
+ for i, c, s in zip(idx, chunk_shape, shape)
304
+ )
305
+ block = np.asarray(arr[sl])
306
+ uniq = np.unique(block)
307
+ uniq = uniq[uniq > 0]
308
+ if uniq.size == 0:
309
+ continue
310
+ lut = np.zeros(int(uniq[-1]) + 1, dtype=np.int64)
311
+ lut[uniq] = np.arange(1, uniq.size + 1) + base
312
+ arr[sl] = lut[block].astype(arr.dtype)
313
+ base += int(uniq.size)
314
+ return base
315
+
316
+
272
317
  def zarr_native_merge(
273
318
  staged_path: str,
274
319
  staged_component: str,
@@ -302,13 +347,17 @@ def zarr_native_merge(
302
347
  -------
303
348
  None
304
349
  """
305
- root = zarr.open_group(staged_path, mode="r")
350
+ root = zarr.open_group(staged_path, mode="r+")
306
351
  arr = root[staged_component]
307
352
  shape, chunk_shape = arr.shape, arr.chunks
308
353
 
309
- max_label = int(
310
- da.from_zarr(staged_path, component=staged_component).max().compute()
311
- )
354
+ # Pass 0 — make labels globally unique. Each tile writes local labels
355
+ # (1..N) that collide across tiles (every tile has a "1"); without this the
356
+ # boundary merge would fuse unrelated objects that merely share a value.
357
+ # Stream chunks in order, remapping each chunk's labels into a fresh
358
+ # contiguous range [base+1, …] — unique *and* compact (so the relabel LUT
359
+ # stays O(n_objects), not O(n_voxels)).
360
+ max_label = _make_globally_unique(arr, shape, chunk_shape)
312
361
  logger.info(
313
362
  "zarr_native_merge: shape=%s chunks=%s max_label=%d",
314
363
  shape,
@@ -330,7 +379,11 @@ def zarr_native_merge(
330
379
  )
331
380
 
332
381
  out_root = zarr.open_group(out_path, mode="a")
333
- _create_zarr_label_array(out_root, out_component, shape, chunk_shape)
382
+ # Match the staged dtype so block-encoded (wide) ids survive the merge;
383
+ # they are compacted to a small range afterwards by relabel_sequential_zarr.
384
+ _create_zarr_label_array(
385
+ out_root, out_component, shape, chunk_shape, dtype=arr.dtype
386
+ )
334
387
 
335
388
  n_per_dim = [(s + c - 1) // c for s, c in zip(shape, chunk_shape)]
336
389
  chunk_slices = [
@@ -260,11 +260,26 @@ def view_in_napari(
260
260
  if glasbey:
261
261
  try:
262
262
  import glasbey as _glasbey
263
-
264
- # bias lighter so colours read on napari's dark canvas
265
- label_kwargs["colormap"] = _glasbey.create_palette(
266
- 256, lightness_bounds=(40, 100)
263
+ import numpy as np
264
+ from napari.utils.colormaps import CyclicLabelColormap
265
+
266
+ # glasbey palette (biased lighter so colours read on the dark
267
+ # canvas), wrapped in a CyclicLabelColormap so each label value
268
+ # cycles through a distinct colour. Passing the raw palette list
269
+ # makes napari map large label IDs past the end -> one flat colour.
270
+ palette = _glasbey.create_palette(256, lightness_bounds=(40, 100))
271
+ colors = np.array(
272
+ [
273
+ [
274
+ int(h[1:3], 16) / 255,
275
+ int(h[3:5], 16) / 255,
276
+ int(h[5:7], 16) / 255,
277
+ 1.0,
278
+ ]
279
+ for h in palette
280
+ ]
267
281
  )
282
+ label_kwargs["colormap"] = CyclicLabelColormap(colors=colors)
268
283
  except ImportError:
269
284
  logger.warning(
270
285
  "glasbey not installed; using napari's default label colours "
@@ -43,3 +43,33 @@ def test_stage_then_merge_stitches_boundary(tmp_path):
43
43
  ).compute()
44
44
  ids = np.unique(merged[merged > 0])
45
45
  assert ids.size == 1, f"object split into {ids.size} labels"
46
+
47
+
48
+ def test_separate_objects_keep_distinct_labels(tmp_path):
49
+ """Different objects in different tiles must NOT collapse to one label.
50
+
51
+ Each tile produces local labels (1..N); without the merge's global-uniqueness
52
+ pass every tile's "1" would fuse into a single object.
53
+ """
54
+ img = np.zeros((16, 32), "uint16")
55
+ # four separate objects, one per (16x16) tile, none touching a boundary
56
+ img[3:6, 3:6] = 500
57
+ img[3:6, 19:22] = 500
58
+ img[10:13, 3:6] = 500
59
+ img[10:13, 19:22] = 500
60
+
61
+ stage = str(tmp_path / "stage.zarr")
62
+ tile = (16, 16)
63
+ create_stage(stage, img.shape, tile)
64
+ for i in range(len(spatial_tiles(img.shape, tile))):
65
+ stage_tile(img, _fn, stage, i, tile_shape=tile, overlap=4)
66
+
67
+ merged = merge_tile_labels(
68
+ stage,
69
+ write_to=str(tmp_path / "out.zarr"),
70
+ input_component="staged",
71
+ sequential_labels=True,
72
+ ).compute()
73
+ ids = np.unique(merged[merged > 0])
74
+ assert ids.size == 4, f"expected 4 distinct objects, got {ids.size}"
75
+ assert set(ids.tolist()) == {1, 2, 3, 4} # contiguous after relabel
File without changes
File without changes
File without changes
File without changes
File without changes