Skip to content
Merged
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
48 changes: 48 additions & 0 deletions MIGRATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,54 @@ Notes:
endpoint ids.
- Non-contiguous labels are copied to contiguous memory before entering C++.

### Distributed Region Adjacency Graphs and Features

For volumes too large to hold in memory, `nifty.distributed` builds a RAG and
its edge features block by block and merges the results. `bioimage-cpp` provides
the equivalent **low-level primitives** under `bic.graph.distributed`; the
orchestration nifty bundles (iterating blocks, sizing halos, and serializing the
per-block subgraphs/features to zarr/N5/HDF5) is intentionally left to the
caller, since I/O and block scheduling belong in Python.

The primitives assume **globally consistent labels** (a segment has the same id
in every block — as after a stitched distributed watershed). A block owns the
pixel-pairs whose reference pixel lies in its inner (non-halo) box, so the
caller reads each block with a halo (≥1 on the forward faces for the region
graph / an edge map; ≥ `max |offset|` per side for affinities) and passes the
owned box as `own_begin` / `own_shape` (e.g. from `bic.utils.Blocking`'s
`get_block_with_halo(...).inner_block_local`).

```python
d = bic.graph.distributed

# per block (labels read with a halo):
edges = d.block_region_adjacency_edges(labels_block, own_begin, own_shape)
block_edges, block_stats = d.block_edge_map_stats(labels_block, edge_map_block, own_begin, own_shape)
block_edges, block_stats = d.block_affinity_stats(labels_block, aff_block, offsets, own_begin, own_shape)

# merge the graph, then build the global graph:
global_edges = d.merge_edges([edges_block_0, edges_block_1, ...])
graph = bic.graph.UndirectedGraph.from_unique_edges(number_of_nodes, global_edges)

# fold per-block features onto the global edges, then finalize:
acc = d.empty_edge_stats(graph.number_of_edges)
for be, bs in per_block_stats:
acc = d.merge_block_edge_stats(graph, acc, be, bs)
features = d.finalize_edge_features(acc, compute_complex_features=True)
```

Notes:

- Blocked results reproduce the whole-volume `region_adjacency_graph` /
`features.*_features` exactly for `size`, `min` and `max`, and to
floating-point tolerance for `mean` / `std` (the running sums depend on thread
count and merge order).
- **Median and percentiles are not distributable.** The distributed complex
output is the moment subset `[mean, std, min, max, size]` — the corresponding
columns of the in-core 12-column complex features.
- Making per-block-local ids globally consistent (label stitching) is a separate
step and is not part of these primitives.

### Breadth-First Search

Nifty has an internal `BreadthFirstSearch` template used during lifted-edge
Expand Down
23 changes: 23 additions & 0 deletions include/bioimage_cpp/detail/grid.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,27 @@ inline bool is_valid_grid_edge(
return valid_offset_target(node, offset, shape, strides, unused);
}

// Given a signed offset `delta` along one axis and the axis `length`, return the
// half-open range of reference coordinates `[lo, hi)` for which `coord + delta`
// stays in `[0, length)`. Returns `lo >= hi` when the offset is larger than the
// axis (no valid reference coordinate). This is the per-axis primitive behind
// the offset-box sweeps in feature accumulation and the distributed block
// extraction — it depends only on grid geometry, not on any array data.
inline void valid_axis_range(
const std::ptrdiff_t delta,
const std::size_t length,
std::size_t &lo,
std::size_t &hi
) {
if (delta >= 0) {
lo = 0;
const auto d = static_cast<std::size_t>(delta);
hi = (d >= length) ? 0 : (length - d);
} else {
const auto d = static_cast<std::size_t>(-delta);
lo = (d >= length) ? length : d;
hi = length;
}
}

} // namespace bioimage_cpp::detail
23 changes: 23 additions & 0 deletions include/bioimage_cpp/detail/label_cast.hxx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#pragma once

#include <cstdint>
#include <stdexcept>
#include <type_traits>

namespace bioimage_cpp::detail {

// Convert a label value to a graph node id (`std::uint64_t`), rejecting negative
// values for signed label dtypes. Shared by the region-adjacency-graph scan,
// edge-feature accumulation, and the distributed block-extraction primitives so
// they all treat labels identically.
template <class T>
std::uint64_t checked_label_to_node(const T value) {
if constexpr (std::is_signed_v<T>) {
if (value < 0) {
throw std::invalid_argument("labels must not contain negative values");
}
}
return static_cast<std::uint64_t>(value);
}

} // namespace bioimage_cpp::detail
Loading
Loading