Skip to content

Commit a7e8a51

Browse files
Add overlap computation functionality
1 parent 97259fe commit a7e8a51

9 files changed

Lines changed: 900 additions & 1 deletion

File tree

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ nanobind_add_module(_core
1414
src/bindings/blocking.cxx
1515
src/bindings/module.cxx
1616
src/bindings/graph.cxx
17+
src/bindings/ground_truth.cxx
1718
src/bindings/segmentation.cxx
1819
src/bindings/utils.cxx
1920
src/cpp/segmentation/mutex_watershed.cxx

MIGRATION_GUIDE.md

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ import bioimage_cpp as bic
1616
```
1717

1818
Graph functionality is under `bic.graph`, segmentation functionality is under
19-
`bic.segmentation`, and small utility functions are under `bic.utils`.
19+
`bic.segmentation`, ground-truth comparison functionality is under
20+
`bic.ground_truth`, and small utility functions are under `bic.utils`.
2021

2122
## Blocking
2223

@@ -220,6 +221,74 @@ Notes:
220221
- `number_of_threads=0` uses the library default; pass a positive integer for a
221222
fixed thread count.
222223

224+
## Segmentation Overlaps
225+
226+
Nifty:
227+
228+
```python
229+
import nifty.ground_truth as ngt
230+
231+
overlap = ngt.overlap(segmentation, ground_truth)
232+
```
233+
234+
bioimage-cpp:
235+
236+
```python
237+
import bioimage_cpp as bic
238+
239+
overlap = bic.ground_truth.segmentation_overlap(segmentation, ground_truth)
240+
```
241+
242+
The first input is called `labels_a` and the second input is called `labels_b`
243+
in the `bioimage-cpp` API. Use named structured tables instead of positional
244+
arrays:
245+
246+
```python
247+
table = overlap.overlap_table()
248+
# fields: "label_a", "label_b", "count"
249+
250+
table = overlap.overlap_table(normalize_by="a")
251+
# fields: "label_a", "label_b", "count", "fraction"
252+
253+
overlaps = overlap.overlaps_for_label_a(12, normalize=True)
254+
# fields: "label", "count", "fraction"
255+
256+
best = overlap.best_overlap_for_label_a(12, ignore_zero=True)
257+
# BestOverlap(label=..., count=..., fraction=..., found=...)
258+
```
259+
260+
Other common queries:
261+
262+
```python
263+
overlap.labels_a
264+
overlap.labels_b
265+
overlap.count_a(12)
266+
overlap.count_b(4)
267+
overlap.overlap_count(12, 4)
268+
overlap.counts_a_table()
269+
overlap.counts_b_table()
270+
overlap.best_overlap_for_label_b(4)
271+
overlap.is_label_a_overlapping_with_zero(12)
272+
overlap.different_overlap(12, 13)
273+
```
274+
275+
Intentional improvements over nifty:
276+
277+
- Labels are stored sparsely, so large sparse label ids do not require a dense
278+
vector up to `max_label + 1`.
279+
- The Python API returns structured arrays with named fields and a
280+
`BestOverlap` dataclass instead of ambiguous positional arrays.
281+
- Both overlap directions are supported explicitly:
282+
`overlaps_for_label_a(...)` and `overlaps_for_label_b(...)`.
283+
- Normalization is explicit via `normalize_by="a"`, `"b"`, or `"total"`.
284+
- Missing labels return count `0`; best-overlap queries expose `found=False`.
285+
286+
Notes:
287+
288+
- Inputs must be integer arrays with identical shape.
289+
- Signed integer inputs must not contain negative labels.
290+
- Inputs are converted to contiguous `uint64` arrays before entering C++.
291+
223292
## Mutex Watershed
224293

225294
Affogato:

include/bioimage_cpp/overlap.hxx

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
#pragma once
2+
3+
#include "bioimage_cpp/array_view.hxx"
4+
5+
#include <algorithm>
6+
#include <cstdint>
7+
#include <stdexcept>
8+
#include <string>
9+
#include <unordered_map>
10+
#include <utility>
11+
#include <vector>
12+
13+
namespace bioimage_cpp::ground_truth {
14+
15+
struct OverlapPair {
16+
std::uint64_t label_a = 0;
17+
std::uint64_t label_b = 0;
18+
std::uint64_t count = 0;
19+
};
20+
21+
class SegmentationOverlap {
22+
public:
23+
void add(const std::uint64_t label_a, const std::uint64_t label_b) {
24+
++overlaps_a_[label_a][label_b];
25+
++overlaps_b_[label_b][label_a];
26+
++counts_a_[label_a];
27+
++counts_b_[label_b];
28+
++total_count_;
29+
}
30+
31+
std::uint64_t total_count() const {
32+
return total_count_;
33+
}
34+
35+
std::uint64_t count_a(const std::uint64_t label) const {
36+
return map_value_or_zero(counts_a_, label);
37+
}
38+
39+
std::uint64_t count_b(const std::uint64_t label) const {
40+
return map_value_or_zero(counts_b_, label);
41+
}
42+
43+
std::uint64_t overlap_count(
44+
const std::uint64_t label_a,
45+
const std::uint64_t label_b
46+
) const {
47+
const auto found_a = overlaps_a_.find(label_a);
48+
if (found_a == overlaps_a_.end()) {
49+
return 0;
50+
}
51+
return map_value_or_zero(found_a->second, label_b);
52+
}
53+
54+
std::vector<std::uint64_t> labels_a() const {
55+
return sorted_keys(counts_a_);
56+
}
57+
58+
std::vector<std::uint64_t> labels_b() const {
59+
return sorted_keys(counts_b_);
60+
}
61+
62+
std::vector<std::pair<std::uint64_t, std::uint64_t>> counts_a() const {
63+
return sorted_label_counts(counts_a_);
64+
}
65+
66+
std::vector<std::pair<std::uint64_t, std::uint64_t>> counts_b() const {
67+
return sorted_label_counts(counts_b_);
68+
}
69+
70+
std::vector<OverlapPair> overlap_pairs() const {
71+
std::vector<OverlapPair> result;
72+
for (const auto &label_overlaps : overlaps_a_) {
73+
for (const auto &overlap : label_overlaps.second) {
74+
result.push_back(OverlapPair{
75+
label_overlaps.first,
76+
overlap.first,
77+
overlap.second,
78+
});
79+
}
80+
}
81+
sort_overlap_pairs(result);
82+
return result;
83+
}
84+
85+
std::vector<std::pair<std::uint64_t, std::uint64_t>> overlaps_for_label_a(
86+
const std::uint64_t label_a
87+
) const {
88+
return overlaps_for_label(overlaps_a_, label_a);
89+
}
90+
91+
std::vector<std::pair<std::uint64_t, std::uint64_t>> overlaps_for_label_b(
92+
const std::uint64_t label_b
93+
) const {
94+
return overlaps_for_label(overlaps_b_, label_b);
95+
}
96+
97+
std::pair<std::uint64_t, std::uint64_t> best_overlap_for_label_a(
98+
const std::uint64_t label_a,
99+
const bool ignore_zero = false
100+
) const {
101+
return best_overlap(overlaps_a_, label_a, ignore_zero);
102+
}
103+
104+
std::pair<std::uint64_t, std::uint64_t> best_overlap_for_label_b(
105+
const std::uint64_t label_b,
106+
const bool ignore_zero = false
107+
) const {
108+
return best_overlap(overlaps_b_, label_b, ignore_zero);
109+
}
110+
111+
bool is_label_a_overlapping_with_zero(const std::uint64_t label_a) const {
112+
return overlap_count(label_a, 0) != 0;
113+
}
114+
115+
bool is_label_b_overlapping_with_zero(const std::uint64_t label_b) const {
116+
const auto found_b = overlaps_b_.find(label_b);
117+
if (found_b == overlaps_b_.end()) {
118+
return false;
119+
}
120+
return found_b->second.find(0) != found_b->second.end();
121+
}
122+
123+
double different_overlap(const std::uint64_t label_a_u, const std::uint64_t label_a_v) const {
124+
const auto found_u = overlaps_a_.find(label_a_u);
125+
const auto found_v = overlaps_a_.find(label_a_v);
126+
if (found_u == overlaps_a_.end() || found_v == overlaps_a_.end()) {
127+
throw std::out_of_range("labels must exist in segmentation A");
128+
}
129+
130+
const auto size_u = static_cast<double>(count_a(label_a_u));
131+
const auto size_v = static_cast<double>(count_a(label_a_v));
132+
double result = 0.0;
133+
for (const auto &overlap_u : found_u->second) {
134+
for (const auto &overlap_v : found_v->second) {
135+
if (overlap_u.first != overlap_v.first) {
136+
result +=
137+
(static_cast<double>(overlap_u.second) / size_u) *
138+
(static_cast<double>(overlap_v.second) / size_v);
139+
}
140+
}
141+
}
142+
return result;
143+
}
144+
145+
private:
146+
using CountMap = std::unordered_map<std::uint64_t, std::uint64_t>;
147+
using OverlapMap = std::unordered_map<std::uint64_t, CountMap>;
148+
149+
static std::uint64_t map_value_or_zero(
150+
const CountMap &map,
151+
const std::uint64_t label
152+
) {
153+
const auto found = map.find(label);
154+
return found == map.end() ? 0 : found->second;
155+
}
156+
157+
static std::vector<std::uint64_t> sorted_keys(const CountMap &map) {
158+
std::vector<std::uint64_t> result;
159+
result.reserve(map.size());
160+
for (const auto &entry : map) {
161+
result.push_back(entry.first);
162+
}
163+
std::sort(result.begin(), result.end());
164+
return result;
165+
}
166+
167+
static std::vector<std::pair<std::uint64_t, std::uint64_t>> sorted_label_counts(
168+
const CountMap &map
169+
) {
170+
std::vector<std::pair<std::uint64_t, std::uint64_t>> result;
171+
result.reserve(map.size());
172+
for (const auto &entry : map) {
173+
result.push_back(entry);
174+
}
175+
std::sort(result.begin(), result.end(), [](const auto &a, const auto &b) {
176+
return a.first < b.first;
177+
});
178+
return result;
179+
}
180+
181+
static void sort_overlap_pairs(std::vector<OverlapPair> &pairs) {
182+
std::sort(pairs.begin(), pairs.end(), [](const auto &a, const auto &b) {
183+
if (a.label_a != b.label_a) {
184+
return a.label_a < b.label_a;
185+
}
186+
return a.label_b < b.label_b;
187+
});
188+
}
189+
190+
static std::vector<std::pair<std::uint64_t, std::uint64_t>> overlaps_for_label(
191+
const OverlapMap &overlaps,
192+
const std::uint64_t label
193+
) {
194+
const auto found = overlaps.find(label);
195+
if (found == overlaps.end()) {
196+
return {};
197+
}
198+
return sorted_label_counts(found->second);
199+
}
200+
201+
static std::pair<std::uint64_t, std::uint64_t> best_overlap(
202+
const OverlapMap &overlaps,
203+
const std::uint64_t label,
204+
const bool ignore_zero
205+
) {
206+
const auto found = overlaps.find(label);
207+
if (found == overlaps.end()) {
208+
return {0, 0};
209+
}
210+
211+
std::uint64_t best_label = 0;
212+
std::uint64_t best_count = 0;
213+
for (const auto &overlap : found->second) {
214+
if (ignore_zero && overlap.first == 0) {
215+
continue;
216+
}
217+
if (
218+
overlap.second > best_count ||
219+
(overlap.second == best_count && overlap.first < best_label)
220+
) {
221+
best_label = overlap.first;
222+
best_count = overlap.second;
223+
}
224+
}
225+
return {best_label, best_count};
226+
}
227+
228+
CountMap counts_a_;
229+
CountMap counts_b_;
230+
OverlapMap overlaps_a_;
231+
OverlapMap overlaps_b_;
232+
std::uint64_t total_count_ = 0;
233+
};
234+
235+
inline std::uint64_t array_size(const std::vector<std::ptrdiff_t> &shape) {
236+
std::uint64_t size = 1;
237+
for (const auto axis_size : shape) {
238+
if (axis_size < 0) {
239+
throw std::invalid_argument("shape entries must be non-negative");
240+
}
241+
size *= static_cast<std::uint64_t>(axis_size);
242+
}
243+
return size;
244+
}
245+
246+
inline SegmentationOverlap segmentation_overlap(
247+
const ConstArrayView<std::uint64_t> &labels_a,
248+
const ConstArrayView<std::uint64_t> &labels_b
249+
) {
250+
if (labels_a.shape != labels_b.shape) {
251+
throw std::invalid_argument("labels_a and labels_b must have the same shape");
252+
}
253+
254+
SegmentationOverlap result;
255+
const auto size = array_size(labels_a.shape);
256+
for (std::uint64_t index = 0; index < size; ++index) {
257+
result.add(labels_a.data[index], labels_b.data[index]);
258+
}
259+
return result;
260+
}
261+
262+
} // namespace bioimage_cpp::ground_truth

0 commit comments

Comments
 (0)