perceptual_hash — IMGFORENSICS 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"))

Usage

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.

References (sample data, literature)

• 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.

Runnable examples (verified samples that actually call this op)

• image_forensics_audit — py -3.11 examples/image_forensics_audit.py

Ops the type connects to (they accept phash as input)

hash_distance · watermark_embed · watermark_capacity

Same category (hash)

hash_distance


*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.