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
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 | "ckey/ckey.h" |
| 5 | "check.h" |
| 6 | "svg_raster.h" |
| 7 | |
| 8 | <cstdint> |
| 9 | <cstdio> |
| 10 | <string> |
| 11 | <vector> |
| 12 | |
| 13 | namespace { |
| 14 | |
| 15 | struct Image { std::vector<uint8_t> px; int side = 0; double lo = 0, hi = 0; }; |
| 16 | |
| 17 | std::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 | |
| 23 | std::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. |
| 44 | Image 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 | |
| 53 | ckey_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 | |
| 64 | void 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. |
| 73 | void 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 | |
| 89 | void 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. |
| 95 | std::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 | |
| 113 | void 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 | |
| 124 | int 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 | } |