patchworks 0.9.1__tar.gz → 0.10.0__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.
- {patchworks-0.9.1 → patchworks-0.10.0}/PKG-INFO +1 -1
- {patchworks-0.9.1 → patchworks-0.10.0}/src/patchworks/_core.py +148 -4
- {patchworks-0.9.1 → patchworks-0.10.0}/.github/workflows/docs.yml +0 -0
- {patchworks-0.9.1 → patchworks-0.10.0}/.github/workflows/lint.yml +0 -0
- {patchworks-0.9.1 → patchworks-0.10.0}/.github/workflows/release.yml +0 -0
- {patchworks-0.9.1 → patchworks-0.10.0}/.gitignore +0 -0
- {patchworks-0.9.1 → patchworks-0.10.0}/.markdownlint-cli2.yaml +0 -0
- {patchworks-0.9.1 → patchworks-0.10.0}/README.md +0 -0
- {patchworks-0.9.1 → patchworks-0.10.0}/cliff.toml +0 -0
- {patchworks-0.9.1 → patchworks-0.10.0}/docs/api/chunks.md +0 -0
- {patchworks-0.9.1 → patchworks-0.10.0}/docs/api/cluster.md +0 -0
- {patchworks-0.9.1 → patchworks-0.10.0}/docs/api/io.md +0 -0
- {patchworks-0.9.1 → patchworks-0.10.0}/docs/api/merge_tile_labels.md +0 -0
- {patchworks-0.9.1 → patchworks-0.10.0}/docs/api/plugins/cellpose.md +0 -0
- {patchworks-0.9.1 → patchworks-0.10.0}/docs/api/plugins/napari.md +0 -0
- {patchworks-0.9.1 → patchworks-0.10.0}/docs/api/plugins/ome_zarr.md +0 -0
- {patchworks-0.9.1 → patchworks-0.10.0}/docs/api/relabel.md +0 -0
- {patchworks-0.9.1 → patchworks-0.10.0}/docs/api/tile_process.md +0 -0
- {patchworks-0.9.1 → patchworks-0.10.0}/docs/examples/cellpose_2d.md +0 -0
- {patchworks-0.9.1 → patchworks-0.10.0}/docs/examples/cellpose_2d.py +0 -0
- {patchworks-0.9.1 → patchworks-0.10.0}/docs/examples/cellpose_3d.md +0 -0
- {patchworks-0.9.1 → patchworks-0.10.0}/docs/examples/cellpose_3d.py +0 -0
- {patchworks-0.9.1 → patchworks-0.10.0}/docs/examples/custom.md +0 -0
- {patchworks-0.9.1 → patchworks-0.10.0}/docs/examples/custom_method.py +0 -0
- {patchworks-0.9.1 → patchworks-0.10.0}/docs/examples/standalone_merge.md +0 -0
- {patchworks-0.9.1 → patchworks-0.10.0}/docs/examples/stardist.md +0 -0
- {patchworks-0.9.1 → patchworks-0.10.0}/docs/examples/stardist_2d.py +0 -0
- {patchworks-0.9.1 → patchworks-0.10.0}/docs/getting_started.md +0 -0
- {patchworks-0.9.1 → patchworks-0.10.0}/docs/guide/gpu_distributed.md +0 -0
- {patchworks-0.9.1 → patchworks-0.10.0}/docs/guide/merging.md +0 -0
- {patchworks-0.9.1 → patchworks-0.10.0}/docs/guide/ome_zarr_napari.md +0 -0
- {patchworks-0.9.1 → patchworks-0.10.0}/docs/guide/performance.md +0 -0
- {patchworks-0.9.1 → patchworks-0.10.0}/docs/guide/pitfalls.md +0 -0
- {patchworks-0.9.1 → patchworks-0.10.0}/docs/guide/skip_empty.md +0 -0
- {patchworks-0.9.1 → patchworks-0.10.0}/docs/guide/tiling.md +0 -0
- {patchworks-0.9.1 → patchworks-0.10.0}/docs/index.md +0 -0
- {patchworks-0.9.1 → patchworks-0.10.0}/mkdocs.yml +0 -0
- {patchworks-0.9.1 → patchworks-0.10.0}/pyproject.toml +0 -0
- {patchworks-0.9.1 → patchworks-0.10.0}/src/patchworks/__init__.py +0 -0
- {patchworks-0.9.1 → patchworks-0.10.0}/src/patchworks/_chunks.py +0 -0
- {patchworks-0.9.1 → patchworks-0.10.0}/src/patchworks/_cluster.py +0 -0
- {patchworks-0.9.1 → patchworks-0.10.0}/src/patchworks/_io.py +0 -0
- {patchworks-0.9.1 → patchworks-0.10.0}/src/patchworks/_merge.py +0 -0
- {patchworks-0.9.1 → patchworks-0.10.0}/src/patchworks/_relabel.py +0 -0
- {patchworks-0.9.1 → patchworks-0.10.0}/src/patchworks/plugins/__init__.py +0 -0
- {patchworks-0.9.1 → patchworks-0.10.0}/src/patchworks/plugins/cellpose.py +0 -0
- {patchworks-0.9.1 → patchworks-0.10.0}/src/patchworks/plugins/napari.py +0 -0
- {patchworks-0.9.1 → patchworks-0.10.0}/src/patchworks/plugins/ome_zarr.py +0 -0
- {patchworks-0.9.1 → patchworks-0.10.0}/tests/test_core.py +0 -0
- {patchworks-0.9.1 → patchworks-0.10.0}/tests/test_napari.py +0 -0
- {patchworks-0.9.1 → patchworks-0.10.0}/tests/test_ome_zarr.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: patchworks
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
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
|
|
@@ -20,6 +20,71 @@ from ._relabel import relabel_sequential_zarr
|
|
|
20
20
|
logger = logging.getLogger(__name__)
|
|
21
21
|
|
|
22
22
|
|
|
23
|
+
def _attach_log_file(path: str) -> None:
|
|
24
|
+
"""Tee the ``patchworks`` INFO log to *path* (replacing a prior auto file).
|
|
25
|
+
|
|
26
|
+
A previous auto-attached handler is removed first so repeated calls do not
|
|
27
|
+
stack handlers. The package logger level is raised to INFO if needed so the
|
|
28
|
+
messages are actually emitted.
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
path : str
|
|
33
|
+
Destination log-file path.
|
|
34
|
+
|
|
35
|
+
Returns
|
|
36
|
+
-------
|
|
37
|
+
None
|
|
38
|
+
"""
|
|
39
|
+
pkg = logging.getLogger("patchworks")
|
|
40
|
+
for h in list(pkg.handlers):
|
|
41
|
+
if getattr(h, "_patchworks_auto", False):
|
|
42
|
+
pkg.removeHandler(h)
|
|
43
|
+
h.close()
|
|
44
|
+
handler = logging.FileHandler(path)
|
|
45
|
+
handler._patchworks_auto = True # tag so we can find/replace it later
|
|
46
|
+
handler.setLevel(logging.INFO)
|
|
47
|
+
handler.setFormatter(
|
|
48
|
+
logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s")
|
|
49
|
+
)
|
|
50
|
+
pkg.addHandler(handler)
|
|
51
|
+
if pkg.level == logging.NOTSET or pkg.level > logging.INFO:
|
|
52
|
+
pkg.setLevel(logging.INFO)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _read_amplification(
|
|
56
|
+
native_chunks: tuple[int, ...], tile_shape: tuple[int, ...]
|
|
57
|
+
) -> float:
|
|
58
|
+
"""Estimate how much extra data each tile read pulls off disk.
|
|
59
|
+
|
|
60
|
+
When the store's on-disk chunks are larger than the tile, every tile read
|
|
61
|
+
decodes whole chunks and discards most of them. Returns the ratio of bytes
|
|
62
|
+
read to bytes used (1.0 = no waste).
|
|
63
|
+
|
|
64
|
+
Parameters
|
|
65
|
+
----------
|
|
66
|
+
native_chunks : tuple of int
|
|
67
|
+
The store's on-disk chunk shape.
|
|
68
|
+
tile_shape : tuple of int
|
|
69
|
+
The processing tile shape.
|
|
70
|
+
|
|
71
|
+
Returns
|
|
72
|
+
-------
|
|
73
|
+
float
|
|
74
|
+
Read-amplification factor (``bytes_read / bytes_used``).
|
|
75
|
+
"""
|
|
76
|
+
import math
|
|
77
|
+
|
|
78
|
+
read = used = 1.0
|
|
79
|
+
for n, t in zip(native_chunks, tile_shape):
|
|
80
|
+
if n <= 0 or t <= 0:
|
|
81
|
+
continue
|
|
82
|
+
touched = math.ceil(t / n) # chunks a tile spans on this axis
|
|
83
|
+
read *= touched * n
|
|
84
|
+
used *= t
|
|
85
|
+
return read / used if used else 1.0
|
|
86
|
+
|
|
87
|
+
|
|
23
88
|
def _stage_to_zarr(
|
|
24
89
|
arr: da.Array, path: str, component: str, show_progress: bool
|
|
25
90
|
) -> None:
|
|
@@ -83,6 +148,7 @@ def tile_process(
|
|
|
83
148
|
empty_threshold: float | None = None,
|
|
84
149
|
stage_dir: Union[str, Path, None] = None,
|
|
85
150
|
keep_stage: bool = False,
|
|
151
|
+
log_file: Union[str, Path, bool, None] = True,
|
|
86
152
|
verbose: bool = False,
|
|
87
153
|
) -> da.Array:
|
|
88
154
|
"""Apply *fn* to every tile of *image* and merge labels globally.
|
|
@@ -180,6 +246,11 @@ def tile_process(
|
|
|
180
246
|
keep_stage:
|
|
181
247
|
Keep the temp stage store after merging (default: delete it). Useful
|
|
182
248
|
for debugging or resuming an interrupted run.
|
|
249
|
+
log_file:
|
|
250
|
+
Where to tee the ``patchworks`` INFO log (including a per-tile
|
|
251
|
+
``processing tile k/N`` counter). ``True`` (default) auto-writes
|
|
252
|
+
``patchworks.log`` next to the output; a path writes there; ``False``/
|
|
253
|
+
``None`` disables the file (logs still go to stderr).
|
|
183
254
|
verbose:
|
|
184
255
|
Log each tile's location and shape as it is processed.
|
|
185
256
|
|
|
@@ -250,10 +321,27 @@ def tile_process(
|
|
|
250
321
|
|
|
251
322
|
# Load + tile
|
|
252
323
|
image_source_path = None if isinstance(image, da.Array) else str(image)
|
|
324
|
+
|
|
325
|
+
# Auto log file (default): tee patchworks' INFO logs to a file next to the
|
|
326
|
+
# output, so a long run leaves a tailable record without notebook setup.
|
|
327
|
+
if log_file:
|
|
328
|
+
if log_file is True:
|
|
329
|
+
if write_to is not None:
|
|
330
|
+
_ldir = os.path.dirname(os.path.abspath(str(write_to)))
|
|
331
|
+
elif image_source_path is not None:
|
|
332
|
+
_ldir = os.path.dirname(os.path.abspath(image_source_path))
|
|
333
|
+
else:
|
|
334
|
+
_ldir = os.getcwd()
|
|
335
|
+
log_file = os.path.join(_ldir, "patchworks.log")
|
|
336
|
+
_attach_log_file(str(log_file))
|
|
337
|
+
logger.info("patchworks log → %s", log_file)
|
|
338
|
+
|
|
253
339
|
_load_chunks: tuple[int, ...] | None = None
|
|
340
|
+
_native_chunks: tuple[int, ...] | None = None
|
|
254
341
|
|
|
255
342
|
if not isinstance(image, da.Array):
|
|
256
343
|
_peek = load_ome_zarr(image, channel=channel, level=level)
|
|
344
|
+
_native_chunks = _peek.chunksize # on-disk zarr chunk shape
|
|
257
345
|
if callable(tile_shape):
|
|
258
346
|
_load_chunks = tuple(tile_shape(_peek.shape, _peek.dtype))
|
|
259
347
|
elif isinstance(tile_shape, str):
|
|
@@ -291,13 +379,30 @@ def tile_process(
|
|
|
291
379
|
logger.info("Rechunked to %s", tile_shape)
|
|
292
380
|
|
|
293
381
|
n_tiles = int(np.prod([len(c) for c in image.chunks]))
|
|
382
|
+
_tile = tuple(c[0] for c in image.chunks)
|
|
294
383
|
logger.info(
|
|
295
384
|
"Processing %d tiles (per-axis %s, tile shape %s)",
|
|
296
385
|
n_tiles,
|
|
297
386
|
tuple(len(c) for c in image.chunks),
|
|
298
|
-
|
|
387
|
+
_tile,
|
|
299
388
|
)
|
|
300
389
|
|
|
390
|
+
# Warn when the store's on-disk chunks are much larger than the tile: every
|
|
391
|
+
# tile read then decodes whole chunks and throws most away (slow I/O).
|
|
392
|
+
if _native_chunks is not None:
|
|
393
|
+
_amp = _read_amplification(_native_chunks, _tile)
|
|
394
|
+
if _amp >= 4:
|
|
395
|
+
logger.warning(
|
|
396
|
+
"Input chunks %s are much larger than the tile %s → ~%.0fx "
|
|
397
|
+
"read amplification (each tile decodes whole chunks and "
|
|
398
|
+
"discards most). Re-chunk the store near the tile size "
|
|
399
|
+
"(e.g. to_ome_zarr(..., chunks=...) without shard=) or read "
|
|
400
|
+
"the source file directly to avoid wasted I/O.",
|
|
401
|
+
_native_chunks,
|
|
402
|
+
_tile,
|
|
403
|
+
_amp,
|
|
404
|
+
)
|
|
405
|
+
|
|
301
406
|
image_for_threshold = image
|
|
302
407
|
|
|
303
408
|
# Overlap — build a per-axis depth dict (clips to fit each axis).
|
|
@@ -313,6 +418,23 @@ def tile_process(
|
|
|
313
418
|
if skip_empty and _skip_thr is None:
|
|
314
419
|
_skip_thr = _auto_empty_threshold(image_for_threshold, channel, level)
|
|
315
420
|
|
|
421
|
+
# Up-front heads-up for a big 3-D GPU job (z-stack tiles, many of them):
|
|
422
|
+
# an accurate ETA is logged after the first few tiles, but warn early that
|
|
423
|
+
# this is the expensive path and point at the faster alternatives.
|
|
424
|
+
if use_gpu and image.ndim >= 3 and _tile[0] > 4 and n_tiles >= 50:
|
|
425
|
+
logger.warning(
|
|
426
|
+
"Large 3-D GPU job: %d tiles of %s. Per-tile 3-D segmentation is "
|
|
427
|
+
"slow on a single device (a live ETA is logged after the first "
|
|
428
|
+
"tiles). If per-slice results are acceptable, 2-D (z=1 tiles) is "
|
|
429
|
+
"typically ~10x faster, or segment a lower pyramid level.",
|
|
430
|
+
n_tiles,
|
|
431
|
+
_tile,
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
# Tile counters + timing (GIL-safe enough for the threaded / in-process
|
|
435
|
+
# schedulers patchworks uses) so the log shows live "tile k/N + ETA".
|
|
436
|
+
_progress = {"done": 0, "seen": 0, "time": 0.0}
|
|
437
|
+
|
|
316
438
|
def active_fn(block, block_info=None):
|
|
317
439
|
"""Run *fn* on one tile, or return zeros for an empty tile.
|
|
318
440
|
|
|
@@ -328,14 +450,36 @@ def tile_process(
|
|
|
328
450
|
np.ndarray
|
|
329
451
|
Integer labels, or an all-zero tile when skipped.
|
|
330
452
|
"""
|
|
453
|
+
import time
|
|
454
|
+
|
|
331
455
|
loc = block_info[0].get("chunk-location") if block_info else "?"
|
|
456
|
+
_progress["seen"] += 1
|
|
332
457
|
if skip_empty and block.size and block.max() <= _skip_thr:
|
|
333
458
|
if verbose:
|
|
334
459
|
logger.debug("skip empty tile %s (max<=%s)", loc, _skip_thr)
|
|
335
460
|
return np.zeros(block.shape, dtype=np.int32)
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
461
|
+
|
|
462
|
+
t0 = time.perf_counter()
|
|
463
|
+
out = fn(block)
|
|
464
|
+
dt = time.perf_counter() - t0
|
|
465
|
+
|
|
466
|
+
_progress["done"] += 1
|
|
467
|
+
_progress["time"] += dt
|
|
468
|
+
done, seen = _progress["done"], _progress["seen"]
|
|
469
|
+
avg = _progress["time"] / done
|
|
470
|
+
# Extrapolate remaining non-empty tiles from the empty fraction seen.
|
|
471
|
+
remaining_nonempty = max(0, n_tiles - seen) * (done / seen)
|
|
472
|
+
eta_h = avg * remaining_nonempty / 3600
|
|
473
|
+
logger.info(
|
|
474
|
+
"tile %d done in %.1fs (avg %.1fs); %d/%d tiles seen; ETA ~%.1fh",
|
|
475
|
+
done,
|
|
476
|
+
dt,
|
|
477
|
+
avg,
|
|
478
|
+
seen,
|
|
479
|
+
n_tiles,
|
|
480
|
+
eta_h,
|
|
481
|
+
)
|
|
482
|
+
return out
|
|
339
483
|
|
|
340
484
|
_meta = np.empty((0,) * image.ndim, dtype=np.int32)
|
|
341
485
|
if overlap > 0:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|