Skip to content

Commit 78c1bcc

Browse files
2 parents 7f3dce3 + fd601d7 commit 78c1bcc

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
@@ -1420,6 +1420,48 @@ Notes:
14201420
- `number_of_threads=0` uses the library default; pass a positive integer for a
14211421
fixed thread count.
14221422

1423+
### Accumulating Labels on a RAG
1424+
1425+
Nifty's `gridRagAccumulateLabels` projects a second label volume onto a RAG
1426+
by taking a per-node majority vote (commonly used to project a ground-truth
1427+
segmentation onto an over-segmentation).
1428+
1429+
Nifty:
1430+
1431+
```python
1432+
import nifty.graph.rag as nrag
1433+
1434+
rag = nrag.gridRag(labels)
1435+
node_labels = nrag.gridRagAccumulateLabels(rag, gt)
1436+
# ignore label 0 in the ground truth (nifty's "ignoreBackground"):
1437+
node_labels = nrag.gridRagAccumulateLabels(rag, gt, ignoreBackground=True)
1438+
```
1439+
1440+
bioimage-cpp:
1441+
1442+
```python
1443+
rag = bic.graph.region_adjacency_graph(labels)
1444+
node_labels = bic.graph.features.accumulate_labels(rag, labels, gt)
1445+
# arbitrary ignore value (covers nifty's ignoreBackground=True by passing 0):
1446+
node_labels = bic.graph.features.accumulate_labels(
1447+
rag, labels, gt, ignore_value=0
1448+
)
1449+
```
1450+
1451+
Notes:
1452+
1453+
- `labels` must be the over-segmentation used to construct `rag`.
1454+
- `other_labels` must have the same shape as `labels`. Supported dtypes for
1455+
both arrays: `uint32`, `uint64`, `int32`, `int64`; they may differ.
1456+
- The output has length `rag.number_of_nodes` and the same dtype as
1457+
`other_labels`. Nodes whose pixels are all ignored receive `0`.
1458+
- Ties in the majority vote are broken by smaller label id (deterministic).
1459+
Nifty's tie-breaking depends on `std::unordered_map` iteration order and
1460+
is therefore platform-dependent; `bic` resolves ties deterministically.
1461+
- `ignore_value` is more general than nifty's boolean `ignoreBackground`:
1462+
pass `0` to reproduce `ignoreBackground=True`, or any other value to skip
1463+
arbitrary sentinels (e.g. `255` or `-1`).
1464+
14231465
### RAG Boundary and Affinity Features
14241466

14251467
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"
@@ -117,6 +118,20 @@ DoubleArray make_double_array(const std::vector<std::size_t> &shape) {
117118
return DoubleArray(data, shape.size(), shape.data(), owner);
118119
}
119120

121+
template <class T>
122+
using TypedArray = nb::ndarray<nb::numpy, T, nb::c_contig>;
123+
124+
template <class T>
125+
TypedArray<T> make_typed_array(const std::vector<std::size_t> &shape) {
126+
std::size_t size = 1;
127+
for (const auto axis_size : shape) {
128+
size *= axis_size;
129+
}
130+
auto *data = new T[size]();
131+
nb::capsule owner(data, [](void *p) noexcept { delete[] static_cast<T *>(p); });
132+
return TypedArray<T>(data, shape.size(), shape.data(), owner);
133+
}
134+
120135
template <class T>
121136
FloatingArray<T> make_floating_array(const std::vector<std::size_t> &shape) {
122137
std::size_t size = 1;
@@ -1301,6 +1316,55 @@ UInt64Array project_node_labels_to_pixels_t(
13011316
return result;
13021317
}
13031318

1319+
template <class LabelT, class OtherT>
1320+
TypedArray<OtherT> accumulate_labels_t(
1321+
const Rag &rag,
1322+
LabelArray<LabelT> labels,
1323+
LabelArray<OtherT> other_labels,
1324+
const bool has_ignore_value,
1325+
const OtherT ignore_value,
1326+
const std::size_t number_of_threads
1327+
) {
1328+
if (labels.ndim() != other_labels.ndim()) {
1329+
throw std::invalid_argument("other_labels shape must match labels shape");
1330+
}
1331+
for (std::size_t axis = 0; axis < labels.ndim(); ++axis) {
1332+
if (labels.shape(axis) != other_labels.shape(axis)) {
1333+
throw std::invalid_argument("other_labels shape must match labels shape");
1334+
}
1335+
}
1336+
1337+
auto result = make_typed_array<OtherT>({static_cast<std::size_t>(rag.number_of_nodes())});
1338+
1339+
ConstArrayView<LabelT> labels_view{
1340+
labels.data(),
1341+
ndarray_shape(labels),
1342+
{},
1343+
};
1344+
ConstArrayView<OtherT> other_labels_view{
1345+
other_labels.data(),
1346+
ndarray_shape(other_labels),
1347+
{},
1348+
};
1349+
ArrayView<OtherT> out_view{
1350+
result.data(),
1351+
{static_cast<std::ptrdiff_t>(rag.number_of_nodes())},
1352+
{},
1353+
};
1354+
1355+
nb::gil_scoped_release release;
1356+
graph::accumulate_labels<LabelT, OtherT>(
1357+
rag,
1358+
labels_view,
1359+
other_labels_view,
1360+
has_ignore_value,
1361+
ignore_value,
1362+
number_of_threads,
1363+
out_view
1364+
);
1365+
return result;
1366+
}
1367+
13041368
} // namespace
13051369

13061370
void bind_graph(nb::module_ &m) {
@@ -1986,6 +2050,37 @@ void bind_graph(nb::module_ &m) {
19862050
nb::arg("node_labels"),
19872051
nb::arg("number_of_threads")
19882052
);
2053+
2054+
#define BIC_BIND_ACCUMULATE_LABELS(LSUF, LT, OSUF, OT) \
2055+
m.def( \
2056+
"_accumulate_labels_" #LSUF "_" #OSUF, \
2057+
&accumulate_labels_t<LT, OT>, \
2058+
nb::arg("rag"), \
2059+
nb::arg("labels"), \
2060+
nb::arg("other_labels"), \
2061+
nb::arg("has_ignore_value"), \
2062+
nb::arg("ignore_value"), \
2063+
nb::arg("number_of_threads") \
2064+
)
2065+
2066+
BIC_BIND_ACCUMULATE_LABELS(uint32, std::uint32_t, uint32, std::uint32_t);
2067+
BIC_BIND_ACCUMULATE_LABELS(uint32, std::uint32_t, uint64, std::uint64_t);
2068+
BIC_BIND_ACCUMULATE_LABELS(uint32, std::uint32_t, int32, std::int32_t);
2069+
BIC_BIND_ACCUMULATE_LABELS(uint32, std::uint32_t, int64, std::int64_t);
2070+
BIC_BIND_ACCUMULATE_LABELS(uint64, std::uint64_t, uint32, std::uint32_t);
2071+
BIC_BIND_ACCUMULATE_LABELS(uint64, std::uint64_t, uint64, std::uint64_t);
2072+
BIC_BIND_ACCUMULATE_LABELS(uint64, std::uint64_t, int32, std::int32_t);
2073+
BIC_BIND_ACCUMULATE_LABELS(uint64, std::uint64_t, int64, std::int64_t);
2074+
BIC_BIND_ACCUMULATE_LABELS(int32, std::int32_t, uint32, std::uint32_t);
2075+
BIC_BIND_ACCUMULATE_LABELS(int32, std::int32_t, uint64, std::uint64_t);
2076+
BIC_BIND_ACCUMULATE_LABELS(int32, std::int32_t, int32, std::int32_t);
2077+
BIC_BIND_ACCUMULATE_LABELS(int32, std::int32_t, int64, std::int64_t);
2078+
BIC_BIND_ACCUMULATE_LABELS(int64, std::int64_t, uint32, std::uint32_t);
2079+
BIC_BIND_ACCUMULATE_LABELS(int64, std::int64_t, uint64, std::uint64_t);
2080+
BIC_BIND_ACCUMULATE_LABELS(int64, std::int64_t, int32, std::int32_t);
2081+
BIC_BIND_ACCUMULATE_LABELS(int64, std::int64_t, int64, std::int64_t);
2082+
2083+
#undef BIC_BIND_ACCUMULATE_LABELS
19892084
}
19902085

19912086
} // namespace bioimage_cpp::bindings

0 commit comments

Comments
 (0)