imagehide / blind_watermark analysis

blind_watermark β€” mechanism, benchmark, ideas to steal

external method eval 2026-05-26
Highlight any text β†’ πŸ’¬ comment. Publishes to GitHub Issues; optional πŸ€– Ask Claude to fix tag for action items.

Source: guofei9987/blind_watermark v0.4.4 (12.9 kβ˜…, pure Python, no associated paper). Method class: classical DWT-DCT-SVD with QIM modulation on the leading singular value. Our eval: 20 DIV2K val images, 512Γ—512, 13-attack compression-only suite mirroring src/exp0_inn_eval.py. Sweep payloads 32 β†’ 8192 b. JSON at results/blindwm_baseline/eval.json and eval_highpayload.json.

This page mines blind_watermark for design ideas to bring back into MWIP, with the explicit acknowledgement that its compression-attack robustness is competitive with most learned baselines in our related-work table and deserves to inform the design rather than be dismissed. Companion markdown at docs/blindwm_report.md.

Β§1Headline numbers

Payload Clean PSNR Clean SSIM Clean bit_acc Avg (13 atks) Worst Worst attack Embed fails
32 37.860.9721.0000.9920.911chain_wechat0
64 37.800.9711.0000.9840.887chain_wechat0
128 37.860.9701.0000.9700.780chain_wechat0
256 37.850.9691.0000.9620.759chain_wechat0
512 37.840.9691.0000.9460.703chain_wechat0
89637.840.9691.0000.9260.667chain_wechat0
1024 37.840.9691.0000.9220.666chain_wechat0
2048 37.840.9690.9990.8790.617chain_wechat0
4096 β€”β€”β€”β€”β€”rejected (capacity) 20 / 20
8192 β€”β€”β€”β€”β€”rejected (capacity) 20 / 20
vs MWIP @ 896 b on the same suite: blind_watermark loses ~7 pp on average, ~30 pp worst-case. PSNR is actually ~0.4 dB higher for blind_watermark.
vs published learned methods (TM1/MS2/MSF7 mixes): blind_watermark at 100 b would deliver ~0.99 bit_acc on the compression subset alone β€” competitive with TrustMark-Q (0.95 Β± 0.09 over mix), and strictly dominating StegaStamp (0.72), RoSteALS (0.94), MetaSeal, HiDDeN on this axis. The averaged "mix" numbers in published tables hide which method actually wins on which attack family.

Β§2Mechanism (verified from source)

Pipeline per image (bwm_core.py:79-138):

RGB β†’ YUV
for each channel (Y, U, V):
    LL, (LH, HL, HH) = dwt2(channel, 'haar')        # LL is 256Γ—256 for 512Β² input
    blocks = LL.reshape into 4Γ—4 tiles               # 64Γ—64 = 4096 blocks
    for block_i in blocks:
        permutation = PRNG(password_img, block_i)    # 16-element permutation
        block_dct  = dct(block)
        block_perm = block_dct.flatten()[permutation].reshape(4,4)
        u, s, v    = svd(block_perm)                 # singular values, length 4
        bit        = wm_bits[block_i % len(wm_bits)] # cyclic assignment
        s[0]       = (s[0] // d1 + 1/4 + 1/2 * bit) * d1   # QIM on largest sv
        block_new  = u @ diag(s) @ v
        block_back = block_new.flatten() un-permuted β†’ reshape β†’ idct
    reassemble blocks β†’ idwt β†’ write

Extraction (bwm_core.py:140-202):

RGB β†’ YUV β†’ dwt β†’ tiles β†’ for each block:
    permute β†’ dct β†’ svd β†’ bit_hat = (s[0] % d1 > d1/2)
collect 3 Γ— 4096 soft bits; for each watermark bit j:
    soft_j = mean(bit_hats[:, j::wm_size])  # average all repetitions Γ— 3 channels
final = soft > 0.5  (or k-means thresholding via extract_with_kmeans)

Design choices worth noting

ChoiceWhereWhy it matters
Embed in DWT LL band (low-freq)dwt2(..., 'haar') then operate on LLLL is the part JPEG and resize preserve best. Counter-intuitive but correct: natural attack-domain alignment makes JPEG robustness almost free.
QIM on singular valuess[0] = (s[0] // d1 + 1/4 + 1/2 * bit) * d1Adds a fixed bias of Β±d1/4 to s[0]. Robust to any attack with magnitude < d1/4 in s[0]-space. d1 is the JPEG-robustness/PSNR knob.
All 3 YUV channels carry the watermarkloop over channel in range(3)Independent 3Γ— repetition for free. Decoder averages β€” ~√3 SNR boost.
Cyclic bit assignmentwm_bits[block_i % wm_size]At payload P, each bit gets floor(block_num / P) Γ— 3 reps. At P=32 β†’ 384 reps/bit; at P=896 β†’ 13.7. Implicit repetition coding scales inversely with payload.
Per-block coefficient permutationblock_dct.flatten()[shuffler[i]]Different blocks have different SVD bases (post-permute), so the modulation pattern is not periodic in pixel coords. This is the "obscurity-keying" via password_img.
Soft averaging + thresholdextract_avg then > 0.5We threshold at 0.5; library also offers k-means thresholding (extract_with_kmeans) that adapts to the soft distribution. We didn't use it β€” likely leaving a few pp on the table.
No training, no neural networkn/aEmbedding parameters (d1, block_shape) are hand-tuned constants. Deploys instantly anywhere with NumPy + OpenCV + PyWavelets.

Why it's slow (despite no NN)

~12 k SVD calls per image on 4Γ—4 matrices, in a Python for loop. LAPACK call overhead dominates the actual SVD compute on tiny matrices. There's an AutoPool (multiprocessing.Pool) but IPC overhead eats the savings at 512Β² scale. A batched-SVD CUDA implementation would be ~100Γ— faster β€” the slowness is implementation, not algorithm.

Β§3Why it works as well as it does

Three structural reasons compression robustness is so good:

(a) Domain alignment

JPEG compresses by quantizing DCT coefficients per 8Γ—8 block. Resize-down-then-up is a low-pass filter. blind_watermark embeds in DWT-LL coefficients of 4Γ—4 blocks β€” both sit in the low-frequency regime that JPEG and resize preserve best. The watermark lives in the part of the spectrum that the attacks don't touch. This is the opposite of HF-embedding intuition: HF gets killed by JPEG; LF survives.

(b) QIM step size as a single explicit knob

The d1 parameter directly controls the PSNR / robustness tradeoff. Larger d1 β†’ bigger singular-value perturbation β†’ more visible (lower PSNR) but more robust to attack noise in s[0]. Library default d1=36 gives PSNR ~38 dB and decent robustness. Single hyperparameter, no training, fully interpretable.

(c) Massive over-redundancy at low payloads

4096 blocks Γ— 3 channels = 12 288 bit-carriers. At 32 bits payload, that's 384 reps/bit. Even random-bit-flip noise from any attack needs to corrupt > 50% of those 384 to actually flip a bit. The "graceful degradation with payload" curve is just this repetition-coding gain decreasing.

Why it's not perfect: chain_wechat (0.3 scale + Q50 JPEG) destroys enough of the LL band that even 384Γ— redundancy isn't enough at high payloads β€” and the per-block QIM has no error-correction structure beyond simple averaging.

Β§4Where it fails

Known (from our eval)

Suspected (untested, high confidence)

Security

Β§5Ideas worth stealing for MWIP

A.Sanity-check that we embed in attack-aligned frequencies
Verify MWIP's INN distributes the watermark signal in spatial-frequency regions that survive the dominant attacks (JPEG mid-band, resize low-band). If our learned residual has significant HF content, we're leaving robustness on the table. Action: plot the spectral distribution of the stego–cover residual and compare to where JPEG actually preserves energy. Quick diagnostic, no training.
B.Explicit per-bit spatial spreading
blind_watermark gets ~13 reps/bit at 896 b via cyclic assignment. MWIP's 32Γ—32 bit grid puts each bit in a single slot. Adding explicit per-bit spreading (each bit modulates k > 1 spatial locations with a deterministic spreading code) gives the same noise-averaging gain at decode time. Trade-off: increases per-image PSNR cost or reduces effective payload β€” but the worst-case bit_acc gain on chain_wechat-style attacks may be worth it. Quick experiment: train with spreading-factor 2 / 4 / 8; measure worst-case bit_acc improvement.
C.Three-channel redundancy
blind_watermark embeds in all 3 YUV channels. If MWIP currently embeds in RGB with a single residual, consider channel-conditioned heads producing 3 independent residuals (Y, Cb, Cr) decoded by 3 detectors and combined. Free ~1.7Γ— SNR if independent. Caveat: the INN may already be implicitly using channel redundancy β€” needs checking.
D.Lean into LL-band embedding
The biggest robustness lesson is embed where the attack doesn't go. For MWIP: ensure the INN's residual is mostly low-spatial-frequency rather than evenly distributed. Encourage via a band-limited loss term during training β€” penalize HF content in the residual. Likely a 1–2 dB PSNR cost for a non-trivial bit_acc gain under chain_wechat-style heavy compression.
E.QIM-style decoder regularization at training
blind_watermark's decoder is exact QIM extraction: (s[0] mod d1) > d1/2. Robustness comes from snapping s[0] to a discrete grid. MWIP's decoder is a learned soft classifier β€” typically noisier than QIM under attack. Idea: add a QIM-like quantization layer at encoder output (snap watermarked features to a discrete grid before re-injection), with straight-through gradient. Could give the same noise-rejection that QIM provides for free.
F.k-means thresholding at extraction
For our reported numbers, we used hard threshold at 0.5 on the soft outputs. blind_watermark's extract_with_kmeans adaptively learns the bit-0 / bit-1 cluster centers from the soft distribution. Same idea applies to MWIP's logits β€” adaptive thresholding (or bias-correction from a small held-out set) could pick up a few pp without retraining.
G.Distortion that's payload-independent
blind_watermark's PSNR is constant across 32 β†’ 2048 bits. This is unusual and useful: the same encoder can carry many payload sizes at the same perceptual cost. If MWIP retrains per payload, we're spending compute on something blind_watermark gets for free via cyclic assignment. Idea: train MWIP at max payload, then cyclic-assign fewer bits at runtime. Or: train with payload randomization (sample payload per batch, mask unused bit slots). Either lets one model serve many payload regimes.
H."Obscurity-keying" as cheap per-instance variation
blind_watermark's password_img permutes block coefficients per-block. Even though it's weak as a crypto key, it gives a runtime parameter that varies the watermark pattern without retraining. For MWIP, an analogous mechanism β€” e.g., a permutation of the 32Γ—32 bit-to-location mapping, seeded by an instance-specific value β€” could give: per-user / per-device customization without retraining; defense against template-extraction attacks (attacker can't average across instances if each has a different permutation); combinable with the cryptographic key in the payload (the permutation seed could be a hash of the public key).
I.No-training deployment as the headline, not the apology
blind_watermark is competitive because it has no training. Embedding parameters are hand-tuned constants matched to attack physics. For MWIP we should be honest about what training actually buys us β€” and ensure the answer isn't "marginal improvement on compression robustness." The training premium should pay off on attacks blind_watermark can't address: geometric, regeneration, adversarial. If our training doesn't deliver on those, we should question why we're training at all.
Top three to prioritize: (D) band-limited loss term and (B) explicit per-bit spreading are the most likely to move worst-case bit_acc. (A) is the quick diagnostic that should happen first to confirm whether (D) is even needed.

Β§6What we haven't tested (and should)

These eval gaps need to close before the MWIP paper can honestly claim differentiation. The compression-only benchmark is not enough.

Β§7Reframing the MWIP paper

The "we have cryptographic key binding, they don't" pitch doesn't survive the observation that any bit-channel can carry a signed payload. The defensible pitch:

MWIP provides a 896-bit channel that maintains robustness across the full attack envelope β€” compression, geometric, regeneration, and adaptive β€” at PSNR comparable to classical baselines. The cryptographic provenance lives in the payload; the channel's job is to deliver it intact under realistic deployment attacks where classical schemes structurally fail.

For the related-work table:

Β§8References