@@ -107,6 +107,13 @@ Important differences:
107107- Bulk methods accept array-like inputs and return NumPy arrays.
108108- Python-style names are preferred. A few nifty-style aliases are still present
109109 on ` UndirectedGraph ` for convenience, but new code should use snake_case.
110+ - ` graph.clone() ` returns an independent deep copy. The C++ class is
111+ move-only (it owns a CSR adjacency buffer), so prefer this over
112+ reassignment-by-value.
113+ - ` graph.freeze() ` eagerly builds the internal adjacency. Call it after a
114+ batch of ` insert_edge ` calls if you intend to hand the graph to multiple
115+ reader threads, or if you want to ensure subsequent ` node_adjacency `
116+ reads carry no first-call rebuild cost.
110117
111118Common method/property mapping:
112119
@@ -126,6 +133,49 @@ Common method/property mapping:
126133| ` extractSubgraphFromNodes ` | ` extract_subgraph_from_nodes ` |
127134| ` edgesFromNodeList ` | ` edges_from_node_list ` |
128135
136+ ## Grid Graphs
137+
138+ Nifty-style regular grid graphs map to explicit 2D or 3D grid graph classes:
139+
140+ ``` python
141+ graph = bic.graph.GridGraph2D((height, width))
142+ graph = bic.graph.GridGraph3D((depth, height, width))
143+ graph = bic.graph.grid_graph((height, width))
144+ ```
145+
146+ Grid graph nodes use NumPy C-order ids. For a 2D shape ` (y, x) ` , node
147+ ` (row, col) ` has id ` row * x + col ` ; for 3D ` (z, y, x) ` , ids follow the same
148+ row-major convention. ` GridGraph2D ` and ` GridGraph3D ` inherit the regular
149+ ` UndirectedGraph ` API, so solvers, connected components, breadth-first search,
150+ and ` uv_ids() ` work unchanged.
151+
152+ Important differences:
153+
154+ - Only nearest-neighbor 2D and 3D grids are exposed for now.
155+ - Edge ids are deterministic axis blocks: axis 0 edges first, then axis 1, and
156+ axis 2 for 3D.
157+ - Scalar boundary maps can be converted to edge weights with
158+ ` grid_boundary_features(graph, boundary_map) ` .
159+ - Local affinity channels can be converted to edge-aligned weights with
160+ ` grid_affinity_features(graph, affinities, offsets) ` .
161+ - Mixed local and long-range affinity offsets can be converted with
162+ ` grid_affinity_features_with_lifted(...) ` , which returns local graph weights
163+ plus explicit long-range ` uv_ids ` and weights for lifted multicut or mutex
164+ watershed style workflows.
165+ - The three grid feature functions preserve ` float32 ` and ` float64 ` input
166+ dtype end-to-end (no internal copy to ` float64 ` ); other dtypes are
167+ promoted to ` float64 ` . Output weight arrays match the input dtype.
168+ - Grid graph construction does not materialize the per-node adjacency
169+ list. If you only need ` uv_ids() ` and edge features (the common case)
170+ you pay nothing for adjacency. The first call to ` node_adjacency ` ,
171+ ` connected_components ` , ` breadth_first_search ` , or
172+ ` extract_subgraph_from_nodes ` on a grid graph triggers a one-shot
173+ rebuild; call ` graph.freeze() ` on the construction thread before
174+ fan-out if you intend to use those from multiple threads.
175+ - Affogato-style masks and seed edges are not part of the public grid feature
176+ API yet; the implementation is structured so these filters/extra edges can
177+ be added later.
178+
129179## Region Adjacency Graphs
130180
131181Nifty:
@@ -677,8 +727,8 @@ shims default to sample A, size small and continue to honor the
677727` BIOIMAGE_CPP_EXTERNAL_MULTICUT_PATH ` and
678728` BIOIMAGE_CPP_EXTERNAL_MULTICUT_CACHE ` environment variables.
679729
680- Lifted multicut problems (2D ISBI slice and full 3D volume, built by
681- ` examples/segmentation/serialize_lifted_problem.py ` ):
730+ Lifted multicut problems (2D ISBI slice, RAG-based 3D volume, and grid-graph
731+ volume ):
682732
683733``` python
684734problem = bic.graph.load_lifted_multicut_problem(size = " 2d" )
@@ -692,6 +742,8 @@ objective = bic.graph.LiftedMulticutObjective(
692742)
693743```
694744
745+ Valid sizes are ` "2d" ` , ` "3d" ` , and ` "grid" ` .
746+
695747Notes:
696748
697749- Every download is integrity-checked against a SHA256 in the registry; a
@@ -790,6 +842,13 @@ Notes:
790842
791843## Mutex Watershed
792844
845+ ` bioimage-cpp ` ships two mutex-watershed entry points, mirroring the two
846+ affogato APIs: one that consumes a dense affinity grid, and one that
847+ consumes an arbitrary graph with a separate list of mutex (long-range
848+ repulsive) edges.
849+
850+ ### Grid-based mutex watershed (affinity volumes)
851+
793852Affogato:
794853
795854``` python
@@ -832,6 +891,73 @@ Important migration notes:
832891 masked pixels are set to label ` 0 ` .
833892- Output labels are ` uint64 ` , consecutive, and 1-based for foreground pixels.
834893
894+ ### Mutex watershed on a generic graph
895+
896+ For mutex watershed on an arbitrary undirected graph (region adjacency graph
897+ or otherwise) with a separate list of long-range repulsive edges,
898+ ` bioimage-cpp ` provides ` bic.graph.mutex_watershed_clustering ` . This is a
899+ port of affogato's ` compute_mws_clustering ` using the same input format as
900+ ` LiftedMulticutObjective ` : a base graph carries the attractive edges, and
901+ long-range (called * mutex* here) edges are supplied alongside as a ` (M, 2) `
902+ node-pair array. The same ` (graph, edge_costs, lifted_uvs, lifted_costs) `
903+ tuple used to build a lifted multicut problem can be passed to the mutex
904+ watershed clustering without any reshaping.
905+
906+ Affogato:
907+
908+ ``` python
909+ from affogato.segmentation import compute_mws_clustering
910+
911+ labels = compute_mws_clustering(
912+ number_of_nodes,
913+ uvs.astype(np.uint64),
914+ mutex_uvs.astype(np.uint64),
915+ weights.astype(np.float32),
916+ mutex_weights.astype(np.float32),
917+ )
918+ ```
919+
920+ bioimage-cpp:
921+
922+ ``` python
923+ import bioimage_cpp as bic
924+
925+ graph = bic.graph.UndirectedGraph.from_edges(number_of_nodes, uvs)
926+ labels = bic.graph.mutex_watershed_clustering(
927+ graph,
928+ weights,
929+ mutex_uvs,
930+ mutex_weights,
931+ )
932+ ```
933+
934+ Notes:
935+
936+ - The attractive edges are the edges of the base graph; the count and the
937+ ordering of ` weights ` must match ` graph.number_of_edges ` . Mutex edges
938+ are supplied separately as ` (M, 2) ` ` uint64 ` pairs with matching
939+ ` mutex_weights ` .
940+ - Both ` weights ` and ` mutex_weights ` accept ` float32 ` and ` float64 ` . The
941+ wrapper dispatches to a templated C++ instantiation per dtype; other
942+ floating dtypes are cast to ` float32 ` . If the two arrays' dtypes do not
943+ match, both are promoted to ` float64 ` rather than silently downcast.
944+ - Higher weights are processed first (in descending order) — the same
945+ convention affogato uses.
946+ - The implementation reuses the union-find and per-root mutex-set helpers
947+ shared with the grid-based mutex watershed (` detail/mutex_storage.hxx ` ),
948+ so behavior is consistent between the two entry points.
949+ - Output labels are dense ` uint64 ` ids in ` 0 .. number_of_clusters - 1 ` ,
950+ assigned in first-occurrence order (matches the convention of the graph
951+ multicut solvers, * not* the 1-based foreground labels produced by the
952+ grid-based variant).
953+ - The function accepts both ` UndirectedGraph ` and ` RegionAdjacencyGraph ` .
954+ - Tie-breaking is deterministic: when weights are equal, attractive edges
955+ are processed before mutex edges, then by index. Affogato's reference
956+ uses a non-stable ` std::sort ` , so on inputs with many ties the two
957+ implementations may produce slightly different (but very similar)
958+ partitions. See ` development/graph/check_mutex_clustering.py ` for a
959+ comparison harness.
960+
835961## Dictionary-Based Relabeling
836962
837963If you used a small helper to apply a dictionary to an integer label array, use
0 commit comments