64-bit DCT perceptual image hashing in pure Go: decide whether two images show the same picture across resolution, recompression and slight color shifts.
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Andrew Tyler e6ed33ae18 README: reference kreeader-server#98 as the extraction tracking issue
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 18:04:15 -05:00
cover_test.go Initial extraction from kreeader-server's components/detection/phash 2026-07-29 04:38:24 -05:00
go.mod Initial extraction from kreeader-server's components/detection/phash 2026-07-29 04:38:24 -05:00
go.sum Initial extraction from kreeader-server's components/detection/phash 2026-07-29 04:38:24 -05:00
phash.go Initial extraction from kreeader-server's components/detection/phash 2026-07-29 04:38:24 -05:00
phash_test.go Initial extraction from kreeader-server's components/detection/phash 2026-07-29 04:38:24 -05:00
README.md README: reference kreeader-server#98 as the extraction tracking issue 2026-08-04 18:04:15 -05:00

perceptual-hash

64-bit DCT perceptual image hashing in pure Go — decide whether two images show the same picture, across resolution changes, JPEG recompression and slight color shifts.

import phash "forgejo.jiggl.in/libraries/perceptual-hash"

What it does

  • Hash(image.Image) uint64 / HashBytes([]byte) (uint64, error) compute the hash. The classic pHash recipe: grayscale, box-average down to 32×32, 2-D DCT-II, take the 8×8 lowest-frequency block, emit one bit per coefficient (set when above the block's median, DC excluded so overall brightness can't skew it).
  • Distance(a, b) int is the Hamming distance: 0 is identical structure, 32 is uncorrelated. Same-image pairs land well under 12; a threshold somewhere in 812 is the usual "these are the same picture" call.
  • Format(uint64) string / Parse(string) (uint64, bool) are the stable 16-hex-digit textual form, for database columns and wire formats.
  • HashCoverBytes(data []byte, otherWidths []int) (uint64, bool, error) handles the cover-matching case where the image under test may be a two-page wraparound spread (a comic's front+back cover, a book's full dust jacket) while the reference art shows only the front. If the image is ≥1.8× the median width of its siblings — or, with no sibling widths given, simply wider than it is tall — only the right half is hashed. The second return value reports whether that rule fired.

Box averaging (not point sampling) on the downsample is deliberate: it is what keeps the hash stable between a full-resolution scan and a thumbnail.

Decoding

HashBytes/HashCoverBytes decode via image.Decode, with GIF, JPEG, PNG and WebP decoders registered. Register any other format you need (_ "golang.org/x/image/tiff", …) in your own program before calling.

Provenance

Extracted verbatim (algorithm unchanged) from kreeader-server's components/detection/phash (tracked as kreeader-server#98), where it has backed cover/metadata matching, duplicate detection and content fingerprinting since 2026. The hash values this package produces are byte-for-byte the same as that in-tree version's.