@@ -486,6 +486,209 @@ Notes:
486486 ` ProposalGenerator ` and provide your own ` _build ` returning a C++
487487 proposal-generator object if you need to extend the set.
488488
489+ ## Lifted Multicut
490+
491+ Nifty exposes lifted multicut through a separate objective + solver hierarchy.
492+ ` bioimage-cpp ` mirrors the structure with ` LiftedMulticutObjective ` and a
493+ ` LiftedMulticutSolver ` class hierarchy.
494+
495+ Nifty:
496+
497+ ``` python
498+ import nifty.graph.opt.lifted_multicut as nlmc
499+
500+ objective = nlmc.liftedMulticutObjective(graph)
501+ objective.insertLiftedEdgesBfs(max_distance = 3 )
502+ for u, v, w in lifted_weights:
503+ objective.setCost(u, v, w)
504+ solver = objective.liftedMulticutGreedyAdditiveFactory().create(objective)
505+ labels = solver.optimize()
506+ ```
507+
508+ bioimage-cpp:
509+
510+ ``` python
511+ import bioimage_cpp as bic
512+
513+ objective = bic.graph.LiftedMulticutObjective(
514+ graph,
515+ edge_costs,
516+ lifted_uvs = lifted_uvs,
517+ lifted_costs = lifted_costs,
518+ bfs_distance = 3 , # optional: also insert zero-weight lifted edges within k hops
519+ )
520+ labels = bic.graph.LiftedGreedyAdditiveMulticut().optimize(objective)
521+ energy = objective.energy(labels)
522+ ```
523+
524+ ` LiftedMulticutObjective ` accepts:
525+
526+ - ` graph ` — an ` UndirectedGraph ` or ` RegionAdjacencyGraph ` . The constructor
527+ copies the topology, so further mutations on the input graph do not affect
528+ the objective.
529+ - ` edge_costs ` — 1D ` float64 ` array of length ` graph.number_of_edges ` .
530+ - ` lifted_uvs ` / ` lifted_costs ` — optional ` (n_lifted, 2) ` uint64 array and 1D
531+ float64 array of equal length, listing the additional lifted edges and
532+ their weights.
533+ - ` bfs_distance ` — optional positive integer. Adds a zero-weight lifted edge
534+ for every pair of nodes within this many base-graph hops of each other
535+ (excluding nodes already connected by a base edge). Pairs with both
536+ ` lifted_uvs ` and ` bfs_distance ` to seed the topology and then update
537+ specific weights.
538+ - ` overwrite_existing ` — when ` True ` , lifted entries that coincide with an
539+ existing edge replace its weight; the default accumulates.
540+
541+ Available solvers (no fusion-move / ILP solvers yet):
542+
543+ | nifty factory | bioimage-cpp solver |
544+ | --- | --- |
545+ | ` liftedMulticutGreedyAdditiveFactory() ` | ` LiftedGreedyAdditiveMulticut() ` |
546+ | ` liftedMulticutKernighanLinFactory(...) ` | ` LiftedKernighanLinMulticut(...) ` |
547+ | ` chainedSolversFactory([...]) ` | ` LiftedChainedSolvers([...]) ` |
548+
549+ A typical warm-started solve combines greedy and KL:
550+
551+ ``` python
552+ solver = bic.graph.LiftedChainedSolvers([
553+ bic.graph.LiftedGreedyAdditiveMulticut(),
554+ bic.graph.LiftedKernighanLinMulticut(number_of_outer_iterations = 10 ),
555+ ])
556+ labels = solver.optimize(objective)
557+ ```
558+
559+ Notes:
560+
561+ - Output labels are dense ` uint64 ` ids in ` 0 .. number_of_clusters - 1 ` .
562+ - Every output cluster is * base-graph connected* — both solvers enforce this
563+ invariant. A strongly attractive lifted edge between two nodes that have no
564+ base-graph path between them will not merge their clusters.
565+ - ` LiftedKernighanLinMulticut ` warm-starts from the lifted greedy-additive
566+ solution when the objective's current labels are the trivial singleton
567+ labeling.
568+ - ` objective.set_cost(u, v, weight, overwrite=False) ` updates or inserts a
569+ single lifted edge.
570+ - The lifted graph is exposed via ` objective.lifted_graph ` ; the first
571+ ` objective.number_of_base_edges ` edges are exactly the base edges in the
572+ same order as in ` graph ` .
573+
574+ ### Building a lifted multicut problem from affinities
575+
576+ For the common case of lifted multicut on a watershed over-segmentation,
577+ nifty offers ` nifty.graph.rag.computeLiftedEdgesFromRagAndOffsets ` (lifted
578+ edge discovery) and per-channel affinity accumulators. bioimage-cpp exposes
579+ two focused helpers that cover the same workflow:
580+
581+ ``` python
582+ # Discover lifted edges implied by long-range affinity offsets. 1-hop offsets
583+ # are skipped automatically, so the full offset list can be passed in.
584+ lifted_uvs = bic.graph.lifted_edges_from_affinities(
585+ rag, oversegmentation, offsets, number_of_threads = 0 ,
586+ )
587+
588+ # Accumulate (mean, size) statistics per lifted edge. Pixel pairs whose
589+ # (u, v) does not appear in `lifted_uvs` are skipped, so local edges are
590+ # never contaminated with long-range affinities.
591+ lifted_features = bic.graph.lifted_affinity_features(
592+ oversegmentation, affinities, offsets, lifted_uvs,
593+ number_of_threads = 0 ,
594+ )
595+ # For the 12-column feature set (mean, median, std, min, max, percentiles, size):
596+ lifted_features = bic.graph.lifted_affinity_features_complex(... )
597+ ```
598+
599+ The output column conventions match the local-edge variants
600+ (` SIMPLE_EDGE_FEATURE_NAMES ` , ` COMPLEX_EDGE_FEATURE_NAMES ` ).
601+
602+ End-to-end pipeline (also in ` examples/segmentation/lifted_multicut_from_affinities.py ` ):
603+
604+ ``` python
605+ rag = bic.graph.region_adjacency_graph(oversegmentation)
606+ local_costs = local_threshold - bic.graph.affinity_features(
607+ rag, oversegmentation, direct_affinities, direct_offsets,
608+ )[:, 0 ]
609+ lifted_uvs = bic.graph.lifted_edges_from_affinities(
610+ rag, oversegmentation, long_range_offsets,
611+ )
612+ lifted_costs = lifted_threshold - bic.graph.lifted_affinity_features(
613+ oversegmentation, long_range_affinities, long_range_offsets, lifted_uvs,
614+ )[:, 0 ]
615+ objective = bic.graph.LiftedMulticutObjective(
616+ rag, local_costs, lifted_uvs = lifted_uvs, lifted_costs = lifted_costs,
617+ )
618+ ```
619+
620+ ## External Problem Instances
621+
622+ bioimage-cpp ships pooch-backed downloaders for the multicut and lifted
623+ multicut benchmark problems used by the development comparison scripts and
624+ the regression tests. Files are cached under ` ~/.cache/bioimage-cpp/ ` ,
625+ overridable via the ` BIOIMAGE_CPP_CACHE ` environment variable.
626+
627+ ` pooch ` is an optional runtime dependency — install via the ` test ` or ` data `
628+ extras, e.g. ` pip install bioimage-cpp[data] ` .
629+
630+ Multicut problems (3 samples × 2 sizes, originally from
631+ ` elf.segmentation.utils.load_multicut_problem ` ):
632+
633+ ``` python
634+ # Returns (UndirectedGraph, edge_costs)
635+ graph, costs = bic.graph.load_multicut_problem(sample = " A" , size = " small" )
636+ # Or just the underlying arrays
637+ uv_ids, costs = bic.graph.load_multicut_problem_data(sample = " B" , size = " medium" )
638+ # Or the cached file path
639+ path = bic.graph.multicut_problem_path(sample = " C" , size = " medium" )
640+ ```
641+
642+ Valid samples are ` "A" ` , ` "B" ` , ` "C" ` ; valid sizes are ` "small" ` and
643+ ` "medium" ` . The legacy ` load_external_multicut_problem ` /
644+ ` load_external_multicut_problem_data ` / ` external_multicut_problem_path `
645+ shims default to sample A, size small and continue to honor the
646+ ` BIOIMAGE_CPP_EXTERNAL_MULTICUT_PATH ` and
647+ ` BIOIMAGE_CPP_EXTERNAL_MULTICUT_CACHE ` environment variables.
648+
649+ Lifted multicut problems (2D ISBI slice and full 3D volume, built by
650+ ` examples/segmentation/serialize_lifted_problem.py ` ):
651+
652+ ``` python
653+ problem = bic.graph.load_lifted_multicut_problem(size = " 2d" )
654+ # Fields: n_nodes (int), local_uvs, local_costs, lifted_uvs, lifted_costs.
655+ graph = bic.graph.UndirectedGraph.from_edges(problem.n_nodes, problem.local_uvs)
656+ objective = bic.graph.LiftedMulticutObjective(
657+ graph,
658+ problem.local_costs,
659+ lifted_uvs = problem.lifted_uvs,
660+ lifted_costs = problem.lifted_costs,
661+ )
662+ ```
663+
664+ Notes:
665+
666+ - Every download is integrity-checked against a SHA256 in the registry; a
667+ corrupted cache file is detected on the next ` load_* ` call.
668+ - Downloads are lazy: nothing happens until you call a loader. Re-runs are
669+ free (the cached file is reused).
670+ - For air-gapped use, fetch the file once on a machine with network access
671+ and copy ` ~/.cache/bioimage-cpp/<filename> ` to the same path on the target
672+ machine.
673+
674+ ## Breadth-First Search
675+
676+ Nifty has an internal ` BreadthFirstSearch ` template used during lifted-edge
677+ insertion. ` bioimage-cpp ` exposes a Python-friendly free function:
678+
679+ ``` python
680+ nodes, distances = bic.graph.breadth_first_search(
681+ graph,
682+ source,
683+ max_distance = 3 , # optional, default: full component
684+ include_source = True , # set to False for k-hop neighborhoods excluding self
685+ )
686+ ```
687+
688+ Both output arrays are 1D ` uint64 ` , listing reached nodes in BFS order with
689+ their hop distance from the source. Useful for building lifted-edge sets
690+ manually, sampling local neighborhoods, or computing graph distances.
691+
489692## Segmentation Overlaps
490693
491694Nifty:
0 commit comments