@@ -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+
599630A 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