1#include "internal/contours.h"
12#define M_PI 3.14159265358979323846
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};
23static inline int dirIndex(
int dy,
int dx) {
24 if (dy == 0 && dx == 1)
26 if (dy == 1 && dx == 1)
28 if (dy == 1 && dx == 0)
30 if (dy == 1 && dx == -1)
32 if (dy == 0 && dx == -1)
34 if (dy == -1 && dx == -1)
36 if (dy == -1 && dx == 0)
38 if (dy == -1 && dx == 1)
43static inline int dirIndexFromTo(
int y,
int x,
int ny,
int nx) {
44 return dirIndex(ny - y, nx - x);
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];
59 auto get = [&](
int y,
int x) ->
int {
60 return f[y * paddedW + x];
63 std::vector<Point> pts;
67 const int dStart = dirIndexFromTo(sy, sx, py, px);
69 throw std::runtime_error(
"traceBorder: invalid previous neighbor (not adjacent).");
74 for (
int t = 0; t < 8; ++t) {
75 int d = (dStart + t) & 7;
76 int ny = sy + DY[d], nx = sx + DX[d];
77 if (get(ny, nx) != 0) {
88 pts.push_back(
Point {
static_cast<float>(sx - 1),
static_cast<float>(sy - 1)});
95 const int firstY = y1, firstX = x1;
100 const int dPrev = dirIndexFromTo(y3, x3, y2, x2);
102 throw std::runtime_error(
"traceBorder: broken neighbor chain.");
105 const int d0 = (dPrev + 7) & 7;
106 bool rightZeroExamined =
false;
107 int y4 = y3, x4 = x3;
109 for (
int t = 0; t < 8; ++t) {
110 int d = (d0 - t) & 7;
111 int ny = y3 + DY[d], nx = x3 + DX[d];
115 if (d == 0 && get(ny, nx) == 0) {
116 rightZeroExamined =
true;
119 if (get(ny, nx) != 0) {
127 if (rightZeroExamined) {
130 if (set(y3, x3) == 1) {
136 pts.push_back(
Point {
static_cast<float>(x3 - 1),
static_cast<float>(y3 - 1)});
139 if (y4 == sy && x4 == sx && y3 == firstY && x3 == firstX) {
153ContoursResult find_contours(
const std::vector<uint8_t>& binary,
int width,
int height) {
154 if (width <= 0 || height <= 0) {
157 if ((
int)binary.size() != width * height) {
158 throw std::invalid_argument(
"binary.size() must be width*height");
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);
166 auto set = [&](
int y,
int x) ->
int& {
167 return f[y * paddedW + x];
169 auto get = [&](
int y,
int x) ->
int {
170 return f[y * paddedW + x];
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;
182 bool is_hole =
false;
184 std::vector<Point> points;
189 std::vector<NodeInfo> nodes(2);
190 nodes[1].is_hole =
true;
191 nodes[1].parent_id = 1;
192 nodes[1].used =
true;
195 for (
int y = 1; y <= height; ++y) {
198 for (
int x = 1; x <= width; ++x) {
203 bool startBorder =
false;
208 if (fij == 1 && get(y, x - 1) == 0) {
216 else if (fij >= 1 && get(y, x + 1) == 0) {
230 if ((
int)nodes.size() <= nbd)
231 nodes.resize(nbd + 1);
233 nodes[nbd].is_hole = isHole;
234 nodes[nbd].used =
true;
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;
244 nodes[nbd].points = traceBorder(f, paddedW, y, x, py, px, nbd);
248 if (get(y, x) != 1) {
249 lnbd = std::abs(get(y, x));
256 std::vector<int> idToIdx(nbd + 1, -1);
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));
263 for (
int id = 2;
id <= nbd; ++id) {
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});
273 for (
int id = 2;
id <= nbd; ++id) {
276 const int idx = idToIdx[id];
277 const int pid = nodes[id].parent_id;
278 out.hierarchy[idx][3] = (pid <= 1) ? -1 : idToIdx[pid];
282 std::vector<int> lastChild(out.contours.size(), -1);
284 for (
int i = 0; i < (int)out.contours.size(); ++i) {
285 int p = out.hierarchy[i][3];
289 if (out.hierarchy[p][2] == -1) {
290 out.hierarchy[p][2] = i;
293 int last = lastChild[p];
294 out.hierarchy[last][0] = i;
295 out.hierarchy[i][1] = last;
306 bool operator<(
const Coord& other)
const {
307 return std::tie(x, y) < std::tie(other.x, other.y);
313 float len = std::sqrt(p.x * p.x + p.y * p.y);
316 return {p.x / len, p.y / len};
320Point getTangent(
const std::vector<Point>& vec,
int i) {
326 int prev = (i == 0) ? 0 : i - 1;
327 int next = (i == n - 1) ? n - 1 : i + 1;
329 return normalize(vec[next] - vec[prev]);
334std::pair<Point, float> getClosestPointOnSegment(
Point p,
Point v,
Point w) {
335 float l2 = Point::distSq(v, w);
337 return {v, Point::distSq(p, v)};
341 float t = ((p.x - v.x) * (w.x - v.x) + (p.y - v.y) * (w.y - v.y)) / l2;
344 t = std::max(0.0f, std::min(1.0f, t));
346 Point projection = {v.x + t * (w.x - v.x), v.y + t * (w.y - v.y)};
347 return {projection, Point::distSq(p, projection)};
381Point getQuadraticTarget(
const std::vector<Point>& pts,
int i) {
382 int n {
static_cast<int>(pts.size())};
384 if (i <= 0 || i >= n - 1) {
391 if (i < 2 || i >= n - 2) {
392 Point prev {pts[i - 1]};
394 Point next {pts[i + 1]};
395 return 0.25f * prev + 0.5f * curr + 0.25f * next;
401 const Point& p2L {pts[i - 2]};
402 const Point& p1L {pts[i - 1]};
403 const Point& p {pts[i]};
404 const Point& p1R {pts[i + 1]};
405 const Point& p2R {pts[i + 2]};
407 return (-3.0f * p2L + 12.0f * p1L + 17.0f * p + 12.0f * p1R - 3.0f * p2R) / 35.0f;
410void coupledSmooth(std::vector<Point>& contourA, std::vector<Point>& contourB) {
411 std::vector<std::vector<Point>> contours = {contourA, contourB};
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(
422 std::vector<std::vector<Point>> targetPos = {contourA, contourB};
425 float pairRadiusSq = 2.0 * 2.0;
427 for (
int c = 0; c < 2; ++c) {
429 for (
int p = 1; p < (int)contours[c].size() - 1; ++p) {
430 Point myPos = contours[c][p];
433 Point myTarget = getQuadraticTarget(contours[c], p);
436 Point sumPartnerTargets = {0, 0};
437 int partnerCount = 0;
439 int gx = (int)std::round(myPos.x);
440 int gy = (int)std::round(myPos.y);
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())
449 for (
const auto& neighbor : it->second) {
450 if (neighbor.cIdx == c)
453 Point otherPos = contours[neighbor.cIdx][neighbor.pIdx];
456 if (Point::distSq(myPos, otherPos) < pairRadiusSq) {
458 int op = neighbor.pIdx;
459 const auto& otherContour = contours[neighbor.cIdx];
462 if (op > 0 && op < (
int)otherContour.size() - 1) {
464 oTarget = getQuadraticTarget(otherContour, op);
470 sumPartnerTargets += oTarget;
480 if (partnerCount > 0) {
481 targetPos[c][p] = (myTarget + sumPartnerTargets) / (1.0 + partnerCount);
483 targetPos[c][p] = myTarget;
488 std::copy(targetPos[0].begin(), targetPos[0].end(), contourA.begin());
489 std::copy(targetPos[1].begin(), targetPos[1].end(), contourB.begin());
492void stitch_smooth(std::vector<Point>& vecA, std::vector<Point>& vecB) {
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;
507 std::vector<Update> updatesA;
508 std::vector<Update> updatesB;
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);
515 float minDst = std::numeric_limits<float>::max();
516 Point bestTarget = vecA[i];
517 bool foundMatch =
false;
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;
527 if (bIdx < (
int)vecB.size() - 1) {
528 auto res = getClosestPointOnSegment(vecA[i], vecB[bIdx], vecB[bIdx + 1]);
529 if (res.second < minDst) {
531 bestTarget = res.first;
538 auto res = getClosestPointOnSegment(vecA[i], vecB[bIdx - 1], vecB[bIdx]);
539 if (res.second < minDst) {
541 bestTarget = res.first;
551 Point mid = (vecA[i] + bestTarget) * 0.5f;
552 updatesA.push_back({(int)i, mid});
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;
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);
567 float minDst = std::numeric_limits<float>::max();
568 Point bestTarget = vecB[i];
569 bool foundMatch =
false;
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;
578 if (aIdx < (
int)vecA.size() - 1) {
579 auto res = getClosestPointOnSegment(vecB[i], vecA[aIdx], vecA[aIdx + 1]);
580 if (res.second < minDst) {
582 bestTarget = res.first;
588 auto res = getClosestPointOnSegment(vecB[i], vecA[aIdx - 1], vecA[aIdx]);
589 if (res.second < minDst) {
591 bestTarget = res.first;
600 Point mid = (vecB[i] + bestTarget) * 0.5f;
601 updatesB.push_back({(int)i, mid});
605 if ((updatesA.size() == 0) | (updatesB.size() == 0)) {
610 for (
const auto& u : updatesA)
611 vecA[u.index] = u.newPos;
612 for (
const auto& u : updatesB)
613 vecB[u.index] = u.newPos;
620 auto smoothVector = [](std::vector<Point>& pts,
const std::vector<Update>& updates) {
621 std::vector<Point> original = pts;
622 for (
const auto& u : updates) {
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];
630 auto laplacianSmooth = [](std::vector<Point>& pts,
const std::vector<Update>& updates) {
631 std::vector<Point> original = pts;
632 for (
const auto& u : updates) {
634 if (i > 0 && i < (
int)pts.size() - 1) {
637 pts[i] = 0.2f * original[i - 1] + 0.6f * original[i] + 0.2f * original[i + 1];
642 smoothVector(vecA, updatesA);
643 smoothVector(vecB, updatesB);
648 float l2 = Point::distSq(v, w);
652 float t = ((p.x - v.x) * (w.x - v.x) + (p.y - v.y) * (w.y - v.y)) / l2;
654 t = std::max(-0.1f, std::min(1.1f, t));
656 return {v.x + t * (w.x - v.x), v.y + t * (w.y - v.y)};
661std::vector<std::vector<bool>>
662createBoundaryMask(
const std::vector<std::vector<Point>>& contours,
Rect bounds) {
663 std::vector<std::vector<bool>> locked(contours.size());
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];
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) {
682void updateLockedMasks(
683 const std::vector<std::vector<Point>>& contours, std::vector<std::vector<bool>>& locked,
684 std::vector<uint8_t>& junctions,
int width
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) {
698std::vector<bool> detectCorners(
const std::vector<Point>& pts,
float angleThresholdDeg = 150.0) {
699 std::vector<bool> isCorner(pts.size(),
false);
702 float threshold = std::cos(angleThresholdDeg * M_PI / 180.0f);
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)
712 isCorner.back() =
true;
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) {
724 pts[i] = 0.25f * original[i - 1] + 0.5f * original[i] + 0.25f * original[i + 1];
729 std::vector<std::vector<Point>>& contours,
const std::vector<std::vector<bool>>& lockedMasks,
730 float pairRadiusSq = 2.25f
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);
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)}]
751 std::vector<std::vector<Point>> targetPos = contours;
754 for (
int c = 0; c < (int)contours.size(); ++c) {
755 for (
int p = 1; p < (int)contours[c].size() - 1; ++p) {
757 if (lockedMasks[c][p])
760 Point myPos = contours[c][p];
761 Point prev = contours[c][p - 1];
762 Point next = contours[c][p + 1];
765 Point myTarget = smoothedContours[c][p];
768 Point sumPartnerTargets = {0, 0};
769 int partnerCount = 0;
771 int gx {
static_cast<int>(myPos.x)};
772 int gy {
static_cast<int>(myPos.y)};
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())
781 for (
const auto& neighbor : it->second) {
782 if (neighbor.cIdx == c)
785 Point otherPos = contours[neighbor.cIdx][neighbor.pIdx];
786 if (Point::distSq(myPos, otherPos) < pairRadiusSq) {
790 const auto& otherContour = contours[neighbor.cIdx];
791 int op = neighbor.pIdx;
794 if (op > 0 && op < (
int)otherContour.size() - 1 &&
796 !lockedMasks[neighbor.cIdx][op]) {
797 Point oPrev = otherContour[op - 1];
798 Point oNext = otherContour[op + 1];
800 Point oTarget = smoothedContours[neighbor.cIdx][op];
802 sumPartnerTargets += oTarget;
808 sumPartnerTargets += otherPos;
817 if (partnerCount > 0) {
818 targetPos[c][p] = (myTarget + sumPartnerTargets) / (1.0f + partnerCount);
821 targetPos[c][p] = myTarget;
826 contours = targetPos;
829void coupled_smooth(std::vector<std::vector<Point>>& contours,
Rect bounds) {
830 auto lockedMasks = createBoundaryMask(contours, bounds);
831 coupledSmooth(contours, lockedMasks, 1.0f);
834void coupled_smooth_junctions(
835 std::vector<std::vector<Point>>& contours,
Rect bounds, std::vector<uint8_t> junctions,
838 auto lockedMasks = createBoundaryMask(contours, bounds);
840 updateLockedMasks(contours, lockedMasks, junctions, width);
842 coupledSmooth(contours, lockedMasks, 1.0f);
846void pack_with_boundary_constraints(
847 std::vector<std::vector<Point>>& contours,
Rect bounds,
int iterations
850 auto lockedMasks = createBoundaryMask(contours, bounds);
856 constexpr float radiusSq = 3.0f * 3.0f;
858 for (
int iter = 0; iter < iterations; ++iter) {
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)}]
868 std::vector<std::vector<Point>> nextContours = contours;
871 for (
int c = 0; c < (int)contours.size(); ++c) {
872 for (
int p = 0; p < (int)contours[c].size(); ++p) {
874 if (lockedMasks[c][p])
877 Point currentPos = contours[c][p];
878 Point sumTargets = {0, 0};
882 int gx =
static_cast<int>(currentPos.x);
883 int gy =
static_cast<int>(currentPos.y);
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())
891 for (
const auto& neighbor : it->second) {
892 if (neighbor.cIdx == c)
894 const auto& other = contours[neighbor.cIdx];
895 int idxB = neighbor.pIdx;
897 auto checkSeg = [&](
int s,
int e) {
898 Point t = getClosestPoint(currentPos, other[s], other[e]);
899 if (Point::distSq(currentPos, t) < radiusSq) {
904 if (idxB < (
int)other.size() - 1)
905 checkSeg(idxB, idxB + 1);
907 checkSeg(idxB - 1, idxB);
912 if (matchCount > 0) {
913 float stiffness {1.5f};
915 (sumTargets + currentPos * stiffness) / (matchCount + stiffness);
920 contours = nextContours;