Skip to content

Commit 53847b1

Browse files
Update MWS checks and examples, speed it up (#5)
1 parent 3636996 commit 53847b1

6 files changed

Lines changed: 77 additions & 73 deletions

File tree

development/segmentation/_mutex_watershed_equivalence.py

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def run_affogato_reference(
6565
offsets: list[tuple[int, ...]],
6666
number_of_attractive_channels: int,
6767
) -> np.ndarray:
68-
from elf.segmentation.mutex_watershed import mutex_watershed
68+
from affogato.segmentation import compute_mws_segmentation
6969

7070
spatial_ndim = len(offsets[0])
7171
if number_of_attractive_channels != spatial_ndim:
@@ -74,7 +74,12 @@ def run_affogato_reference(
7474
f"spatial axis, got ndim={spatial_ndim}, attractive channels="
7575
f"{number_of_attractive_channels}"
7676
)
77-
return mutex_watershed(affinities.copy(), offsets, strides=[1] * spatial_ndim)
77+
affs = affinities.copy()
78+
affs[:spatial_ndim] *= -1
79+
affs[:spatial_ndim] += 1
80+
return compute_mws_segmentation(affs, offsets,
81+
number_of_attractive_channels=number_of_attractive_channels,
82+
strides=[1] * spatial_ndim)
7883

7984

8085
def _load_validation_metrics():
@@ -126,21 +131,41 @@ def compare_segmentations(
126131
return metrics
127132

128133

129-
def time_function(
130-
run: Callable[[np.ndarray, list[tuple[int, ...]], int], np.ndarray],
134+
def time_functions_interleaved(
135+
first: Callable[[np.ndarray, list[tuple[int, ...]], int], np.ndarray],
136+
second: Callable[[np.ndarray, list[tuple[int, ...]], int], np.ndarray],
131137
affinities: np.ndarray,
132138
offsets: list[tuple[int, ...]],
133139
number_of_attractive_channels: int,
134140
repeats: int,
135-
) -> tuple[list[float], np.ndarray]:
136-
timings = []
137-
result = None
138-
for _ in range(repeats):
141+
) -> tuple[list[float], np.ndarray, list[float], np.ndarray]:
142+
def timed_call(run):
139143
start = perf_counter()
140144
result = run(affinities, offsets, number_of_attractive_channels)
141-
timings.append(perf_counter() - start)
142-
assert result is not None
143-
return timings, result
145+
return perf_counter() - start, result
146+
147+
# Warm up imports, extension initialization, and first allocation paths
148+
# outside the timed section.
149+
first(affinities, offsets, number_of_attractive_channels)
150+
second(affinities, offsets, number_of_attractive_channels)
151+
152+
first_timings = []
153+
second_timings = []
154+
first_result = None
155+
second_result = None
156+
for repeat in range(repeats):
157+
if repeat % 2 == 0:
158+
first_time, first_result = timed_call(first)
159+
second_time, second_result = timed_call(second)
160+
else:
161+
second_time, second_result = timed_call(second)
162+
first_time, first_result = timed_call(first)
163+
first_timings.append(first_time)
164+
second_timings.append(second_time)
165+
166+
assert first_result is not None
167+
assert second_result is not None
168+
return first_timings, first_result, second_timings, second_result
144169

145170

146171
def print_report(
@@ -193,11 +218,13 @@ def run_check(
193218
else:
194219
raise ValueError(f"ndim must be 2 or 3, got {ndim}")
195220

196-
ref_timings, ref_seg = time_function(
197-
run_affogato_reference, affs, used_offsets, attractive_channels, repeats
198-
)
199-
bic_timings, bic_seg = time_function(
200-
run_bioimage_cpp, affs, used_offsets, attractive_channels, repeats
221+
ref_timings, ref_seg, bic_timings, bic_seg = time_functions_interleaved(
222+
run_affogato_reference,
223+
run_bioimage_cpp,
224+
affs,
225+
used_offsets,
226+
attractive_channels,
227+
repeats,
201228
)
202229
metrics = compare_segmentations(bic_seg, ref_seg)
203230
print_report(

development/segmentation/check_mutex_watershed_2d.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def main() -> None:
2121
type=int,
2222
nargs=2,
2323
metavar=("Y", "X"),
24-
default=(128, 128),
24+
default=(512, 512),
2525
help="Spatial crop shape for the 2D check.",
2626
)
2727
args = parser.parse_args()

development/segmentation/check_mutex_watershed_3d.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def main() -> None:
1515
type=int,
1616
nargs=3,
1717
metavar=("Z", "Y", "X"),
18-
default=(6, 96, 96),
18+
default=(6, 512, 512),
1919
help="Spatial crop shape for the 3D check.",
2020
)
2121
args = parser.parse_args()

examples/segmentation/mutex_watershed.py

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,6 @@ def mws_bic(affinities, offsets):
1818
return segmentation
1919

2020

21-
def mws_affogato(affinities, offsets):
22-
from elf.segmentation.mutex_watershed import mutex_watershed as mws
23-
print("Start MWS ...")
24-
affs = affinities.copy()
25-
segmentation = mws(affs, offsets, strides=[1, 1, 1])
26-
print("done ...")
27-
return segmentation
28-
29-
3021
def _filter_2d(affinities, offsets):
3122
chans_2d = [i for i, off in enumerate(offsets) if off[0] == 0]
3223
affinities = affinities[chans_2d][:, 0]
@@ -42,12 +33,7 @@ def main():
4233
check_2d = True
4334
if check_2d:
4435
affinities, offsets = _filter_2d(affinities, offsets)
45-
46-
use_reference_impl = False
47-
if use_reference_impl:
48-
segmentation = mws_affogato(affinities, offsets)
49-
else:
50-
segmentation = mws_bic(affinities, offsets)
36+
segmentation = mws_bic(affinities, offsets)
5137

5238
with open_file(data_path, "r") as f:
5339
raw = f["raw"][0] if check_2d else f["raw"][:]

include/bioimage_cpp/segmentation/mutex_watershed.hxx

Lines changed: 30 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
#include <numeric>
99
#include <stdexcept>
1010
#include <string>
11-
#include <unordered_map>
1211
#include <unordered_set>
1312
#include <vector>
1413

@@ -45,6 +44,12 @@ private:
4544

4645
using MutexStorage = std::vector<std::unordered_set<std::uint64_t>>;
4746

47+
template <class T>
48+
struct WeightedGridEdge {
49+
T weight;
50+
std::uint64_t id;
51+
};
52+
4853
inline bool check_mutex(
4954
const std::uint64_t first,
5055
const std::uint64_t second,
@@ -97,22 +102,6 @@ inline std::vector<std::ptrdiff_t> c_order_strides(const std::vector<std::ptrdif
97102
return strides;
98103
}
99104

100-
inline bool is_valid_grid_edge(
101-
const std::uint64_t node,
102-
const std::vector<std::ptrdiff_t> &offset,
103-
const std::vector<std::ptrdiff_t> &shape,
104-
const std::vector<std::ptrdiff_t> &strides
105-
) {
106-
for (std::size_t axis = 0; axis < shape.size(); ++axis) {
107-
const auto coord = static_cast<std::ptrdiff_t>(node / static_cast<std::uint64_t>(strides[axis])) % shape[axis];
108-
const auto neighbor = coord + offset[axis];
109-
if (neighbor < 0 || neighbor >= shape[axis]) {
110-
return false;
111-
}
112-
}
113-
return true;
114-
}
115-
116105
template <class T>
117106
void mutex_watershed_grid(
118107
const ConstArrayView<T> &affinities,
@@ -178,58 +167,59 @@ void mutex_watershed_grid(
178167
}
179168

180169
const auto number_of_edges = number_of_nodes * number_of_channels;
181-
std::vector<std::uint64_t> edge_order(static_cast<std::size_t>(number_of_edges));
182-
std::iota(edge_order.begin(), edge_order.end(), std::uint64_t{0});
183-
std::sort(edge_order.begin(), edge_order.end(), [&](const std::uint64_t first, const std::uint64_t second) {
184-
const T first_weight = affinities.data[first];
185-
const T second_weight = affinities.data[second];
186-
if (first_weight == second_weight) {
187-
return first < second;
170+
std::vector<WeightedGridEdge<T>> edge_order;
171+
edge_order.reserve(static_cast<std::size_t>(number_of_edges));
172+
for (std::uint64_t edge_id = 0; edge_id < number_of_edges; ++edge_id) {
173+
if (valid_edges.data[edge_id] != 0) {
174+
edge_order.push_back(WeightedGridEdge<T>{affinities.data[edge_id], edge_id});
188175
}
189-
return first_weight > second_weight;
176+
}
177+
std::sort(edge_order.begin(), edge_order.end(), [](const auto &first, const auto &second) {
178+
if (first.weight == second.weight) {
179+
return first.id < second.id;
180+
}
181+
return first.weight > second.weight;
190182
});
191183

192184
DisjointSets sets(static_cast<std::size_t>(number_of_nodes));
193185
MutexStorage mutexes(static_cast<std::size_t>(number_of_nodes));
194186

195-
for (const auto edge_id : edge_order) {
196-
if (valid_edges.data[edge_id] == 0) {
197-
continue;
198-
}
199-
187+
for (const auto &edge : edge_order) {
188+
const auto edge_id = edge.id;
200189
const auto channel = static_cast<std::size_t>(edge_id / number_of_nodes);
201190
const auto u = edge_id % number_of_nodes;
202-
if (!is_valid_grid_edge(u, offsets[channel], spatial_shape, spatial_strides)) {
203-
continue;
204-
}
205191

206192
const auto v_signed = static_cast<std::int64_t>(u) + static_cast<std::int64_t>(offset_strides[channel]);
207193
const auto v = static_cast<std::uint64_t>(v_signed);
208194
std::uint64_t root_u = sets.find(u);
209195
std::uint64_t root_v = sets.find(v);
210-
if (root_u == root_v || check_mutex(root_u, root_v, mutexes)) {
196+
if (root_u == root_v) {
211197
continue;
212198
}
213199

214200
const bool is_mutex_edge = channel >= number_of_attractive_channels;
215201
if (is_mutex_edge) {
216202
insert_mutex(root_u, root_v, mutexes);
217203
} else {
204+
if (check_mutex(root_u, root_v, mutexes)) {
205+
continue;
206+
}
218207
const auto new_root = sets.unite_roots(root_u, root_v);
219208
const auto old_root = (new_root == root_u) ? root_v : root_u;
220209
merge_mutexes(old_root, new_root, mutexes);
221210
}
222211
}
223212

224-
std::unordered_map<std::uint64_t, std::uint64_t> label_map;
213+
std::vector<std::uint64_t> root_labels(static_cast<std::size_t>(number_of_nodes), 0);
214+
std::uint64_t next_label = 1;
225215
for (std::uint64_t node = 0; node < number_of_nodes; ++node) {
226216
const auto root = sets.find(node);
227-
auto found = label_map.find(root);
228-
if (found == label_map.end()) {
229-
const auto next_label = static_cast<std::uint64_t>(label_map.size() + 1);
230-
found = label_map.emplace(root, next_label).first;
217+
auto &label = root_labels[static_cast<std::size_t>(root)];
218+
if (label == 0) {
219+
label = next_label;
220+
++next_label;
231221
}
232-
out.data[node] = found->second;
222+
out.data[node] = label;
233223
}
234224
}
235225

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ test = ["pytest"]
2828

2929
[tool.scikit-build]
3030
cmake.version = ">=3.21"
31+
cmake.build-type = "Release"
3132
wheel.packages = ["src/bioimage_cpp"]
3233

3334
[tool.pytest.ini_options]

0 commit comments

Comments
 (0)