Add label merging function (and improve ndmeasure.label)#409
Conversation
|
Hey @m-albert I am currently very overwhelmed with stuff but I wanted to say this is awesome 🤩 and something I have wanted for about 15 years! |
|
Thanks @jni 🤗 Glad you like it and that you had the same idea! Regarding this PR I still need to get an (obscurely) failing test under control... |
…w/high bit encoding rather than additive scheme
…includes new functionality) and relabel_sequential. merge_labels_across_chunk_boundaries. Refactor ndmeasure.label to use the new functions. Rewrite relabel_blocks to handle sparse and large values relabeling efficiently. Add tests for new functionality.
66ede85 to
c5f57de
Compare
There was a problem hiding this comment.
Pull request overview
This PR expands dask_image.ndmeasure’s labeling capabilities to better support chunkwise/overlapped segmentations by (1) improving ndmeasure.label parallelism/label-id behavior and (2) adding APIs to relabel and merge labels across chunk boundaries.
Changes:
- Add
produce_sequential_labelsoption tondmeasure.labeland introducerelabel_sequential. - Add
merge_labels_across_chunk_boundariesto merge independently-labeled chunks (touching merge foroverlap_depth=0, IoU-based merge foroverlap_depth>0). - Add/extend tests covering the new behaviors and helpers.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 11 comments.
| File | Description |
|---|---|
dask_image/ndmeasure/__init__.py |
Adds/rewires labeling pipeline, introduces chunk-boundary merge + sequential relabeling utilities. |
dask_image/ndmeasure/_utils/_label.py |
Implements core helpers for uniqueness encoding, adjacency discovery, connected-components mapping, and relabeling. |
tests/test_dask_image/test_ndmeasure/test_core.py |
Updates label tests for sequential option; adds tests for uniqueness + chunk-boundary merging. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| matching = np.stack((labels0.ravel(), labels1.ravel()), axis=1) | ||
| unique_matching = dask_image.ndmeasure._label._unique_axis(matching) | ||
| bincount0 = np.bincount(unique_matching[:, 0]) | ||
| bincount1 = np.bincount(unique_matching[:, 1]) | ||
| assert np.all(bincount0 == 1) | ||
| assert np.all(bincount1 == 1) | ||
| assert len(np.unique(unique_matching[:, 0])) == \ | ||
| len(np.unique(unique_matching[:, 1])) |
| np.random.seed(42) | ||
| labels = da.random.randint(0, 1, size=(6, 6), chunks=(3, 3)) | ||
| unique_labels = dask_image.ndmeasure._label._make_labels_unique(labels) |
| dim_segmentation_merged = dask_image.ndmeasure\ | ||
| .merge_labels_across_chunk_boundaries( | ||
| dim_chunkwise_segmentation, | ||
| overlap_depth=overlap_depth, | ||
| iou_threshold=0, | ||
| )['labels'] |
| import dask.array as da | ||
| import numpy as np | ||
| import pandas as pd | ||
| import scipy.ndimage |
| def relabel_block(block, mapping): | ||
| # Convert block to pandas Series for mapping | ||
| s = pd.Series(block.flatten()) | ||
| # Vectorized lookup using reindex | ||
| relabeled_block = s.replace(mapping).to_numpy().reshape(block.shape) | ||
| return relabeled_block |
| face_slice_infos = _chunk_faces( | ||
| labels.chunks, labels.shape, structure, | ||
| wrap_axes=wrap_axes, overlap_depth=overlap_depth | ||
| ) | ||
| all_mappings = [da.empty((2, 0), dtype=LABEL_DTYPE, chunks=1)] | ||
|
|
| """ | ||
| Make labels unique across blocks by using | ||
| - lower bits to encode the block ID | ||
| - higher bits to encode the label ID within the block | ||
|
|
| block_id = np.ravel_multi_index( | ||
| block_id, | ||
| numblocks, | ||
| ) | ||
| block = block.astype(encoding_dtype) | ||
| block = _encode_label(block, block_id, encoding_dtype=encoding_dtype) | ||
|
|
| mapping = delayed( | ||
| lambda x: {k: v for k, v in zip(x, np.arange(len(x)))})(u) | ||
|
|
| relabeled = _label.relabel( | ||
| block_labeled_unique, mapping | ||
| ) | ||
|
|
||
| relabeled = relabeled.astype(_label.LABEL_DTYPE) | ||
|
|

PR
This PR contains work I've been wanting to discuss and push here for a while.
Highlights:
ndmeasure.label:ndmeasure.labeldoesn't output contiguous (sequential) labels (also before implementing the point above). This PR adds a parameterproduce_sequential_labelstodask_image.ndmeasure.labelto output contiguous labels.skimage.segmentation.relabel_sequential:ndmeasure.relabel_sequential(this provides the functionality e.g. of the point above, happy to discuss whether the function should be public)ndmeasure.merge_labels_across_chunk_boundaries(details below)merge_labels_across_chunk_boundaries
What's the idea behind
ndmeasure.merge_labels_across_chunk_boundaries?Visual abstract
Cases of overlap = 0 (top) and overlap > 0 (bottom)
Description
From the docstring:
Example
Consider the following problem.
The user wants to segment a large input image, i.e. a dask array.
and has a segmentation method that cannot be applied on the entire image (e.g. memory or method constraints) but works well on regions of the input image. The user can use
map_overlapto apply the segmentation on the chunks of the input image and include overlap so that the segmentation method performs well on the chunk boundaries:At this point, the user faces two challenges:
This is where
ndmeasure.merge_labels_across_chunk_boundariescomes in to merge labels across chunk boundaries.First use case / usage pattern
Similarly to the implementation of
ndmeasure.labeland e.g. the approach provided in distributed cellpose (ping @GFleishman), one approach for merging labels consists in merging labels that touch across chunk boundaries.ndmeasure.merge_labels_across_chunk_boundariesallows to do this in a way that's agnostic of the segmentation method:Second use case
The approach above joins all labels that form part of the same connected component in a boundary slice of thickness one. This can lead to artefacts. In fact, the example image above contains two merged cells which are clearly separated in the chunkwise segmentation. To separate these,
dask_image.ndmeasure.merge_labels_across_chunk_boundarieshas the option to merge labels based on the IoU within an overlap region.The labels of the two cells below which previously had been joined now remain separated.
Finally
In my personal projects it's been useful to have a function that merges labels across chunks. I think
dask-imagecould be a good place for providing such functionality to the community and I've had some offline feedback at meetings that this could be useful. Happy about any further feedback / discussion over here!