Skip to content

Commit 68b10ef

Browse files
Merge branch 'main' into multicut
Conflict in include/bioimage_cpp/segmentation/mutex_watershed.hxx: - c_order_strides: kept multicut's resolution (function lives in detail/grid.hxx after the duplicate-helper consolidation). - is_valid_grid_edge in-loop check: dropped in favor of main's version. Python's _compute_valid_edges already excludes out-of-bounds source pixels, so valid_edges + the existing pre-filter make the check redundant. Multicut benchmarks unchanged after merge.
2 parents 3562963 + 24e5ff2 commit 68b10ef

21 files changed

Lines changed: 2097 additions & 58 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,5 @@ venv/
2727

2828
# Data
2929
*.h5
30+
31+
CLAUDE.md

AGENTS.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,11 @@ Public functions should have concise docstrings documenting:
435435
- background-label behavior, if relevant
436436
- axis and coordinate conventions
437437

438+
When adding or changing public functionality, update `MIGRATION_GUIDE.md` so
439+
users migrating from `nifty`, `affogato`, or related packages know the
440+
corresponding `bioimage-cpp` API, important behavioral differences, and any
441+
intentional improvements.
442+
438443
## What an agent should do when modifying this repository
439444

440445
When adding or changing code:

CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ find_package(nanobind CONFIG REQUIRED)
1111

1212
nanobind_add_module(_core
1313
NB_STATIC
14+
src/bindings/blocking.cxx
1415
src/bindings/module.cxx
1516
src/bindings/graph.cxx
17+
src/bindings/ground_truth.cxx
1618
src/bindings/segmentation.cxx
1719
src/bindings/utils.cxx
1820
src/cpp/segmentation/mutex_watershed.cxx

MIGRATION_GUIDE.md

Lines changed: 362 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,362 @@
1+
# Migration Guide
2+
3+
This guide explains how to migrate code that used selected `nifty` or
4+
`affogato` functionality to `bioimage-cpp`.
5+
6+
`bioimage-cpp` is not a drop-in compatibility layer. The package keeps a
7+
smaller, NumPy-first API with Python-style method names, explicit dtype support,
8+
and no I/O dependencies in the C++ core.
9+
10+
## Imports
11+
12+
Use:
13+
14+
```python
15+
import bioimage_cpp as bic
16+
```
17+
18+
Graph functionality is under `bic.graph`, segmentation functionality is under
19+
`bic.segmentation`, ground-truth comparison functionality is under
20+
`bic.ground_truth`, and small utility functions are under `bic.utils`.
21+
22+
## Blocking
23+
24+
Nifty:
25+
26+
```python
27+
import nifty.tools as nt
28+
29+
blocking = nt.blocking([0, 0], [100, 80], [32, 32])
30+
block = blocking.getBlock(0)
31+
block_with_halo = blocking.getBlockWithHalo(0, [8, 8])
32+
```
33+
34+
bioimage-cpp:
35+
36+
```python
37+
import bioimage_cpp as bic
38+
39+
blocking = bic.Blocking([0, 0], [100, 80], [32, 32])
40+
block = blocking.get_block(0)
41+
block_with_halo = blocking.get_block_with_halo(0, [8, 8])
42+
```
43+
44+
Name changes:
45+
46+
| nifty-style name | bioimage-cpp name |
47+
| --- | --- |
48+
| `roiBegin` | `roi_begin` |
49+
| `roiEnd` | `roi_end` |
50+
| `blockShape` | `block_shape` |
51+
| `blockShift` | `block_shift` |
52+
| `blocksPerAxis` | `blocks_per_axis` |
53+
| `numberOfBlocks` | `number_of_blocks` |
54+
| `blockGridPosition` | `block_grid_position` |
55+
| `getNeighborId` | `get_neighbor_id` |
56+
| `getBlock` | `get_block` |
57+
| `getBlockWithHalo` | `get_block_with_halo` |
58+
| `addHalo` | `add_halo` |
59+
| `coordinatesToBlockId` | `coordinates_to_block_id` |
60+
| `getBlockIdsInBoundingBox` | `get_block_ids_in_bounding_box` |
61+
| `getBlockIdsOverlappingBoundingBox` | `get_block_ids_overlapping_bounding_box` |
62+
| `getLocalOverlaps` | `get_local_overlaps` |
63+
| `getBlockIdsInSlice` | `get_block_ids_in_slice` |
64+
65+
Intentional improvements over nifty:
66+
67+
- `coordinates_to_block_id` accounts for both `roi_begin` and `block_shift`.
68+
- `get_block_ids_overlapping_bounding_box` works for any dimensionality, not
69+
only 3D.
70+
- Bounding boxes use NumPy-style half-open intervals: `[begin, end)`.
71+
- `get_local_overlaps` returns `None` if blocks do not overlap; otherwise it
72+
returns `(begin_a, end_a, begin_b, end_b)` in local coordinates.
73+
74+
## Undirected Graphs
75+
76+
Nifty:
77+
78+
```python
79+
import nifty.graph as ng
80+
81+
graph = ng.undirectedGraph(4)
82+
edge_id = graph.insertEdge(0, 1)
83+
uvs = graph.uvIds()
84+
```
85+
86+
bioimage-cpp:
87+
88+
```python
89+
import bioimage_cpp as bic
90+
91+
graph = bic.graph.UndirectedGraph(4)
92+
edge_id = graph.insert_edge(0, 1)
93+
uvs = graph.uv_ids()
94+
```
95+
96+
The convenience constructor is:
97+
98+
```python
99+
graph = bic.graph.undirected_graph(4)
100+
graph = bic.graph.UndirectedGraph.from_edges(4, [[0, 1], [1, 2]])
101+
```
102+
103+
Important differences:
104+
105+
- Nodes are fixed at construction and have ids `0 .. number_of_nodes - 1`.
106+
- Re-inserting an existing undirected edge returns the existing edge id.
107+
- Bulk methods accept array-like inputs and return NumPy arrays.
108+
- Python-style names are preferred. A few nifty-style aliases are still present
109+
on `UndirectedGraph` for convenience, but new code should use snake_case.
110+
111+
Common method/property mapping:
112+
113+
| nifty-style name | bioimage-cpp name |
114+
| --- | --- |
115+
| `numberOfNodes` | `number_of_nodes` |
116+
| `numberOfEdges` | `number_of_edges` |
117+
| `nodeIdUpperBound` | `node_id_upper_bound` |
118+
| `edgeIdUpperBound` | `edge_id_upper_bound` |
119+
| `insertEdge` | `insert_edge` |
120+
| `insertEdges` | `insert_edges` |
121+
| `findEdge` | `find_edge` |
122+
| `findEdges` | `find_edges` |
123+
| `uvIds` | `uv_ids` |
124+
| `nodeAdjacency` | `node_adjacency` |
125+
| `serializationSize` | `serialization_size` |
126+
| `extractSubgraphFromNodes` | `extract_subgraph_from_nodes` |
127+
| `edgesFromNodeList` | `edges_from_node_list` |
128+
129+
## Region Adjacency Graphs
130+
131+
Nifty:
132+
133+
```python
134+
import nifty.graph.rag as nrag
135+
136+
rag = nrag.gridRag(labels)
137+
uvs = rag.uvIds()
138+
```
139+
140+
bioimage-cpp:
141+
142+
```python
143+
import bioimage_cpp as bic
144+
145+
rag = bic.graph.region_adjacency_graph(labels)
146+
uvs = rag.uv_ids()
147+
```
148+
149+
Notes:
150+
151+
- Supported label dtypes are `uint32`, `uint64`, `int32`, and `int64`.
152+
- Labels must be 2D or 3D.
153+
- Negative signed labels are rejected.
154+
- Nodes correspond to label ids from `0` to `labels.max()`.
155+
- Edge ids are deterministic; RAG edges are sorted lexicographically by
156+
endpoint ids.
157+
- Non-contiguous labels are copied to contiguous memory before entering C++.
158+
159+
## RAG Boundary and Affinity Features
160+
161+
Nifty has RAG feature helpers such as `accumulateEdgeMeanAndLength`,
162+
`accumulateEdgeStandartFeatures`, and affinity feature accumulation helpers.
163+
In `bioimage-cpp`, these are exposed as explicit NumPy-returning functions.
164+
165+
Simple edge-map features:
166+
167+
```python
168+
rag = bic.graph.region_adjacency_graph(labels)
169+
features = bic.graph.edge_map_features(rag, labels, edge_map)
170+
```
171+
172+
The columns are:
173+
174+
```python
175+
bic.graph.SIMPLE_EDGE_FEATURE_NAMES
176+
# ("mean", "size")
177+
```
178+
179+
Complex edge-map features:
180+
181+
```python
182+
features = bic.graph.edge_map_features_complex(rag, labels, edge_map)
183+
```
184+
185+
The columns are:
186+
187+
```python
188+
bic.graph.COMPLEX_EDGE_FEATURE_NAMES
189+
# ("mean", "median", "std", "min", "max", "p5", "p10",
190+
# "p25", "p75", "p90", "p95", "size")
191+
```
192+
193+
Affinity features:
194+
195+
```python
196+
features = bic.graph.affinity_features(
197+
rag,
198+
labels,
199+
affinities,
200+
offsets=[[0, 1], [1, 0]],
201+
)
202+
```
203+
204+
Complex affinity features:
205+
206+
```python
207+
features = bic.graph.affinity_features_complex(
208+
rag,
209+
labels,
210+
affinities,
211+
offsets=[[0, 1], [1, 0]],
212+
)
213+
```
214+
215+
Notes:
216+
217+
- `edge_map` must have the same shape as `labels`.
218+
- `affinities` must have shape `(channels, *labels.shape)`.
219+
- `offsets` must have one offset per channel in NumPy axis order.
220+
- Feature arrays use `float64` output.
221+
- `number_of_threads=0` uses the library default; pass a positive integer for a
222+
fixed thread count.
223+
224+
## Segmentation Overlaps
225+
226+
Nifty:
227+
228+
```python
229+
import nifty.ground_truth as ngt
230+
231+
overlap = ngt.overlap(segmentation, ground_truth)
232+
```
233+
234+
bioimage-cpp:
235+
236+
```python
237+
import bioimage_cpp as bic
238+
239+
overlap = bic.ground_truth.segmentation_overlap(segmentation, ground_truth)
240+
```
241+
242+
The first input is called `labels_a` and the second input is called `labels_b`
243+
in the `bioimage-cpp` API. Use named structured tables instead of positional
244+
arrays:
245+
246+
```python
247+
table = overlap.overlap_table()
248+
# fields: "label_a", "label_b", "count"
249+
250+
table = overlap.overlap_table(normalize_by="a")
251+
# fields: "label_a", "label_b", "count", "fraction"
252+
253+
overlaps = overlap.overlaps_for_label_a(12, normalize=True)
254+
# fields: "label", "count", "fraction"
255+
256+
best = overlap.best_overlap_for_label_a(12, ignore_zero=True)
257+
# BestOverlap(label=..., count=..., fraction=..., found=...)
258+
```
259+
260+
Other common queries:
261+
262+
```python
263+
overlap.labels_a
264+
overlap.labels_b
265+
overlap.count_a(12)
266+
overlap.count_b(4)
267+
overlap.overlap_count(12, 4)
268+
overlap.counts_a_table()
269+
overlap.counts_b_table()
270+
overlap.best_overlap_for_label_b(4)
271+
overlap.is_label_a_overlapping_with_zero(12)
272+
overlap.different_overlap(12, 13)
273+
```
274+
275+
Intentional improvements over nifty:
276+
277+
- Labels are stored sparsely, so large sparse label ids do not require a dense
278+
vector up to `max_label + 1`.
279+
- The Python API returns structured arrays with named fields and a
280+
`BestOverlap` dataclass instead of ambiguous positional arrays.
281+
- Both overlap directions are supported explicitly:
282+
`overlaps_for_label_a(...)` and `overlaps_for_label_b(...)`.
283+
- Normalization is explicit via `normalize_by="a"`, `"b"`, or `"total"`.
284+
- Missing labels return count `0`; best-overlap queries expose `found=False`.
285+
286+
Notes:
287+
288+
- Inputs must be integer arrays with identical shape.
289+
- Signed integer inputs must not contain negative labels.
290+
- Inputs are converted to contiguous `uint64` arrays before entering C++.
291+
292+
## Mutex Watershed
293+
294+
Affogato:
295+
296+
```python
297+
from affogato.segmentation import compute_mws_segmentation
298+
299+
seg = compute_mws_segmentation(
300+
weights,
301+
offsets,
302+
number_of_attractive_channels=3,
303+
strides=[1, 1, 1],
304+
)
305+
```
306+
307+
bioimage-cpp:
308+
309+
```python
310+
import bioimage_cpp as bic
311+
312+
seg = bic.segmentation.mutex_watershed(
313+
weights,
314+
offsets,
315+
number_of_attractive_channels=3,
316+
strides=[1, 1, 1],
317+
)
318+
```
319+
320+
Important migration notes:
321+
322+
- `bioimage-cpp` expects the first `number_of_attractive_channels` channels to
323+
be attractive merge-edge weights and the remaining channels to be mutex-edge
324+
weights.
325+
- Supported affinity dtypes are `float32` and `float64`.
326+
- Inputs must represent 2D or 3D grids with shapes `(channels, y, x)` or
327+
`(channels, z, y, x)`.
328+
- Non-contiguous affinity arrays are copied to contiguous memory.
329+
- `strides` sub-sample mutex edges only; attractive edges are always kept.
330+
- `randomized_strides=True` uses NumPy's global random state, so existing
331+
`np.random.seed(...)` workflows remain deterministic.
332+
- A boolean `mask` may be passed. Edges touching `False` pixels are ignored and
333+
masked pixels are set to label `0`.
334+
- Output labels are `uint64`, consecutive, and 1-based for foreground pixels.
335+
336+
## Dictionary-Based Relabeling
337+
338+
If you used a small helper to apply a dictionary to an integer label array, use
339+
`take_dict`:
340+
341+
```python
342+
labels = np.array([1, 3, 2, 1], dtype=np.uint64)
343+
relabeling = {1: 10, 2: 20, 3: 30}
344+
345+
out = bic.utils.take_dict(relabeling, labels)
346+
```
347+
348+
Notes:
349+
350+
- Supported input dtypes are `uint32`, `uint64`, `int32`, and `int64`.
351+
- Output has the same shape and dtype as the input array.
352+
- Every value in the input must be present in the mapping.
353+
- Non-contiguous inputs are copied before entering C++.
354+
355+
## I/O and Build Dependencies
356+
357+
`bioimage-cpp` intentionally does not replace nifty or affogato I/O helpers.
358+
Load TIFF, HDF5, zarr, N5, OME-NGFF, and related formats with existing Python
359+
libraries, then pass NumPy arrays to `bioimage-cpp`.
360+
361+
The package is designed for small PyPI wheels and does not depend on nifty,
362+
vigra, HDF5, z5, xtensor, pybind11, or other large C++ libraries.

0 commit comments

Comments
 (0)