1#include "internal/contours.h"
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};
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;
30static inline int dirIndexFromTo(
int y,
int x,
int ny,
int nx) {
31 return dirIndex(ny - y, nx - x);
41static std::vector<Point> traceBorder(std::vector<int> &f,
int paddedW,
int sy,
int sx,
int py,
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]; };
46 std::vector<Point> pts;
50 const int dStart = dirIndexFromTo(sy, sx, py, px);
52 throw std::runtime_error(
"traceBorder: invalid previous neighbor (not adjacent).");
57 for (
int t = 0; t < 8; ++t) {
58 int d = (dStart + t) & 7;
59 int ny = sy + DY[d], nx = sx + DX[d];
60 if (get(ny, nx) != 0) {
71 pts.push_back(
Point{
static_cast<float>(sx - 1),
static_cast<float>(sy - 1)});
78 const int firstY = y1, firstX = x1;
83 const int dPrev = dirIndexFromTo(y3, x3, y2, x2);
85 throw std::runtime_error(
"traceBorder: broken neighbor chain.");
88 const int d0 = (dPrev + 7) & 7;
89 bool rightZeroExamined =
false;
92 for (
int t = 0; t < 8; ++t) {
94 int ny = y3 + DY[d], nx = x3 + DX[d];
98 if (d == 0 && get(ny, nx) == 0) {
99 rightZeroExamined =
true;
102 if (get(ny, nx) != 0) {
110 if (rightZeroExamined) {
113 if (set(y3, x3) == 1) {
119 pts.push_back(
Point{
static_cast<float>(x3 - 1),
static_cast<float>(y3 - 1)});
122 if (y4 == sy && x4 == sx && y3 == firstY && x3 == firstX) {
136ContoursResult find_contours(
const std::vector<uint8_t> &binary,
int width,
int height) {
137 if (width <= 0 || height <= 0) {
140 if ((
int)binary.size() != width * height) {
141 throw std::invalid_argument(
"binary.size() must be width*height");
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);
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]; };
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;
161 bool is_hole =
false;
163 std::vector<Point> points;
168 std::vector<NodeInfo> nodes(2);
169 nodes[1].is_hole =
true;
170 nodes[1].parent_id = 1;
171 nodes[1].used =
true;
174 for (
int y = 1; y <= height; ++y) {
177 for (
int x = 1; x <= width; ++x) {
179 if (fij == 0)
continue;
181 bool startBorder =
false;
186 if (fij == 1 && get(y, x - 1) == 0) {
194 else if (fij >= 1 && get(y, x + 1) == 0) {
208 if ((
int)nodes.size() <= nbd) nodes.resize(nbd + 1);
210 nodes[nbd].is_hole = isHole;
211 nodes[nbd].used =
true;
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;
221 nodes[nbd].points = traceBorder(f, paddedW, y, x, py, px, nbd);
225 if (get(y, x) != 1) {
226 lnbd = std::abs(get(y, x));
233 std::vector<int> idToIdx(nbd + 1, -1);
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));
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});
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];
257 std::vector<int> lastChild(out.contours.size(), -1);
259 for (
int i = 0; i < (int)out.contours.size(); ++i) {
260 int p = out.hierarchy[i][3];
263 if (out.hierarchy[p][2] == -1) {
264 out.hierarchy[p][2] = i;
267 int last = lastChild[p];
268 out.hierarchy[last][0] = i;
269 out.hierarchy[i][1] = last;
280 bool operator<(
const Coord &other)
const {
281 return std::tie(x, y) < std::tie(other.x, other.y);
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};
293Point getTangent(
const std::vector<Point> &vec,
int i) {
295 if (n < 2)
return {0, 0};
298 int prev = (i == 0) ? 0 : i - 1;
299 int next = (i == n - 1) ? n - 1 : i + 1;
301 return normalize(vec[next] - vec[prev]);
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)};
312 float t = ((p.x - v.x) * (w.x - v.x) + (p.y - v.y) * (w.y - v.y)) / l2;
315 t = std::max(0.0f, std::min(1.0f, t));
317 Point projection = {v.x + t * (w.x - v.x), v.y + t * (w.y - v.y)};
318 return {projection, Point::distSq(p, projection)};
352Point getQuadraticTarget(
const std::vector<Point> &pts,
int i) {
353 int n{
static_cast<int>(pts.size())};
355 if (i <= 0 || i >= n - 1) {
362 if (i < 2 || i >= n - 2) {
363 Point prev{pts[i - 1]};
365 Point next{pts[i + 1]};
366 return 0.25f * prev + 0.5f * curr + 0.25f * next;
372 const Point &p2L{pts[i - 2]};
373 const Point &p1L{pts[i - 1]};
374 const Point &p{pts[i]};
375 const Point &p1R{pts[i + 1]};
376 const Point &p2R{pts[i + 2]};
378 return (-3.0f * p2L + 12.0f * p1L + 17.0f * p + 12.0f * p1R - 3.0f * p2R) / 35.0f;
381void coupledSmooth(std::vector<Point> &contourA, std::vector<Point> &contourB) {
382 std::vector<std::vector<Point>> contours = {contourA, contourB};
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(
392 std::vector<std::vector<Point>> targetPos = {contourA, contourB};
395 float pairRadiusSq = 2.0 * 2.0;
397 for (
int c = 0; c < 2; ++c) {
399 for (
int p = 1; p < (int)contours[c].size() - 1; ++p) {
400 Point myPos = contours[c][p];
403 Point myTarget = getQuadraticTarget(contours[c], p);
406 Point sumPartnerTargets = {0, 0};
407 int partnerCount = 0;
409 int gx = (int)std::round(myPos.x);
410 int gy = (int)std::round(myPos.y);
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;
418 for (
const auto &neighbor : it->second) {
419 if (neighbor.cIdx == c)
continue;
421 Point otherPos = contours[neighbor.cIdx][neighbor.pIdx];
424 if (Point::distSq(myPos, otherPos) < pairRadiusSq) {
426 int op = neighbor.pIdx;
427 const auto &otherContour = contours[neighbor.cIdx];
430 if (op > 0 && op < (
int)otherContour.size() - 1) {
432 oTarget = getQuadraticTarget(otherContour, op);
438 sumPartnerTargets += oTarget;
448 if (partnerCount > 0) {
449 targetPos[c][p] = (myTarget + sumPartnerTargets) / (1.0 + partnerCount);
451 targetPos[c][p] = myTarget;
456 std::copy(targetPos[0].begin(), targetPos[0].end(), contourA.begin());
457 std::copy(targetPos[1].begin(), targetPos[1].end(), contourB.begin());
460void stitch_smooth(std::vector<Point> &vecA, std::vector<Point> &vecB) {
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;
475 std::vector<Update> updatesA;
476 std::vector<Update> updatesB;
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);
483 float minDst = std::numeric_limits<float>::max();
484 Point bestTarget = vecA[i];
485 bool foundMatch =
false;
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;
495 if (bIdx < (
int)vecB.size() - 1) {
496 auto res = getClosestPointOnSegment(vecA[i], vecB[bIdx], vecB[bIdx + 1]);
497 if (res.second < minDst) {
499 bestTarget = res.first;
506 auto res = getClosestPointOnSegment(vecA[i], vecB[bIdx - 1], vecB[bIdx]);
507 if (res.second < minDst) {
509 bestTarget = res.first;
519 Point mid = (vecA[i] + bestTarget) * 0.5f;
520 updatesA.push_back({(int)i, mid});
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;
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);
535 float minDst = std::numeric_limits<float>::max();
536 Point bestTarget = vecB[i];
537 bool foundMatch =
false;
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;
546 if (aIdx < (
int)vecA.size() - 1) {
547 auto res = getClosestPointOnSegment(vecB[i], vecA[aIdx], vecA[aIdx + 1]);
548 if (res.second < minDst) {
550 bestTarget = res.first;
556 auto res = getClosestPointOnSegment(vecB[i], vecA[aIdx - 1], vecA[aIdx]);
557 if (res.second < minDst) {
559 bestTarget = res.first;
568 Point mid = (vecB[i] + bestTarget) * 0.5f;
569 updatesB.push_back({(int)i, mid});
573 if ((updatesA.size() == 0) | (updatesB.size() == 0)) {
578 for (
const auto &u : updatesA) vecA[u.index] = u.newPos;
579 for (
const auto &u : updatesB) vecB[u.index] = u.newPos;
586 auto smoothVector = [](std::vector<Point> &pts,
const std::vector<Update> &updates) {
587 std::vector<Point> original = pts;
588 for (
const auto &u : updates) {
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];
596 auto laplacianSmooth = [](std::vector<Point> &pts,
const std::vector<Update> &updates) {
597 std::vector<Point> original = pts;
598 for (
const auto &u : updates) {
600 if (i > 0 && i < (
int)pts.size() - 1) {
603 pts[i] = 0.2f * original[i - 1] + 0.6f * original[i] + 0.2f * original[i + 1];
608 smoothVector(vecA, updatesA);
609 smoothVector(vecB, updatesB);
614 float l2 = Point::distSq(v, w);
615 if (l2 == 0.0f)
return v;
617 float t = ((p.x - v.x) * (w.x - v.x) + (p.y - v.y) * (w.y - v.y)) / l2;
619 t = std::max(-0.1f, std::min(1.1f, t));
621 return {v.x + t * (w.x - v.x), v.y + t * (w.y - v.y)};
626std::vector<std::vector<bool>> createBoundaryMask(
const std::vector<std::vector<Point>> &contours,
628 std::vector<std::vector<bool>> locked(contours.size());
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];
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) {
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);
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;
660 isCorner.back() =
true;
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) {
669 if (isLocked[i])
continue;
671 pts[i] = 0.25f * original[i - 1] + 0.5f * original[i] + 0.25f * original[i + 1];
675void coupledSmooth(std::vector<std::vector<Point>> &contours,
676 const std::vector<std::vector<bool>> &lockedMasks,
float pairRadiusSq = 2.25f) {
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);
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)}]
696 std::vector<std::vector<Point>> targetPos = contours;
699 for (
int c = 0; c < (int)contours.size(); ++c) {
700 for (
int p = 1; p < (int)contours[c].size() - 1; ++p) {
702 if (lockedMasks[c][p])
continue;
704 Point myPos = contours[c][p];
705 Point prev = contours[c][p - 1];
706 Point next = contours[c][p + 1];
709 Point myTarget = smoothedContours[c][p];
712 Point sumPartnerTargets = {0, 0};
713 int partnerCount = 0;
715 int gx{
static_cast<int>(myPos.x)};
716 int gy{
static_cast<int>(myPos.y)};
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;
724 for (
const auto &neighbor : it->second) {
725 if (neighbor.cIdx == c)
continue;
727 Point otherPos = contours[neighbor.cIdx][neighbor.pIdx];
728 if (Point::distSq(myPos, otherPos) < pairRadiusSq) {
732 const auto &otherContour = contours[neighbor.cIdx];
733 int op = neighbor.pIdx;
736 if (op > 0 && op < (
int)otherContour.size() - 1 &&
738 !lockedMasks[neighbor.cIdx][op]) {
739 Point oPrev = otherContour[op - 1];
740 Point oNext = otherContour[op + 1];
742 Point oTarget = smoothedContours[neighbor.cIdx][op];
744 sumPartnerTargets += oTarget;
750 sumPartnerTargets += otherPos;
759 if (partnerCount > 0) {
760 targetPos[c][p] = (myTarget + sumPartnerTargets) / (1.0f + partnerCount);
763 targetPos[c][p] = myTarget;
768 contours = targetPos;
771void coupled_smooth(std::vector<std::vector<Point>> &contours,
Rect bounds) {
772 auto lockedMasks = createBoundaryMask(contours, bounds);
773 coupledSmooth(contours, lockedMasks, 1.0f);
777void pack_with_boundary_constraints(std::vector<std::vector<Point>> &contours,
Rect bounds,
780 auto lockedMasks = createBoundaryMask(contours, bounds);
786 constexpr float radiusSq = 3.0f * 3.0f;
788 for (
int iter = 0; iter < iterations; ++iter) {
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)}]
798 std::vector<std::vector<Point>> nextContours = contours;
801 for (
int c = 0; c < (int)contours.size(); ++c) {
802 for (
int p = 0; p < (int)contours[c].size(); ++p) {
804 if (lockedMasks[c][p])
continue;
806 Point currentPos = contours[c][p];
807 Point sumTargets = {0, 0};
811 int gx =
static_cast<int>(currentPos.x);
812 int gy =
static_cast<int>(currentPos.y);
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;
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;
824 auto checkSeg = [&](
int s,
int e) {
825 Point t = getClosestPoint(currentPos, other[s], other[e]);
826 if (Point::distSq(currentPos, t) < radiusSq) {
831 if (idxB < (
int)other.size() - 1) checkSeg(idxB, idxB + 1);
832 if (idxB > 0) checkSeg(idxB - 1, idxB);
837 if (matchCount > 0) {
838 float stiffness{1.5f};
840 (sumTargets + currentPos * stiffness) / (matchCount + stiffness);
845 contours = nextContours;