Viewing is open to everyone. Downloading and starring need a GitHub sign-in.
Sign in with GitHub
print_sim.h
v0.1.0-build.85
Latest
Commit ede38f5
Sign in with GitHub to download. Each download is recorded with your GitHub username.
| 1 | // Print + camera simulation for CKey symbols (tests and tools only). |
| 2 | // A symbol SVG is printed at a device resolution (binary ink dots, no grey), |
| 3 | // ink spreads by a radius and blurs into the paper, then a camera with a |
| 4 | // given resolution (pixels per millimetre), optical blur, and noise views it. |
| 5 | // The output is the grayscale frame a decoder would see. |
| 6 | |
| 7 | |
| 8 | |
| 9 | "svg_raster.h" |
| 10 | |
| 11 | <cmath> |
| 12 | <cstdint> |
| 13 | <vector> |
| 14 | |
| 15 | namespace printsim { |
| 16 | |
| 17 | struct Printer { double dpi; double spread_mm; double blur_mm; double compensate_mm = 0; }; // ink spread (dot gain), paper wicking, pre-shrink of ink |
| 18 | struct Camera { double px_per_mm; double blur_px; double noise; }; // optics blur sigma; noise amplitude (grey levels) |
| 19 | |
| 20 | inline void gaussian(std::vector<double> &img, int w, int h, double sigma) { |
| 21 | if (sigma < 0.05) return; |
| 22 | const int r = static_cast<int>(std::ceil(3 * sigma)); std::vector<double> k(2 * r + 1); double sum = 0; |
| 23 | for (int i = -r; i <= r; ++i) sum += k[i + r] = std::exp(-0.5 * i * i / (sigma * sigma)); |
| 24 | for (auto &v : k) v /= sum; |
| 25 | std::vector<double> tmp(img.size()); |
| 26 | 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[i + r] * img[static_cast<size_t>(y) * w + std::clamp(x + i, 0, w - 1)]; tmp[static_cast<size_t>(y) * w + x] = a; } |
| 27 | 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[i + r] * tmp[static_cast<size_t>(std::clamp(y + i, 0, h - 1)) * w + x]; img[static_cast<size_t>(y) * w + x] = a; } |
| 28 | } |
| 29 | |
| 30 | // marks_are_ink: print the symbol's marks in ink on white paper (dark-on- |
| 31 | // light). Otherwise the background is inked and the marks are bare paper. |
| 32 | // Returns the camera frame; side_px receives its size. The symbol's 31-unit |
| 33 | // canvas is printed at width_mm. |
| 34 | inline std::vector<uint8_t> print_and_capture(const std::string &svg_text, double width_mm, const Printer &pr, const Camera &cam, |
| 35 | bool marks_are_ink, uint32_t seed, int &side_px) { |
| 36 | const auto svg = svgtest::parse(svg_text); |
| 37 | const int dev = std::max(8, static_cast<int>(std::lround(width_mm / 25.4 * pr.dpi))); |
| 38 | const auto cover = svgtest::rasterize(svg, dev, dev, 3, 0, 255); |
| 39 | std::vector<uint8_t> ink(cover.size()); |
| 40 | for (size_t i = 0; i < cover.size(); ++i) ink[i] = marks_are_ink ? cover[i] >= 128 : cover[i] < 128; // binary device dots |
| 41 | if (pr.compensate_mm > 0) { // print-side compensation: erode ink, keeping at least one dot of every mark |
| 42 | std::vector<uint8_t> paper_mask(ink.size()); for (size_t i = 0; i < ink.size(); ++i) paper_mask[i] = !ink[i]; |
| 43 | const auto to_paper = svgtest::distance_to_set(paper_mask, dev, dev); |
| 44 | const double c = pr.compensate_mm / 25.4 * pr.dpi; |
| 45 | for (size_t i = 0; i < ink.size(); ++i) if (ink[i] && to_paper[i] <= c) ink[i] = 0; |
| 46 | } |
| 47 | // Dot gain: ink spreads outward by spread_mm. |
| 48 | const auto dist = svgtest::distance_to_set(ink, dev, dev); |
| 49 | const double spread_px = pr.spread_mm / 25.4 * pr.dpi; |
| 50 | std::vector<double> paper(cover.size()); |
| 51 | for (size_t i = 0; i < paper.size(); ++i) paper[i] = dist[i] <= spread_px ? 28.0 : 236.0; |
| 52 | gaussian(paper, dev, dev, pr.blur_mm / 25.4 * pr.dpi); |
| 53 | // Camera: area-sample the printed sheet at px_per_mm, then optics + noise. |
| 54 | side_px = std::max(8, static_cast<int>(std::lround(width_mm * cam.px_per_mm))); |
| 55 | std::vector<double> frame(static_cast<size_t>(side_px) * side_px); |
| 56 | const double scale = static_cast<double>(dev) / side_px; |
| 57 | for (int y = 0; y < side_px; ++y) for (int x = 0; x < side_px; ++x) { |
| 58 | const double x0 = x * scale, y0 = y * scale; double a = 0; int n = 0; |
| 59 | for (double yy = y0; yy < y0 + scale; yy += std::max(0.5, scale / 4)) for (double xx = x0; xx < x0 + scale; xx += std::max(0.5, scale / 4)) { |
| 60 | a += paper[static_cast<size_t>(std::min(dev - 1, static_cast<int>(yy))) * dev + std::min(dev - 1, static_cast<int>(xx))]; ++n; |
| 61 | } |
| 62 | frame[static_cast<size_t>(y) * side_px + x] = a / n; |
| 63 | } |
| 64 | gaussian(frame, side_px, side_px, cam.blur_px); |
| 65 | std::vector<uint8_t> out(frame.size()); |
| 66 | for (size_t i = 0; i < out.size(); ++i) { |
| 67 | seed = seed * 1664525u + 1013904223u; |
| 68 | out[i] = static_cast<uint8_t>(std::clamp(frame[i] + cam.noise * (((seed >> 8) & 0xffff) / 32768.0 - 1.0), 0.0, 255.0)); |
| 69 | } |
| 70 | return out; |
| 71 | } |
| 72 | |
| 73 | } // namespace printsim |
| 74 |