Img2Num C++ (Internal Developer Docs) dev
API Documentation
Loading...
Searching...
No Matches
graph.cpp
1#include "internal/graph.h"
2
3#include "internal/bezier.h"
4#include "internal/douglas_peucker.h"
5#include "internal/Pixel.h"
6#include "internal/shared_contours.h"
7
8#include <algorithm>
9#include <cmath>
10#include <cstdlib>
11#include <iterator>
12#include <queue>
13#include <string>
14/*
15 Graph class - manages Node class
16*/
17
18static inline float
19colorDistance(const ImageLib::RGBPixel<uint8_t>& a, const ImageLib::RGBPixel<uint8_t>& b) {
21 static_cast<float>(a.red), static_cast<float>(a.green), static_cast<float>(a.blue)};
23 static_cast<float>(b.red), static_cast<float>(b.green), static_cast<float>(b.blue)};
24 return std::sqrt(
25 (af.red - bf.red) * (af.red - bf.red) + (af.green - bf.green) * (af.green - bf.green) +
26 (af.blue - bf.blue) * (af.blue - bf.blue)
27 );
28}
29
30/*
31To quickly search m_nodes (std::vector) for the index of a node id
32create an std::unordered_map of node id - index pairs
33indexing time of std::vector by value is O(N)
34lookup time of std::unordered_map by key is O(log(N))
35*/
36void Graph::hash_node_ids() {
37 for (int32_t i {0}; i < m_nodes->size(); i++) {
38 const int32_t key {m_nodes->at(i)->id()};
39 m_node_ids[key] = i;
40 }
41}
42
43bool Graph::all_areas_bigger_than(int32_t min_area) {
44 for (auto& n : *m_nodes) {
45 if (n->area() < min_area) {
46 return false;
47 }
48 }
49
50 return true;
51}
52
53bool Graph::add_edge(int32_t node_id1, int32_t node_id2) {
54 auto end_node_ids {m_node_ids.end()};
55 auto node1_it {m_node_ids.find(node_id1)};
56 auto node2_it {m_node_ids.find(node_id2)};
57
58 if (node1_it == end_node_ids || node2_it == end_node_ids) {
59 return false;
60 }
61
62 const int32_t idx1 {node1_it->second};
63 const int32_t idx2 {node2_it->second};
64
65 m_nodes->at(idx1)->add_edge(m_nodes->at(idx2));
66 m_nodes->at(idx2)->add_edge(m_nodes->at(idx1));
67 return true;
68}
69
70bool Graph::merge_nodes(const Node_ptr& node_to_keep, const Node_ptr& node_to_remove) {
71 auto end_node_ids {m_node_ids.end()};
72 auto node1_it {m_node_ids.find(node_to_keep->id())};
73 auto node2_it {m_node_ids.find(node_to_remove->id())};
74
75 if (node1_it == end_node_ids || node2_it == end_node_ids) {
76 return false;
77 }
78
79 const int32_t idx_k {node1_it->second};
80 const int32_t idx_r {node2_it->second};
81
82 // transfer edges from node_to_remove to node_to_keep
83 for (Node_ptr n : m_nodes->at(idx_r)->edges()) {
84 if (n->id() != node_to_keep->id()) {
85 // prevents self referencing
86 n->remove_edge(node_to_remove);
87 n->add_edge(node_to_keep);
88 node_to_keep->add_edge(n);
89 }
90 }
91
92 node_to_keep->add_pixels(node_to_remove->get_pixels());
93
94 node_to_remove->clear_all();
95
96 return true;
97}
98
99void Graph::clear_unconnected_nodes() {
100 std::vector<Node_ptr>& nodes {*m_nodes};
101
102 nodes.erase(
103 std::remove_if(
104 nodes.begin(), nodes.end(), [](const Node_ptr& n) { return n->area() == 0; }
105 ),
106 nodes.end()
107 );
108
109 hash_node_ids();
110}
111
112void Graph::discover_edges(
113 const std::vector<int32_t>& region_labels, const int32_t width, const int32_t height
114) {
115 // Moore 8-connected neighbourhood
116 constexpr int8_t dirs[8][2] {{1, 0}, {-1, 0}, {0, 1}, {0, -1},
117 {1, 1}, {-1, -1}, {-1, 1}, {1, -1}};
118
119 int32_t rneigh[8];
120
121 for (int32_t y {0}; y < height; ++y) {
122 for (int32_t x {0}; x < width; ++x) {
123 const int32_t idx {y * width + x};
124 const int32_t rid {region_labels[idx]};
125
126 for (int32_t k {0}; k < 8; ++k) {
127 const int32_t nx {x + dirs[k][0]};
128 const int32_t ny {y + dirs[k][1]};
129
130 if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
131 rneigh[k] = region_labels[ny * width + nx];
132 } else {
133 rneigh[k] = rid; // ignore out-of-bounds
134 }
135 }
136
137 for (int32_t r : rneigh) {
138 if (r != rid) {
139 add_edge(rid, r);
140 }
141 }
142 }
143 }
144}
145
146void Graph::process_overlapping_edges() {
147 // 1. Build the Global Label Map ONCE (0 = background, else = node->id())
148 std::vector<int32_t> label_map(m_width * m_height, 0);
149
150 for (const Node_ptr& n : get_nodes()) {
151 if (n->area() == 0)
152 continue;
153
154 for (auto& [_, p] : n->get_pixels()) {
155 label_map[p.y * m_width + p.x] = n->id();
156 }
157 }
158
159 constexpr int8_t dirs[8][2] {{1, 0}, {-1, 0}, {0, 1}, {0, -1},
160 {1, 1}, {-1, -1}, {-1, 1}, {1, -1}};
161
162 // 2. Iterate directly over the pixels of each node
163 for (const Node_ptr& n : get_nodes()) {
164 if (n->area() == 0)
165 continue;
166 int32_t val = n->id();
167
168 for (auto& [_, p] : n->get_pixels()) {
169 int x = p.x;
170 int y = p.y;
171
172 // Check 8 neighbors in the global map
173 for (int k = 0; k < 8; ++k) {
174 int nx = x + dirs[k][0];
175 int ny = y + dirs[k][1];
176
177 // Fast boundary check (replaces std::clamp)
178 if (nx < 0 || nx >= m_width || ny < 0 || ny >= m_height)
179 continue;
180
181 int32_t n_val = label_map[ny * m_width + nx];
182
183 // Is it a neighbor? AND have we not processed this pairing yet?
184 if (n_val != 0 && n_val != val && val < n_val) {
185 bool is_too_thin = false;
186
187 // Check around the neighbor pixel for a 3rd region (pinching)
188 for (int mk = 0; mk < 8; ++mk) {
189 int mx = nx + dirs[mk][0];
190 int my = ny + dirs[mk][1];
191
192 if (mx < 0 || mx >= m_width || my < 0 || my >= m_height)
193 continue;
194
195 int32_t m_val = label_map[my * m_width + mx];
196
197 if (m_val != 0 && m_val != val && m_val != n_val) {
198 is_too_thin = true;
199 break; // CRITICAL: Stop checking immediately once proven thin!
200 }
201 }
202
203 if (is_too_thin) {
204 // Give our pixel to the neighbor
205 Node_ptr neighbor_node =
206 m_nodes->at(m_node_ids[n_val]); // get_node_by_id(n_val); // Assuming
207 // you have this lookup
208 if (neighbor_node) {
209 neighbor_node->add_edge_pixel(XY {x, y});
210 }
211 } else {
212 // Take the neighbor's pixel
213 n->add_edge_pixel(XY {nx, ny});
214 }
215 }
216 }
217 }
218 }
219}
220
221std::vector<uint8_t> Graph::analyzeJunctions(const std::vector<uint8_t>& skel, int w, int h) {
222 std::vector<uint8_t> junction_map(w * h, 0);
223
224 // 8-Neighbor Order (Clockwise)
225 // P9 P2 P3
226 // P8 P1 P4
227 // P7 P6 P5
228 int dx[] = {0, 1, 1, 1, 0, -1, -1, -1};
229 int dy[] = {-1, -1, 0, 1, 1, 1, 0, -1};
230
231 for (int y = 0; y < h; ++y) {
232 for (int x = 0; x < w; ++x) {
233 if (getPixel(skel, w, h, x, y) == 0)
234 continue;
235
236 // 1. Get Neighbors in Circular Order
237 int p[8];
238 for (int k = 0; k < 8; ++k) {
239 p[k] = getPixel(skel, w, h, x + dx[k], y + dy[k]) ? 1 : 0;
240 }
241
242 // 2. Count Transitions (0 -> 1)
243 // This is the Crossing Number / 2
244 int transitions = 0;
245 for (int k = 0; k < 8; ++k) {
246 if (p[k] == 0 && p[(k + 1) % 8] == 1)
247 transitions++;
248 }
249
250 // 3. Count Total Neighbors (for Endpoint check)
251 int neighbors = 0;
252 for (int k = 0; k < 8; ++k)
253 neighbors += p[k];
254
255 // 4. Classify
256 if (transitions >= 3) {
257 junction_map[y * w + x] = 1;
258 }
259 }
260 }
261 return junction_map;
262}
263
264void Graph::compute_contours() {
265
266 /*
267 Shared-edge mode: build region boundaries on the crack grid so
268 neighbouring contours are exactly coincident along shared edges -- no
269 overlap band, no gaps
270 */
271
272 float eps = 0.25f;
273
274 std::vector<int32_t> labels(static_cast<size_t>(m_width) * m_height, -1);
275 for (const Node_ptr& n : get_nodes()) {
276 if (n->area() == 0)
277 continue;
278 for (auto& p : n->get_pixels())
279 labels[static_cast<size_t>(p.position.y) * m_width + p.position.x] = n->id();
280 }
281
282 auto loops = build_shared_loops(labels, m_width, m_height, eps);
283
284 for (const Node_ptr& n : get_nodes()) {
285 if (n->area() == 0)
286 continue;
287 n->clear_contour();
288 auto it = loops.find(n->id());
289 if (it == loops.end())
290 continue;
291 ImageLib::RGBPixel<uint8_t> c = n->color();
292 ImageLib::RGBAPixel<uint8_t> col {c.red, c.green, c.blue, 255};
293 for (std::vector<QuadBezier>& curve : it->second) {
294 std::vector<Point> anchors; // keep contours[] parallel to curves[]
295 anchors.reserve(curve.size() + 1);
296 for (const QuadBezier& q : curve)
297 anchors.push_back(q.p0);
298 if (!curve.empty())
299 anchors.push_back(curve.back().p2);
300 n->m_contours.contours.push_back(std::move(anchors));
301 n->m_contours.curves.push_back(std::move(curve));
302 n->m_contours.colors.push_back(col);
303 n->m_contours.hierarchy.push_back({-1, -1, -1, -1});
304 n->m_contours.is_hole.push_back(false);
305 }
306 }
307}
308
309namespace {
310
311// 1D squared-distance transform (Felzenszwalb & Huttenlocher): for every q,
312// d[q] = min_p ( (q - p)^2 + f[p] ). O(n).
313void dt_1d(const std::vector<float>& f, std::vector<float>& d, int n) {
314 constexpr float INF = 1e20f;
315 std::vector<int> v(n);
316 std::vector<float> z(n + 1);
317 int k = 0;
318 v[0] = 0;
319 z[0] = -INF;
320 z[1] = INF;
321 for (int q = 1; q < n; ++q) {
322 float s;
323 while (true) {
324 s = ((f[q] + static_cast<float>(q) * q) - (f[v[k]] + static_cast<float>(v[k]) * v[k])) /
325 (2.0f * static_cast<float>(q - v[k]));
326 if (s <= z[k] && k > 0) {
327 --k;
328 } else {
329 break;
330 }
331 }
332 ++k;
333 v[k] = q;
334 z[k] = s;
335 z[k + 1] = INF;
336 }
337 k = 0;
338 for (int q = 0; q < n; ++q) {
339 while (z[k + 1] < static_cast<float>(q))
340 ++k;
341 const float dq = static_cast<float>(q - v[k]);
342 d[q] = dq * dq + f[v[k]];
343 }
344}
345
346// Largest inscribed-disk radius (in pixels) of a region: the maximum over all
347// region pixels of the Euclidean distance to the nearest non-region pixel.
348// Computed with an exact squared Euclidean distance transform, so the result is
349// independent of how long or how curved the region is -- a property that a
350// bounding-box aspect ratio does not have. Thickness ~= 2 * this radius.
351float max_inscribed_radius(const Node_ptr& n) {
352 std::vector<uint8_t> mask;
353 const std::array<int, 4> xywh = n->create_binary_image(mask); // tight bbox
354 const int w = xywh[2];
355 const int h = xywh[3];
356 if (w <= 0 || h <= 0)
357 return 0.0f;
358
359 // Pad by one pixel so the region's boundary against the exterior is treated
360 // as background by the distance transform.
361 const int pw = w + 2;
362 const int ph = h + 2;
363 constexpr float INF = 1e20f;
364
365 std::vector<float> grid(static_cast<size_t>(pw) * ph);
366 for (int y = 0; y < ph; ++y) {
367 for (int x = 0; x < pw; ++x) {
368 const bool inside = x >= 1 && x <= w && y >= 1 && y <= h &&
369 mask[static_cast<size_t>(y - 1) * w + (x - 1)];
370 grid[static_cast<size_t>(y) * pw + x] = inside ? INF : 0.0f;
371 }
372 }
373
374 // Separable two-pass transform: columns first, then rows.
375 std::vector<float> in, out(std::max(pw, ph));
376 in.resize(ph);
377 for (int x = 0; x < pw; ++x) {
378 for (int y = 0; y < ph; ++y)
379 in[y] = grid[static_cast<size_t>(y) * pw + x];
380 dt_1d(in, out, ph);
381 for (int y = 0; y < ph; ++y)
382 grid[static_cast<size_t>(y) * pw + x] = out[y];
383 }
384 in.resize(pw);
385 float max_d2 = 0.0f;
386 for (int y = 0; y < ph; ++y) {
387 for (int x = 0; x < pw; ++x)
388 in[x] = grid[static_cast<size_t>(y) * pw + x];
389 dt_1d(in, out, pw);
390 for (int x = 0; x < pw; ++x)
391 if (out[x] > max_d2)
392 max_d2 = out[x];
393 }
394 return std::sqrt(max_d2);
395}
396
397} // namespace
398
399void Graph::merge_small_area_nodes(const int32_t min_area, const int32_t min_thickness) {
400 // Keep merging while any pass still makes progress. Using "did this pass
401 // merge anything?" as the loop guard (instead of re-testing every node)
402 // also avoids spinning forever on a node that is too small/thin but has no
403 // valid neighbour to merge into.
404 bool merged_any = true;
405 while (merged_any) {
406 merged_any = false;
407
408 for (const Node_ptr& n : get_nodes()) {
409 if (n->area() == 0)
410 continue;
411
412 bool needs_merge = n->area() < static_cast<size_t>(min_area);
413 if (!needs_merge && min_thickness > 0) {
414 // too thin == no inscribed disk of radius min_thickness/2 fits.
415 needs_merge = 2.0f * max_inscribed_radius(n) < static_cast<float>(min_thickness);
416 }
417 if (!needs_merge)
418 continue;
419
420 ImageLib::RGBPixel<uint8_t> col = n->color();
421
422 Node_ptr best_neighbor = nullptr;
423 float best_score = std::numeric_limits<float>::max();
424 for (const Node_ptr& ne : n->edges()) {
425 if (ne->area() > 0) {
426 float cdist = ImageLib::RGBPixel<uint8_t>::colorDistance(ne->color(), col);
427 float score = static_cast<float>(ne->area()) + 10.f * cdist;
428 if (score < best_score) {
429 best_score = score;
430 best_neighbor = ne;
431 }
432 }
433 }
434
435 // no valid neighbor found, skip this node
436 if (!best_neighbor) {
437 continue;
438 }
439
440 if (best_neighbor->area() >= n->area()) {
441 merge_nodes(best_neighbor, n);
442 } else {
443 merge_nodes(n, best_neighbor);
444 }
445 merged_any = true;
446 }
447
448 clear_unconnected_nodes();
449 }
450}
uint8_t getPixel(const std::vector< uint8_t > &img, int w, int h, int x, int y)
Definition graph.h:52
std::vector< uint8_t > analyzeJunctions(const std::vector< uint8_t > &skel, int w, int h)
Definition graph.cpp:221
Definition node.h:38