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