Skip to content

Commit 2b9db07

Browse files
Add rag label accumulation (#43)
1 parent f5dec3b commit 2b9db07

5 files changed

Lines changed: 640 additions & 0 deletions

File tree

MIGRATION_GUIDE.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1385,6 +1385,48 @@ Notes:
13851385
- `number_of_threads=0` uses the library default; pass a positive integer for a
13861386
fixed thread count.
13871387

1388+
### Accumulating Labels on a RAG
1389+
1390+
Nifty's `gridRagAccumulateLabels` projects a second label volume onto a RAG
1391+
by taking a per-node majority vote (commonly used to project a ground-truth
1392+
segmentation onto an over-segmentation).
1393+
1394+
Nifty:
1395+
1396+
```python
1397+
import nifty.graph.rag as nrag
1398+
1399+
rag = nrag.gridRag(labels)
1400+
node_labels = nrag.gridRagAccumulateLabels(rag, gt)
1401+
# ignore label 0 in the ground truth (nifty's "ignoreBackground"):
1402+
node_labels = nrag.gridRagAccumulateLabels(rag, gt, ignoreBackground=True)
1403+
```
1404+
1405+
bioimage-cpp:
1406+
1407+
```python
1408+
rag = bic.graph.region_adjacency_graph(labels)
1409+
node_labels = bic.graph.features.accumulate_labels(rag, labels, gt)
1410+
# arbitrary ignore value (covers nifty's ignoreBackground=True by passing 0):
1411+
node_labels = bic.graph.features.accumulate_labels(
1412+
rag, labels, gt, ignore_value=0
1413+
)
1414+
```
1415+
1416+
Notes:
1417+
1418+
- `labels` must be the over-segmentation used to construct `rag`.
1419+
- `other_labels` must have the same shape as `labels`. Supported dtypes for
1420+
both arrays: `uint32`, `uint64`, `int32`, `int64`; they may differ.
1421+
- The output has length `rag.number_of_nodes` and the same dtype as
1422+
`other_labels`. Nodes whose pixels are all ignored receive `0`.
1423+
- Ties in the majority vote are broken by smaller label id (deterministic).
1424+
Nifty's tie-breaking depends on `std::unordered_map` iteration order and
1425+
is therefore platform-dependent; `bic` resolves ties deterministically.
1426+
- `ignore_value` is more general than nifty's boolean `ignoreBackground`:
1427+
pass `0` to reproduce `ignoreBackground=True`, or any other value to skip
1428+
arbitrary sentinels (e.g. `255` or `-1`).
1429+
13881430
### RAG Boundary and Affinity Features
13891431

13901432
Nifty has RAG feature helpers such as `accumulateEdgeMeanAndLength`,
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
#pragma once
2+
3+
#include "bioimage_cpp/array_view.hxx"
4+
#include "bioimage_cpp/detail/threading.hxx"
5+
#include "bioimage_cpp/graph/node_label_projection.hxx"
6+
#include "bioimage_cpp/graph/region_adjacency_graph.hxx"
7+
8+
#include <cstddef>
9+
#include <cstdint>
10+
#include <numeric>
11+
#include <stdexcept>
12+
#include <string>
13+
#include <unordered_map>
14+
#include <vector>
15+
16+
namespace bioimage_cpp::graph {
17+
18+
namespace detail_label_accumulation {
19+
20+
inline std::size_t number_of_pixels(const std::vector<std::ptrdiff_t> &shape) {
21+
return static_cast<std::size_t>(std::accumulate(
22+
shape.begin(),
23+
shape.end(),
24+
std::ptrdiff_t{1},
25+
[](const std::ptrdiff_t a, const std::ptrdiff_t b) { return a * b; }
26+
));
27+
}
28+
29+
template <class LabelT, class OtherT>
30+
void scan_chunk(
31+
const LabelT *labels,
32+
const OtherT *other_labels,
33+
const std::size_t pixel_begin,
34+
const std::size_t pixel_end,
35+
const bool has_ignore_value,
36+
const OtherT ignore_value,
37+
std::vector<std::unordered_map<OtherT, std::uint64_t>> &histograms
38+
) {
39+
for (std::size_t index = pixel_begin; index < pixel_end; ++index) {
40+
const auto other = other_labels[index];
41+
if (has_ignore_value && other == ignore_value) {
42+
continue;
43+
}
44+
const auto node = detail::checked_label_to_node(labels[index]);
45+
++histograms[static_cast<std::size_t>(node)][other];
46+
}
47+
}
48+
49+
} // namespace detail_label_accumulation
50+
51+
template <class LabelT, class OtherT>
52+
void accumulate_labels(
53+
const RegionAdjacencyGraph &rag,
54+
const ConstArrayView<LabelT> &labels,
55+
const ConstArrayView<OtherT> &other_labels,
56+
const bool has_ignore_value,
57+
const OtherT ignore_value,
58+
const std::size_t number_of_threads,
59+
const ArrayView<OtherT> &out
60+
) {
61+
if (labels.ndim() != 2 && labels.ndim() != 3) {
62+
throw std::invalid_argument(
63+
"labels must be a 2D or 3D array, got ndim=" +
64+
std::to_string(labels.ndim())
65+
);
66+
}
67+
detail_projection::require_rag_shape_matches_labels(rag, labels.shape);
68+
if (other_labels.shape != labels.shape) {
69+
throw std::invalid_argument("other_labels shape must match labels shape");
70+
}
71+
const auto number_of_nodes = static_cast<std::size_t>(rag.number_of_nodes());
72+
if (out.shape != std::vector<std::ptrdiff_t>{static_cast<std::ptrdiff_t>(number_of_nodes)}) {
73+
throw std::invalid_argument(
74+
"out shape must be (number_of_nodes,)"
75+
);
76+
}
77+
if (detail::max_label(labels) >= static_cast<std::uint64_t>(number_of_nodes)) {
78+
throw std::invalid_argument("labels contain a node id outside the rag");
79+
}
80+
81+
const auto n_pixels = detail_label_accumulation::number_of_pixels(labels.shape);
82+
const auto n_threads = detail::normalize_thread_count(number_of_threads, n_pixels);
83+
84+
std::vector<std::vector<std::unordered_map<OtherT, std::uint64_t>>> per_thread(
85+
n_threads,
86+
std::vector<std::unordered_map<OtherT, std::uint64_t>>(number_of_nodes)
87+
);
88+
89+
bioimage_cpp::detail::parallel_for_chunks(
90+
n_threads,
91+
n_pixels,
92+
[&](const std::size_t thread_id, const std::size_t begin, const std::size_t end) {
93+
detail_label_accumulation::scan_chunk(
94+
labels.data,
95+
other_labels.data,
96+
begin,
97+
end,
98+
has_ignore_value,
99+
ignore_value,
100+
per_thread[thread_id]
101+
);
102+
}
103+
);
104+
105+
// Merge per-thread histograms and pick majority per node in one pass over
106+
// nodes. This is embarrassingly parallel across nodes.
107+
const auto node_threads = detail::normalize_thread_count(number_of_threads, number_of_nodes);
108+
bioimage_cpp::detail::parallel_for_chunks(
109+
node_threads,
110+
number_of_nodes,
111+
[&](const std::size_t, const std::size_t node_begin, const std::size_t node_end) {
112+
for (std::size_t node = node_begin; node < node_end; ++node) {
113+
std::unordered_map<OtherT, std::uint64_t> merged;
114+
for (auto &thread_histograms : per_thread) {
115+
auto &node_hist = thread_histograms[node];
116+
for (const auto &entry : node_hist) {
117+
merged[entry.first] += entry.second;
118+
}
119+
node_hist.clear();
120+
}
121+
OtherT best_label = OtherT{0};
122+
std::uint64_t best_count = 0;
123+
bool has_best = false;
124+
for (const auto &entry : merged) {
125+
if (!has_best ||
126+
entry.second > best_count ||
127+
(entry.second == best_count && entry.first < best_label)) {
128+
best_label = entry.first;
129+
best_count = entry.second;
130+
has_best = true;
131+
}
132+
}
133+
out.data[node] = has_best ? best_label : OtherT{0};
134+
}
135+
}
136+
);
137+
}
138+
139+
} // namespace bioimage_cpp::graph

src/bindings/graph.cxx

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include "bioimage_cpp/graph/feature_accumulation.hxx"
88
#include "bioimage_cpp/graph/grid_features.hxx"
99
#include "bioimage_cpp/graph/grid_graph.hxx"
10+
#include "bioimage_cpp/graph/label_accumulation.hxx"
1011
#include "bioimage_cpp/graph/lifted_from_affinities.hxx"
1112
#include "bioimage_cpp/graph/lifted_multicut.hxx"
1213
#include "bioimage_cpp/graph/lifted_multicut/fusion_move.hxx"
@@ -113,6 +114,20 @@ DoubleArray make_double_array(const std::vector<std::size_t> &shape) {
113114
return DoubleArray(data, shape.size(), shape.data(), owner);
114115
}
115116

117+
template <class T>
118+
using TypedArray = nb::ndarray<nb::numpy, T, nb::c_contig>;
119+
120+
template <class T>
121+
TypedArray<T> make_typed_array(const std::vector<std::size_t> &shape) {
122+
std::size_t size = 1;
123+
for (const auto axis_size : shape) {
124+
size *= axis_size;
125+
}
126+
auto *data = new T[size]();
127+
nb::capsule owner(data, [](void *p) noexcept { delete[] static_cast<T *>(p); });
128+
return TypedArray<T>(data, shape.size(), shape.data(), owner);
129+
}
130+
116131
template <class T>
117132
FloatingArray<T> make_floating_array(const std::vector<std::size_t> &shape) {
118133
std::size_t size = 1;
@@ -1245,6 +1260,55 @@ UInt64Array project_node_labels_to_pixels_t(
12451260
return result;
12461261
}
12471262

1263+
template <class LabelT, class OtherT>
1264+
TypedArray<OtherT> accumulate_labels_t(
1265+
const Rag &rag,
1266+
LabelArray<LabelT> labels,
1267+
LabelArray<OtherT> other_labels,
1268+
const bool has_ignore_value,
1269+
const OtherT ignore_value,
1270+
const std::size_t number_of_threads
1271+
) {
1272+
if (labels.ndim() != other_labels.ndim()) {
1273+
throw std::invalid_argument("other_labels shape must match labels shape");
1274+
}
1275+
for (std::size_t axis = 0; axis < labels.ndim(); ++axis) {
1276+
if (labels.shape(axis) != other_labels.shape(axis)) {
1277+
throw std::invalid_argument("other_labels shape must match labels shape");
1278+
}
1279+
}
1280+
1281+
auto result = make_typed_array<OtherT>({static_cast<std::size_t>(rag.number_of_nodes())});
1282+
1283+
ConstArrayView<LabelT> labels_view{
1284+
labels.data(),
1285+
ndarray_shape(labels),
1286+
{},
1287+
};
1288+
ConstArrayView<OtherT> other_labels_view{
1289+
other_labels.data(),
1290+
ndarray_shape(other_labels),
1291+
{},
1292+
};
1293+
ArrayView<OtherT> out_view{
1294+
result.data(),
1295+
{static_cast<std::ptrdiff_t>(rag.number_of_nodes())},
1296+
{},
1297+
};
1298+
1299+
nb::gil_scoped_release release;
1300+
graph::accumulate_labels<LabelT, OtherT>(
1301+
rag,
1302+
labels_view,
1303+
other_labels_view,
1304+
has_ignore_value,
1305+
ignore_value,
1306+
number_of_threads,
1307+
out_view
1308+
);
1309+
return result;
1310+
}
1311+
12481312
} // namespace
12491313

12501314
void bind_graph(nb::module_ &m) {
@@ -1889,6 +1953,37 @@ void bind_graph(nb::module_ &m) {
18891953
nb::arg("node_labels"),
18901954
nb::arg("number_of_threads")
18911955
);
1956+
1957+
#define BIC_BIND_ACCUMULATE_LABELS(LSUF, LT, OSUF, OT) \
1958+
m.def( \
1959+
"_accumulate_labels_" #LSUF "_" #OSUF, \
1960+
&accumulate_labels_t<LT, OT>, \
1961+
nb::arg("rag"), \
1962+
nb::arg("labels"), \
1963+
nb::arg("other_labels"), \
1964+
nb::arg("has_ignore_value"), \
1965+
nb::arg("ignore_value"), \
1966+
nb::arg("number_of_threads") \
1967+
)
1968+
1969+
BIC_BIND_ACCUMULATE_LABELS(uint32, std::uint32_t, uint32, std::uint32_t);
1970+
BIC_BIND_ACCUMULATE_LABELS(uint32, std::uint32_t, uint64, std::uint64_t);
1971+
BIC_BIND_ACCUMULATE_LABELS(uint32, std::uint32_t, int32, std::int32_t);
1972+
BIC_BIND_ACCUMULATE_LABELS(uint32, std::uint32_t, int64, std::int64_t);
1973+
BIC_BIND_ACCUMULATE_LABELS(uint64, std::uint64_t, uint32, std::uint32_t);
1974+
BIC_BIND_ACCUMULATE_LABELS(uint64, std::uint64_t, uint64, std::uint64_t);
1975+
BIC_BIND_ACCUMULATE_LABELS(uint64, std::uint64_t, int32, std::int32_t);
1976+
BIC_BIND_ACCUMULATE_LABELS(uint64, std::uint64_t, int64, std::int64_t);
1977+
BIC_BIND_ACCUMULATE_LABELS(int32, std::int32_t, uint32, std::uint32_t);
1978+
BIC_BIND_ACCUMULATE_LABELS(int32, std::int32_t, uint64, std::uint64_t);
1979+
BIC_BIND_ACCUMULATE_LABELS(int32, std::int32_t, int32, std::int32_t);
1980+
BIC_BIND_ACCUMULATE_LABELS(int32, std::int32_t, int64, std::int64_t);
1981+
BIC_BIND_ACCUMULATE_LABELS(int64, std::int64_t, uint32, std::uint32_t);
1982+
BIC_BIND_ACCUMULATE_LABELS(int64, std::int64_t, uint64, std::uint64_t);
1983+
BIC_BIND_ACCUMULATE_LABELS(int64, std::int64_t, int32, std::int32_t);
1984+
BIC_BIND_ACCUMULATE_LABELS(int64, std::int64_t, int64, std::int64_t);
1985+
1986+
#undef BIC_BIND_ACCUMULATE_LABELS
18921987
}
18931988

18941989
} // namespace bioimage_cpp::bindings

0 commit comments

Comments
 (0)