Img2Num C++ (Internal Developer Docs) dev
API Documentation
Loading...
Searching...
No Matches
shared_contours.cpp
1#include "internal/shared_contours.h"
2
3#include "internal/bezier.h"
4
5#include <algorithm>
6#include <cmath>
7#include <limits>
8#include <map>
9#include <set>
10#include <unordered_map>
11#include <utility>
12#include <vector>
13
14constexpr int SMOOTHING_ITERATIONS {5};
15constexpr int32_t OUTSIDE = std::numeric_limits<int32_t>::min(); // image exterior label
16
17// Endpoint- and border-preserving smoothing of a corner polyline. Points sitting
18// on the image frame are locked so the canvas rectangle stays crisp.
19void smooth_edge(std::vector<Point>& p, int w, int h, int iters) {
20 const int n = static_cast<int>(p.size());
21 if (n < 3)
22 return;
23 auto on_border = [&](const Point& q) {
24 return q.x <= 0.0f || q.y <= 0.0f || q.x >= w || q.y >= h;
25 };
26 for (int it = 0; it < iters; ++it) {
27 std::vector<Point> q = p;
28 for (int i = 1; i < n - 1; ++i) {
29 if (on_border(p[i]))
30 continue;
31 const Point& a = p[i - 1];
32 const Point& b = p[i];
33 const Point& c = p[i + 1];
34 q[i] = {0.25f * a.x + 0.5f * b.x + 0.25f * c.x, 0.25f * a.y + 0.5f * b.y + 0.25f * c.y};
35 }
36 p.swap(q);
37 }
38}
39
40// Smooth a corner chain (endpoints fixed) and fit it to quadratic beziers. Done
41// once per canonical edge; both adjacent regions reuse the result, so the fitted
42// curve is shared and the two regions stay exactly coincident.
43std::vector<QuadBezier> fit_edge(const std::vector<Point>& corners, int w, int h, float eps) {
44 std::vector<Point> pts = corners;
45 smooth_edge(pts, w, h, SMOOTHING_ITERATIONS);
46 if (pts.size() < 2)
47 return {};
48 std::vector<std::vector<Point>> chain {pts};
49 std::vector<std::vector<QuadBezier>> res;
50 fit_curve_reduction(chain, res, eps);
51 return res.empty() ? std::vector<QuadBezier> {} : res[0];
52}
53
54void reverse_curve(std::vector<QuadBezier>& c) {
55 std::reverse(c.begin(), c.end());
56 for (QuadBezier& q : c)
57 std::swap(q.p0, q.p2);
58}
59
60std::unordered_map<int32_t, std::vector<std::vector<QuadBezier>>>
61build_shared_loops(const std::vector<int32_t>& labels, int w, int h, float eps) {
62 const int W1 = w + 1; // corner grid width
63 auto L = [&](int x, int y) -> int32_t {
64 if (x < 0 || x >= w || y < 0 || y >= h)
65 return OUTSIDE;
66 return labels[static_cast<size_t>(y) * w + x];
67 };
68 auto cidx = [&](int cx, int cy) {
69 return cy * W1 + cx;
70 };
71 auto cx_of = [&](int idx) {
72 return idx % W1;
73 };
74 auto cy_of = [&](int idx) {
75 return idx / W1;
76 };
77 auto pt_of = [&](int idx) {
78 return Point {static_cast<float>(cx_of(idx)), static_cast<float>(cy_of(idx))};
79 };
80
81 // --- 1. Undirected crack adjacency over corners ---------------------------
82 std::unordered_map<int, std::vector<int>> adj;
83 auto add_crack = [&](int a, int b) {
84 adj[a].push_back(b);
85 adj[b].push_back(a);
86 };
87 for (int cy = 0; cy <= h; ++cy)
88 for (int cx = 0; cx <= w; ++cx) {
89 if (cy < h && L(cx - 1, cy) != L(cx, cy))
90 add_crack(cidx(cx, cy), cidx(cx, cy + 1));
91 if (cx < w && L(cx, cy - 1) != L(cx, cy))
92 add_crack(cidx(cx, cy), cidx(cx + 1, cy));
93 }
94
95 // A corner is a junction wherever its crack degree != 2: degree 3/4 are branch
96 // points (incl. diagonal pixel touches), degree 1 is a dangling end. Degree-2
97 // corners are interior to a single two-region edge.
98 auto is_junction = [&](int idx) {
99 auto it = adj.find(idx);
100 return it == adj.end() ? false : it->second.size() != 2;
101 };
102
103 // --- 2. Extract canonical edges (junction -> junction chains) -------------
104 struct Edge {
105 int a, b; // endpoint corners
106 bool closed; // junction-free loop
107 std::vector<int> path; // full corner sequence (closed: ring, no repeat)
108 std::vector<QuadBezier> curve; // fitted curve (front@a .. back@b)
109 };
110 std::vector<Edge> edges;
111 std::map<std::pair<int, int>, int> crack_edge; // undirected crack -> edge id
112 auto ckey = [](int a, int b) {
113 return std::make_pair(std::min(a, b), std::max(a, b));
114 };
115
116 auto store_edge = [&](std::vector<int> seq, bool closed) {
117 std::vector<int> cseq = seq;
118 if (closed)
119 cseq.push_back(seq.front()); // close the ring for geometry
120 Edge e;
121 e.closed = closed;
122 e.path = std::move(seq);
123 e.a = cseq.front();
124 e.b = cseq.back();
125 std::vector<Point> corners;
126 corners.reserve(cseq.size());
127 for (int c : cseq)
128 corners.push_back(pt_of(c));
129 e.curve = fit_edge(corners, w, h, eps);
130 int id = static_cast<int>(edges.size());
131 for (size_t i = 0; i + 1 < cseq.size(); ++i)
132 crack_edge[ckey(cseq[i], cseq[i + 1])] = id;
133 edges.push_back(std::move(e));
134 };
135
136 auto walk_edge = [&](int start, int first_next) {
137 std::vector<int> seq {start};
138 int prev = start, cur = first_next;
139 while (true) {
140 seq.push_back(cur);
141 if (is_junction(cur))
142 break;
143 int nxt = -1;
144 for (int nb : adj[cur])
145 if (nb != prev) {
146 nxt = nb;
147 break;
148 }
149 if (nxt < 0 || cur == start)
150 break;
151 prev = cur;
152 cur = nxt;
153 }
154 return seq;
155 };
156
157 // 2a. edges between junctions
158 for (auto& kv : adj) {
159 if (!is_junction(kv.first))
160 continue;
161 for (int nb : kv.second)
162 if (!crack_edge.count(ckey(kv.first, nb)))
163 store_edge(walk_edge(kv.first, nb), false);
164 }
165 // 2b. junction-free closed loops
166 for (auto& kv : adj) {
167 for (int nb : kv.second) {
168 if (crack_edge.count(ckey(kv.first, nb)))
169 continue;
170 std::vector<int> seq {kv.first};
171 int prev = kv.first, cur = nb;
172 const size_t cap = adj.size() + 4;
173 while (cur != kv.first && seq.size() < cap) {
174 seq.push_back(cur);
175 int nxt = -1;
176 for (int x : adj[cur])
177 if (x != prev) {
178 nxt = x;
179 break;
180 }
181 if (nxt < 0)
182 break;
183 prev = cur;
184 cur = nxt;
185 }
186 // canonicalise start to smallest corner so both regions agree.
187 int mpos = 0;
188 for (size_t i = 0; i < seq.size(); ++i)
189 if (seq[i] < seq[mpos])
190 mpos = static_cast<int>(i);
191 const int mm = static_cast<int>(seq.size());
192 std::vector<int> rot;
193 rot.reserve(mm);
194 for (int i = 0; i < mm; ++i)
195 rot.push_back(seq[(mpos + i) % mm]);
196 store_edge(rot, true);
197 }
198 }
199
200 // --- 3. Per-region directed boundary cracks (region kept on the RIGHT) ----
201 std::unordered_map<int32_t, std::map<int, std::vector<int>>> region_dir;
202 for (int y = 0; y < h; ++y)
203 for (int x = 0; x < w; ++x) {
204 int32_t r = L(x, y);
205 auto& dir = region_dir[r];
206 const int c00 = cidx(x, y), c10 = cidx(x + 1, y);
207 const int c11 = cidx(x + 1, y + 1), c01 = cidx(x, y + 1);
208 if (L(x, y - 1) != r)
209 dir[c00].push_back(c10); // top
210 if (L(x + 1, y) != r)
211 dir[c10].push_back(c11); // right
212 if (L(x, y + 1) != r)
213 dir[c11].push_back(c01); // bottom
214 if (L(x - 1, y) != r)
215 dir[c01].push_back(c00); // left
216 }
217
218 auto unit = [&](int from, int to, int& dx, int& dy) {
219 dx = cx_of(to) - cx_of(from);
220 dy = cy_of(to) - cy_of(from);
221 };
222
223 // --- 4. Assemble each region's loops from canonical shared curves ---------
224 std::unordered_map<int32_t, std::vector<std::vector<QuadBezier>>> result;
225 for (auto& rkv : region_dir) {
226 int32_t r = rkv.first;
227 if (r == OUTSIDE)
228 continue;
229 std::map<int, std::vector<int>> dir = rkv.second; // erased as consumed
230
231 // right-hand rule: prefer right, straight, left, back of incoming dir.
232 auto take_from = [&](int from, int dx_in, int dy_in) -> int {
233 auto it = dir.find(from);
234 if (it == dir.end() || it->second.empty())
235 return -1;
236 auto& outs = it->second;
237 int pref[4][2] = {{-dy_in, dx_in}, {dx_in, dy_in}, {dy_in, -dx_in}, {-dx_in, -dy_in}};
238 for (auto& pr : pref)
239 for (size_t k = 0; k < outs.size(); ++k) {
240 int dx, dy;
241 unit(from, outs[k], dx, dy);
242 if (dx == pr[0] && dy == pr[1]) {
243 int to = outs[k];
244 outs.erase(outs.begin() + k);
245 return to;
246 }
247 }
248 int to = outs.back();
249 outs.pop_back();
250 return to;
251 };
252
253 std::vector<std::vector<int>> loops;
254 for (auto& it : dir) {
255 while (!it.second.empty()) {
256 int start = it.first;
257 int nxt = take_from(start, 1, 0);
258 if (nxt < 0)
259 break;
260 std::vector<int> loop {start};
261 int dx_in, dy_in;
262 unit(start, nxt, dx_in, dy_in);
263 int cur = nxt;
264 while (cur != start) {
265 loop.push_back(cur);
266 int nn = take_from(cur, dx_in, dy_in);
267 if (nn < 0)
268 break;
269 unit(cur, nn, dx_in, dy_in);
270 cur = nn;
271 }
272 loops.push_back(std::move(loop));
273 }
274 }
275
276 std::vector<std::vector<QuadBezier>>& out_loops = result[r];
277 for (auto& loop : loops) {
278 int m = static_cast<int>(loop.size());
279 if (m < 2)
280 continue;
281 // rotate so the loop starts at a junction (edges are entered at ends).
282 int js = -1;
283 for (int t = 0; t < m; ++t)
284 if (is_junction(loop[t])) {
285 js = t;
286 break;
287 }
288 if (js > 0)
289 std::rotate(loop.begin(), loop.begin() + js, loop.end());
290
291 std::vector<QuadBezier> curve;
292 int i = 0;
293 while (i < m) {
294 int from = loop[i];
295 int to = loop[(i + 1) % m];
296 auto eit = crack_edge.find(ckey(from, to));
297 if (eit == crack_edge.end()) {
298 ++i;
299 continue;
300 }
301 const Edge& e = edges[eit->second];
302 std::vector<QuadBezier> seg = e.curve;
303 if (e.closed) {
304 int mm = static_cast<int>(e.path.size());
305 int p = 0;
306 while (p < mm && e.path[p] != from)
307 ++p;
308 bool fwd = (p < mm) && (e.path[(p + 1) % mm] == to);
309 if (!fwd)
310 reverse_curve(seg);
311 for (const QuadBezier& q : seg)
312 curve.push_back(q);
313 i = m;
314 } else {
315 bool fwd = (from == e.a);
316 if (!fwd)
317 reverse_curve(seg);
318 for (const QuadBezier& q : seg)
319 curve.push_back(q);
320 int other = fwd ? e.b : e.a;
321 int j = i + 1;
322 while (j < m && loop[j] != other)
323 ++j;
324 i = j;
325 }
326 }
327 if (curve.size() >= 2)
328 out_loops.push_back(std::move(curve));
329 }
330 }
331
332 return result;
333}
Definition Point.h:5