hash op• Data kinds: image2d → phash
• Call: import fullseye as fs; fs.ledger.perceptual_hash(image, mode: 'str' = 'dct', hash_size: 'int' = 8) -> 'np.ndarray' (to call the implementation directly, import imgforensics; imgforensics.perceptual_hash(image, mode: 'str' = 'dct', hash_size: 'int' = 8) -> 'np.ndarray'; from the registry, opsimgforensics.get("perceptual_hash"))
A perceptual hash. Returns a 1-D bool bit string.
mode:
"dct" downscale to a (hash_size * 4) square → 2-D DCT-II (orthogonal) → take the top-left
hash_size x hash_size block and compare against the median excluding DC.
Same procedure as the pHash of Zauner 2010. Length hash_size**2 bits.
"average" downscale to a hash_size square → compare against the mean (aHash). Same length. "difference" downscale to (hash_size, hash_size + 1) → compare which of horizontally adjacent pixels is larger (dHash). Same length, robust to changes in mean luminance.
Measured (tests/test_imgforensics.py pins it in numbers; 256x256 1/f^1.6 images, hash_size=8 = 64 bits. The distance between 2 unrelated images is dct 31.3 / average 34.2 / difference 32.6 (mean over 20 pairs; the theoretical value is 32 = half the bit length)):
===================== ======== ========== ============
Transform dct average difference
===================== ======== ========== ============
JPEG q60 recompress 0 0 0
JPEG q30 recompress 2 0 0
1/2 downscale 0 0 0
1/4 downscale 0 0 0
Brightness +0.1 0 0 0
Horizontal flip 28 42 34
90 deg rotation 32 30 32
Different image 28 40 34
===================== ======== ========== ============
What this op cannot tell you:
• A small distance does not mean the same image. Since it drops to a coarse 8x8 downscale, fine-detail tampering passes at distance 0. Measured: with a 24x24 copy-move inserted into a 512x512 image, the distance is dct 0 / average 0 / difference 1.
• Nor does a large distance mean a different image. As the table above shows, flips and rotations cannot be told apart from "a different image" (both fall in the same distance range as 2 unrelated images). There is no invariance to geometric transforms at all.
• The return value is in the phash vocabulary, not signal. A bool 1-D array satisfies all the existing predicates of signal / indices / descriptor, so a mix-up makes signal1d.lowpass and others return finite, plausible values (measured on 5 ops). That is why hash_distance checks the dtype.
• 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
phash as input)hash_distance · watermark_embed · watermark_capacity
hash)*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.