Enh: add IncrementalWatershedCut for interactive seeded watershed#299
Enh: add IncrementalWatershedCut for interactive seeded watershed#299lapertor wants to merge 5 commits into
Conversation
956d3c3 to
03054ba
Compare
| } | ||
|
|
||
| // Local relabeling: find merged component and relabel from remaining seeds | ||
| relabel_merged_component(v); |
| * (BFS from v respecting current cuts), reset labels to 0, then | ||
| * relabel from all remaining seeds in the component. | ||
| */ | ||
| void relabel_merged_component(index_t v) { |
There was a problem hiding this comment.
This is sub-optimal. When a seed is removed we also know the edge of the mst that is reactivated which can be used to determine the label of the "other side" (except if the other side is also removed in the same batch, in which case relabelling will start somewhere else).
Add `incremental_watershed_cut` C++ class and `IncrementalWatershedCut` Python class that maintain a cached BPT to update seeded watershed cuts incrementally, avoiding a full recomputation on each seed change. Based on the algorithm described in: Q. Lebon, J. Lefevre, J. Cousty, B. Perret. 'Interactive Segmentation With Incremental Watershed Cuts', CIARP 2023. https://hal.science/hal-04069187v1 Signed-off-by: lapertor <raphael.lapertot@gmail.com>
…ions, optimize BFS - Fix visitCount batch removal bug: walk-up loop now always decrements visitCount and breaks on 2->1 transition (matching Lebon's removeMarker), instead of stopping on visitCount==1 without decrementing. - Add hg_assert in unreachable else-branch of Pass 2a (both sides of de-cut with different seed labels should not happen). - Remove unused #include <unordered_set>. - Optimize component_seed_label: replace per-call std::vector allocation with m_visited generation-counter pattern (zero-cost reset). - Add trailing newline to watershed.rst. - Clean test_watershed.py diff (73 insertions, no whitespace changes). - Add 3 regression tests: batch remove equals sequential, both sides of edge, and interactive churn. - Add C++ test for batch remove sibling subtrees visitCount. Signed-off-by: lapertor <raphael.lapertot@gmail.com>
03054ba to
b1430a7
Compare
- Add a :Complexity: section to the IncrementalWatershedCut Python
docstring and to the Doxygen block of the C++ class. It breaks down
construction (O(n log n), dominated by the bpt_canonical edge sort),
add_seeds (O(K * d + S) with S <= num_vertices, by the visitCount==2
disjointness invariant on the K new MST-forest components),
remove_seeds (O(K * d + S') with S' <= num_vertices for batches whose
de-cuts touch mostly disjoint regions, and up to O(K * num_vertices)
in the worst case of cascading merges within the same batch), and
get_labeling (O(1), labeling maintained incrementally).
- Document the no-relabeling restriction in add_seeds: a vertex that
is already a seed must be removed first if its label must change.
- Rewrite the Pass 1 comment in remove_seeds: the de-cut collection
order is the batch insertion order, not a walk-up order. Correctness
of the resulting labeling does not depend on the relative BPT depth
of the de-cuts within the batch.
- Strengthen the rationale for the unreachable else branch in Pass 2a:
cite the 2->1 visitCount transition invariant and Lebon Algorithm 2;
the release-mode fallback now reads as an explicit safety net.
- Add three regression tests (Python and C++):
- decuts_ancestor_descendant: 1x8 path with controlled weights so
a batch remove produces de-cuts at different BPT depths (one
node is the descendant of the other). Compared to the full
labelisation_seeded_watershed for every result.
- add_seed_already_exists: assertion is raised on duplicate seed.
- add_seed_label_zero: assertion is raised on label == 0.
No behavioral change on the public API.
Signed-off-by: lapertor <raphael.lapertot@gmail.com>
Add regression tests for add_seeds and remove_seeds batch duplicate detection. Tests verify that invalid batches raise exceptions WITHOUT leaving the object in a partially mutated state. Python tests: - test_add_seeds_batch_duplicate_no_partial_mutation - test_add_seeds_cross_batch_duplicate_no_partial_mutation - test_remove_seeds_batch_duplicate_no_partial_mutation - test_remove_seeds_cross_batch_duplicate_no_partial_mutation C++ tests: - incremental watershed cut add seeds batch duplicate throws - incremental watershed cut add seeds cross batch duplicate throws - incremental watershed cut remove seeds batch duplicate throws - incremental watershed cut remove seeds non existent in batch throws Signed-off-by: Raphael Lapertot <raphael.lapertot@gmail.com>
Add Pass 0 pre-validation to IncrementalWatershedCut::add_seeds and ::remove_seeds to prevent partial batch mutation. Before this fix, invalid elements in a batch (duplicate vertices, out-of-range indices, non-existent seeds) would trigger an exception after some elements had already mutated internal state, leaving the object in an inconsistent state. Pass 0 validates the entire batch BEFORE any mutation: - add_seeds: checks vertex range, label != 0, intra-batch duplicates, and cross-batch duplicates (already a seed) - remove_seeds: checks vertex range, intra-batch duplicates, and seed existence Uses std::unordered_set<index_t> for O(1) intra-batch duplicate detection. All checks use hg_assert (consistent with existing code, no-op in Release builds). No changes to Pass 1/2/2a/2b loop bodies. No rollback/transaction semantics introduced. Fixes: PR299 review finding (Medium severity) Signed-off-by: Raphael Lapertot <raphael.lapertot@gmail.com>
|
Pushed a final revision covering all review points, plus a pre-validation fix discovered during testing. 1.
|
Closes #298
Motivation
labelisation_seeded_watershedrecomputes the full watershed from scratch on every seed change, which is expensive in interactive segmentation scenarios. This PR introducesIncrementalWatershedCut, a stateful class that maintains a cached Binary Partition Tree (BPT) and updates only the affected regions when seeds are added or removed.The implementation follows the algorithm described in:
Algorithm
The key data structures are:
bpt_canonicalvisitCountarray on BPT nodes: tracking how many seed paths pass through each internal nodeis_cutboolean array on MST edges: an MST edge is a watershed edge when its corresponding BPT node hasvisitCount >= 2When a seed is added, the algorithm walks up the BPT from the seed leaf, incrementing
visitCountat each node; when it reaches 2, the edge is marked as a cut. When a seed is removed, the walk decrementsvisitCount; when it drops to 1, the edge is unmarked. A subsequent BFS re-labels the graph. The BFS correctly handles both splits (add seed) and merges (remove seed), which is why a union-find structure is not used here.Changes
include/higra/algo/watershed.hpp: newincremental_watershed_cutclasshigra/algo/py_watershed.cpp: pybind11 binding forIncrementalWatershedCuthigra/algo/watershed.py: Python wrapper classIncrementalWatershedCuttest/cpp/algo/test_watershed.cpp: 5 new C++ teststest/python/test_algo/test_watershed.py: 10 new Python tests in classTestIncrementalWatershedUsage
Performance
Benchmarks on a 500x500 image (20 interactions, 5 seeds each):
labelisation_seeded_watershedIncrementalWatershedCut(update only)On 1000x1000: ~11x speedup per interaction. The speedup is stable across
interactions regardless of the number of accumulated seeds.
Consistency
test_consistency_with_seeded_watershedverifies thatIncrementalWatershedCutproduces the same labeling aslabelisation_seeded_watershedgiven identical seeds, ensuring backward compatibility.Benchmark script
Benchmark output (Windows, Intel Core i7-14700KF, Python 3.13.12)