Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ nanobind_add_module(_core
src/bindings/filters.cxx
src/bindings/flow.cxx
src/bindings/graph.cxx
src/bindings/graph_contraction.cxx
src/bindings/ground_truth.cxx
src/bindings/label_multiset.cxx
src/bindings/mesh.cxx
Expand Down Expand Up @@ -68,6 +69,46 @@ if(BIOIMAGE_FLOW_FMA_DISPATCH)
endif()
endif()

option(
BIOIMAGE_FILTERS_AVX2_DISPATCH
"Build x86 AVX2/FMA filter kernels with runtime CPU dispatch"
ON
)
if(BIOIMAGE_FILTERS_AVX2_DISPATCH)
string(TOLOWER "${CMAKE_SYSTEM_PROCESSOR}" _bioimage_filters_processor)
if(_bioimage_filters_processor MATCHES "^(x86_64|amd64|i[3-6]86)$")
if(MSVC)
target_sources(
_core PRIVATE
src/cpp/filters/dispatch.cxx
src/cpp/filters/convolve_avx2.cxx
src/cpp/filters/eigenvalues_avx2.cxx
)
set_property(
SOURCE
src/cpp/filters/convolve_avx2.cxx
src/cpp/filters/eigenvalues_avx2.cxx
APPEND PROPERTY COMPILE_OPTIONS /arch:AVX2
)
target_compile_definitions(_core PRIVATE BIOIMAGE_FILTERS_AVX2_DISPATCH=1)
elseif(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
target_sources(
_core PRIVATE
src/cpp/filters/dispatch.cxx
src/cpp/filters/convolve_avx2.cxx
src/cpp/filters/eigenvalues_avx2.cxx
)
set_property(
SOURCE
src/cpp/filters/convolve_avx2.cxx
src/cpp/filters/eigenvalues_avx2.cxx
APPEND PROPERTY COMPILE_OPTIONS -mavx2 -mfma
)
target_compile_definitions(_core PRIVATE BIOIMAGE_FILTERS_AVX2_DISPATCH=1)
endif()
endif()
endif()

option(BIOIMAGE_PROFILE "Enable per-phase profiling instrumentation (development only)" OFF)
if(BIOIMAGE_PROFILE)
target_compile_definitions(_core PRIVATE BIOIMAGE_PROFILE)
Expand Down
134 changes: 125 additions & 9 deletions MIGRATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ Notes:
wrapper dispatches to a templated C++ instantiation per dtype; other
floating dtypes are cast to `float32`. If the two arrays' dtypes do not
match, both are promoted to `float64` rather than silently downcast.
- Both weight arrays must contain only finite values.
- Higher weights are processed first (in descending order) — the same
convention affogato uses.
- The implementation reuses the union-find and per-root mutex-set helpers
Expand Down Expand Up @@ -331,6 +332,7 @@ Notes:
- All three weight arrays (`weights`, `mutex_weights`, `semantic_weights`)
must have the same floating dtype, or all three are promoted to
`float64`.
- All three weight arrays must contain only finite values.
- Output `labels` are dense `uint64` ids in `0 .. number_of_clusters - 1`
(first-occurrence order, matching the regular graph mutex watershed —
*not* the 1-based foreground labels produced by the grid variant).
Expand Down Expand Up @@ -460,6 +462,75 @@ Common method/property mapping:
| `extractSubgraphFromNodes` | `extract_subgraph_from_nodes` |
| `edgesFromNodeList` | `edges_from_node_list` |

`node_degrees()` returns the degree of every node as a `uint64` array.

### Mutable Graph Contractions

`ContractionGraph` supports custom graph postprocessing with stable ids and
named value maps:

```python
import numpy as np
import bioimage_cpp as bic

graph = bic.graph.UndirectedGraph.from_edges(
3,
[[0, 1], [1, 2], [0, 2]],
)
work = bic.graph.ContractionGraph(graph)
work.add_node_values(
"position",
np.array([[0.0], [2.0], [10.0]], dtype=np.float32),
reduction="mean",
)
work.add_edge_values(
"weight",
np.array([100.0, 2.0, 3.0], dtype=np.float64),
reduction="sum",
)

work.contract_edge(0, keep_node=0)
result = work.materialize()

contracted = result.graph
positions = result.node_values["position"]
weights = result.edge_values["weight"]
```

Register all value maps before the first mutation. Maps can use `float32` or
`float64` independently. The supported reductions are `sum`, `mean`, `min`,
and `max`.

Node values reduce when an edge contraction merges two nodes. Edge values
reduce when parallel edges merge. The contracted edge becomes internal and has
no output value. `suppress_node` removes a degree-2 node and reduces its two
incident edge values into the replacement edge.

Use `can_suppress_node(node)` before suppression when the mutable graph can
contain parallel edges. The method also rejects a suppression that would create
a self-edge.

The input must be a simple `UndirectedGraph`. Use
`parallel_edges="keep"` to keep parallel edges that mutations create:

```python
work = bic.graph.ContractionGraph(graph, parallel_edges="keep")
```

The default `parallel_edges="merge"` folds these edges immediately. In keep
mode, `find_edges(u, v)` returns all matching active edge ids.

`materialize()` always returns a simple-graph snapshot. In keep mode, it folds
parallel edges in the snapshot with each edge map's reduction. It does not
change the mutable graph. Rows in `node_values` and `edge_values` match the ids
in the materialized graph. `node_mapping` and `edge_mapping` map input ids to
materialized ids. Multiple input edges can map to one output edge. The mappings
use `-1` when no output item represents an input item.

The mutable object is not an `UndirectedGraph`. Active ids stay stable and can
have gaps after mutations. Materialization creates the dense
`UndirectedGraph`.

### Region Adjacency Graphs

Nifty:
Expand Down Expand Up @@ -877,7 +948,7 @@ Notes:
- `edge_weights` must be 1D with length `graph.number_of_edges`. Supported
dtypes are `float32` and `float64`; other floating dtypes are promoted to
`float32` (matching nifty, whose Python binding is `float32`-only). Non-float
dtypes raise `TypeError`.
dtypes raise `TypeError`. All weights must be finite.
- `seeds` must be 1D with length `graph.number_of_nodes`. Supported dtypes are
`uint32`, `uint64`, `int32`, `int64`. The value `0` marks unlabeled nodes;
non-zero ids are propagated along low-weight paths. Signed seed arrays must
Expand Down Expand Up @@ -1084,6 +1155,12 @@ Notes:
`downsampleMultiset` returns only four of them and leaves the caller to
reconstruct `entry_offsets` / `entry_sizes`; `bioimage-cpp` returns them
directly so multi-level downsample chains compose without bookkeeping.
- `offsets` and `unique_offsets` are element offsets into `ids` and `counts`.
They are not byte offsets.
- Construction checks integer ranges and the complete flat-storage structure.
Native operations repeat the checks because `LabelMultiset` is mutable.
- Stored entries must contain at least one `(id, count)` pair. A completely
empty top-level `LabelMultiset` is valid.
- `multiset_from_labels(labels, block_shape)` builds the level-0 multiset
from a `uint32` or `uint64` label volume in one call. There is no nifty
equivalent.
Expand Down Expand Up @@ -1141,10 +1218,10 @@ energy = objective.energy(labels)
- `graph` — an `UndirectedGraph` or `RegionAdjacencyGraph`. The constructor
copies the topology, so further mutations on the input graph do not affect
the objective.
- `edge_costs` — 1D `float64` array of length `graph.number_of_edges`.
- `edge_costs` — 1D array of finite values with length
`graph.number_of_edges`; compatible inputs are converted to `float64`.
- `lifted_uvs` / `lifted_costs` — optional `(n_lifted, 2)` uint64 array and 1D
float64 array of equal length, listing the additional lifted edges and
their weights.
float64 array of equal length. Lifted costs must be finite.
- `bfs_distance` — optional positive integer. Adds a zero-weight lifted edge
for every pair of nodes within this many base-graph hops of each other
(excluding nodes already connected by a base edge). Pairs with both
Expand Down Expand Up @@ -1325,10 +1402,11 @@ labels = bic.graph.multicut.GreedyAdditiveMulticut().optimize(objective)
energy = objective.energy(labels)
```

`MulticutObjective` accepts an `UndirectedGraph` or a `RegionAdjacencyGraph` and
a 1D `edge_costs` array of length `graph.number_of_edges`. The objective owns
the current best `labels`; `optimize` updates them in place and also returns
the new array.
`MulticutObjective` accepts an `UndirectedGraph` or a `RegionAdjacencyGraph`.
It converts a 1D `edge_costs` array to `float64`. The array length must equal
`graph.number_of_edges`, and all costs must be finite. The objective owns the
current best `labels`; `optimize` updates them in place and also returns the
new array.

Available solvers:

Expand Down Expand Up @@ -1390,12 +1468,24 @@ labels = solver.optimize(objective)

Notes:

- `edge_costs` must be `float64` and 1D with length `graph.number_of_edges`.
- `edge_costs` must be finite and 1D with length `graph.number_of_edges`.
- Output labels are dense `uint64` ids in `0 .. number_of_clusters - 1`.
- `MulticutObjective.energy(labels)` is the multicut energy used internally; it
matches `nmc.multicutObjective(...).evalNodeLabels(labels)`.
- `objective.reset_labels()` restores the per-node initial labeling, useful when
re-running solvers from a clean state.
- `number_of_threads=0` on `MulticutDecomposer` uses available hardware
concurrency. A positive value sets the maximum worker count.
- `MulticutDecomposer` runs decomposition and component solvers in C++. It
accepts `GreedyAdditiveMulticut`, `GreedyFixationMulticut`,
`KernighanLinMulticut`, and chains composed only of these solvers.
- `MulticutDecomposer` does not accept custom Python solvers,
`FusionMoveMulticut`, or another `MulticutDecomposer` as a component or
fallthrough solver. Use one of the supported native solvers instead.
- Multicut solvers no longer expose `clone()`. Native decomposition creates
independent C++ solver instances for its workers.
- Components can finish in any order. The decomposer applies labels in
component order, so scheduling does not change output labels.

Intentional differences vs. nifty:

Expand All @@ -1407,6 +1497,9 @@ Intentional differences vs. nifty:
- `MulticutDecomposer` short-circuits the trivial case where the sub-solver is
`GreedyAdditiveMulticut` and no fallthrough is given — the greedy solver
already operates on each connected component internally.
- Unlike nifty's factory-based decomposer, the native decomposer supports only
the solver types listed above. This keeps worker execution outside the GIL
and avoids Python callbacks from native threads.

#### Fusion Moves

Expand Down Expand Up @@ -1546,6 +1639,13 @@ Differences from nifty:
underlying loop is the same.
- Both `float32` and `float64` inputs are accepted; computation runs in
`float64` internally.
- All indicators, weights, sizes, and node features that affect priorities
must contain finite values.
- `MalaClusterPolicy.num_bins` must be an integer greater than or equal to `2`.
- `MalaClusterPolicy.num_edges_stop` counts all active edges after each
contraction. This includes the contracted edge and folded parallel edges.
Earlier bioimage-cpp versions counted only folds and could stop too late on
sparse graphs. The new behavior matches nifty's contraction graph.
- Tie-breaks follow the deterministic order of edge ids returned by
`UndirectedGraph`, which may differ from nifty's. On inputs where many
edges share the same indicator value, this combines with the
Expand Down Expand Up @@ -2606,6 +2706,22 @@ Important differences and current scope:
heap. Compact IDs avoid full-volume shortest-path fields; the public Dijkstra
functions remain dense and exact FP64.

### Skeleton graph postprocessing

`remove_ticks(vertices, edges, tick_length, radii=None)` removes short terminal
branches from a skeleton graph. It compresses degree-2 paths, sums their
physical lengths, and repeatedly removes the shortest terminal branch below
the strict threshold.

The mutable topology and length reduction run in the C++ contraction backend.
The Python wrapper selects branches and compacts the output arrays. Distinct
compressed paths between the same branch nodes remain separate, so cyclic and
theta-shaped skeletons keep their degree semantics.

Standalone paths and components that contain only a cycle remain unchanged.
The result keeps the existing array contract: vertices are `float64`, edges are
`int64`, and optional radii preserve their dtype.

### Blockwise skeletonization and exact stitching

`bioimage_cpp.skeleton.distributed` provides the dependency-light primitives
Expand Down
Loading
Loading