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.
| Payload | Clean PSNR | Clean SSIM | Clean bit_acc | Avg (13 atks) | Worst | Worst attack | Embed fails |
|---|---|---|---|---|---|---|---|
| 32 | 37.86 | 0.972 | 1.000 | 0.992 | 0.911 | chain_wechat | 0 |
| 64 | 37.80 | 0.971 | 1.000 | 0.984 | 0.887 | chain_wechat | 0 |
| 128 | 37.86 | 0.970 | 1.000 | 0.970 | 0.780 | chain_wechat | 0 |
| 256 | 37.85 | 0.969 | 1.000 | 0.962 | 0.759 | chain_wechat | 0 |
| 512 | 37.84 | 0.969 | 1.000 | 0.946 | 0.703 | chain_wechat | 0 |
| 896 | 37.84 | 0.969 | 1.000 | 0.926 | 0.667 | chain_wechat | 0 |
| 1024 | 37.84 | 0.969 | 1.000 | 0.922 | 0.666 | chain_wechat | 0 |
| 2048 | 37.84 | 0.969 | 0.999 | 0.879 | 0.617 | chain_wechat | 0 |
| 4096 | β | β | β | β | β | rejected (capacity) 20 / 20 | |
| 8192 | β | β | β | β | β | rejected (capacity) 20 / 20 | |
d1, not by bit count.wm_size < block_num.jpeg_q30 (0.73 at 896 b) and chain_wechat (0.67 at 896 b). Most other attacks stay β₯ 0.95 even at 896 b.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)
| Choice | Where | Why it matters |
|---|---|---|
| Embed in DWT LL band (low-freq) | dwt2(..., 'haar') then operate on LL | LL is the part JPEG and resize preserve best. Counter-intuitive but correct: natural attack-domain alignment makes JPEG robustness almost free. |
| QIM on singular values | s[0] = (s[0] // d1 + 1/4 + 1/2 * bit) * d1 | Adds 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 watermark | loop over channel in range(3) | Independent 3Γ repetition for free. Decoder averages β ~β3 SNR boost. |
| Cyclic bit assignment | wm_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 permutation | block_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 + threshold | extract_avg then > 0.5 | We 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 network | n/a | Embedding parameters (d1, block_shape) are hand-tuned constants. Deploys instantly anywhere with NumPy + OpenCV + PyWavelets. |
~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.
Three structural reasons compression robustness is so good:
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.
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.
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.
block_num β 1 (4095 b for 512Β², block_shape=(4,4)). No graceful overflow β library throws on embed.chain_wechat worst-case is 0.67 at 896 b, well below practical ECC threshold (~0.85 recoverable with BCH+RS at MWIP's rates).jpeg_q30 drops fast (0.73 at 896 b). Aggressive single-pass JPEG is harsher than chained mild JPEG because it quantizes the LL band coarser.s[0] snapped to a d1/2-spaced grid. An attacker who knows the algorithm computes the stego's singular values, identifies the snapping pattern, and adds noise to disrupt the modular structure without changing the perceptual image. The password_img permutation doesn't help β the snapping itself is the signature.numpy.RandomState(int) seed. 2Β³Β² keyspace per password; 2βΆβ΄ for both. Brute-force is ~weeks on a cluster (vs known-plaintext recovery, even faster).chain_wechat-style attacks may be worth it. Quick experiment: train with spreading-factor 2 / 4 / 8; measure worst-case bit_acc improvement.chain_wechat-style heavy compression.(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.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.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).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:
For the related-work table:
~/.conda/envs/hang/lib/python3.11/site-packages/blind_watermark/bwm_core.pyresults/blindwm_baseline/eval.json Β· eval_highpayload.jsonsrc/exp0_blindwm_eval.pyscripts/exp0_blindwm_eval.sbatch Β· scripts/exp0_blindwm_eval_high.sbatchsrc/exp0_inn_eval.py:163-191docs/blindwm_report.md