Skip to content

Commit 809f171

Browse files
Refactor python package layout
1 parent 59802cf commit 809f171

48 files changed

Lines changed: 2465 additions & 2224 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

MIGRATION_GUIDE.md

Lines changed: 58 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,22 @@ Graph functionality is under `bic.graph`, segmentation functionality is under
1919
`bic.segmentation`, and utility functionality (blocking, relabeling, overlap
2020
measurement, union-find, etc.) is under `bic.utils`.
2121

22+
`bic.graph` keeps the core graph types and graph-level algorithms
23+
(`UndirectedGraph`, `GridGraph2D`, `GridGraph3D`, `RegionAdjacencyGraph`,
24+
`connected_components`, `breadth_first_search`, `edge_weighted_watershed`,
25+
`region_adjacency_graph`, `project_node_labels_to_pixels`) at the top level.
26+
Algorithmic domains live in dedicated submodules:
27+
28+
- `bic.graph.multicut` — multicut objective and solvers, fusion-move
29+
proposal generators, multicut problem loaders.
30+
- `bic.graph.lifted_multicut` — lifted multicut objective and solvers,
31+
lifted multicut problem loaders. Proposal generators are re-exported
32+
from `bic.graph.multicut` here for convenience.
33+
- `bic.graph.mutex_watershed` — graph-based mutex watershed clustering
34+
(with and without semantic constraints).
35+
- `bic.graph.features` — edge-feature accumulation for RAGs and grid
36+
graphs (boundary maps, affinity channels, lifted edge features).
37+
2238
## Affogato
2339

2440
### Affinities
@@ -142,7 +158,7 @@ Important migration notes:
142158

143159
For mutex watershed on an arbitrary undirected graph (region adjacency graph
144160
or otherwise) with a separate list of long-range repulsive edges,
145-
`bioimage-cpp` provides `bic.graph.mutex_watershed_clustering`. This is a
161+
`bioimage-cpp` provides `bic.graph.mutex_watershed.mutex_watershed_clustering`. This is a
146162
port of affogato's `compute_mws_clustering` using the same input format as
147163
`LiftedMulticutObjective`: a base graph carries the attractive edges, and
148164
long-range (called *mutex* here) edges are supplied alongside as a `(M, 2)`
@@ -170,7 +186,7 @@ bioimage-cpp:
170186
import bioimage_cpp as bic
171187

172188
graph = bic.graph.UndirectedGraph.from_edges(number_of_nodes, uvs)
173-
labels = bic.graph.mutex_watershed_clustering(
189+
labels = bic.graph.mutex_watershed.mutex_watershed_clustering(
174190
graph,
175191
weights,
176192
mutex_uvs,
@@ -292,7 +308,7 @@ bioimage-cpp:
292308
import bioimage_cpp as bic
293309

294310
graph = bic.graph.UndirectedGraph.from_edges(number_of_nodes, uvs)
295-
labels, semantic_labels = bic.graph.semantic_mutex_watershed_clustering(
311+
labels, semantic_labels = bic.graph.mutex_watershed.semantic_mutex_watershed_clustering(
296312
graph,
297313
weights,
298314
mutex_uvs,
@@ -806,11 +822,11 @@ Multicut problems (3 samples × 2 sizes, originally from
806822

807823
```python
808824
# Returns (UndirectedGraph, edge_costs)
809-
graph, costs = bic.graph.load_multicut_problem(sample="A", size="small")
825+
graph, costs = bic.graph.multicut.load_multicut_problem(sample="A", size="small")
810826
# Or just the underlying arrays
811-
uv_ids, costs = bic.graph.load_multicut_problem_data(sample="B", size="medium")
827+
uv_ids, costs = bic.graph.multicut.load_multicut_problem_data(sample="B", size="medium")
812828
# Or the cached file path
813-
path = bic.graph.multicut_problem_path(sample="C", size="medium")
829+
path = bic.graph.multicut.multicut_problem_path(sample="C", size="medium")
814830
```
815831

816832
Valid samples are `"A"`, `"B"`, `"C"`; valid sizes are `"small"` and
@@ -824,10 +840,10 @@ Lifted multicut problems (2D ISBI slice, RAG-based 3D volume, and grid-graph
824840
volume):
825841

826842
```python
827-
problem = bic.graph.load_lifted_multicut_problem(size="2d")
843+
problem = bic.graph.lifted_multicut.load_lifted_multicut_problem(size="2d")
828844
# Fields: n_nodes (int), local_uvs, local_costs, lifted_uvs, lifted_costs.
829845
graph = bic.graph.UndirectedGraph.from_edges(problem.n_nodes, problem.local_uvs)
830-
objective = bic.graph.LiftedMulticutObjective(
846+
objective = bic.graph.lifted_multicut.LiftedMulticutObjective(
831847
graph,
832848
problem.local_costs,
833849
lifted_uvs=problem.lifted_uvs,
@@ -914,14 +930,14 @@ bioimage-cpp:
914930
```python
915931
import bioimage_cpp as bic
916932

917-
objective = bic.graph.LiftedMulticutObjective(
933+
objective = bic.graph.lifted_multicut.LiftedMulticutObjective(
918934
graph,
919935
edge_costs,
920936
lifted_uvs=lifted_uvs,
921937
lifted_costs=lifted_costs,
922938
bfs_distance=3, # optional: also insert zero-weight lifted edges within k hops
923939
)
924-
labels = bic.graph.LiftedGreedyAdditiveMulticut().optimize(objective)
940+
labels = bic.graph.lifted_multicut.LiftedGreedyAdditiveMulticut().optimize(objective)
925941
energy = objective.energy(labels)
926942
```
927943

@@ -969,11 +985,11 @@ safety net). The differences are:
969985
pluggable via `sub_solver=`.
970986

971987
```python
972-
solver = bic.graph.FusionMoveLiftedMulticut(
973-
proposal_generator=bic.graph.WatershedProposalGenerator(
988+
solver = bic.graph.lifted_multicut.FusionMoveLiftedMulticut(
989+
proposal_generator=bic.graph.lifted_multicut.WatershedProposalGenerator(
974990
sigma=1.0, n_seeds_fraction=0.1, seed=0,
975991
),
976-
sub_solver=bic.graph.LiftedKernighanLinMulticut(number_of_outer_iterations=3),
992+
sub_solver=bic.graph.lifted_multicut.LiftedKernighanLinMulticut(number_of_outer_iterations=3),
977993
number_of_iterations=10,
978994
stop_if_no_improvement=4,
979995
number_of_threads=4,
@@ -984,9 +1000,9 @@ labels = solver.optimize(objective)
9841000
A typical warm-started solve combines greedy and KL:
9851001

9861002
```python
987-
solver = bic.graph.LiftedChainedSolvers([
988-
bic.graph.LiftedGreedyAdditiveMulticut(),
989-
bic.graph.LiftedKernighanLinMulticut(number_of_outer_iterations=10),
1003+
solver = bic.graph.lifted_multicut.LiftedChainedSolvers([
1004+
bic.graph.lifted_multicut.LiftedGreedyAdditiveMulticut(),
1005+
bic.graph.lifted_multicut.LiftedKernighanLinMulticut(number_of_outer_iterations=10),
9901006
])
9911007
labels = solver.optimize(objective)
9921008
```
@@ -1016,19 +1032,19 @@ two focused helpers that cover the same workflow:
10161032
```python
10171033
# Discover lifted edges implied by long-range affinity offsets. 1-hop offsets
10181034
# are skipped automatically, so the full offset list can be passed in.
1019-
lifted_uvs = bic.graph.lifted_edges_from_affinities(
1035+
lifted_uvs = bic.graph.features.lifted_edges_from_affinities(
10201036
rag, oversegmentation, offsets, number_of_threads=0,
10211037
)
10221038

10231039
# Accumulate (mean, size) statistics per lifted edge. Pixel pairs whose
10241040
# (u, v) does not appear in `lifted_uvs` are skipped, so local edges are
10251041
# never contaminated with long-range affinities.
1026-
lifted_features = bic.graph.lifted_affinity_features(
1042+
lifted_features = bic.graph.features.lifted_affinity_features(
10271043
oversegmentation, affinities, offsets, lifted_uvs,
10281044
number_of_threads=0,
10291045
)
10301046
# For the 12-column feature set (mean, median, std, min, max, percentiles, size):
1031-
lifted_features = bic.graph.lifted_affinity_features_complex(...)
1047+
lifted_features = bic.graph.features.lifted_affinity_features_complex(...)
10321048
```
10331049

10341050
The output column conventions match the local-edge variants
@@ -1038,16 +1054,16 @@ End-to-end pipeline (also in `examples/segmentation/lifted_multicut_from_affinit
10381054

10391055
```python
10401056
rag = bic.graph.region_adjacency_graph(oversegmentation)
1041-
local_costs = local_threshold - bic.graph.affinity_features(
1057+
local_costs = local_threshold - bic.graph.features.affinity_features(
10421058
rag, oversegmentation, direct_affinities, direct_offsets,
10431059
)[:, 0]
1044-
lifted_uvs = bic.graph.lifted_edges_from_affinities(
1060+
lifted_uvs = bic.graph.features.lifted_edges_from_affinities(
10451061
rag, oversegmentation, long_range_offsets,
10461062
)
1047-
lifted_costs = lifted_threshold - bic.graph.lifted_affinity_features(
1063+
lifted_costs = lifted_threshold - bic.graph.features.lifted_affinity_features(
10481064
oversegmentation, long_range_affinities, long_range_offsets, lifted_uvs,
10491065
)[:, 0]
1050-
objective = bic.graph.LiftedMulticutObjective(
1066+
objective = bic.graph.lifted_multicut.LiftedMulticutObjective(
10511067
rag, local_costs, lifted_uvs=lifted_uvs, lifted_costs=lifted_costs,
10521068
)
10531069
```
@@ -1074,8 +1090,8 @@ bioimage-cpp:
10741090
```python
10751091
import bioimage_cpp as bic
10761092

1077-
objective = bic.graph.MulticutObjective(graph, edge_costs)
1078-
labels = bic.graph.GreedyAdditiveMulticut().optimize(objective)
1093+
objective = bic.graph.multicut.MulticutObjective(graph, edge_costs)
1094+
labels = bic.graph.multicut.GreedyAdditiveMulticut().optimize(objective)
10791095
energy = objective.energy(labels)
10801096
```
10811097

@@ -1111,7 +1127,7 @@ Constructor argument mapping:
11111127
Kernighan-Lin example:
11121128

11131129
```python
1114-
solver = bic.graph.KernighanLinMulticut(number_of_outer_iterations=5)
1130+
solver = bic.graph.multicut.KernighanLinMulticut(number_of_outer_iterations=5)
11151131
labels = solver.optimize(objective)
11161132
```
11171133

@@ -1123,9 +1139,9 @@ warm-start, set `objective.set_labels(...)` to a non-trivial labeling first.
11231139
Chaining solvers:
11241140

11251141
```python
1126-
solver = bic.graph.ChainedMulticutSolvers([
1127-
bic.graph.GreedyAdditiveMulticut(),
1128-
bic.graph.KernighanLinMulticut(number_of_outer_iterations=5),
1142+
solver = bic.graph.multicut.ChainedMulticutSolvers([
1143+
bic.graph.multicut.GreedyAdditiveMulticut(),
1144+
bic.graph.multicut.KernighanLinMulticut(number_of_outer_iterations=5),
11291145
])
11301146
labels = solver.optimize(objective)
11311147
```
@@ -1134,9 +1150,9 @@ Decomposing a problem into positive-cost connected components and solving each
11341150
sub-problem with a cheaper solver:
11351151

11361152
```python
1137-
solver = bic.graph.MulticutDecomposer(
1138-
sub_solver=bic.graph.KernighanLinMulticut(number_of_outer_iterations=5),
1139-
fallthrough_solver=bic.graph.GreedyAdditiveMulticut(),
1153+
solver = bic.graph.multicut.MulticutDecomposer(
1154+
sub_solver=bic.graph.multicut.KernighanLinMulticut(number_of_outer_iterations=5),
1155+
fallthrough_solver=bic.graph.multicut.GreedyAdditiveMulticut(),
11401156
number_of_threads=0,
11411157
)
11421158
labels = solver.optimize(objective)
@@ -1190,12 +1206,12 @@ bioimage-cpp:
11901206
```python
11911207
import bioimage_cpp as bic
11921208

1193-
objective = bic.graph.MulticutObjective(graph, edge_costs)
1194-
solver = bic.graph.FusionMoveMulticut(
1195-
proposal_generator=bic.graph.WatershedProposalGenerator(
1209+
objective = bic.graph.multicut.MulticutObjective(graph, edge_costs)
1210+
solver = bic.graph.multicut.FusionMoveMulticut(
1211+
proposal_generator=bic.graph.multicut.WatershedProposalGenerator(
11961212
sigma=1.0, n_seeds_fraction=0.1, seed=0,
11971213
),
1198-
sub_solver=bic.graph.GreedyAdditiveMulticut(),
1214+
sub_solver=bic.graph.multicut.GreedyAdditiveMulticut(),
11991215
number_of_iterations=10,
12001216
stop_if_no_improvement=4,
12011217
)
@@ -1283,34 +1299,34 @@ Simple edge-map features:
12831299

12841300
```python
12851301
rag = bic.graph.region_adjacency_graph(labels)
1286-
features = bic.graph.edge_map_features(rag, labels, edge_map)
1302+
features = bic.graph.features.edge_map_features(rag, labels, edge_map)
12871303
```
12881304

12891305
The columns are:
12901306

12911307
```python
1292-
bic.graph.SIMPLE_EDGE_FEATURE_NAMES
1308+
bic.graph.features.SIMPLE_EDGE_FEATURE_NAMES
12931309
# ("mean", "size")
12941310
```
12951311

12961312
Complex edge-map features:
12971313

12981314
```python
1299-
features = bic.graph.edge_map_features_complex(rag, labels, edge_map)
1315+
features = bic.graph.features.edge_map_features_complex(rag, labels, edge_map)
13001316
```
13011317

13021318
The columns are:
13031319

13041320
```python
1305-
bic.graph.COMPLEX_EDGE_FEATURE_NAMES
1321+
bic.graph.features.COMPLEX_EDGE_FEATURE_NAMES
13061322
# ("mean", "median", "std", "min", "max", "p5", "p10",
13071323
# "p25", "p75", "p90", "p95", "size")
13081324
```
13091325

13101326
Affinity features:
13111327

13121328
```python
1313-
features = bic.graph.affinity_features(
1329+
features = bic.graph.features.affinity_features(
13141330
rag,
13151331
labels,
13161332
affinities,
@@ -1321,7 +1337,7 @@ features = bic.graph.affinity_features(
13211337
Complex affinity features:
13221338

13231339
```python
1324-
features = bic.graph.affinity_features_complex(
1340+
features = bic.graph.features.affinity_features_complex(
13251341
rag,
13261342
labels,
13271343
affinities,

development/graph/_grid_affinity_compatibility.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ def bioimage_cpp_local(affinities: np.ndarray, offsets: list[tuple[int, ...]]):
104104
import bioimage_cpp as bic
105105

106106
graph = bic.graph.grid_graph(affinities.shape[1:])
107-
weights, _ = bic.graph.grid_affinity_features(graph, affinities, offsets)
107+
weights, _ = bic.graph.features.grid_affinity_features(graph, affinities, offsets)
108108
return graph.uv_ids(), weights
109109

110110

@@ -119,7 +119,7 @@ def bioimage_cpp_local_weights_only(
119119
"""
120120
import bioimage_cpp as bic
121121

122-
weights, _ = bic.graph.grid_affinity_features(graph, affinities, offsets)
122+
weights, _ = bic.graph.features.grid_affinity_features(graph, affinities, offsets)
123123
return weights
124124

125125

@@ -131,7 +131,7 @@ def bioimage_cpp_local_with_uvs(
131131
``compute_nh_and_weights``, both of which return uvs in their output."""
132132
import bioimage_cpp as bic
133133

134-
weights, _ = bic.graph.grid_affinity_features(graph, affinities, offsets)
134+
weights, _ = bic.graph.features.grid_affinity_features(graph, affinities, offsets)
135135
return graph.uv_ids(), weights
136136

137137

@@ -140,7 +140,7 @@ def bioimage_cpp_lifted(affinities: np.ndarray, offsets: list[tuple[int, ...]]):
140140

141141
graph = bic.graph.grid_graph(affinities.shape[1:])
142142
local_weights, _, lifted_uvs, lifted_weights, _ = (
143-
bic.graph.grid_affinity_features_with_lifted(graph, affinities, offsets)
143+
bic.graph.features.grid_affinity_features_with_lifted(graph, affinities, offsets)
144144
)
145145
return graph, graph.uv_ids(), local_weights, lifted_uvs, lifted_weights
146146

@@ -152,7 +152,7 @@ def bioimage_cpp_lifted_features_only(
152152
import bioimage_cpp as bic
153153

154154
local_weights, _, lifted_uvs, lifted_weights, _ = (
155-
bic.graph.grid_affinity_features_with_lifted(graph, affinities, offsets)
155+
bic.graph.features.grid_affinity_features_with_lifted(graph, affinities, offsets)
156156
)
157157
return local_weights, lifted_uvs, lifted_weights
158158

@@ -164,7 +164,7 @@ def bioimage_cpp_lifted_with_uvs(
164164
import bioimage_cpp as bic
165165

166166
local_weights, _, lifted_uvs, lifted_weights, _ = (
167-
bic.graph.grid_affinity_features_with_lifted(graph, affinities, offsets)
167+
bic.graph.features.grid_affinity_features_with_lifted(graph, affinities, offsets)
168168
)
169169
return graph.uv_ids(), local_weights, lifted_uvs, lifted_weights
170170

@@ -173,7 +173,7 @@ def assert_local_offsets_cover_all_edges(graph, affinities, offsets) -> None:
173173
"""One-shot correctness check called outside of the timing loop."""
174174
import bioimage_cpp as bic
175175

176-
_, valid_edges = bic.graph.grid_affinity_features(graph, affinities, offsets)
176+
_, valid_edges = bic.graph.features.grid_affinity_features(graph, affinities, offsets)
177177
if not np.all(valid_edges):
178178
raise AssertionError("local offsets did not cover all grid edges")
179179

development/graph/_rag_compatibility.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ def compare_boundary_features(
144144
# what its parallelism is tuned for, and forcing a single block (== labels
145145
# shape) starves nifty's worker pool.
146146
bic_timings, bic_features = time_call(
147-
lambda: bic.graph.edge_map_features(
147+
lambda: bic.graph.features.edge_map_features(
148148
bic_rag, labels, boundary_map, number_of_threads=threads
149149
),
150150
repeats,
@@ -189,7 +189,7 @@ def compare_affinity_features(
189189
max_val = min_val + 1.0
190190

191191
bic_timings, bic_features = time_call(
192-
lambda: bic.graph.affinity_features(
192+
lambda: bic.graph.features.affinity_features(
193193
bic_rag,
194194
labels,
195195
affs_float32,

development/graph/lifted_multicut/_compatibility.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def load_problem(size: str, *, timeout: float):
5151
import nifty
5252
import nifty.graph.opt.lifted_multicut as nlmc
5353

54-
problem = bic.graph.load_lifted_multicut_problem(size, timeout=timeout)
54+
problem = bic.graph.lifted_multicut.load_lifted_multicut_problem(size, timeout=timeout)
5555

5656
bic_graph = bic.graph.UndirectedGraph.from_edges(
5757
problem.n_nodes, problem.local_uvs
@@ -84,7 +84,7 @@ def time_call(function: Callable[[], np.ndarray], repeats: int):
8484
def optimize_bic_solver(make_bic_solver, bic_graph, problem):
8585
import bioimage_cpp as bic
8686

87-
objective = bic.graph.LiftedMulticutObjective(
87+
objective = bic.graph.lifted_multicut.LiftedMulticutObjective(
8888
bic_graph,
8989
problem.local_costs,
9090
lifted_uvs=problem.lifted_uvs,
@@ -96,7 +96,7 @@ def optimize_bic_solver(make_bic_solver, bic_graph, problem):
9696
def bic_energy(bic_graph, problem, labels: np.ndarray) -> float:
9797
import bioimage_cpp as bic
9898

99-
objective = bic.graph.LiftedMulticutObjective(
99+
objective = bic.graph.lifted_multicut.LiftedMulticutObjective(
100100
bic_graph,
101101
problem.local_costs,
102102
lifted_uvs=problem.lifted_uvs,

development/graph/lifted_multicut/check_fusion_move.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ def main() -> None:
3636
)
3737
run_comparison(
3838
"lifted_fusion_move",
39-
lambda: bic.graph.FusionMoveLiftedMulticut(
40-
proposal_generator=bic.graph.WatershedProposalGenerator(),
39+
lambda: bic.graph.lifted_multicut.FusionMoveLiftedMulticut(
40+
proposal_generator=bic.graph.lifted_multicut.WatershedProposalGenerator(),
4141
number_of_iterations=10,
4242
stop_if_no_improvement=4,
4343
number_of_threads=threads,

0 commit comments

Comments
 (0)