Viewing is open to everyone. Downloading and starring need a GitHub sign-in.
Sign in with GitHub
reader_pipeline_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 | // Simulated reader: renders symbols, composites them into full camera-sized |
| 2 | // frames with rotation, keystone, blur, noise, and clutter, then runs the |
| 3 | // exact pipeline the iPhone reader uses -- ckey_find_borders_gray8, then |
| 4 | // ckey_refine_quad_gray8 per profile, then the ckey_decode_*_gray8 calls. |
| 5 | "ckey/ckey.h" |
| 6 | "check.h" |
| 7 | "svg_raster.h" |
| 8 | |
| 9 | <cmath> |
| 10 | <cstdio> |
| 11 | <string> |
| 12 | <vector> |
| 13 | |
| 14 | namespace { |
| 15 | |
| 16 | struct Rng { uint32_t s; uint32_t next() { s = s * 1664525u + 1013904223u; return s; } double unit() { return (next() >> 8) / 16777216.0; } }; |
| 17 | |
| 18 | // Profile 0 symbols are encoded at this version (set per case). |
| 19 | uint32_t g_scalable_version = 1; |
| 20 | ckey_scalable_layout g_scalable_layout = CKEY_LAYOUT_STANDARD; // Profile 0 (standard) or 0.1 (print) |
| 21 | |
| 22 | std::vector<uint8_t> payload_for(int profile, uint32_t seed) { |
| 23 | const size_t n = profile == 0 ? ckey_scalable_capacity(g_scalable_version, CKEY_ECC_M, g_scalable_layout) |
| 24 | : profile == 1 ? 16 : profile == 2 ? 120 : 1200; |
| 25 | std::vector<uint8_t> p(n); Rng r{seed}; for (auto &b : p) b = static_cast<uint8_t>(r.next() >> 24); return p; |
| 26 | } |
| 27 | |
| 28 | std::string render(int profile, const std::vector<uint8_t> &payload) { |
| 29 | ckey_diagnostics d{}; size_t n = 0; std::string svg; |
| 30 | if (profile == 0) { ckey_scalable_symbol s{}; ckey_encode_scalable(payload.data(), payload.size(), g_scalable_version, CKEY_ECC_M, g_scalable_layout, &s, &d); ckey_render_scalable_svg(&s, 8, nullptr, 0, &n); svg.resize(n); ckey_render_scalable_svg(&s, 8, svg.data(), n, &n); } |
| 31 | else if (profile == 1) { ckey_symbol s{}; ckey_encode_locator(payload.data(), payload.size(), &s, &d); ckey_render_svg(&s, 8, nullptr, 0, &n); svg.resize(n); ckey_render_svg(&s, 8, svg.data(), n, &n); } |
| 32 | else if (profile == 2) { ckey_text_symbol s{}; ckey_encode_text(payload.data(), payload.size(), &s, &d); ckey_render_text_svg(&s, 8, nullptr, 0, &n); svg.resize(n); ckey_render_text_svg(&s, 8, svg.data(), n, &n); } |
| 33 | else { ckey_extended_symbol s{}; ckey_encode_extended(payload.data(), payload.size(), &s, &d); ckey_render_extended_svg(&s, 8, nullptr, 0, &n); svg.resize(n); ckey_render_extended_svg(&s, 8, svg.data(), n, &n); } |
| 34 | svg.resize(n - 1); return svg; |
| 35 | } |
| 36 | |
| 37 | void solve(const double src[8], const double dst[8], double h[8]) { |
| 38 | double a[8][9]{}; |
| 39 | for (int i = 0; i < 4; ++i) { |
| 40 | const double x = src[2 * i], y = src[2 * i + 1], u = dst[2 * i], v = dst[2 * i + 1]; |
| 41 | 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}; |
| 42 | for (int j = 0; j < 9; ++j) { a[2 * i][j] = r0[j]; a[2 * i + 1][j] = r1[j]; } |
| 43 | } |
| 44 | for (int c = 0; c < 8; ++c) { |
| 45 | int p = c; for (int r = c + 1; r < 8; ++r) if (std::abs(a[r][c]) > std::abs(a[p][c])) p = r; |
| 46 | for (int j = 0; j < 9; ++j) std::swap(a[p][j], a[c][j]); |
| 47 | for (int j = 8; j >= c; --j) a[c][j] /= a[c][c]; |
| 48 | 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]; } |
| 49 | } |
| 50 | for (int i = 0; i < 8; ++i) h[i] = a[i][8]; |
| 51 | } |
| 52 | |
| 53 | struct Scene { int profile; int frame_w, frame_h; double symbol_px; double rotation_deg; double keystone; bool blur; bool dark_on_light; uint32_t seed; }; |
| 54 | |
| 55 | // Builds a camera frame: clutter background, the symbol (including its own |
| 56 | // quiet margin) warped by rotation + keystone, optional 3x3 blur, noise. |
| 57 | std::vector<uint8_t> make_frame(const Scene &sc, const std::string &svg) { |
| 58 | const int units = sc.profile == 3 ? 124 : 31; |
| 59 | const int raster = static_cast<int>(std::ceil(sc.symbol_px * units / (sc.profile == 3 ? 116.0 : 29.0))); |
| 60 | auto sym = svgtest::rasterize(svgtest::parse(svg), raster, raster, 3); |
| 61 | if (sc.dark_on_light) for (auto &v : sym) v = static_cast<uint8_t>(250 - v); |
| 62 | std::vector<uint8_t> frame(static_cast<size_t>(sc.frame_w) * sc.frame_h); |
| 63 | Rng rng{sc.seed}; |
| 64 | for (int y = 0; y < sc.frame_h; ++y) for (int x = 0; x < sc.frame_w; ++x) |
| 65 | frame[static_cast<size_t>(y) * sc.frame_w + x] = static_cast<uint8_t>(70 + 50.0 * x / sc.frame_w + 30.0 * y / sc.frame_h); |
| 66 | for (int k = 0; k < 14; ++k) { // clutter rectangles of varied brightness |
| 67 | const int x0 = static_cast<int>(rng.unit() * sc.frame_w), y0 = static_cast<int>(rng.unit() * sc.frame_h); |
| 68 | const int w = 20 + static_cast<int>(rng.unit() * sc.frame_w / 5), h = 20 + static_cast<int>(rng.unit() * sc.frame_h / 5); |
| 69 | const uint8_t v = static_cast<uint8_t>(rng.unit() * 255); |
| 70 | for (int y = y0; y < std::min(sc.frame_h, y0 + h); ++y) for (int x = x0; x < std::min(sc.frame_w, x0 + w); ++x) frame[static_cast<size_t>(y) * sc.frame_w + x] = v; |
| 71 | } |
| 72 | // Destination corners of the symbol raster in the frame. |
| 73 | const double cx = sc.frame_w / 2.0, cy = sc.frame_h / 2.0, half = raster / 2.0, a = sc.rotation_deg * 3.14159265358979 / 180; |
| 74 | double dst[8]; const double corner[4][2] = {{-1, -1}, {1, -1}, {1, 1}, {-1, 1}}; |
| 75 | for (int i = 0; i < 4; ++i) { |
| 76 | double x = corner[i][0] * half, y = corner[i][1] * half; |
| 77 | x *= 1.0 - sc.keystone * (corner[i][1] < 0 ? 1 : 0); // narrow the top edge |
| 78 | dst[2 * i] = cx + x * std::cos(a) - y * std::sin(a); dst[2 * i + 1] = cy + x * std::sin(a) + y * std::cos(a); |
| 79 | } |
| 80 | const double src[8] = {0, 0, static_cast<double>(raster), 0, static_cast<double>(raster), static_cast<double>(raster), 0, static_cast<double>(raster)}; |
| 81 | double inv[8]; solve(dst, src, inv); |
| 82 | double x0 = 1e9, x1 = -1e9, y0 = 1e9, y1 = -1e9; for (int i = 0; i < 4; ++i) { x0 = std::min(x0, dst[2 * i]); x1 = std::max(x1, dst[2 * i]); y0 = std::min(y0, dst[2 * i + 1]); y1 = std::max(y1, dst[2 * i + 1]); } |
| 83 | for (int y = std::max(0, static_cast<int>(y0)); y < std::min(sc.frame_h, static_cast<int>(y1) + 1); ++y) |
| 84 | for (int x = std::max(0, static_cast<int>(x0)); x < std::min(sc.frame_w, static_cast<int>(x1) + 1); ++x) { |
| 85 | const double d = inv[6] * x + inv[7] * y + 1, u = (inv[0] * x + inv[1] * y + inv[2]) / d - .5, v = (inv[3] * x + inv[4] * y + inv[5]) / d - .5; |
| 86 | if (u < 0 || v < 0 || u >= raster - 1 || v >= raster - 1) continue; |
| 87 | const int iu = static_cast<int>(u), iv = static_cast<int>(v); const double fu = u - iu, fv = v - iv; |
| 88 | const auto at = [&](int xx, int yy) { return static_cast<double>(sym[static_cast<size_t>(yy) * raster + xx]); }; |
| 89 | frame[static_cast<size_t>(y) * sc.frame_w + x] = static_cast<uint8_t>(std::lround( |
| 90 | (at(iu, iv) * (1 - fu) + at(iu + 1, iv) * fu) * (1 - fv) + (at(iu, iv + 1) * (1 - fu) + at(iu + 1, iv + 1) * fu) * fv)); |
| 91 | } |
| 92 | if (sc.blur) { |
| 93 | std::vector<uint8_t> out(frame); |
| 94 | for (int y = 1; y < sc.frame_h - 1; ++y) for (int x = 1; x < sc.frame_w - 1; ++x) { |
| 95 | int t = 0; for (int dy = -1; dy <= 1; ++dy) for (int dx = -1; dx <= 1; ++dx) t += frame[static_cast<size_t>(y + dy) * sc.frame_w + x + dx]; |
| 96 | out[static_cast<size_t>(y) * sc.frame_w + x] = static_cast<uint8_t>(t / 9); |
| 97 | } |
| 98 | frame.swap(out); |
| 99 | } |
| 100 | for (auto &v : frame) v = static_cast<uint8_t>(std::clamp(static_cast<int>(v) + static_cast<int>(rng.unit() * 13) - 6, 0, 255)); |
| 101 | return frame; |
| 102 | } |
| 103 | |
| 104 | // The reader's loop: up to six border candidates; for each, refine to every |
| 105 | // profile's field and try each decoder. Returns the decoded profile or 0. |
| 106 | int scan(const std::vector<uint8_t> &frame, int w, int h, const std::vector<uint8_t> &expected, size_t *borders_found) { |
| 107 | double borders[6 * 8]; |
| 108 | const size_t n = ckey_find_borders_gray8(frame.data(), w, h, w, borders, 6); |
| 109 | if (borders_found) *borders_found = n; |
| 110 | std::vector<uint8_t> out(65535); size_t len = 0; ckey_diagnostics d{}; // P0 payloads exceed 2,048 bytes |
| 111 | for (size_t i = 0; i < n; ++i) { |
| 112 | double q[8]; uint32_t version = 0; |
| 113 | if (ckey_refine_quad_gray8(frame.data(), w, h, w, borders + 8 * i, CKEY_FIELD_SCALABLE, q) > 0 && |
| 114 | ckey_decode_scalable_gray8(frame.data(), w, h, w, q, out.data(), out.size(), &len, &version, nullptr, &d) == CKEY_OK && |
| 115 | len == expected.size() && std::equal(expected.begin(), expected.end(), out.begin())) return 0; |
| 116 | if (ckey_refine_quad_gray8(frame.data(), w, h, w, borders + 8 * i, CKEY_FIELD_LOCATOR, q) > 0 && |
| 117 | ckey_decode_gray8(frame.data(), w, h, w, q, out.data(), out.size(), &d) == CKEY_OK && |
| 118 | expected.size() == 16 && std::equal(expected.begin(), expected.end(), out.begin())) return 1; |
| 119 | if (ckey_refine_quad_gray8(frame.data(), w, h, w, borders + 8 * i, CKEY_FIELD_TEXT, q) > 0 && |
| 120 | ckey_decode_text_gray8(frame.data(), w, h, w, q, out.data(), out.size(), &len, &d) == CKEY_OK && |
| 121 | len == expected.size() && std::equal(expected.begin(), expected.end(), out.begin())) return 2; |
| 122 | if (ckey_refine_quad_gray8(frame.data(), w, h, w, borders + 8 * i, CKEY_FIELD_EXTENDED, q) > 0 && |
| 123 | ckey_decode_extended_gray8(frame.data(), w, h, w, q, out.data(), out.size(), &len, &d) == CKEY_OK && |
| 124 | len == expected.size() && std::equal(expected.begin(), expected.end(), out.begin())) return 3; |
| 125 | } |
| 126 | return -1; |
| 127 | } |
| 128 | |
| 129 | } // namespace |
| 130 | |
| 131 | int main() { |
| 132 | struct Case { int profile, frame_w, frame_h; double px_per_unit; uint32_t version; ckey_scalable_layout layout = CKEY_LAYOUT_STANDARD; }; |
| 133 | // Field width in pixels = px/unit x field units; frame sizes match the |
| 134 | // reader's portrait capture (1080p and 4K). |
| 135 | // Profile 0 cases give px/unit as 8 px per module (px/unit = 8 / pitch). |
| 136 | const Case cases[] = {{1, 1080, 1920, 10, 0}, {1, 1080, 1920, 14, 0}, {2, 1080, 1920, 14, 0}, {2, 1080, 1920, 20, 0}, |
| 137 | {3, 2160, 3840, 13, 0}, {3, 2160, 3840, 16, 0}, |
| 138 | {0, 1080, 1920, 8.0 * 29 / 29, 1}, {0, 1080, 1920, 8.0 * 31 / 29, 2}, {0, 1080, 1920, 8.0 * 58 / 29, 11}, |
| 139 | {0, 1080, 1920, 8.0 * 108 / 29, 20}, {0, 2160, 3840, 7.0 * 232 / 29, 31}, |
| 140 | // Profile 0.1 (print layout): the reader must find and decode it through the same pipeline. |
| 141 | {0, 1080, 1920, 8.0 * 31 / 29, 2, CKEY_LAYOUT_PRINT}, {0, 1080, 1920, 8.0 * 58 / 29, 11, CKEY_LAYOUT_PRINT}, |
| 142 | {0, 1080, 1920, 6.0 * 108 / 29, 20, CKEY_LAYOUT_PRINT}}; |
| 143 | const double rotations[] = {0, 7, 18, 33}; |
| 144 | int total = 0, passed = 0, skipped = 0; |
| 145 | for (const auto &c : cases) { |
| 146 | g_scalable_version = c.version; g_scalable_layout = c.layout; |
| 147 | int ok = 0, n = 0; |
| 148 | for (double rotation : rotations) for (double keystone : {0.0, 0.08}) for (int variant = 0; variant < 2; ++variant) { |
| 149 | const Scene sc{c.profile, c.frame_w, c.frame_h, c.px_per_unit * (c.profile == 3 ? 116 : 29), rotation, keystone, |
| 150 | variant == 1, variant == 1 && c.profile != 3, static_cast<uint32_t>(rotation * 13 + keystone * 100 + variant)}; |
| 151 | // Skip scenes where the rotated symbol cannot fit inside the frame |
| 152 | // (no reader could see its whole border). |
| 153 | const double extent = sc.symbol_px * (c.profile == 3 ? 124.0 / 116 : 31.0 / 29) * |
| 154 | (std::abs(std::cos(rotation * 3.14159265358979 / 180)) + std::abs(std::sin(rotation * 3.14159265358979 / 180))); |
| 155 | if (extent > 0.98 * std::min(c.frame_w, c.frame_h)) { ++skipped; continue; } |
| 156 | const auto payload = payload_for(c.profile, 7u + static_cast<uint32_t>(rotation) + variant); |
| 157 | const auto frame = make_frame(sc, render(c.profile, payload)); |
| 158 | size_t borders = 0; |
| 159 | const int result = scan(frame, c.frame_w, c.frame_h, payload, &borders); |
| 160 | ++n; ok += result == c.profile; |
| 161 | if (result != c.profile) |
| 162 | std::printf(" miss: P%d %.0f px/unit rot %.0f keystone %.2f %s%s (borders found %zu)\n", c.profile, c.px_per_unit, rotation, |
| 163 | keystone, variant ? "blur" : "sharp", sc.dark_on_light ? " dark-on-light" : "", borders); |
| 164 | } |
| 165 | if (c.profile == 0) std::printf("P0%s v%u %dx%d @ %.0f px/unit: %d/%d scenes decoded\n", c.layout == CKEY_LAYOUT_PRINT ? ".1" : "", c.version, c.frame_w, c.frame_h, c.px_per_unit, ok, n); |
| 166 | else std::printf("P%d %dx%d @ %.0f px/unit: %d/%d scenes decoded\n", c.profile, c.frame_w, c.frame_h, c.px_per_unit, ok, n); |
| 167 | total += n; passed += ok; |
| 168 | } |
| 169 | std::printf("simulated reader: %d/%d scenes decoded (%d impossible scenes skipped)\n", passed, total, skipped); |
| 170 | CKEY_CHECK(passed == total); |
| 171 | std::printf("CKey reader pipeline tests passed\n"); |
| 172 | } |