Channel D — Deep-Learning Pixel Watermark (camera-photo-robust)
Status: research → pre-integration. Model trained and proven robust to a real phone photo
(100% bit recovery on the user's own captured photo). CPU/ONNX decode proven (~100 ms/tile, no GPU
at serve time). Invisibility hardening in progress (see "Invisibility" below). This doc is the plan to
wire it into Obscura as a first-class channel, and an honest statement of what it can and can't do.
Why a fourth channel
Obscura already has three carriers, each with a different survival envelope:
| Channel | Carrier | Survives | Invisible? | Blind? |
|---|---|---|---|---|
| V | reference-line baseline shift | PDF, high-DPI render | yes | yes |
| W | inter-word gap (word-gap QIM) | PDF + screenshot | yes | yes |
| K | one-sided kerning + starmap | PDF (authenticity, non-blind) | yes | no (needs starmap) |
| D | learned pixel residual (StegaStamp-class) | PDF, screenshot, + camera photo of a screen or print | partly (see below) | yes |
The gap D fills: a photograph of the document — someone points a phone at a monitor or a printout.
Every geometric channel (V/W/K) is destroyed by that channel's perspective warp, moiré, resampling and
lighting. D is trained end-to-end through a differentiable model of exactly those distortions, so it is
the only carrier that comes back from a camera. That robustness is the whole reason to accept its cost.
The cost (be honest about this)
D is a pixel mark. To survive a camera photo of a mostly-white page it must carry real signal energy,
and on white paper that energy is visible to some degree. This is a fundamental SNR floor, not a tuning
bug: truly-invisible and camera-robust is not achievable on a blank page. The invisibility work below
minimizes and reshapes that energy (kill colour, hide near text, push below perceptual threshold), but the
honest positioning is:
- Default OFF. D is opt-in, for documents where camera-capture leakage is the threat and some faint
texture is acceptable, or for documents that already carry visual content (figures, photos, colour
letterhead) where the mark hides genuinely well. - For plain white text where invisibility is paramount, W stays the default (invisible,
screenshot-robust). D is the escalation when "survives a photo" outranks "perfectly invisible."
Invisibility hardening (current work)
Training recipe (AHU H100, ~/stegastamp):
- Base robust model (runs/doc2, bitacc 0.9994 through the screen-photo channel) — warm-start.
- Fine-tune adds, as budgets (hinges), not unbounded minimization (this matters — unbounded
invisibility losses collapse the encoder to a zero residual that carries nothing):
- PSNR budget — penalize residual energy only until PSNR hits ~30 dB, then stop.
- VGG perceptual budget — L1 over VGG16 features, hinged to a target, so the mark hides where human
vision is least sensitive rather than being spread uniformly.
- Flatness relocation — push energy off flat white margins toward text edges (this relocates, it
can't zero the mark, so it's collapse-safe).
- Adversarial critic — a PatchGAN trained to spot "this page is marked"; the encoder is trained to
fool it, gated on bit-acc > 0.9 so invisibility is only chased once the mark is still robust.
- --gray option: achromatic residual by construction → colour (the most visible artifact on a
document) is structurally zero.
- best.pt = most-perceptually-invisible checkpoint whose bit-acc through the channel clears the floor.
Serving architecture (no torch/GPU in production)
D lives in the existing Python gRPC sidecar (stego/obscura_stego/), as a new channel_d/ module using
onnxruntime CPU — the same pattern as the other channels, no torch, no GPU. Two small ONNX files
(encoder.onnx, decoder.onnx, a few MB each) ship with the sidecar.
Encode (channel_d.encode(pdf, key_epoch, seq)):
1. Rasterize each page to an image (reuse surgery rasterization).
2. Tile into 256×256 tiles; embed the same RS-coded payload into every tile (redundancy = robustness).
3. Recomposite tiles → marked page raster.
4. Overlay the residual as a faint image layer on top of the original page content (preserves the text
layer + searchability), OR replace the page raster (simpler, loses text layer — decide per doc type).
5. Re-emit PDF.
Decode (channel_d.decode_blind(images, key_epoch)):
1. Rectify the uploaded photo/screenshot (full-page detection or an embedded registration frame; cv2).
2. Per-tile alignment + rotation search (already prototyped in demo/dlwm_decode.py / decode_rot.py).
3. Run decoder.onnx per tile → per-bit logits; majority-vote across tiles.
4. RS-decode → verify truncated MAC → seq → attribution. Blind: payload is self-contained (no starmap).
Payload: 100 model bits → RS → ~50 usable bits → seq (issuance) + truncated HMAC, enough for 1:N
attribution. Info-floor: D needs enough tiles for the vote, so it marks whole pages, not short excerpts.
Wiring points
- Sidecar —
channel_d/module (encode/decode) + ONNX assets.DecodeRequestalready carries images. - Orchestrator (
orchestrator.py) —encode()applies D when policy requests photo-robustness;
decode()trieschannel_d.decode_blindon the rasterized images alongside W/V. - Go adapter / policy — a
photo_robustpolicy flag (protection profile). No new ledger state (blind). - Admin UI — a mode/option toggle: "Survive camera photos (adds faint visible texture)" — worded so
the visibility tradeoff is explicit at the point of choice.
Signature safety (reuse the existing gate)
D re-rasterizes / overlays page content, so like the geometric channels it will break a PDF's PAdES
/ByteRange hash. It must reuse the same alreadySigned egress gate that W/K now use — skip D on any
already-signed PDF (e-signature / e-Meterai / e-stamp). No new signature-destruction surface.
Phasing & effort
- Phase 0 (done) — model trained; camera-photo robustness proven on a real photo; CPU/ONNX decode proven.
- Phase 1 (in progress) — invisibility hardening; deliverable = encoder/decoder ONNX at acceptable
invisibility with photo-robustness preserved. - Phase 2 —
channel_dsidecar module (encode overlay + decode rectify/tile/vote) + RS payload + unit
tests against simulated photos. ~multi-day. - Phase 3 — orchestrator + proto + Go adapter + policy flag + Admin UI toggle. ~multi-day.
- Phase 4 — real-device validation (print → photo → decode; screen → photo → decode), tune
tiling/registration, deploy viadeploy/update.sh.
Open decisions
- Overlay vs raster-replace the page (searchability + file size vs implementation simplicity).
- Default policy: opt-in per document, or auto-on for a document class (e.g. anything with images)?
- Gray vs colour residual: gray = zero colour but louder luminance texture; colour = more invisible in
aggregate but reintroduces faint colour specks. Current lean: gray (colour on a document is the most
alarming artifact), pending the side-by-side render from the current training runs.