|
34 | 34 | import zarr |
35 | 35 |
|
36 | 36 | from ._chunks import cpu_allocation |
| 37 | +from ._io import zarr_compressor_kwargs |
37 | 38 |
|
38 | 39 | try: |
39 | 40 | from tqdm.auto import tqdm as _tqdm |
|
45 | 46 | _ZARR_V3 = int(zarr.__version__.split(".")[0]) >= 3 |
46 | 47 | _LUT_WARN_THRESHOLD = 100_000_000 # warn when max_label > 100 M (LUT > 800 MB) |
47 | 48 |
|
| 49 | +# Attributes recording how far an in-place merge got. Needed because an |
| 50 | +# in-place merge destroys its own input: a second pass over already-global ids |
| 51 | +# would add the per-tile offsets again and can collide unrelated objects. |
| 52 | +_MERGE_STATE = "patchworks_merge_state" |
| 53 | +_MERGE_COUNT = "patchworks_n_objects" |
| 54 | + |
48 | 55 | # Per-worker globals set by _init_worker. |
49 | 56 | # LUT is memory-mapped from disk so it is shared read-only across all workers |
50 | 57 | # (OS page cache, no per-process copy). Passing the LUT directly via pickle |
@@ -335,10 +342,13 @@ def _create_zarr_label_array( |
335 | 342 | """ |
336 | 343 | if name in group: |
337 | 344 | del group[name] |
| 345 | + kwargs = zarr_compressor_kwargs() |
338 | 346 | if _ZARR_V3: |
339 | | - return group.create_array(name, shape=shape, chunks=chunks, dtype=dtype) |
| 347 | + return group.create_array( |
| 348 | + name, shape=shape, chunks=chunks, dtype=dtype, **kwargs |
| 349 | + ) |
340 | 350 | return group.zeros( |
341 | | - name, shape=shape, chunks=chunks, dtype=dtype, overwrite=True |
| 351 | + name, shape=shape, chunks=chunks, dtype=dtype, overwrite=True, **kwargs |
342 | 352 | ) |
343 | 353 |
|
344 | 354 |
|
@@ -635,6 +645,29 @@ def zarr_native_merge( |
635 | 645 | "output_chunks cannot differ from the source chunking when " |
636 | 646 | "merging in place (the array is rewritten, not recreated)" |
637 | 647 | ) |
| 648 | + # Running this twice over the same array would add the per-tile |
| 649 | + # offsets to ids that are already global, which can land two unrelated |
| 650 | + # objects on the same id. The source is destroyed as we go, so the |
| 651 | + # state has to be recorded rather than inferred. |
| 652 | + state = arr.attrs.get(_MERGE_STATE) |
| 653 | + if state == "running": |
| 654 | + raise RuntimeError( |
| 655 | + f"{out_path}/{out_component} was left half-merged by an " |
| 656 | + "earlier attempt, so its labels are part local and part " |
| 657 | + "global and cannot be merged again. Re-run the segmentation " |
| 658 | + "for this label (delete the label group and its tiles.json) " |
| 659 | + "to rebuild it from scratch." |
| 660 | + ) |
| 661 | + if state == "done": |
| 662 | + n = arr.attrs.get(_MERGE_COUNT) |
| 663 | + logger.info( |
| 664 | + "zarr_native_merge: %s/%s is already merged (%s objects); " |
| 665 | + "nothing to do", |
| 666 | + out_path, |
| 667 | + out_component, |
| 668 | + n, |
| 669 | + ) |
| 670 | + return n |
638 | 671 | logger.info("zarr_native_merge: relabeling in place, no second store") |
639 | 672 |
|
640 | 673 | out_root = zarr.open_group(out_path, mode="a") |
@@ -699,6 +732,12 @@ def zarr_native_merge( |
699 | 732 | np.save(lut_path, lut) |
700 | 733 | del lut # parent no longer needs it; workers load via mmap |
701 | 734 |
|
| 735 | + if in_place: |
| 736 | + # From here the source is being overwritten. Record that, so a crash |
| 737 | + # leaves evidence the array is half-converted rather than looking like |
| 738 | + # untouched input to the next attempt. |
| 739 | + arr.attrs[_MERGE_STATE] = "running" |
| 740 | + |
702 | 741 | try: |
703 | 742 | if n_w <= 1: |
704 | 743 | _init_worker( |
@@ -731,6 +770,13 @@ def zarr_native_merge( |
731 | 770 |
|
732 | 771 | shutil.rmtree(_lut_dir, ignore_errors=True) |
733 | 772 |
|
| 773 | + if in_place: |
| 774 | + # Every chunk is now global. Recording the count as well makes a retry |
| 775 | + # after a *later* failure (the pyramid, say) a cheap no-op instead of |
| 776 | + # forcing a full re-segmentation. |
| 777 | + arr.attrs[_MERGE_COUNT] = n_objects |
| 778 | + arr.attrs[_MERGE_STATE] = "done" |
| 779 | + |
734 | 780 | return n_objects |
735 | 781 |
|
736 | 782 |
|
|
0 commit comments