watermark op• Data kinds: image2d × phash → image2d
• Call: import fullseye as fs; fs.ledger.watermark_embed(image, bits, strength: 'float' = 0.1, wavelet: 'str' = 'haar', level: 'int' = 1) -> 'np.ndarray' (to call the implementation directly, import imgforensics; imgforensics.watermark_embed(image, bits, strength: 'float' = 0.1, wavelet: 'str' = 'haar', level: 'int' = 1) -> 'np.ndarray'; from the registry, opsimgforensics.get("watermark_embed"))
DWT-DCT watermark embedding. Returns the watermarked image (H, W). image2d.
Cuts the LL subband of a 1-level DWT (default haar) into 8x8 blocks and represents 1 bit by which is larger in a pair of mid-band coefficients (3,1) and (1,3) of each block's orthogonal DCT-II (the mid-band coefficient-pair method of Hsu & Wu 1999). If the difference is below strength, both are moved symmetrically until the difference reaches strength (moving only one would bias the block's energy). Extraction does not need the original image (blind).
The capacity is (LL height // 8) * (LL width // 8) bits. If bits is shorter than that, it fills from the start and leaves the remaining blocks untouched. If longer, ValueError.
bits is a bool 1-D array (phash vocabulary). An int array of 0/1 is also accepted, but float is not (it does not silently decide the rounding direction).
The trade-off between strength and image quality is swept and returned as a table by watermark_capacity. Measured (256x256 texture image, 128 bits, LL is 128x128 = capacity 256 bits; tests/test_imgforensics.py::test_watermark_strength_psnr_tradeoff):
========= ========== ====== ================= =================
strength PSNR (dB) BER BER (JPEG q 90) BER (JPEG q 75)
========= ========== ====== ================= =================
0.02 45.48 0.000 0.008 0.195
0.05 44.48 0.000 0.000 0.008
0.10 42.95 0.000 0.000 0.000
0.20 40.32 0.000 0.000 0.000
0.40 36.37 0.000 0.000 0.000
========= ========== ====== ================= =================
This is what the trade-off actually is. Unprocessed, even the weakest 0.02 gives 0 errors, but through JPEG quality 75, 19.5% are corrupted. The default 0.1 was chosen as the point "0 errors down to quality 75, PSNR 42.95 dB" (clipped is 0 at every strength).
PyWavelets required (ImportError).
• Sample-data catalog (download URLs / licences) — 2-D uses skimage.data (BSD/public domain) plus synthetic images; 3-D lists download URLs for real data sources (Stanford, PDS, …).
• Operator provenance and references — the sources of the research/methods this op family came from.
• The canonical algorithm (author, year) and its uses are named in the family usage guide above.
• image_forensics_audit — py -3.11 examples/image_forensics_audit.py
image2d as input)perceptual_hash · fingerprint_correlate · error_level_map · jpeg_quality_estimate · jpeg_ghost_map · noise_inconsistency_map · copy_move_regions · watermark_extract
watermark)watermark_extract · watermark_capacity
*Provenance: imgforensics.py — IMGFORENSICS operator registry. This per-op note is generated by tools/opdocs.py md (do not hand-edit).*
© 2026 Kazufumi Furuse — Fullseye operator documentation. Licensed under Apache-2.0.