Img2Num C++ (Internal Developer Docs) dev
API Documentation
Loading...
Searching...
No Matches
contours.cpp
1#include "internal/contours.h"
2
3#include <algorithm>
4#include <cmath>
5#include <iterator>
6#include <map>
7#include <set>
8#include <utility>
9
10// M_PI is not defined by default on MSVC
11#ifndef M_PI
12#define M_PI 3.14159265358979323846
13#endif
14
15namespace contours {
16
17// 8-neighborhood, clockwise order starting from "right" (dx=+1,dy=0).
18// (y increases downward)
19static constexpr int DX[8] = {+1, +1, 0, -1, -1, -1, 0, +1};
20static constexpr int DY[8] = {0, +1, +1, +1, 0, -1, -1, -1};
21
22// Map (dy,dx) to direction index in [0..7]
23static inline int dirIndex(int dy, int dx) {
24 if (dy == 0 && dx == 1)
25 return 0;
26 if (dy == 1 && dx == 1)
27 return 1;
28 if (dy == 1 && dx == 0)
29 return 2;
30 if (dy == 1 && dx == -1)
31 return 3;
32 if (dy == 0 && dx == -1)
33 return 4;
34 if (dy == -1 && dx == -1)
35 return 5;
36 if (dy == -1 && dx == 0)
37 return 6;
38 if (dy == -1 && dx == 1)
39 return 7;
40 return -1; // not a neighbor
41}
42
43static inline int dirIndexFromTo(int y, int x, int ny, int nx) {
44 return dirIndex(ny - y, nx - x);
45}
46
47// Border following (Algorithm 1, steps 3.1..3.5):
48// - f is a padded image (0=background; nonzero=object/labels)
49// - (sy,sx) is the border start point (nonzero)
50// - (py,px) is the "previous" neighbor used to define search direction (usually
51// a 0-pixel)
52// - nbd is the new border label
53// Returns the contour as points in *unpadded* coordinates (x-1,y-1).
54static std::vector<Point>
55traceBorder(std::vector<int>& f, int paddedW, int sy, int sx, int py, int px, int nbd) {
56 auto set = [&](int y, int x) -> int& {
57 return f[y * paddedW + x];
58 };
59 auto get = [&](int y, int x) -> int {
60 return f[y * paddedW + x];
61 };
62
63 std::vector<Point> pts;
64
65 // (3.1) Find first nonzero neighbor around (sy,sx), scanning clockwise from
66 // (py,px)
67 const int dStart = dirIndexFromTo(sy, sx, py, px);
68 if (dStart < 0) {
69 throw std::runtime_error("traceBorder: invalid previous neighbor (not adjacent).");
70 }
71
72 int y1 = 0, x1 = 0;
73 bool found = false;
74 for (int t = 0; t < 8; ++t) {
75 int d = (dStart + t) & 7; // clockwise
76 int ny = sy + DY[d], nx = sx + DX[d];
77 if (get(ny, nx) != 0) {
78 y1 = ny;
79 x1 = nx;
80 found = true;
81 break;
82 }
83 }
84
85 // If isolated pixel: set f(sy,sx) = -NBD and return single-point contour
86 if (!found) {
87 set(sy, sx) = -nbd;
88 pts.push_back(Point {static_cast<float>(sx - 1), static_cast<float>(sy - 1)});
89 return pts;
90 }
91
92 // (3.2)
93 int y2 = y1, x2 = x1; // previous border pixel
94 int y3 = sy, x3 = sx; // current border pixel
95 const int firstY = y1, firstX = x1;
96
97 while (true) {
98 // (3.3) Search CCW around (y3,x3), starting from "next after (y2,x2) in CCW
99 // order"
100 const int dPrev = dirIndexFromTo(y3, x3, y2, x2);
101 if (dPrev < 0) {
102 throw std::runtime_error("traceBorder: broken neighbor chain.");
103 }
104
105 const int d0 = (dPrev + 7) & 7; // one step CCW from dPrev
106 bool rightZeroExamined = false;
107 int y4 = y3, x4 = x3; // next border pixel (to be found)
108
109 for (int t = 0; t < 8; ++t) {
110 int d = (d0 - t) & 7; // scan CCW (decrement index)
111 int ny = y3 + DY[d], nx = x3 + DX[d];
112
113 // (3.4a) depends on whether pixel to the right (dx=+1,dy=0 => dir 0)
114 // was examined during (3.3) and was a 0-pixel
115 if (d == 0 && get(ny, nx) == 0) {
116 rightZeroExamined = true;
117 }
118
119 if (get(ny, nx) != 0) {
120 y4 = ny;
121 x4 = nx;
122 break;
123 }
124 }
125
126 // (3.4) Marking policy
127 if (rightZeroExamined) {
128 set(y3, x3) = -nbd;
129 } else {
130 if (set(y3, x3) == 1) {
131 set(y3, x3) = nbd;
132 }
133 }
134
135 // Record current point in unpadded coordinates
136 pts.push_back(Point {static_cast<float>(x3 - 1), static_cast<float>(y3 - 1)});
137
138 // (3.5) Termination check
139 if (y4 == sy && x4 == sx && y3 == firstY && x3 == firstX) {
140 break;
141 }
142
143 // Advance
144 y2 = y3;
145 x2 = x3;
146 y3 = y4;
147 x3 = x4;
148 }
149
150 return pts;
151}
152
153ContoursResult find_contours(const std::vector<uint8_t>& binary, int width, int height) {
154 if (width <= 0 || height <= 0) {
155 return {};
156 }
157 if ((int)binary.size() != width * height) {
158 throw std::invalid_argument("binary.size() must be width*height");
159 }
160
161 // Pad image with a 1-pixel 0-frame
162 const int paddedW = width + 2;
163 const int paddedH = height + 2;
164 std::vector<int> f(static_cast<std::size_t>(paddedW) * static_cast<std::size_t>(paddedH), 0);
165
166 auto set = [&](int y, int x) -> int& {
167 return f[y * paddedW + x];
168 };
169 auto get = [&](int y, int x) -> int {
170 return f[y * paddedW + x];
171 };
172
173 for (int y = 0; y < height; ++y) {
174 for (int x = 0; x < width; ++x) {
175 set(y + 1, x + 1) = (binary[y * width + x] != 0) ? 1 : 0;
176 }
177 }
178
179 // Border node info stored by border id (NBD). ID=1 is the frame (special hole
180 // border).
181 struct NodeInfo {
182 bool is_hole = false;
183 int parent_id = 0; // parent border id
184 std::vector<Point> points; // contour points (unpadded coords)
185 bool used = false;
186 };
187
188 int nbd = 1;
189 std::vector<NodeInfo> nodes(2);
190 nodes[1].is_hole = true; // frame is treated as a hole border
191 nodes[1].parent_id = 1; // self-parent for convenience
192 nodes[1].used = true;
193
194 // Raster scan (Algorithm 1, step 1..4)
195 for (int y = 1; y <= height; ++y) {
196 int lnbd = 1; // reset each row
197
198 for (int x = 1; x <= width; ++x) {
199 int fij = get(y, x);
200 if (fij == 0)
201 continue; // Algorithm processes only fij != 0
202
203 bool startBorder = false;
204 bool isHole = false;
205 int py = 0, px = 0;
206
207 // (1a) Outer border start: fij==1 and left is 0
208 if (fij == 1 && get(y, x - 1) == 0) {
209 ++nbd;
210 startBorder = true;
211 isHole = false;
212 py = y;
213 px = x - 1;
214 }
215 // (1b) Hole border start: fij>=1 and right is 0
216 else if (fij >= 1 && get(y, x + 1) == 0) {
217 ++nbd;
218 startBorder = true;
219 isHole = true;
220 py = y;
221 px = x + 1;
222
223 // LNBD <- fij if fij > 1 (per Appendix I)
224 if (fij > 1) {
225 lnbd = fij;
226 }
227 }
228
229 if (startBorder) {
230 if ((int)nodes.size() <= nbd)
231 nodes.resize(nbd + 1);
232
233 nodes[nbd].is_hole = isHole;
234 nodes[nbd].used = true;
235
236 // (2) Decide parent using Table 1:
237 // If types match -> parent(B)=parent(B'), else parent(B)=B'
238 // where B' is border with id LNBD.
239 const bool bprimeIsHole = nodes[lnbd].is_hole;
240 const int parent_id = (isHole == bprimeIsHole) ? nodes[lnbd].parent_id : lnbd;
241 nodes[nbd].parent_id = parent_id;
242
243 // (3) Follow border and mark pixels with +/-NBD
244 nodes[nbd].points = traceBorder(f, paddedW, y, x, py, px, nbd);
245 }
246
247 // (4) Update LNBD if fij != 1 (note fij may have changed after tracing)
248 if (get(y, x) != 1) {
249 lnbd = std::abs(get(y, x));
250 }
251 }
252 }
253
254 // Convert from border ids (2..nbd) to compact 0-based contour indices (frame
255 // excluded)
256 std::vector<int> idToIdx(nbd + 1, -1);
257 ContoursResult out;
258
259 out.contours.reserve(std::max(0, nbd - 1));
260 out.hierarchy.reserve(std::max(0, nbd - 1));
261 out.is_hole.reserve(std::max(0, nbd - 1));
262
263 for (int id = 2; id <= nbd; ++id) {
264 if (!nodes[id].used)
265 continue;
266 idToIdx[id] = (int)out.contours.size();
267 out.contours.push_back(nodes[id].points);
268 out.is_hole.push_back(nodes[id].is_hole);
269 out.hierarchy.push_back({-1, -1, -1, -1});
270 }
271
272 // Fill parent index
273 for (int id = 2; id <= nbd; ++id) {
274 if (!nodes[id].used)
275 continue;
276 const int idx = idToIdx[id];
277 const int pid = nodes[id].parent_id;
278 out.hierarchy[idx][3] = (pid <= 1) ? -1 : idToIdx[pid]; // parent
279 }
280
281 // Build child/sibling links (first_child, next, prev)
282 std::vector<int> lastChild(out.contours.size(), -1);
283
284 for (int i = 0; i < (int)out.contours.size(); ++i) {
285 int p = out.hierarchy[i][3];
286 if (p < 0)
287 continue;
288
289 if (out.hierarchy[p][2] == -1) {
290 out.hierarchy[p][2] = i; // first_child
291 lastChild[p] = i;
292 } else {
293 int last = lastChild[p];
294 out.hierarchy[last][0] = i; // next sibling
295 out.hierarchy[i][1] = last; // prev sibling
296 lastChild[p] = i;
297 }
298 }
299
300 return out;
301}
302
303// Helper: 2D integer coordinate for map keys
304struct Coord {
305 int x, y;
306 bool operator<(const Coord& other) const {
307 return std::tie(x, y) < std::tie(other.x, other.y);
308 }
309};
310
311// Helper: Normalize a vector
312Point normalize(Point p) {
313 float len = std::sqrt(p.x * p.x + p.y * p.y);
314 if (len == 0)
315 return {0, 0};
316 return {p.x / len, p.y / len};
317}
318
319// Helper: Calculate Tangent of A at index i using neighbors
320Point getTangent(const std::vector<Point>& vec, int i) {
321 int n = vec.size();
322 if (n < 2)
323 return {0, 0};
324
325 // Use previous and next points to determine the "flow" of the line
326 int prev = (i == 0) ? 0 : i - 1;
327 int next = (i == n - 1) ? n - 1 : i + 1;
328
329 return normalize(vec[next] - vec[prev]);
330}
331
332// Calculate the closest point on the segment V -> W from point P
333// Returns {ClosestPoint, DistanceSquared}
334std::pair<Point, float> getClosestPointOnSegment(Point p, Point v, Point w) {
335 float l2 = Point::distSq(v, w);
336 if (l2 == 0.0)
337 return {v, Point::distSq(p, v)};
338
339 // Project p onto line v-w
340 // t is the parameterized distance along the line (0.0 to 1.0)
341 float t = ((p.x - v.x) * (w.x - v.x) + (p.y - v.y) * (w.y - v.y)) / l2;
342
343 // Clamp to segment
344 t = std::max(0.0f, std::min(1.0f, t));
345
346 Point projection = {v.x + t * (w.x - v.x), v.y + t * (w.y - v.y)};
347 return {projection, Point::distSq(p, projection)};
348}
349
350// Identifies a specific point: {ContourIndex, PointIndex}
351struct PointID {
352 int cIdx;
353 int pIdx;
354};
355
381Point getQuadraticTarget(const std::vector<Point>& pts, int i) {
382 int n {static_cast<int>(pts.size())};
383 // Endpoints cannot be smoothed - return as-is
384 if (i <= 0 || i >= n - 1) {
385 return pts[i];
386 }
387
388 // 1. BOUNDARY FALLBACK:
389 // If we are too close to the end (indices 1 or n-2), we don't have 5 points.
390 // Fall back to standard Linear Laplacian (0.25, 0.5, 0.25).
391 if (i < 2 || i >= n - 2) {
392 Point prev {pts[i - 1]};
393 Point curr {pts[i]};
394 Point next {pts[i + 1]};
395 return 0.25f * prev + 0.5f * curr + 0.25f * next;
396 }
397
398 // 2. QUADRATIC FILTER (Savitzky-Golay Window 5, Degree 2):
399 // Coefficients: [-3, 12, 17, 12, -3] / 35
400 // This fits a local parabola and evaluates it at the center.
401 const Point& p2L {pts[i - 2]}; // 2 Left
402 const Point& p1L {pts[i - 1]}; // 1 Left
403 const Point& p {pts[i]}; // Center
404 const Point& p1R {pts[i + 1]}; // 1 Right
405 const Point& p2R {pts[i + 2]}; // 2 Right
406
407 return (-3.0f * p2L + 12.0f * p1L + 17.0f * p + 12.0f * p1R - 3.0f * p2R) / 35.0f;
408}
409
410void coupledSmooth(std::vector<Point>& contourA, std::vector<Point>& contourB) {
411 std::vector<std::vector<Point>> contours = {contourA, contourB};
412 // 1. Build Spatial Grid for O(1) partner lookup
413 std::map<Coord, std::vector<PointID>> grid;
414 for (int c = 0; c < 2; ++c) {
415 for (int p = 0; p < (int)contours[c].size(); ++p) {
416 grid[{(int)std::round(contours[c][p].x), (int)std::round(contours[c][p].y)}].push_back(
417 {c, p}
418 );
419 }
420 }
421
422 std::vector<std::vector<Point>> targetPos = {contourA, contourB};
423
424 // Increased radius slightly to ensure we catch partners even on curves
425 float pairRadiusSq = 2.0 * 2.0;
426
427 for (int c = 0; c < 2; ++c) {
428 // Skip endpoints (p=0 and p=last are usually locked anchors)
429 for (int p = 1; p < (int)contours[c].size() - 1; ++p) {
430 Point myPos = contours[c][p];
431
432 // --- STEP A: Calculate My Ideal Position (Quadratic) ---
433 Point myTarget = getQuadraticTarget(contours[c], p);
434
435 // --- STEP B: Find Partners & Calculate Their Ideal Positions ---
436 Point sumPartnerTargets = {0, 0};
437 int partnerCount = 0;
438
439 int gx = (int)std::round(myPos.x);
440 int gy = (int)std::round(myPos.y);
441
442 // 3x3 Neighbor Search
443 for (int dy = -2; dy <= 2; ++dy) {
444 for (int dx = -2; dx <= 2; ++dx) {
445 auto it = grid.find({gx + dx, gy + dy});
446 if (it == grid.end())
447 continue;
448
449 for (const auto& neighbor : it->second) {
450 if (neighbor.cIdx == c)
451 continue; // Ignore self
452
453 Point otherPos = contours[neighbor.cIdx][neighbor.pIdx];
454
455 // If close enough to be a "Partner"
456 if (Point::distSq(myPos, otherPos) < pairRadiusSq) {
457 // Check if partner is constrained
458 int op = neighbor.pIdx;
459 const auto& otherContour = contours[neighbor.cIdx];
460
461 Point oTarget;
462 if (op > 0 && op < (int)otherContour.size() - 1) {
463 // Partner is free: Calculate THEIR Quadratic Target
464 oTarget = getQuadraticTarget(otherContour, op);
465 } else {
466 // Partner is locked: They want to stay put
467 oTarget = otherPos;
468 }
469
470 sumPartnerTargets += oTarget;
471 partnerCount++;
472 }
473 }
474 }
475 }
476
477 // --- STEP C: Consensus Averaging ---
478 // "I want to be a parabola" vs "My partner wants to be a parabola"
479 // We average the two perfect quadratic fits.
480 if (partnerCount > 0) {
481 targetPos[c][p] = (myTarget + sumPartnerTargets) / (1.0 + partnerCount);
482 } else {
483 targetPos[c][p] = myTarget;
484 }
485 }
486 }
487
488 std::copy(targetPos[0].begin(), targetPos[0].end(), contourA.begin());
489 std::copy(targetPos[1].begin(), targetPos[1].end(), contourB.begin());
490}
491
492void stitch_smooth(std::vector<Point>& vecA, std::vector<Point>& vecB) {
493 // 1. Map Vector B indices to Grid (Optimization)
494 // We map a coordinate to the INDEX in vector B
495 std::map<Coord, int> mapB;
496 for (size_t i = 0; i < vecB.size(); ++i) {
497 mapB[{(int)std::round(vecB[i].x), (int)std::round(vecB[i].y)}] = i;
498 }
499
500 // We store the calculated "Target Positions" here.
501 // We do NOT update in place immediately, or the math for the next point will
502 // be wrong.
503 struct Update {
504 int index;
505 Point newPos;
506 };
507 std::vector<Update> updatesA;
508 std::vector<Update> updatesB;
509
510 // --- Process Vector A (Snap to B's Geometry) ---
511 for (size_t i = 0; i < vecA.size(); ++i) {
512 int ax = (int)std::round(vecA[i].x);
513 int ay = (int)std::round(vecA[i].y);
514
515 float minDst = std::numeric_limits<float>::max();
516 Point bestTarget = vecA[i];
517 bool foundMatch = false;
518
519 // Search 3x3 Neighborhood
520 for (int dy = -1; dy <= 1; ++dy) {
521 for (int dx = -1; dx <= 1; ++dx) {
522 auto it = mapB.find({ax + dx, ay + dy});
523 if (it != mapB.end()) {
524 int bIdx = it->second;
525
526 // CHECK FORWARD SEGMENT: B[bIdx] -> B[bIdx+1]
527 if (bIdx < (int)vecB.size() - 1) {
528 auto res = getClosestPointOnSegment(vecA[i], vecB[bIdx], vecB[bIdx + 1]);
529 if (res.second < minDst) {
530 minDst = res.second;
531 bestTarget = res.first;
532 foundMatch = true;
533 }
534 }
535
536 // CHECK BACKWARD SEGMENT: B[bIdx-1] -> B[bIdx]
537 if (bIdx > 0) {
538 auto res = getClosestPointOnSegment(vecA[i], vecB[bIdx - 1], vecB[bIdx]);
539 if (res.second < minDst) {
540 minDst = res.second;
541 bestTarget = res.first;
542 foundMatch = true;
543 }
544 }
545 }
546 }
547 }
548
549 if (foundMatch) {
550 // Move A to the midpoint between itself and the closest spot on B's line
551 Point mid = (vecA[i] + bestTarget) * 0.5f;
552 updatesA.push_back({(int)i, mid});
553 }
554 }
555
556 // --- Process Vector B (Snap to A's Geometry) ---
557 // (We need a map for A now to do the reverse)
558 std::map<Coord, int> mapA;
559 for (size_t i = 0; i < vecA.size(); ++i) {
560 mapA[{(int)std::round(vecA[i].x), (int)std::round(vecA[i].y)}] = i;
561 }
562
563 for (size_t i = 0; i < vecB.size(); ++i) {
564 int bx = (int)std::round(vecB[i].x);
565 int by = (int)std::round(vecB[i].y);
566
567 float minDst = std::numeric_limits<float>::max();
568 Point bestTarget = vecB[i];
569 bool foundMatch = false;
570
571 for (int dy = -1; dy <= 1; ++dy) {
572 for (int dx = -1; dx <= 1; ++dx) {
573 auto it = mapA.find({bx + dx, by + dy});
574 if (it != mapA.end()) {
575 int aIdx = it->second;
576
577 // Check Forward Segment A
578 if (aIdx < (int)vecA.size() - 1) {
579 auto res = getClosestPointOnSegment(vecB[i], vecA[aIdx], vecA[aIdx + 1]);
580 if (res.second < minDst) {
581 minDst = res.second;
582 bestTarget = res.first;
583 foundMatch = true;
584 }
585 }
586 // Check Backward Segment A
587 if (aIdx > 0) {
588 auto res = getClosestPointOnSegment(vecB[i], vecA[aIdx - 1], vecA[aIdx]);
589 if (res.second < minDst) {
590 minDst = res.second;
591 bestTarget = res.first;
592 foundMatch = true;
593 }
594 }
595 }
596 }
597 }
598
599 if (foundMatch) {
600 Point mid = (vecB[i] + bestTarget) * 0.5f;
601 updatesB.push_back({(int)i, mid});
602 }
603 }
604
605 if ((updatesA.size() == 0) | (updatesB.size() == 0)) {
606 return;
607 }
608
609 // --- Apply Updates ---
610 for (const auto& u : updatesA)
611 vecA[u.index] = u.newPos;
612 for (const auto& u : updatesB)
613 vecB[u.index] = u.newPos;
614
615 // --- OPTIONAL FINAL POLISH: Laplacian Smooth ---
616 // This removes any remaining high-frequency noise from the seam
617 // Only run this on the points that were actually touched/updated
618 // Formula: P_i = 0.25*P_prev + 0.5*P_curr + 0.25*P_next
619
620 auto smoothVector = [](std::vector<Point>& pts, const std::vector<Update>& updates) {
621 std::vector<Point> original = pts;
622 for (const auto& u : updates) {
623 int i = u.index;
624 if (i > 0 && i < (int)pts.size() - 1) {
625 pts[i] = 0.25f * original[i - 1] + 0.5f * original[i] + 0.25f * original[i + 1];
626 }
627 }
628 };
629
630 auto laplacianSmooth = [](std::vector<Point>& pts, const std::vector<Update>& updates) {
631 std::vector<Point> original = pts;
632 for (const auto& u : updates) {
633 int i = u.index;
634 if (i > 0 && i < (int)pts.size() - 1) {
635 // Heavier weight on self (0.6) to preserve shape, but smooth noise (0.2
636 // neighbors)
637 pts[i] = 0.2f * original[i - 1] + 0.6f * original[i] + 0.2f * original[i + 1];
638 }
639 }
640 };
641
642 smoothVector(vecA, updatesA);
643 smoothVector(vecB, updatesB);
644}
645
646// Project p onto segment v-w. Returns closest point.
647Point getClosestPoint(Point p, Point v, Point w) {
648 float l2 = Point::distSq(v, w);
649 if (l2 == 0.0f)
650 return v;
651
652 float t = ((p.x - v.x) * (w.x - v.x) + (p.y - v.y) * (w.y - v.y)) / l2;
653 // Allow slight extension (0.01) to help corners "kiss"
654 t = std::max(-0.1f, std::min(1.1f, t));
655
656 return {v.x + t * (w.x - v.x), v.y + t * (w.y - v.y)};
657}
658
659// --- Step 1: Boundary Locking ---
660// Returns a mask: true = Locked (On Boundary), false = Free to move
661std::vector<std::vector<bool>>
662createBoundaryMask(const std::vector<std::vector<Point>>& contours, Rect bounds) {
663 std::vector<std::vector<bool>> locked(contours.size());
664 float eps = 0.1; // Tolerance for "on the boundary"
665
666 for (size_t c = 0; c < contours.size(); ++c) {
667 locked[c].resize(contours[c].size(), false);
668 for (size_t p = 0; p < contours[c].size(); ++p) {
669 Point pt = contours[c][p];
670 // Check Left, Right, Top, Bottom
671 if (std::abs(pt.x - bounds.x) < eps ||
672 std::abs(pt.x - (bounds.x + bounds.width - 1.0f)) < eps ||
673 std::abs(pt.y - bounds.y) < eps ||
674 std::abs(pt.y - (bounds.y + bounds.height - 1.0f)) < eps) {
675 locked[c][p] = true;
676 }
677 }
678 }
679 return locked;
680}
681
682void updateLockedMasks(
683 const std::vector<std::vector<Point>>& contours, std::vector<std::vector<bool>>& locked,
684 std::vector<uint8_t>& junctions, int width
685) {
686 for (size_t c = 0; c < contours.size(); ++c) {
687 for (size_t p = 0; p < contours[c].size(); ++p) {
688 Point pt = contours[c][p];
689 int idx = pt.y * width + pt.x;
690 if (junctions[idx] > 0) {
691 locked[c][p] = true;
692 }
693 }
694 }
695}
696
697// --- Corner Detection (Feature Preservation) ---
698std::vector<bool> detectCorners(const std::vector<Point>& pts, float angleThresholdDeg = 150.0) {
699 std::vector<bool> isCorner(pts.size(), false);
700 if (pts.size() < 3)
701 return isCorner;
702 float threshold = std::cos(angleThresholdDeg * M_PI / 180.0f);
703
704 for (size_t i = 1; i < pts.size() - 1; ++i) {
705 Point v1 = normalize(pts[i] - pts[i - 1]);
706 Point v2 = normalize(pts[i + 1] - pts[i]);
707 if (v1.x * v2.x + v1.y * v2.y < threshold)
708 isCorner[i] = true;
709 }
710 // Endpoints are corners
711 isCorner[0] = true;
712 isCorner.back() = true;
713 return isCorner;
714}
715
716// --- Selective Smoothing ---
717void selectiveSmooth(std::vector<Point>& pts, const std::vector<bool>& isLocked) {
718 std::vector<Point> original = pts;
719 for (size_t i = 1; i < pts.size() - 1; ++i) {
720 // DO NOT move if it's a Corner OR if it's Locked on the boundary
721 if (isLocked[i])
722 continue;
723
724 pts[i] = 0.25f * original[i - 1] + 0.5f * original[i] + 0.25f * original[i + 1];
725 }
726}
727
728void coupledSmooth(
729 std::vector<std::vector<Point>>& contours, const std::vector<std::vector<bool>>& lockedMasks,
730 float pairRadiusSq = 2.25f
731) {
732 SavitzkyGolay sg(3, 2); // radius, polynomial order
733
734 // first fit
735 std::vector<std::vector<Point>> smoothedContours;
736 for (int c = 0; c < (int)contours.size(); ++c) {
737 std::vector<Point> sc = sg.filter_wrap(contours[c]);
738 smoothedContours.push_back(sc);
739 }
740
741 // 1. Build Spatial Grid to find partners quickly
742 std::map<Coord, std::vector<PointID>> grid;
743 for (int c = 0; c < (int)contours.size(); ++c) {
744 for (int p = 0; p < (int)contours[c].size(); ++p) {
745 grid[{static_cast<int>(contours[c][p].x), static_cast<int>(contours[c][p].y)}]
746 .push_back({c, p});
747 }
748 }
749
750 // We calculate ALL targets before applying any updates to maintain stability
751 std::vector<std::vector<Point>> targetPos = contours;
752 // float pairRadiusSq = 1.5f * 1.5f; // Radius to define "Connected/Paired"
753
754 for (int c = 0; c < (int)contours.size(); ++c) {
755 for (int p = 1; p < (int)contours[c].size() - 1; ++p) {
756 // SKIP Constraints
757 if (lockedMasks[c][p])
758 continue;
759
760 Point myPos = contours[c][p];
761 Point prev = contours[c][p - 1];
762 Point next = contours[c][p + 1];
763
764 // 1. Calculate My Laplacian Target (Where I want to go to be smooth)
765 Point myTarget = smoothedContours[c][p];
766
767 // 2. Find Partners in OTHER contours
768 Point sumPartnerTargets = {0, 0};
769 int partnerCount = 0;
770
771 int gx {static_cast<int>(myPos.x)};
772 int gy {static_cast<int>(myPos.y)};
773
774 // Check 3x3 grid
775 for (int dy = -1; dy <= 1; ++dy) {
776 for (int dx = -1; dx <= 1; ++dx) {
777 auto it = grid.find({gx + dx, gy + dy});
778 if (it == grid.end())
779 continue;
780
781 for (const auto& neighbor : it->second) {
782 if (neighbor.cIdx == c)
783 continue; // Ignore self
784
785 Point otherPos = contours[neighbor.cIdx][neighbor.pIdx];
786 if (Point::distSq(myPos, otherPos) < pairRadiusSq) {
787 // Found a partner!
788 // Calculate where the PARTNER wants to go
789 // (We need safe access to partner's neighbors)
790 const auto& otherContour = contours[neighbor.cIdx];
791 int op = neighbor.pIdx;
792
793 // Only calculate partner target if they are not constrained
794 if (op > 0 && op < (int)otherContour.size() - 1 &&
795 // !cornerMasks[neighbor.cIdx][op] &&
796 !lockedMasks[neighbor.cIdx][op]) {
797 Point oPrev = otherContour[op - 1];
798 Point oNext = otherContour[op + 1];
799
800 Point oTarget = smoothedContours[neighbor.cIdx][op];
801
802 sumPartnerTargets += oTarget;
803 partnerCount++;
804 } else {
805 // If partner is constrained (e.g. corner),
806 // we should probably snap to THEM, not smooth them.
807 // For simplicity, we treat their current pos as their target.
808 sumPartnerTargets += otherPos;
809 partnerCount++;
810 }
811 }
812 }
813 }
814 }
815
816 // 3. Average "My Desire" with "Partners' Desires"
817 if (partnerCount > 0) {
818 targetPos[c][p] = (myTarget + sumPartnerTargets) / (1.0f + partnerCount);
819 } else {
820 // No partners, just smooth myself
821 targetPos[c][p] = myTarget;
822 }
823 }
824 }
825
826 contours = targetPos;
827}
828
829void coupled_smooth(std::vector<std::vector<Point>>& contours, Rect bounds) {
830 auto lockedMasks = createBoundaryMask(contours, bounds);
831 coupledSmooth(contours, lockedMasks, 1.0f);
832}
833
834void coupled_smooth_junctions(
835 std::vector<std::vector<Point>>& contours, Rect bounds, std::vector<uint8_t> junctions,
836 int width
837) {
838 auto lockedMasks = createBoundaryMask(contours, bounds);
839
840 updateLockedMasks(contours, lockedMasks, junctions, width);
841
842 coupledSmooth(contours, lockedMasks, 1.0f);
843}
844
845// --- Main Solver ---
846void pack_with_boundary_constraints(
847 std::vector<std::vector<Point>>& contours, Rect bounds, int iterations
848) {
849 // 1. Identify Locked Points (Boundary Constraint)
850 auto lockedMasks = createBoundaryMask(contours, bounds);
851
852 // 2. Identify Feature Corners (Shape Constraint)
853 // std::vector<std::vector<bool>> cornerMasks;
854 // for (const auto& c : contours) cornerMasks.push_back(detectCorners(c));
855
856 constexpr float radiusSq = 3.0f * 3.0f; // Search radius
857
858 for (int iter = 0; iter < iterations; ++iter) {
859 // Build Grid
860 std::map<Coord, std::vector<PointID>> grid;
861 for (int c = 0; c < (int)contours.size(); ++c) {
862 for (int p = 0; p < (int)contours[c].size(); ++p) {
863 grid[{static_cast<int>(contours[c][p].x), static_cast<int>(contours[c][p].y)}]
864 .push_back({c, p});
865 }
866 }
867
868 std::vector<std::vector<Point>> nextContours = contours;
869
870 // Apply Forces
871 for (int c = 0; c < (int)contours.size(); ++c) {
872 for (int p = 0; p < (int)contours[c].size(); ++p) {
873 // CRITICAL CHECK: If on boundary, skip all movement logic
874 if (lockedMasks[c][p])
875 continue;
876
877 Point currentPos = contours[c][p];
878 Point sumTargets = {0, 0};
879 int matchCount = 0;
880
881 // Scan Neighborhood
882 int gx = static_cast<int>(currentPos.x);
883 int gy = static_cast<int>(currentPos.y);
884
885 for (int dy = -1; dy <= 1; ++dy) {
886 for (int dx = -1; dx <= 1; ++dx) {
887 auto it = grid.find({gx + dx, gy + dy});
888 if (it == grid.end())
889 continue;
890
891 for (const auto& neighbor : it->second) {
892 if (neighbor.cIdx == c)
893 continue;
894 const auto& other = contours[neighbor.cIdx];
895 int idxB = neighbor.pIdx;
896
897 auto checkSeg = [&](int s, int e) {
898 Point t = getClosestPoint(currentPos, other[s], other[e]);
899 if (Point::distSq(currentPos, t) < radiusSq) {
900 sumTargets += t;
901 matchCount++;
902 }
903 };
904 if (idxB < (int)other.size() - 1)
905 checkSeg(idxB, idxB + 1);
906 if (idxB > 0)
907 checkSeg(idxB - 1, idxB);
908 }
909 }
910 }
911
912 if (matchCount > 0) {
913 float stiffness {1.5f};
914 nextContours[c][p] =
915 (sumTargets + currentPos * stiffness) / (matchCount + stiffness);
916 }
917 }
918 }
919
920 contours = nextContours;
921 }
922}
923
924} // namespace contours
Definition Point.h:5