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

ckey_bench.cpp

v0.1.0-build.85 Latest Commit ede38f5

Size
17 KB
Lines
278
Language
cpp
SHA-256
90fa6e3a51b7…
View on GitHub

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

1// CKey optical benchmark harness (specification §5, §6).
2//
3// Generate -> degrade -> decode -> measure, with the real CKey engine and
4// the same frames for every symbology. For each payload, acquisition
5// condition and printed side (in camera pixels, including each symbology's
6// quiet zone, so symbols are compared at equal area) it measures:
7// - payload recovery rate (exact bytes),
8// - corrected Reed-Solomon bytes (a bit-error proxy),
9// - per-frame latency (find borders -> refine -> decode), as percentiles,
10// - the smallest side that decodes in >= 90% of trials, and the payload
11// bytes recoverable per 1,000 px^2 at that size,
12// plus a false-positive count on symbol-free clutter frames.
13//
14// Built with -DCKEY_BENCH_ZXING=ON and zxing-cpp installed, the same frames
15// are also generated and read as QR Code and Data Matrix for comparison.
16// All frames are synthetic; results are not real-camera measurements.
17//
18// ckey-bench [--quick] [--json out.json] [--md out.md]
19#include "ckey/ckey.h"
20
21#ifdef CKEY_BENCH_ZXING
22#include <ZXing/BitMatrix.h>
23#include <ZXing/MultiFormatWriter.h>
24#include <ZXing/ReadBarcode.h>
25#endif
26
27#include <algorithm>
28#include <chrono>
29#include <cmath>
30#include <cstdio>
31#include <cstring>
32#include <fstream>
33#include <functional>
34#include <iostream>
35#include <memory>
36#include <sstream>
37#include <string>
38#include <vector>
39
40namespace {
41
42struct Rng { uint32_t s; uint32_t next() { s = s * 1664525u + 1013904223u; return s; } double unit() { return (next() >> 8) / 16777216.0; }
43 double gauss() { const double u1 = unit() + 1e-9, u2 = unit(); return std::sqrt(-2 * std::log(u1)) * std::cos(6.283185307179586 * u2); } };
44
45struct Condition { const char *name; double blur, noise, rotation_deg, keystone, glare; };
46struct Image { int side = 0; std::vector<uint8_t> px; }; // square symbol raster, quiet zone included
47struct Outcome { bool ok = false; double ms = 0; int corrected = 0; };
48
49struct Symbology {
50 std::string name;
51 std::function<bool(const std::string &, int side, Image &)> render; // false: payload does not fit
52 std::function<Outcome(const std::vector<uint8_t> &, int, int, const std::string &)> scan;
53};
54
55double now_ms() { return std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now().time_since_epoch()).count(); }
56
57void solve(const double src[8], const double dst[8], double h[8]) {
58 double a[8][9]{};
59 for (int i = 0; i < 4; ++i) {
60 const double x = src[2 * i], y = src[2 * i + 1], u = dst[2 * i], v = dst[2 * i + 1];
61 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};
62 for (int j = 0; j < 9; ++j) { a[2 * i][j] = r0[j]; a[2 * i + 1][j] = r1[j]; }
63 }
64 for (int c = 0; c < 8; ++c) {
65 int p = c; for (int r = c + 1; r < 8; ++r) if (std::abs(a[r][c]) > std::abs(a[p][c])) p = r;
66 for (int j = 0; j < 9; ++j) std::swap(a[p][j], a[c][j]);
67 for (int j = 8; j >= c; --j) a[c][j] /= a[c][c];
68 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]; }
69 }
70 for (int i = 0; i < 8; ++i) h[i] = a[i][8];
71}
72
73void blur(std::vector<double> &img, int w, int h, double sigma) {
74 if (sigma <= 0.05) return;
75 const int r = std::max(1, static_cast<int>(std::ceil(3 * sigma)));
76 std::vector<double> k(static_cast<size_t>(2 * r + 1)); double sum = 0;
77 for (int i = -r; i <= r; ++i) sum += k[static_cast<size_t>(i + r)] = std::exp(-0.5 * i * i / (sigma * sigma));
78 for (auto &v : k) v /= sum;
79 std::vector<double> t(img.size());
80 for (int y = 0; y < h; ++y) for (int x = 0; x < w; ++x) { double a = 0; for (int i = -r; i <= r; ++i) a += k[static_cast<size_t>(i + r)] * img[static_cast<size_t>(y) * w + std::clamp(x + i, 0, w - 1)]; t[static_cast<size_t>(y) * w + x] = a; }
81 for (int y = 0; y < h; ++y) for (int x = 0; x < w; ++x) { double a = 0; for (int i = -r; i <= r; ++i) a += k[static_cast<size_t>(i + r)] * t[static_cast<size_t>(std::clamp(y + i, 0, h - 1)) * w + x]; img[static_cast<size_t>(y) * w + x] = a; }
82}
83
84// A camera frame: lit gradient, clutter rectangles, the symbol warped by
85// rotation and keystone, then glare, defocus and sensor noise.
86std::vector<uint8_t> make_frame(const Image *sym, int W, int H, const Condition &c, uint32_t seed) {
87 Rng rng{seed};
88 std::vector<double> f(static_cast<size_t>(W) * H);
89 for (int y = 0; y < H; ++y) for (int x = 0; x < W; ++x) f[static_cast<size_t>(y) * W + x] = 0.35 + 0.25 * x / W + 0.15 * y / H;
90 for (int k = 0; k < 16; ++k) {
91 const int x0 = static_cast<int>(rng.unit() * W), y0 = static_cast<int>(rng.unit() * H), w = 10 + static_cast<int>(rng.unit() * W / 5), h = 10 + static_cast<int>(rng.unit() * H / 5);
92 const double v = rng.unit();
93 for (int y = y0; y < std::min(H, y0 + h); ++y) for (int x = x0; x < std::min(W, x0 + w); ++x) f[static_cast<size_t>(y) * W + x] = v;
94 }
95 if (sym) {
96 const int S = sym->side;
97 const double cx = W / 2.0 + (rng.unit() - .5) * W * 0.2, cy = H / 2.0 + (rng.unit() - .5) * H * 0.2, half = S / 2.0, a = c.rotation_deg * 3.14159265358979 / 180;
98 double dst[8]; const double corner[4][2] = {{-1, -1}, {1, -1}, {1, 1}, {-1, 1}};
99 for (int i = 0; i < 4; ++i) {
100 double x = corner[i][0] * half, y = corner[i][1] * half;
101 x *= 1.0 - c.keystone * (corner[i][1] < 0 ? 1 : 0);
102 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);
103 }
104 const double src[8] = {0, 0, static_cast<double>(S), 0, static_cast<double>(S), static_cast<double>(S), 0, static_cast<double>(S)};
105 double inv[8]; solve(dst, src, inv);
106 for (int y = 0; y < H; ++y) for (int x = 0; x < W; ++x) {
107 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;
108 if (u < 0 || v < 0 || u >= S - 1 || v >= S - 1) continue;
109 const int iu = static_cast<int>(u), iv = static_cast<int>(v); const double fu = u - iu, fv = v - iv;
110 const auto at = [&](int xx, int yy) { return sym->px[static_cast<size_t>(yy) * S + xx] / 255.0; };
111 f[static_cast<size_t>(y) * W + x] = (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;
112 }
113 }
114 for (auto &v : f) v = v * (1 - c.glare) + c.glare;
115 blur(f, W, H, c.blur);
116 std::vector<uint8_t> out(f.size());
117 for (size_t i = 0; i < f.size(); ++i) out[i] = static_cast<uint8_t>(std::lround(std::clamp(f[i] + rng.gauss() * c.noise, 0.0, 1.0) * 255));
118 return out;
119}
120
121// ---- CKey (Profile 0, level M, default style) ----
122
123bool render_ckey(const std::string &text, int side, Image &img) {
124 auto s = std::make_unique<ckey_scalable_symbol>(); ckey_diagnostics d{};
125 if (ckey_encode_scalable(reinterpret_cast<const uint8_t *>(text.data()), text.size(), 0, CKEY_ECC_M, CKEY_LAYOUT_STANDARD, s.get(), &d) != CKEY_OK) return false;
126 ckey_raster_options o; ckey_raster_options_default(&o);
127 o.size_px = static_cast<uint32_t>(std::max(16, side)); o.format = CKEY_PIXELS_GRAY8;
128 img.side = static_cast<int>(o.size_px); img.px.resize(static_cast<size_t>(img.side) * img.side); size_t n = 0;
129 return ckey_render_scalable_raster(s.get(), nullptr, &o, img.px.data(), img.px.size(), &n) == CKEY_OK;
130}
131
132Outcome scan_ckey(const std::vector<uint8_t> &frame, int w, int h, const std::string &expected) {
133 Outcome r; const double t0 = now_ms();
134 double borders[6 * 8];
135 const size_t n = ckey_find_borders_gray8(frame.data(), w, h, w, borders, 6);
136 static std::vector<uint8_t> out(65535);
137 for (size_t i = 0; i < n && !r.ok; ++i) {
138 double q[8]; size_t len = 0; uint32_t version = 0; ckey_diagnostics d{};
139 if (ckey_refine_quad_gray8(frame.data(), w, h, w, borders + 8 * i, CKEY_FIELD_SCALABLE, q) > 0 &&
140 ckey_decode_scalable_gray8(frame.data(), w, h, w, q, out.data(), out.size(), &len, &version, nullptr, &d) == CKEY_OK) {
141 r.ok = len == expected.size() && std::memcmp(out.data(), expected.data(), len) == 0;
142 r.corrected = static_cast<int>(d.corrected_symbols);
143 if (!r.ok) r.corrected = -1; // decoded something else: a false positive
144 }
145 }
146 r.ms = now_ms() - t0;
147 return r;
148}
149
150#ifdef CKEY_BENCH_ZXING
151// zxing-cpp 2.x. Quiet zones: 4 modules (QR), 2 modules (Data Matrix).
152bool render_zxing(ZXing::BarcodeFormat format, int quiet, const std::string &text, int side, Image &img) {
153 try {
154 ZXing::MultiFormatWriter writer(format);
155 writer.setMargin(0);
156 if (format == ZXing::BarcodeFormat::QRCode) writer.setEccLevel(4); // zxing-cpp maps 0..8 onto L/M/Q/H; 4 = M
157 const ZXing::BitMatrix m = writer.encode(text, 0, 0);
158 const int modules = m.width() + 2 * quiet;
159 img.side = side; img.px.assign(static_cast<size_t>(side) * side, 255);
160 const int ss = 3;
161 for (int y = 0; y < side; ++y) for (int x = 0; x < side; ++x) {
162 int ink = 0;
163 for (int sy = 0; sy < ss; ++sy) for (int sx = 0; sx < ss; ++sx) {
164 const int mx = static_cast<int>(std::floor((x + (sx + .5) / ss) * modules / side)) - quiet, my = static_cast<int>(std::floor((y + (sy + .5) / ss) * modules / side)) - quiet;
165 ink += mx >= 0 && my >= 0 && mx < m.width() && my < m.height() && m.get(mx, my);
166 }
167 img.px[static_cast<size_t>(y) * side + x] = static_cast<uint8_t>(255 - 255 * ink / (ss * ss));
168 }
169 return true;
170 } catch (...) { return false; }
171}
172
173Outcome scan_zxing(ZXing::BarcodeFormat format, const std::vector<uint8_t> &frame, int w, int h, const std::string &expected) {
174 Outcome r; const double t0 = now_ms();
175 ZXing::DecodeHints hints; hints.setFormats(format); hints.setTryHarder(true);
176 const auto result = ZXing::ReadBarcode(ZXing::ImageView(frame.data(), w, h, ZXing::ImageFormat::Lum), hints);
177 r.ms = now_ms() - t0;
178 if (result.isValid()) { r.ok = result.text() == expected; if (!r.ok) r.corrected = -1; }
179 return r;
180}
181#endif
182
183std::string payload_text(size_t n, uint32_t seed) {
184 static const char *alphabet = "abcdefghijklmnopqrstuvwxyz0123456789-._~";
185 std::string s = "https://openckey.org/";
186 Rng r{seed}; while (s.size() < n) s += alphabet[r.next() % 40];
187 s.resize(n); return s;
188}
189
190double pct(std::vector<double> v, double q) { if (v.empty()) return 0; std::sort(v.begin(), v.end()); return v[std::min(v.size() - 1, static_cast<size_t>(q * (v.size() - 1) + 0.5))]; }
191
192} // namespace
193
194int main(int argc, char **argv) {
195 bool quick = false; std::string json_path, md_path;
196 for (int i = 1; i < argc; ++i) {
197 const std::string a = argv[i];
198 if (a == "--quick") quick = true;
199 else if (a == "--json" && i + 1 < argc) json_path = argv[++i];
200 else if (a == "--md" && i + 1 < argc) md_path = argv[++i];
201 else { std::cerr << "usage: ckey-bench [--quick] [--json out.json] [--md out.md]\n"; return 2; }
202 }
203 const int W = 960, H = 720, trials = quick ? 4 : 10, fp_frames = quick ? 60 : 300;
204 const std::vector<size_t> payloads = quick ? std::vector<size_t>{33, 120} : std::vector<size_t>{24, 33, 80, 120, 300};
205 const std::vector<int> sides = quick ? std::vector<int>{70, 100, 140, 200, 280} : std::vector<int>{60, 70, 85, 100, 120, 140, 170, 200, 240, 280, 340};
206 const std::vector<Condition> conditions = {
207 {"clean", 0.0, 0.01, 0, 0, 0.0},
208 {"camera", 1.0, 0.03, 15, 0.06, 0.15},
209 {"hard", 1.8, 0.06, 30, 0.10, 0.30},
210 };
211 std::vector<Symbology> syms = {{"CKey P0 (M)", render_ckey, scan_ckey}};
212#ifdef CKEY_BENCH_ZXING
213 syms.push_back({"QR Code (M)", [](const std::string &t, int s, Image &i) { return render_zxing(ZXing::BarcodeFormat::QRCode, 4, t, s, i); },
214 [](const std::vector<uint8_t> &f, int w, int h, const std::string &e) { return scan_zxing(ZXing::BarcodeFormat::QRCode, f, w, h, e); }});
215 syms.push_back({"Data Matrix", [](const std::string &t, int s, Image &i) { return render_zxing(ZXing::BarcodeFormat::DataMatrix, 2, t, s, i); },
216 [](const std::vector<uint8_t> &f, int w, int h, const std::string &e) { return scan_zxing(ZXing::BarcodeFormat::DataMatrix, f, w, h, e); }});
217#endif
218 std::ostringstream md, js;
219 md << "# CKey optical benchmark (synthetic)\n\nGenerated by `ckey-bench" << (quick ? " --quick" : "") << "` with core " << ckey_get_software_version()
220 << ". **Synthetic frames, not real-camera measurements.** Frames are " << W << "×" << H << " px with clutter; sides include each symbology's quiet zone, "
221 << "so symbols are compared at equal area. " << trials << " trials per cell.\n\n";
222 md << "| Condition | blur σ px | noise σ | rotation | keystone | glare |\n|---|---|---|---|---|---|\n";
223 for (const auto &c : conditions) md << "| " << c.name << " | " << c.blur << " | " << c.noise << " | " << c.rotation_deg << "° | " << c.keystone * 100 << "% | " << c.glare << " |\n";
224 js << "{\n \"core\": \"" << ckey_get_software_version() << "\", \"synthetic\": true, \"frame\": [" << W << ", " << H << "], \"trials\": " << trials << ",\n \"results\": [\n";
225 bool first = true;
226 for (const auto &sym : syms) {
227 md << "\n## " << sym.name << "\n\n| Payload | Condition | Smallest side ≥ 90% | Bytes per 1,000 px² | Success by side (px) | Mean RS bytes corrected | Latency p50 / p90 / p99 ms |\n|---|---|---|---|---|---|---|\n";
228 for (size_t bytes : payloads) {
229 const std::string text = payload_text(bytes, static_cast<uint32_t>(bytes));
230 for (const auto &c : conditions) {
231 std::vector<double> latency; std::ostringstream by_side; int best = -1; double corrected = 0; int corrected_n = 0;
232 for (int side : sides) {
233 Image img;
234 if (!sym.render(text, side, img)) { by_side << side << ":n/a "; continue; }
235 int ok = 0;
236 for (int t = 0; t < trials; ++t) {
237 const auto frame = make_frame(&img, W, H, c, static_cast<uint32_t>(side * 1000 + t * 7 + bytes));
238 const Outcome o = sym.scan(frame, W, H, text);
239 latency.push_back(o.ms);
240 if (o.ok) { ++ok; corrected += o.corrected; ++corrected_n; }
241 }
242 by_side << side << ":" << 100 * ok / trials << "% ";
243 if (best < 0 && ok * 10 >= trials * 9) best = side;
244 }
245 const double density = best > 0 ? 1000.0 * bytes / (static_cast<double>(best) * best) : 0;
246 md << "| " << bytes << " B | " << c.name << " | " << (best > 0 ? std::to_string(best) + " px" : "none") << " | ";
247 if (best > 0) { char b[16]; std::snprintf(b, sizeof(b), "%.2f", density); md << b; } else md << "–";
248 char lat[64]; std::snprintf(lat, sizeof(lat), "%.1f / %.1f / %.1f", pct(latency, .5), pct(latency, .9), pct(latency, .99));
249 char cor[16]; std::snprintf(cor, sizeof(cor), "%.1f", corrected_n ? corrected / corrected_n : 0.0);
250 md << " | " << by_side.str() << "| " << cor << " | " << lat << " |\n";
251 js << (first ? "" : ",\n") << " {\"symbology\": \"" << sym.name << "\", \"payload_bytes\": " << bytes << ", \"condition\": \"" << c.name
252 << "\", \"min_side_px_90\": " << best << ", \"bytes_per_kpx2\": " << density << ", \"latency_ms_p50\": " << pct(latency, .5)
253 << ", \"latency_ms_p90\": " << pct(latency, .9) << ", \"latency_ms_p99\": " << pct(latency, .99) << "}";
254 first = false;
255 std::cerr << sym.name << " " << bytes << "B " << c.name << ": min side " << best << '\n';
256 }
257 }
258 // False positives: symbol-free clutter frames.
259 int fp = 0; std::vector<double> fp_latency;
260 for (int i = 0; i < fp_frames; ++i) {
261 const auto frame = make_frame(nullptr, W, H, conditions[static_cast<size_t>(i % 3)], 0xF00D0000u + static_cast<uint32_t>(i));
262 const Outcome o = sym.scan(frame, W, H, "\x01never");
263 fp_latency.push_back(o.ms);
264 if (o.ok || o.corrected < 0) ++fp;
265 }
266 char lat[64]; std::snprintf(lat, sizeof(lat), "%.1f / %.1f ms", pct(fp_latency, .5), pct(fp_latency, .99));
267 md << "\nFalse positives on " << fp_frames << " symbol-free clutter frames: **" << fp << "** (scan latency p50 / p99 " << lat << ").\n";
268 js << ",\n {\"symbology\": \"" << sym.name << "\", \"false_positive_frames\": " << fp_frames << ", \"false_positives\": " << fp << "}";
269 }
270#ifndef CKEY_BENCH_ZXING
271 md << "\nQR Code and Data Matrix columns need zxing-cpp: configure with `-DCKEY_BENCH_ZXING=ON` (CI's `benchmarks` job does).\n";
272#endif
273 js << "\n ]\n}\n";
274 std::cout << md.str();
275 if (!md_path.empty()) std::ofstream(md_path) << md.str();
276 if (!json_path.empty()) std::ofstream(json_path) << js.str();
277 return 0;
278}