Skip to content

Commit 37e51da

Browse files
Add edge projection to grid graph (#49)
1 parent 8058623 commit 37e51da

5 files changed

Lines changed: 669 additions & 0 deletions

File tree

MIGRATION_GUIDE.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -899,6 +899,18 @@ Important differences:
899899
`grid_affinity_features_with_lifted(...)`, which returns local graph weights
900900
plus explicit long-range `uv_ids` and weights for lifted multicut or mutex
901901
watershed style workflows.
902+
- Nifty's `projectEdgeIdsToPixels` and `projectEdgeIdsToPixelsWithOffsets` map
903+
to `graph.project_edge_ids_to_pixels()` and
904+
`graph.project_edge_ids_to_pixels_with_offsets(offsets, *, strides=None, mask=None)`
905+
on `GridGraph2D` / `GridGraph3D`. The basic form returns an `int64` array of
906+
shape `(ndim, *graph.shape)` with each grid edge id written at its pivot
907+
pixel and `-1` elsewhere. The offsets form returns
908+
`(array, n_valid)`: an `int64` array of shape `(len(offsets), *graph.shape)`
909+
whose non-`-1` entries are a sequential counter over the in-bounds (and
910+
filter-accepted) targets, plus the total count. `strides` keeps only coords
911+
aligned along every axis; `mask` keeps only coords where a boolean array
912+
of shape `(len(offsets), *graph.shape)` is true. Like in nifty, `strides`
913+
and `mask` are mutually exclusive — passing both raises `ValueError`.
902914
- The three grid feature functions preserve `float32` and `float64` input
903915
dtype end-to-end (no internal copy to `float64`); other dtypes are
904916
promoted to `float64`. Output weight arrays match the input dtype.
Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
#pragma once
2+
3+
#include "bioimage_cpp/array_view.hxx"
4+
#include "bioimage_cpp/graph/grid_graph.hxx"
5+
6+
#include <array>
7+
#include <cstddef>
8+
#include <cstdint>
9+
#include <stdexcept>
10+
#include <vector>
11+
12+
namespace bioimage_cpp::graph {
13+
14+
namespace detail_grid_edge_projection {
15+
16+
// Walk a sub-shape in C-order, computing a flat output offset incrementally
17+
// using `strides` (the strides of the *full* containing array, not of the
18+
// sub-shape itself). Calls `callback(flat)` at every leaf. With `Axis` known
19+
// at compile time the compiler unrolls the entire nest for D = 2 / D = 3.
20+
template <std::size_t Axis, std::size_t D, class Callback>
21+
void enumerate_in_c_order(
22+
const std::array<std::uint64_t, D> &subshape,
23+
const std::array<std::ptrdiff_t, D> &strides,
24+
std::ptrdiff_t base,
25+
Callback &&callback
26+
) {
27+
if constexpr (Axis == D) {
28+
callback(base);
29+
} else {
30+
for (std::uint64_t i = 0; i < subshape[Axis]; ++i) {
31+
enumerate_in_c_order<Axis + 1, D>(subshape, strides, base, callback);
32+
base += strides[Axis];
33+
}
34+
}
35+
}
36+
37+
// Walk every coordinate in C-order over `shape`, tracking whether
38+
// `coord + offset` is in bounds, and call `callback(in_bounds, coord)` at the
39+
// leaf. `coord` is filled in-place and re-used across calls so the sampler
40+
// can read it without copying.
41+
template <std::size_t Axis, std::size_t D, class Callback>
42+
void enumerate_with_offset(
43+
const std::array<std::uint64_t, D> &shape,
44+
const std::array<std::ptrdiff_t, D> &offset,
45+
std::array<std::ptrdiff_t, D> &coord,
46+
bool in_bounds,
47+
Callback &&callback
48+
) {
49+
if constexpr (Axis == D) {
50+
callback(in_bounds, coord);
51+
} else {
52+
for (std::uint64_t c = 0; c < shape[Axis]; ++c) {
53+
coord[Axis] = static_cast<std::ptrdiff_t>(c);
54+
const std::ptrdiff_t target = coord[Axis] + offset[Axis];
55+
const bool axis_ok =
56+
target >= 0 &&
57+
target < static_cast<std::ptrdiff_t>(shape[Axis]);
58+
enumerate_with_offset<Axis + 1, D>(
59+
shape, offset, coord, in_bounds && axis_ok, callback
60+
);
61+
}
62+
}
63+
}
64+
65+
template <std::size_t D>
66+
std::array<std::ptrdiff_t, D> full_c_strides(
67+
const std::array<std::uint64_t, D> &shape
68+
) {
69+
std::array<std::ptrdiff_t, D> strides{};
70+
std::ptrdiff_t s = 1;
71+
for (std::size_t i = D; i-- > 0; ) {
72+
strides[i] = s;
73+
s *= static_cast<std::ptrdiff_t>(shape[i]);
74+
}
75+
return strides;
76+
}
77+
78+
template <std::size_t D>
79+
std::ptrdiff_t shape_product(const std::array<std::uint64_t, D> &shape) {
80+
std::ptrdiff_t p = 1;
81+
for (std::size_t i = 0; i < D; ++i) {
82+
p *= static_cast<std::ptrdiff_t>(shape[i]);
83+
}
84+
return p;
85+
}
86+
87+
template <std::size_t D>
88+
void require_projection_output_shape(
89+
const std::array<std::uint64_t, D> &graph_shape,
90+
const std::vector<std::ptrdiff_t> &out_shape,
91+
const std::ptrdiff_t expected_leading,
92+
const char *argument_name
93+
) {
94+
if (out_shape.size() != D + 1) {
95+
throw std::invalid_argument(
96+
std::string(argument_name) + " must have ndim == graph.ndim + 1"
97+
);
98+
}
99+
if (out_shape[0] != expected_leading) {
100+
throw std::invalid_argument(
101+
std::string(argument_name) + " leading dimension does not match expected length"
102+
);
103+
}
104+
for (std::size_t d = 0; d < D; ++d) {
105+
if (out_shape[d + 1] != static_cast<std::ptrdiff_t>(graph_shape[d])) {
106+
throw std::invalid_argument(
107+
std::string(argument_name) + " spatial shape must match graph shape"
108+
);
109+
}
110+
}
111+
}
112+
113+
} // namespace detail_grid_edge_projection
114+
115+
// Writes graph edge ids into `output` of shape (D, *graph.shape) and dtype
116+
// int64. The caller pre-fills `output` with -1; this function then sets
117+
// output[axis, c_0, ..., c_{D-1}] = edge_id for every edge whose spanning
118+
// axis is `axis` and whose pivot (smaller-endpoint) coordinate is
119+
// (c_0, ..., c_{D-1}). Slots where c_axis == shape[axis] - 1 are not visited
120+
// and keep their pre-filled -1.
121+
//
122+
// Exploits the fact that the GridGraph stores edges axis-major + C-order
123+
// within each axis: the k-th edge of axis `d` corresponds exactly to the
124+
// k-th pivot coordinate of the sub-shape (s_0, ..., s_d - 1, ..., s_{D-1})
125+
// walked in C-order. So we walk that sub-shape with the *full* output
126+
// strides and emit consecutive edge ids — no per-edge coordinate division.
127+
template <std::size_t D>
128+
void project_edge_ids_to_pixels(
129+
const GridGraph<D> &graph,
130+
const ArrayView<std::int64_t> &output
131+
) {
132+
namespace dgep = detail_grid_edge_projection;
133+
const auto &shape = graph.shape();
134+
dgep::require_projection_output_shape<D>(
135+
shape, output.shape, static_cast<std::ptrdiff_t>(D), "output"
136+
);
137+
138+
const auto strides = dgep::full_c_strides<D>(shape);
139+
const std::ptrdiff_t plane = dgep::shape_product<D>(shape);
140+
141+
auto *data = output.data;
142+
std::int64_t edge_id = 0;
143+
for (std::size_t axis = 0; axis < D; ++axis) {
144+
auto pivot_shape = shape;
145+
if (pivot_shape[axis] == 0) {
146+
continue;
147+
}
148+
--pivot_shape[axis];
149+
const std::ptrdiff_t base =
150+
static_cast<std::ptrdiff_t>(axis) * plane;
151+
dgep::enumerate_in_c_order<0, D>(
152+
pivot_shape, strides, base,
153+
[data, &edge_id](std::ptrdiff_t flat) {
154+
data[flat] = edge_id++;
155+
}
156+
);
157+
}
158+
}
159+
160+
// Enumerates "lifted" edges defined by a set of per-channel grid offsets.
161+
// Walks (offset_idx, *coord) in C-order over (offsets.size(), *graph.shape).
162+
// For each coord whose target `coord + offsets[offset_idx]` stays in bounds
163+
// AND the sampler accepts, writes a sequential counter starting at 0;
164+
// rejected slots get -1. Returns the total number of valid entries written.
165+
//
166+
// The counter is NOT a graph edge id — it indexes into the implicit array
167+
// of lifted edges derived from an affinity volume.
168+
template <std::size_t D, class Sampler>
169+
std::uint64_t project_edge_ids_to_pixels_with_offsets_impl(
170+
const GridGraph<D> &graph,
171+
const std::vector<std::array<std::ptrdiff_t, D>> &offsets,
172+
Sampler &&sampler,
173+
const ArrayView<std::int64_t> &output
174+
) {
175+
namespace dgep = detail_grid_edge_projection;
176+
const auto &shape = graph.shape();
177+
dgep::require_projection_output_shape<D>(
178+
shape, output.shape, static_cast<std::ptrdiff_t>(offsets.size()), "output"
179+
);
180+
181+
auto *out = output.data;
182+
std::int64_t edge_id = 0;
183+
std::size_t out_idx = 0;
184+
std::array<std::ptrdiff_t, D> coord{};
185+
for (std::size_t off_idx = 0; off_idx < offsets.size(); ++off_idx) {
186+
const auto &off = offsets[off_idx];
187+
dgep::enumerate_with_offset<0, D>(
188+
shape, off, coord, /*in_bounds=*/true,
189+
[&](bool in_bounds, const std::array<std::ptrdiff_t, D> &c) {
190+
if (in_bounds && sampler(off_idx, c)) {
191+
out[out_idx++] = edge_id++;
192+
} else {
193+
out[out_idx++] = -1;
194+
}
195+
}
196+
);
197+
}
198+
return static_cast<std::uint64_t>(edge_id);
199+
}
200+
201+
// Convenience overload: no filter.
202+
template <std::size_t D>
203+
std::uint64_t project_edge_ids_to_pixels_with_offsets(
204+
const GridGraph<D> &graph,
205+
const std::vector<std::array<std::ptrdiff_t, D>> &offsets,
206+
const ArrayView<std::int64_t> &output
207+
) {
208+
return project_edge_ids_to_pixels_with_offsets_impl<D>(
209+
graph, offsets,
210+
[](std::size_t, const std::array<std::ptrdiff_t, D> &) { return true; },
211+
output
212+
);
213+
}
214+
215+
// Convenience overload: only coords aligned with `strides` along every axis.
216+
template <std::size_t D>
217+
std::uint64_t project_edge_ids_to_pixels_with_offsets(
218+
const GridGraph<D> &graph,
219+
const std::vector<std::array<std::ptrdiff_t, D>> &offsets,
220+
const std::array<std::ptrdiff_t, D> &strides,
221+
const ArrayView<std::int64_t> &output
222+
) {
223+
for (std::size_t d = 0; d < D; ++d) {
224+
if (strides[d] <= 0) {
225+
throw std::invalid_argument("strides must be positive");
226+
}
227+
}
228+
return project_edge_ids_to_pixels_with_offsets_impl<D>(
229+
graph, offsets,
230+
[&strides](std::size_t, const std::array<std::ptrdiff_t, D> &c) {
231+
for (std::size_t d = 0; d < D; ++d) {
232+
if (c[d] % strides[d] != 0) {
233+
return false;
234+
}
235+
}
236+
return true;
237+
},
238+
output
239+
);
240+
}
241+
242+
// Convenience overload: only coords where the (n_offsets, *graph.shape)
243+
// `mask` is non-zero.
244+
template <std::size_t D>
245+
std::uint64_t project_edge_ids_to_pixels_with_offsets(
246+
const GridGraph<D> &graph,
247+
const std::vector<std::array<std::ptrdiff_t, D>> &offsets,
248+
const ConstArrayView<std::uint8_t> &mask,
249+
const ArrayView<std::int64_t> &output
250+
) {
251+
namespace dgep = detail_grid_edge_projection;
252+
const auto &shape = graph.shape();
253+
dgep::require_projection_output_shape<D>(
254+
shape, mask.shape, static_cast<std::ptrdiff_t>(offsets.size()), "mask"
255+
);
256+
257+
const auto mask_spatial_strides = dgep::full_c_strides<D>(shape);
258+
const std::ptrdiff_t mask_plane = dgep::shape_product<D>(shape);
259+
const auto *mdata = mask.data;
260+
return project_edge_ids_to_pixels_with_offsets_impl<D>(
261+
graph, offsets,
262+
[mdata, mask_spatial_strides, mask_plane](
263+
std::size_t off_idx, const std::array<std::ptrdiff_t, D> &c
264+
) {
265+
std::ptrdiff_t flat =
266+
static_cast<std::ptrdiff_t>(off_idx) * mask_plane;
267+
for (std::size_t d = 0; d < D; ++d) {
268+
flat += c[d] * mask_spatial_strides[d];
269+
}
270+
return mdata[flat] != 0;
271+
},
272+
output
273+
);
274+
}
275+
276+
} // namespace bioimage_cpp::graph

0 commit comments

Comments
 (0)