- u_async.c: process immediate_queue BEFORE epoll_wait in uasync_poll - stcp_link.c: defensive NULL etcp_conn/etcp_link in stcp_link_close - utun_instance.c: move stcp_server_list_destroy_all to Phase J (before ETCP), add deferred drain in both Phase J and L - stcp.h/c: STCP_HS_ENC_CLIENT 6→38, STCP_HS_ENC_SERVER 7→39, exchange ed25519_pubkey in both handshake directions - etcp_connections.c: copy peer_ed25519 from STCP to ETCP_CONN in etcp_link_enter_ready_tcp; guard link->etcp in tcp_link_close_cb - chat_sync.c: fix lk->conn NULL dereference for TCP links - test_stcp.c: update stcp_server_create/stcp_client_connect callsites - tools/chat_tcp_test: change transport udp→tcp
243 lines
8.0 KiB
C++
243 lines
8.0 KiB
C++
// Copyright 2026 Axel Waggershauser
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
#include "LocalGrid.h"
|
|
|
|
#include "LogMatrix.h"
|
|
#include "StdGenerator.h"
|
|
#include "ZXAlgorithms.h"
|
|
|
|
#include <algorithm>
|
|
#include <optional>
|
|
#include <ranges>
|
|
#include <span>
|
|
#include <vector>
|
|
|
|
#ifndef PRINT_DEBUG
|
|
#define printf(...){}
|
|
#endif
|
|
|
|
namespace ZXing {
|
|
|
|
// returns the average of the largest cluster of values where cluster is defined as values that are within threshold of each other
|
|
double clusterAvg(std::ranges::range auto& v, double threshold)
|
|
{
|
|
std::ranges::sort(v);
|
|
size_t bestStart = 0, bestLen = 0, start = 0;
|
|
for (size_t end = 0; end < v.size(); ++end) {
|
|
while (v[end] - v[start] >= threshold)
|
|
++start;
|
|
if (end - start + 1 > bestLen) {
|
|
bestLen = end - start + 1;
|
|
bestStart = start;
|
|
}
|
|
}
|
|
double sum = 0;
|
|
for (size_t i = bestStart; i < bestStart + bestLen; ++i)
|
|
sum += v[i];
|
|
sum /= bestLen;
|
|
|
|
#ifdef PRINT_DEBUG
|
|
printf("ds: ");
|
|
for (auto d : v)
|
|
printf("%5.2f ", d);
|
|
printf(" -> len: %zu, avg: %5.2f\n", bestLen, sum);
|
|
#endif
|
|
return sum;
|
|
};
|
|
|
|
void LocalGrid::adjustOriginAndStep(PointF& step, int radius, const std::span<const PointF> offsets)
|
|
{
|
|
auto modSize = length(step);
|
|
int limit = int(6 * modSize);
|
|
auto dir = bresenhamDirection(step);
|
|
modSize /= length(dir); // mod size in terms of steps in the given direction
|
|
printf("step: (%.2f, %.2f) %.2f: ", step.x, step.y, modSize);
|
|
|
|
auto nearestHalfResidual = [](double n, double s) { return n - (std::round((n - s / 2) / s) * s + s / 2); };
|
|
|
|
struct DistMod {
|
|
double dist, modSize;
|
|
};
|
|
thread_local std::vector<DistMod> distMod;
|
|
distMod.clear();
|
|
distMod.reserve(offsets.size() * radius + 1);
|
|
for (int r = 0; r <= radius; ++r)
|
|
for (auto offset : offsets) {
|
|
auto start = origin + r * offset;
|
|
auto startC = centered(start);
|
|
int stepsPos = BitMatrixCursorF(*img, startC, dir).stepToEdge(1, limit);
|
|
int stepsNeg = BitMatrixCursorF(*img, startC, -dir).stepToEdge(1, limit);
|
|
auto distPos = stepsPos - dot(start - startC, dir) - 0.5; // +0.5 because the center of the pixel is at .5, .5
|
|
auto distNeg = stepsNeg - dot(start - startC, -dir) - 0.5;
|
|
|
|
if (stepsPos && stepsNeg) {
|
|
int blockSize = stepsPos + stepsNeg - 1;
|
|
auto localModSize = blockSize / std::max(1.0, std::round(blockSize / modSize));
|
|
distMod.emplace_back(distPos, blockSize / modSize < 5 ? localModSize : 0.0);
|
|
// if (r == 0 && (std::min(distNeg, distPos) < modSize / 4)) {
|
|
// origin += (distNeg < distPos ? 1 : -1) * modSize / 4 * dir;
|
|
// continue;
|
|
// }
|
|
}
|
|
else if (stepsPos)
|
|
distMod.emplace_back(distPos, 0.0);
|
|
else if (stepsNeg)
|
|
distMod.emplace_back(-distNeg, 0.0);
|
|
|
|
printf("+%.2f -%.2f (%.1f) | ", distPos, distNeg, distMod.empty() ? 0.0 : distMod.back().modSize);
|
|
|
|
if (r == 0)
|
|
break; // only do the center point once
|
|
}
|
|
|
|
// calcuate the average local module size from the points where we found one...
|
|
double localModSize = 0.0;
|
|
int n = 0;
|
|
for (const auto& t : distMod | std::views::filter([](const DistMod& t) { return t.modSize > 0; }))
|
|
localModSize += t.modSize, ++n;
|
|
if (n == 0)
|
|
return;
|
|
localModSize /= n;
|
|
printf("\nlocal mod size: %.2f\n", localModSize);
|
|
|
|
// ... and use it for the points where we didn't find one
|
|
thread_local std::vector<double> d;
|
|
d.clear();
|
|
d.reserve(distMod.size());
|
|
for (auto& t : distMod) {
|
|
if (t.modSize == 0.0)
|
|
t.modSize = localModSize;
|
|
d.push_back(t.dist < 0 ? -nearestHalfResidual(-t.dist, t.modSize) : nearestHalfResidual(t.dist, t.modSize));
|
|
printf("%.2f (%.1f) -> %.2f | ", t.dist, t.modSize, d.back());
|
|
}
|
|
printf("\n");
|
|
|
|
origin += clusterAvg(d, modSize / 2) * dir;
|
|
|
|
// if we found local mod sizes for each point (hopefully at the timing pattern crosses), we update the step size
|
|
if (n == Size(distMod) && std::abs(localModSize - modSize) > modSize * 0.1) {
|
|
step = localModSize / modSize * step;
|
|
printf(" adjusted mod size from %.2f to %.2f\n", modSize, localModSize);
|
|
}
|
|
}
|
|
|
|
LocalGrid::LocalGrid(const BitMatrix& image, const PerspectiveTransform& mod2Pix, PointI p, PointI dim, PointI offset)
|
|
: img(&image), dim(dim), center(p)
|
|
{
|
|
origin = mod2Pix(centered(p));
|
|
stepX = mod2Pix(centered(p) + PointF{1, 0}) - origin;
|
|
stepY = mod2Pix(centered(p) + PointF{0, 1}) - origin;
|
|
|
|
printf("LocalGrid @ (%d, %d), initial origin: (%.2f, %.2f), offset: (%d, %d), stepX: (%.2f, %.2f), stepY: (%.2f, %.2f)\n",
|
|
center.x, center.y, origin.x, origin.y, offset.x, offset.y, stepX.x, stepX.y, stepY.x, stepY.y);
|
|
log(origin, 3);
|
|
|
|
auto offsets = std::array{-stepX, -stepY, stepX, stepY}; // works better for DataMatrix (especially near the symbol edges)
|
|
// auto offsets = std::array{-stepX - stepY, stepX - stepY, stepX + stepY, -stepX + stepY};
|
|
// auto offsets = std::array{-stepX, -stepY, stepX, stepY, -stepX - stepY, stepX - stepY, stepX + stepY, -stepX + stepY};
|
|
|
|
// evaluate the image at origin + offset (allows to effectively work near the border of the symbol)
|
|
origin = getPos(PointF(offset));
|
|
for (int i = 0; i < 2; ++i) {
|
|
adjustOriginAndStep(stepX, 2, offsets);
|
|
adjustOriginAndStep(stepY, 2, offsets);
|
|
printf("\n");
|
|
}
|
|
origin = getPos(PointF(-offset));
|
|
}
|
|
|
|
bool LocalGrid::isTimingPatternCross(PointI p, bool isBlack, int radius, int errorThreshold)
|
|
{
|
|
// TODO: look into replacing this with something along the lines of CheckSymmetricAztecCenterPattern
|
|
auto wrapOffset = [&](int center, int offset, int dim) {
|
|
int pos = center + offset;
|
|
return pos < 0 ? radius - pos : (pos >= dim ? -(radius + (pos - dim) + 1) : offset);
|
|
};
|
|
|
|
int errors = 0;
|
|
for (int r = 0; r <= radius; ++r) {
|
|
auto check = [&](int x, int y) {
|
|
x = wrapOffset(center.x, x, dim.x);
|
|
y = wrapOffset(center.y, y, dim.y);
|
|
auto d = PointI(x, y);
|
|
errors += !findValue(p + d, d, Value((x + y) % 2 == (isBlack ? 0 : 1)));
|
|
};
|
|
check(-r, 0);
|
|
check(r, 0);
|
|
check(0, -r);
|
|
check(0, r);
|
|
if (errors > errorThreshold)
|
|
return false;
|
|
}
|
|
return errors <= errorThreshold;
|
|
};
|
|
|
|
std::optional<PointF> LocalGrid::findTimingPatternCross(bool isBlack, int radius)
|
|
{
|
|
for (auto p : Spiral(3)) {
|
|
// check if there is a timing pattern cross candidate centered at p with half the radius
|
|
if (isTimingPatternCross(p, isBlack, radius / 2)) {
|
|
auto original = origin;
|
|
origin = getPos(p);
|
|
// adjust origin and step with full radius and only in the direction of the timing pattern
|
|
printf("timing pattern:\n");
|
|
adjustOriginAndStep(stepX, radius, std::array{-stepX, stepX});
|
|
adjustOriginAndStep(stepY, radius, std::array{-stepY, stepY});
|
|
|
|
// check again, now with the full radius, to make sure we are correctly aligned to the timing pattern
|
|
if (isTimingPatternCross(PointI{0, 0}, isBlack, radius))
|
|
return origin;
|
|
|
|
origin = original;
|
|
}
|
|
}
|
|
return {};
|
|
}
|
|
|
|
bool LocalGrid::findPattern(int radius, PointI timingStart, Directions timingDirs, PointI blackStart, Directions blackDirs,
|
|
PointI whiteStart, Directions whiteDirs)
|
|
{
|
|
auto isPatternAt = [&](PointI p) {
|
|
for (int r = 0; r <= radius; ++r) {
|
|
for (auto d : timingDirs)
|
|
if (!findValue(p + timingStart + r * d, d, Value(r % 2 == 1)))
|
|
return false;
|
|
for (auto d : blackDirs)
|
|
if (!findValue(p + blackStart + r * d, d, Value(true)))
|
|
return false;
|
|
for (auto d : whiteDirs)
|
|
if (!findValue(p + whiteStart + r * d, d, Value(false)))
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
for (auto p : Spiral(3)) {
|
|
if (isPatternAt(p)) {
|
|
auto original = origin;
|
|
origin = getPos(PointF(p));
|
|
// adjust origin and step with full radius and only in the direction of the timing pattern
|
|
printf("found pattern at (%.2f, %.2f)\n", origin.x, origin.y);
|
|
std::vector<PointF> stepsX, stepsY;
|
|
for (auto d : timingDirs)
|
|
d.y == 0 ? stepsX.push_back(d.x * stepX) : stepsY.push_back(d.y * stepY);
|
|
|
|
if (!stepsX.empty())
|
|
adjustOriginAndStep(stepX, radius, stepsX);
|
|
if (!stepsY.empty())
|
|
adjustOriginAndStep(stepY, radius, stepsY);
|
|
if ((!stepsX.empty() || !stepsY.empty()) && !isPatternAt(PointI{0, 0})) {
|
|
origin = original;
|
|
printf("pattern lost after adjusting for timing pattern, reverting origin\n");
|
|
}
|
|
printf("\n");
|
|
return true;
|
|
}
|
|
}
|
|
printf("\n");
|
|
return false;
|
|
}
|
|
|
|
} // namespace ZXing
|