@@ -958,6 +958,142 @@ Notes:
958958 partitions. See ` development/graph/check_mutex_clustering.py ` for a
959959 comparison harness.
960960
961+ ### Semantic mutex watershed
962+
963+ ` bioimage-cpp ` mirrors affogato's two semantic-mutex-watershed entry points
964+ — ` compute_semantic_mws_segmentation ` for affinity volumes and
965+ ` compute_semantic_mws_clustering ` for an arbitrary graph. Both extend the
966+ regular mutex watershed with a third edge type: per-pixel (or per-node)
967+ "semantic edges" that tag each cluster with a class id. Two clusters that
968+ have been tagged with different class ids cannot subsequently merge.
969+
970+ #### Grid-based semantic mutex watershed (affinity volumes)
971+
972+ Affogato:
973+
974+ ``` python
975+ from affogato.segmentation import compute_semantic_mws_segmentation
976+
977+ labels, semantic_labels = compute_semantic_mws_segmentation(
978+ weights,
979+ offsets,
980+ number_of_attractive_channels = 3 ,
981+ strides = [1 , 1 , 1 ],
982+ )
983+ ```
984+
985+ bioimage-cpp:
986+
987+ ``` python
988+ import bioimage_cpp as bic
989+
990+ labels, semantic_labels = bic.segmentation.semantic_mutex_watershed(
991+ weights,
992+ offsets,
993+ number_of_attractive_channels = 3 ,
994+ strides = [1 , 1 , 1 ],
995+ )
996+ ```
997+
998+ Channel layout (identical to affogato):
999+
1000+ - Channels ` [0, number_of_attractive_channels) ` are attractive grid edges.
1001+ - Channels ` [number_of_attractive_channels, len(offsets)) ` are mutex grid
1002+ edges.
1003+ - Channels ` [len(offsets), affinities.shape[0]) ` are per-semantic-class
1004+ affinities; channel ` len(offsets) + c ` scores how strongly each pixel
1005+ belongs to class ` c ` .
1006+
1007+ Important migration notes:
1008+
1009+ - Inputs must represent 2D or 3D grids with shapes ` (channels, y, x) ` or
1010+ ` (channels, z, y, x) ` and ` channels > len(offsets) ` (use
1011+ ` bic.segmentation.mutex_watershed ` if there are no semantic channels).
1012+ - Supported affinity dtypes are ` float32 ` and ` float64 ` .
1013+ - Returned ` labels ` are ` uint64 ` , consecutive, and 1-based for foreground
1014+ pixels (matching the regular grid-based mutex watershed). ` semantic_labels `
1015+ is ` int64 ` with ` -1 ` reserved for clusters that received no class
1016+ assignment.
1017+ - A boolean ` mask ` may be passed. Edges touching ` False ` pixels are
1018+ ignored. Masked pixels are set to label ` 0 ` in ` labels ` and to the
1019+ ` mask_label ` parameter (default ` 0 ` ) in ` semantic_labels ` .
1020+ - ` strides ` and ` randomized_strides ` follow the same convention as the
1021+ regular grid mutex watershed (mutex channels only; attractive channels
1022+ are always kept).
1023+
1024+ #### Graph-based semantic mutex watershed
1025+
1026+ Affogato:
1027+
1028+ ``` python
1029+ from affogato.segmentation import compute_semantic_mws_clustering
1030+
1031+ labels, semantic_labels = compute_semantic_mws_clustering(
1032+ number_of_nodes,
1033+ uvs.astype(np.uint64),
1034+ mutex_uvs.astype(np.uint64),
1035+ semantic_node_classes.astype(np.uint64),
1036+ weights.astype(np.float32),
1037+ mutex_weights.astype(np.float32),
1038+ semantic_weights.astype(np.float32),
1039+ )
1040+ ```
1041+
1042+ bioimage-cpp:
1043+
1044+ ``` python
1045+ import bioimage_cpp as bic
1046+
1047+ graph = bic.graph.UndirectedGraph.from_edges(number_of_nodes, uvs)
1048+ labels, semantic_labels = bic.graph.semantic_mutex_watershed_clustering(
1049+ graph,
1050+ weights,
1051+ mutex_uvs,
1052+ mutex_weights,
1053+ semantic_node_classes,
1054+ semantic_weights,
1055+ )
1056+ ```
1057+
1058+ Input format mirrors ` mutex_watershed_clustering ` plus two extra arrays:
1059+
1060+ - ` semantic_node_classes ` is an ` (n_semantic, 2) ` ` uint64 ` table. Column 0
1061+ is a node id and column 1 is the semantic class id (a non-negative
1062+ integer interpreted as ` int64 ` internally).
1063+ - ` semantic_weights ` is a 1D ` float32 ` /` float64 ` array of length
1064+ ` n_semantic ` giving one weight per ` (node, class) ` candidate.
1065+
1066+ Notes:
1067+
1068+ - All three weight arrays (` weights ` , ` mutex_weights ` , ` semantic_weights ` )
1069+ must have the same floating dtype, or all three are promoted to
1070+ ` float64 ` .
1071+ - Output ` labels ` are dense ` uint64 ` ids in ` 0 .. number_of_clusters - 1 `
1072+ (first-occurrence order, matching the regular graph mutex watershed —
1073+ * not* the 1-based foreground labels produced by the grid variant).
1074+ ` semantic_labels ` is ` int64 ` with ` -1 ` for unassigned clusters.
1075+ - Accepts both ` UndirectedGraph ` and ` RegionAdjacencyGraph ` .
1076+
1077+ #### Divergence from affogato
1078+
1079+ The bioimage-cpp port fixes a missing ` merge_semantic_labels ` call on
1080+ attractive merges in affogato's graph kernel (` compute_semantic_mws_clustering ` ):
1081+ without that call, a node that has been tagged with a class can have its
1082+ tag dropped when it later becomes the non-root of a merge. Affogato's
1083+ array kernel additionally invokes ` boost::disjoint_sets::link(u, v) ` on
1084+ raw node ids rather than their roots, which corrupts the union-find tree
1085+ on multi-class inputs and over-fragments the result. The unit-test
1086+ problems shipped with affogato do not exercise these paths heavily, so
1087+ this only shows up on realistic multi-class data.
1088+
1089+ For most inputs the two implementations agree. On dense multi-class
1090+ inputs they may not; the development scripts under
1091+ ` development/segmentation/check_semantic_mutex_watershed_{2d,3d}.py ` and
1092+ ` development/graph/check_semantic_mutex_clustering.py ` print VI/ARI
1093+ partition metrics and a semantic-label match fraction so the deviation is
1094+ measurable. The bioimage-cpp partitions match an independent Python
1095+ reference implementation of the algorithm.
1096+
9611097## Dictionary-Based Relabeling
9621098
9631099If you used a small helper to apply a dictionary to an integer label array, use
0 commit comments