@@ -811,6 +811,13 @@ Notes:
811811
812812## Mutex Watershed
813813
814+ ` bioimage-cpp ` ships two mutex-watershed entry points, mirroring the two
815+ affogato APIs: one that consumes a dense affinity grid, and one that
816+ consumes an arbitrary graph with a separate list of mutex (long-range
817+ repulsive) edges.
818+
819+ ### Grid-based mutex watershed (affinity volumes)
820+
814821Affogato:
815822
816823``` python
@@ -853,6 +860,73 @@ Important migration notes:
853860 masked pixels are set to label ` 0 ` .
854861- Output labels are ` uint64 ` , consecutive, and 1-based for foreground pixels.
855862
863+ ### Mutex watershed on a generic graph
864+
865+ For mutex watershed on an arbitrary undirected graph (region adjacency graph
866+ or otherwise) with a separate list of long-range repulsive edges,
867+ ` bioimage-cpp ` provides ` bic.graph.mutex_watershed_clustering ` . This is a
868+ port of affogato's ` compute_mws_clustering ` using the same input format as
869+ ` LiftedMulticutObjective ` : a base graph carries the attractive edges, and
870+ long-range (called * mutex* here) edges are supplied alongside as a ` (M, 2) `
871+ node-pair array. The same ` (graph, edge_costs, lifted_uvs, lifted_costs) `
872+ tuple used to build a lifted multicut problem can be passed to the mutex
873+ watershed clustering without any reshaping.
874+
875+ Affogato:
876+
877+ ``` python
878+ from affogato.segmentation import compute_mws_clustering
879+
880+ labels = compute_mws_clustering(
881+ number_of_nodes,
882+ uvs.astype(np.uint64),
883+ mutex_uvs.astype(np.uint64),
884+ weights.astype(np.float32),
885+ mutex_weights.astype(np.float32),
886+ )
887+ ```
888+
889+ bioimage-cpp:
890+
891+ ``` python
892+ import bioimage_cpp as bic
893+
894+ graph = bic.graph.UndirectedGraph.from_edges(number_of_nodes, uvs)
895+ labels = bic.graph.mutex_watershed_clustering(
896+ graph,
897+ weights,
898+ mutex_uvs,
899+ mutex_weights,
900+ )
901+ ```
902+
903+ Notes:
904+
905+ - The attractive edges are the edges of the base graph; the count and the
906+ ordering of ` weights ` must match ` graph.number_of_edges ` . Mutex edges
907+ are supplied separately as ` (M, 2) ` ` uint64 ` pairs with matching
908+ ` mutex_weights ` .
909+ - Both ` weights ` and ` mutex_weights ` accept ` float32 ` and ` float64 ` . The
910+ wrapper dispatches to a templated C++ instantiation per dtype; other
911+ floating dtypes are cast to ` float32 ` . If the two arrays' dtypes do not
912+ match, both are promoted to ` float64 ` rather than silently downcast.
913+ - Higher weights are processed first (in descending order) — the same
914+ convention affogato uses.
915+ - The implementation reuses the union-find and per-root mutex-set helpers
916+ shared with the grid-based mutex watershed (` detail/mutex_storage.hxx ` ),
917+ so behavior is consistent between the two entry points.
918+ - Output labels are dense ` uint64 ` ids in ` 0 .. number_of_clusters - 1 ` ,
919+ assigned in first-occurrence order (matches the convention of the graph
920+ multicut solvers, * not* the 1-based foreground labels produced by the
921+ grid-based variant).
922+ - The function accepts both ` UndirectedGraph ` and ` RegionAdjacencyGraph ` .
923+ - Tie-breaking is deterministic: when weights are equal, attractive edges
924+ are processed before mutex edges, then by index. Affogato's reference
925+ uses a non-stable ` std::sort ` , so on inputs with many ties the two
926+ implementations may produce slightly different (but very similar)
927+ partitions. See ` development/graph/check_mutex_clustering.py ` for a
928+ comparison harness.
929+
856930## Dictionary-Based Relabeling
857931
858932If you used a small helper to apply a dictionary to an integer label array, use
0 commit comments