Skip to content

Commit 0af5c98

Browse files
Merge branch 'main' into affinities
2 parents c6aef54 + 7d5c937 commit 0af5c98

31 files changed

Lines changed: 5467 additions & 153 deletions

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ nanobind_add_module(_core
1414
src/bindings/affinities.cxx
1515
src/bindings/blocking.cxx
1616
src/bindings/module.cxx
17+
src/bindings/filters.cxx
1718
src/bindings/graph.cxx
1819
src/bindings/ground_truth.cxx
1920
src/bindings/segmentation.cxx

MIGRATION_GUIDE.md

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -588,14 +588,45 @@ energy = objective.energy(labels)
588588
- `overwrite_existing` — when `True`, lifted entries that coincide with an
589589
existing edge replace its weight; the default accumulates.
590590

591-
Available solvers (no fusion-move / ILP solvers yet):
591+
Available solvers (no ILP solvers yet):
592592

593593
| nifty factory | bioimage-cpp solver |
594594
| --- | --- |
595595
| `liftedMulticutGreedyAdditiveFactory()` | `LiftedGreedyAdditiveMulticut()` |
596596
| `liftedMulticutKernighanLinFactory(...)` | `LiftedKernighanLinMulticut(...)` |
597+
| `fusionMoveBasedFactory(...)` | `FusionMoveLiftedMulticut(...)` |
597598
| `chainedSolversFactory([...])` | `LiftedChainedSolvers([...])` |
598599

600+
`FusionMoveLiftedMulticut` mirrors `FusionMoveMulticut` (same proposal-generator
601+
plumbing, same threading + multi-proposal joint-fuse semantics, same best-of
602+
safety net). The differences are:
603+
604+
- Proposal generators operate on the *base* graph and base edge costs (only
605+
base-graph edges are candidate cut edges; lifted edges contribute to energy
606+
but cannot be contracted directly). The driver extracts the base costs from
607+
`objective.weights[:objective.number_of_base_edges]` automatically.
608+
- Each fuse contracts the base graph by agreement, aggregates *both* base and
609+
lifted weights onto the contracted lifted-multicut subproblem (lifted edges
610+
whose endpoints land on already-existing contracted base edges fold into
611+
them; the rest become new contracted lifted edges), and solves the
612+
subproblem with a `LiftedMulticutSolver`.
613+
- The default sub-solver and warm-start are `LiftedGreedyAdditiveMulticut`.
614+
Both `LiftedGreedyAdditiveMulticut` and `LiftedKernighanLinMulticut` are
615+
pluggable via `sub_solver=`.
616+
617+
```python
618+
solver = bic.graph.FusionMoveLiftedMulticut(
619+
proposal_generator=bic.graph.WatershedProposalGenerator(
620+
sigma=1.0, n_seeds_fraction=0.1, seed=0,
621+
),
622+
sub_solver=bic.graph.LiftedKernighanLinMulticut(number_of_outer_iterations=3),
623+
number_of_iterations=10,
624+
stop_if_no_improvement=4,
625+
number_of_threads=4,
626+
)
627+
labels = solver.optimize(objective)
628+
```
629+
599630
A typical warm-started solve combines greedy and KL:
600631

601632
```python
@@ -946,6 +977,88 @@ Notes:
946977
- Every value in the input must be present in the mapping.
947978
- Non-contiguous inputs are copied before entering C++.
948979

980+
## Image Filters
981+
982+
`bioimage-cpp` ships a small Gaussian-derivative filter set under
983+
`bic.filters`. The scope is the "ilastik filter set" exposed by
984+
`fastfilters`, which is also the most-used subset of `vigra.filters`.
985+
986+
Vigra / fastfilters:
987+
988+
```python
989+
import vigra.filters as vf
990+
out = vf.gaussianSmoothing(img, sigma=1.5)
991+
ev = vf.hessianOfGaussianEigenvalues(img, scale=1.5)
992+
993+
import fastfilters as ff
994+
out = ff.gaussianSmoothing(img, sigma=1.5)
995+
ev = ff.hessianOfGaussianEigenvalues(img, scale=1.5)
996+
```
997+
998+
bioimage-cpp:
999+
1000+
```python
1001+
import bioimage_cpp as bic
1002+
1003+
out = bic.filters.gaussian_smoothing(img, sigma=1.5)
1004+
ev = bic.filters.hessian_of_gaussian_eigenvalues(img, sigma=1.5)
1005+
```
1006+
1007+
Name mapping:
1008+
1009+
| vigra / fastfilters name | bioimage-cpp name |
1010+
| --- | --- |
1011+
| `gaussianSmoothing` | `gaussian_smoothing` |
1012+
| `gaussianDerivative` | `gaussian_derivative` |
1013+
| `gaussianGradientMagnitude` | `gaussian_gradient_magnitude` |
1014+
| `laplacianOfGaussian` | `laplacian_of_gaussian` |
1015+
| `hessianOfGaussianEigenvalues` | `hessian_of_gaussian_eigenvalues` |
1016+
| `structureTensorEigenvalues` | `structure_tensor_eigenvalues` |
1017+
1018+
Common parameters:
1019+
1020+
- `sigma` is a positive scalar or a per-axis sequence of length
1021+
`image.ndim`. Anisotropic sigma is supported on every filter.
1022+
- `gaussian_derivative` takes an `order` argument that is a scalar or a
1023+
per-axis sequence of ints in `{0, 1, 2}`.
1024+
- `structure_tensor_eigenvalues` takes positional `inner_sigma` and
1025+
`outer_sigma` (vigra calls them `innerScale` / `outerScale`).
1026+
- `window_size` controls the kernel radius:
1027+
`radius = ceil(window_size * sigma)`. `0.0` (the default) selects the
1028+
vigra-style default `3 + 0.5 * order`. Matches the same-named parameter
1029+
in vigra/fastfilters.
1030+
1031+
Important differences from vigra and fastfilters:
1032+
1033+
- Only 2D and 3D scalar (single-channel) inputs are supported in v1.
1034+
Channels and leading batch axes should be looped externally — matches
1035+
fastfilters' convention. Vigra's `taggedView`/`AxisInfo` machinery is
1036+
not reproduced.
1037+
- C++ kernels operate on `float32`. `float64` inputs are accepted and the
1038+
output is cast back to `float64`. `uint8` and `uint16` are accepted with
1039+
a `float32` output (the typical ML-feature use case).
1040+
- Boundary handling is `mirror` (matches scipy `mode="mirror"`
1041+
reflection without edge-pixel repeat). Other boundary modes are not
1042+
exposed yet; the C++ layer carries an enum for future tiled processing.
1043+
- Eigenvalue outputs have a trailing axis of size `image.ndim`, sorted
1044+
largest → smallest. This matches `fastfilters`. To get vigra's
1045+
ascending order, reverse with `result[..., ::-1]`.
1046+
- No IIR / recursive Gaussian, no `convolve` / `recursiveFilter2D`, no
1047+
morphology, no distance transforms, no nonlinear diffusion, and no
1048+
non-local means in v1. Use `scipy.ndimage`, `skimage`, or the original
1049+
vigra/fastfilters bindings if you need those.
1050+
1051+
Implementation notes:
1052+
1053+
- All six filters are written as portable C++20 scalar code that the
1054+
compiler auto-vectorizes. No SIMD intrinsics, no per-file ISA flags, no
1055+
runtime CPU dispatch, no vendored SIMD library. This keeps the build
1056+
light enough to ship as portable PyPI wheels across Linux/macOS/Windows
1057+
and x86_64/arm64.
1058+
- Single-threaded for now. Threading can be added later via
1059+
`detail/threading.hxx::parallel_for_chunks` without changing the
1060+
public API.
1061+
9491062
## I/O and Build Dependencies
9501063

9511064
`bioimage-cpp` intentionally does not replace nifty or affogato I/O helpers.

0 commit comments

Comments
 (0)