Viewing is open to everyone. Downloading and starring need a GitHub sign-in. Sign in with GitHub

roundtrip_tests.cpp

v0.1.0-build.85 Latest Commit ede38f5

Size
11 KB
Lines
190
Language
cpp
SHA-256
6d9f5f23fb5f…
View on GitHub

Sign in with GitHub to download. Each download is recorded with your GitHub username.

1// Render -> rasterize -> decode round trips for all three profiles. The
2// raster comes from the emitted SVG (tests/svg_raster.h), so these tests fail
3// if the printed geometry and the image decoder ever disagree.
4#include "ckey/ckey.h"
5#include "check.h"
6#include "svg_raster.h"
7
8#include <cstdint>
9#include <cstdio>
10#include <string>
11#include <vector>
12
13namespace {
14
15struct Image { std::vector<uint8_t> px; int side = 0; double lo = 0, hi = 0; };
16
17std::vector<uint8_t> pseudo_random(size_t n, uint32_t seed) {
18 std::vector<uint8_t> out(n);
19 for (auto &b : out) { seed = seed * 1664525u + 1013904223u; b = static_cast<uint8_t>(seed >> 24); }
20 return out;
21}
22
23std::string render(int profile, const std::vector<uint8_t> &payload) {
24 ckey_diagnostics d{}; size_t n = 0; std::string svg;
25 if (profile == 1) {
26 ckey_symbol s{}; CKEY_CHECK(ckey_encode_locator(payload.data(), payload.size(), &s, &d) == CKEY_OK);
27 CKEY_CHECK(ckey_render_svg(&s, 8, nullptr, 0, &n) == CKEY_INSUFFICIENT_BUFFER); svg.resize(n);
28 CKEY_CHECK(ckey_render_svg(&s, 8, svg.data(), n, &n) == CKEY_OK);
29 } else if (profile == 2) {
30 ckey_text_symbol s{}; CKEY_CHECK(ckey_encode_text(payload.data(), payload.size(), &s, &d) == CKEY_OK);
31 CKEY_CHECK(ckey_render_text_svg(&s, 8, nullptr, 0, &n) == CKEY_INSUFFICIENT_BUFFER); svg.resize(n);
32 CKEY_CHECK(ckey_render_text_svg(&s, 8, svg.data(), n, &n) == CKEY_OK);
33 } else {
34 ckey_extended_symbol s{}; CKEY_CHECK(ckey_encode_extended(payload.data(), payload.size(), &s, &d) == CKEY_OK);
35 CKEY_CHECK(ckey_render_extended_svg(&s, 8, nullptr, 0, &n) == CKEY_INSUFFICIENT_BUFFER); svg.resize(n);
36 CKEY_CHECK(ckey_render_extended_svg(&s, 8, svg.data(), n, &n) == CKEY_OK);
37 }
38 svg.resize(n - 1);
39 return svg;
40}
41
42// Canonical symbol coordinates span [0,29] (P1/P2) or [0,116] (P3) inside a
43// 31- or 124-unit viewBox with a 1- or 4-unit margin.
44Image raster(int profile, const std::string &svg, int px_per_unit) {
45 const int units = profile == 3 ? 124 : 31, margin = profile == 3 ? 4 : 1, field = profile == 3 ? 116 : 29;
46 Image img; img.side = units * px_per_unit;
47 img.px = svgtest::rasterize(svgtest::parse(svg), img.side, img.side, 4);
48 img.lo = static_cast<double>(margin * px_per_unit);
49 img.hi = static_cast<double>((margin + field) * px_per_unit);
50 return img;
51}
52
53ckey_status decode(int profile, const std::vector<uint8_t> &px, size_t side, const double quad[8],
54 std::vector<uint8_t> &out) {
55 std::vector<uint8_t> buffer(CKEY_EXTENDED_PAYLOAD_MAX_BYTES); size_t length = 0; ckey_diagnostics d{};
56 ckey_status st;
57 if (profile == 1) { st = ckey_decode_gray8(px.data(), side, side, side, quad, buffer.data(), buffer.size(), &d); length = 16; }
58 else if (profile == 2) st = ckey_decode_text_gray8(px.data(), side, side, side, quad, buffer.data(), buffer.size(), &length, &d);
59 else st = ckey_decode_extended_gray8(px.data(), side, side, side, quad, buffer.data(), buffer.size(), &length, &d);
60 out.assign(buffer.begin(), buffer.begin() + static_cast<std::ptrdiff_t>(st == CKEY_OK ? length : 0));
61 return st;
62}
63
64void expect_decodes(int profile, const Image &img, const std::vector<uint8_t> &payload, const char *label) {
65 const double q[8] = {img.lo, img.lo, img.hi, img.lo, img.hi, img.hi, img.lo, img.hi};
66 std::vector<uint8_t> out;
67 const ckey_status st = decode(profile, img.px, static_cast<size_t>(img.side), q, out);
68 if (st != CKEY_OK || out != payload) std::fprintf(stderr, "profile %d round trip failed: %s (status %d)\n", profile, label, st);
69 CKEY_CHECK(st == CKEY_OK && out == payload);
70}
71
72// Projective map from four source corners to four destination corners.
73void homography(const double src[8], const double dst[8], double h[8]) {
74 double a[8][9]{};
75 for (int i = 0; i < 4; ++i) {
76 const double x = src[2 * i], y = src[2 * i + 1], u = dst[2 * i], v = dst[2 * i + 1];
77 const double r0[9] = {x, y, 1, 0, 0, 0, -u * x, -u * y, u}, r1[9] = {0, 0, 0, x, y, 1, -v * x, -v * y, v};
78 for (int j = 0; j < 9; ++j) { a[2 * i][j] = r0[j]; a[2 * i + 1][j] = r1[j]; }
79 }
80 for (int c = 0; c < 8; ++c) {
81 int p = c; for (int r = c + 1; r < 8; ++r) if (std::abs(a[r][c]) > std::abs(a[p][c])) p = r;
82 for (int j = 0; j < 9; ++j) std::swap(a[p][j], a[c][j]);
83 for (int j = 8; j >= c; --j) a[c][j] /= a[c][c];
84 for (int r = 0; r < 8; ++r) if (r != c) { const double f = a[r][c]; for (int j = c; j < 9; ++j) a[r][j] -= f * a[c][j]; }
85 }
86 for (int i = 0; i < 8; ++i) h[i] = a[i][8];
87}
88
89void apply(const double h[8], double x, double y, double &u, double &v) {
90 const double d = h[6] * x + h[7] * y + 1; u = (h[0] * x + h[1] * y + h[2]) / d; v = (h[3] * x + h[4] * y + h[5]) / d;
91}
92
93// Resample through a mild keystone (top edge narrowed by 6% per side) with
94// bilinear inverse mapping; returns the warped image and the symbol quad.
95std::vector<uint8_t> keystone(const Image &img, double quad_out[8]) {
96 const double s = img.side, inset = 0.06 * s;
97 const double full[8] = {0, 0, s, 0, s, s, 0, s}, warped[8] = {inset, 0, s - inset, 0, s, s, 0, s};
98 double forward[8], inverse[8]; homography(full, warped, forward); homography(warped, full, inverse);
99 const int n = img.side; std::vector<uint8_t> out(img.px.size(), 5);
100 for (int y = 0; y < n; ++y) for (int x = 0; x < n; ++x) {
101 double u, v; apply(inverse, x + .5, y + .5, u, v); u -= .5; v -= .5;
102 if (u < 0 || v < 0 || u >= n - 1 || v >= n - 1) continue;
103 const int ix = static_cast<int>(u), iy = static_cast<int>(v); const double fx = u - ix, fy = v - iy;
104 const auto at = [&](int xx, int yy) { return static_cast<double>(img.px[static_cast<size_t>(yy) * n + xx]); };
105 out[static_cast<size_t>(y) * n + x] = static_cast<uint8_t>(std::lround(
106 (at(ix, iy) * (1 - fx) + at(ix + 1, iy) * fx) * (1 - fy) + (at(ix, iy + 1) * (1 - fx) + at(ix + 1, iy + 1) * fx) * fy));
107 }
108 const double corners[8] = {img.lo, img.lo, img.hi, img.lo, img.hi, img.hi, img.lo, img.hi};
109 for (int i = 0; i < 4; ++i) apply(forward, corners[2 * i], corners[2 * i + 1], quad_out[2 * i], quad_out[2 * i + 1]);
110 return out;
111}
112
113void box_blur(Image &img) {
114 const int n = img.side; std::vector<uint8_t> out(img.px);
115 for (int y = 1; y < n - 1; ++y) for (int x = 1; x < n - 1; ++x) {
116 int sum = 0; for (int dy = -1; dy <= 1; ++dy) for (int dx = -1; dx <= 1; ++dx) sum += img.px[static_cast<size_t>(y + dy) * n + x + dx];
117 out[static_cast<size_t>(y) * n + x] = static_cast<uint8_t>(sum / 9);
118 }
119 img.px = out;
120}
121
122} // namespace
123
124int main() {
125 // Pixels per symbol unit. Measured clean-render floors with an exact quad
126 // are P1 5-6, P2 10, P3 11; these densities leave margin for the blur and
127 // keystone cases below (P1 needs 10 to survive the 3x3 blur).
128 const int density[4] = {0, 10, 14, 16};
129 for (int profile = 1; profile <= 3; ++profile) {
130 const size_t max = profile == 1 ? 16 : profile == 2 ? CKEY_TEXT_PAYLOAD_MAX_BYTES : CKEY_EXTENDED_PAYLOAD_MAX_BYTES;
131 std::vector<std::pair<const char *, std::vector<uint8_t>>> payloads = {
132 {"random", pseudo_random(max, 0xC0FFEEu + profile)},
133 {"all-zero", std::vector<uint8_t>(max, 0x00)},
134 {"all-0xFF", std::vector<uint8_t>(max, 0xFF)},
135 };
136 if (profile != 1) payloads.push_back({"short", std::vector<uint8_t>{'C', 'K', 'e', 'y'}});
137 if (profile == 3) payloads.push_back({"tier-768", pseudo_random(700, 7)});
138 for (const auto &[label, payload] : payloads) expect_decodes(profile, raster(profile, render(profile, payload), density[profile]), payload, label);
139
140 // Orientation, winding, and polarity are resolved by the decoder.
141 const auto payload = pseudo_random(profile == 1 ? 16 : profile == 2 ? 120 : 1500, 99u + profile);
142 Image img = raster(profile, render(profile, payload), density[profile]);
143 std::vector<uint8_t> out;
144 const double rotated[8] = {img.hi, img.lo, img.hi, img.hi, img.lo, img.hi, img.lo, img.lo};
145 const double reversed[8] = {img.lo, img.lo, img.lo, img.hi, img.hi, img.hi, img.hi, img.lo};
146 CKEY_CHECK(decode(profile, img.px, img.side, rotated, out) == CKEY_OK && out == payload);
147 CKEY_CHECK(decode(profile, img.px, img.side, reversed, out) == CKEY_OK && out == payload);
148 Image inverted = img; for (auto &p : inverted.px) p = static_cast<uint8_t>(255 - p);
149 expect_decodes(profile, inverted, payload, "inverted polarity");
150
151 // Mild perspective (6% keystone) and a 3x3 box blur.
152 double quad[8]; const auto warped = keystone(img, quad);
153 CKEY_CHECK(decode(profile, warped, img.side, quad, out) == CKEY_OK && out == payload);
154 Image blurred = img; box_blur(blurred);
155 expect_decodes(profile, blurred, payload, "3x3 box blur");
156 std::printf("profile %d: round trips passed at %d px/unit\n", profile, density[profile]);
157 }
158 // Blur across densities (dense profiles): one 3x3 blur from 12 px/unit,
159 // two from 12 (P3) or 14 (P2). Before ring-context thresholds, every P3
160 // case here failed: blurred isolated ring dots fell below the arc-
161 // dominated class threshold.
162 for (int profile = 2; profile <= 3; ++profile) {
163 const size_t length = profile == 2 ? CKEY_TEXT_PAYLOAD_MAX_BYTES : CKEY_EXTENDED_PAYLOAD_MAX_BYTES;
164 const auto payload = pseudo_random(length, 4242u + profile);
165 const std::string svg = render(profile, payload);
166 Image once = raster(profile, svg, 12); box_blur(once);
167 expect_decodes(profile, once, payload, "one blur at 12 px/unit");
168 Image twice = raster(profile, svg, profile == 2 ? 14 : 12); box_blur(twice); box_blur(twice);
169 expect_decodes(profile, twice, payload, "two blurs");
170 }
171 std::printf("dense profiles: blur sweep passed\n");
172
173 // Noise is rejected by every decoder (and the dense key gate rejects it
174 // before full-site sampling).
175 {
176 Image noise; noise.side = 31 * 16; noise.px = pseudo_random(static_cast<size_t>(noise.side) * noise.side, 77);
177 const double q[8] = {16, 16, 480, 16, 480, 480, 16, 480};
178 std::vector<uint8_t> out;
179 for (int profile = 1; profile <= 3; ++profile) CKEY_CHECK(decode(profile, noise.px, noise.side, q, out) != CKEY_OK);
180 }
181
182 // Profile 1 near its clean-render floor: 24 random locators at 6 px/unit.
183 // (Dev.48 failed about half of these: its empty reference was a payload module.)
184 for (uint32_t t = 0; t < 24; ++t) {
185 const auto payload = pseudo_random(16, 1234u + 77u * t);
186 expect_decodes(1, raster(1, render(1, payload), 6), payload, "6 px/unit sweep");
187 }
188 std::printf("profile 1: 24 random locators decoded at 6 px/unit\n");
189 std::printf("CKey render/decode round-trip tests passed\n");
190}