Repository navigation
Replace imagehash with a bit-identical pure-Python pHash - #233
Merged
Merged
Conversation
imagehash was used for one call, phash(), and dragged in numpy, scipy and PyWavelets: about 165 MB installed, two copies of OpenBLAS on Linux, and ~0.6 s of import time the first time a cover is hashed. cover_hash.phash_int reproduces imagehash 4.3.2's phash bit for bit: LANCZOS shrink to 32x32 grayscale, scipy.fftpack-style unnormalized DCT-II on both axes, low 8x8 against their median. The DCT is a radix-2 FFT with Makhoul's reordering, not a direct cosine sum: the butterflies cancel equal inputs exactly the way scipy's FFT does, so blank, flat and mirrored pages land on the same side of the median as imagehash. Metron's cover hashes come from imagehash, so the two must agree. - compute_phash still returns 16 lowercase hex digits. parse_hash returns an int and raises ValueError unless given exactly 16 hex digits, so a malformed candidate hash still trips the matcher's except around cover_score instead of scoring. hamming_distance is (a ^ b).bit_count(). - imagehash moves to the test group. test_cover_hash_parity compares against it live on every real test image (CBZ/PDF first pages and the loose Captain Science pages), their JPEG-q25, 1/5-scale, 5%-crop and mirrored variants, and 59 synthetics (solids, gradients, checkerboard, split, noise, 50 seeded block images): 106 cases. An imagehash release that changes phash output fails CI instead of drifting. - cover_hash_golden.json pins today's hashes of the real test images without needing imagehash. - doctor: the imagehash row becomes "cover hash", a self-test that hashes a built-in card and checks the answer imagehash gives for it. The old 8x8 x*y*4 card was transposition-symmetric, which ties at the median; the new 16x12 card is asymmetric and keeps its hash under every smoothing filter Pillow has. A crash or wrong answer is an ERROR with a reinstall hint. A broad sweep of 2,591 hashes (every unique CBZ/CBR/CB7/CBT/PDF cover under ~/Code with the same four variants, plus every jpg/png/webp/gif) matched imagehash on all but one: a Pillow APNG test fixture whose mirror-symmetric pixels make a mathematically zero coefficient sit on an exactly-zero median, so rounding noise decides one bit. No comic cover differed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
imagehash was used for one
phash()call but pulled in numpy, scipy and PyWavelets: about 165 MB on Linux (two bundled OpenBLAS copies) and ~0.6 s of import. That stack is ~208 MB of the codex Docker image.cover_hash.phash_intreproduces imagehash 4.3.2'sphashbit for bit: LANCZOS to 32×32 grayscale, an unnormalized DCT-II on both axes, and the low 8×8 against its median.parse_hashreturns anintand raisesValueErrorunless given exactly 16 hex digits. A malformed Metron hash still trips the matcher'sexceptinstead of scoring.hamming_distanceis(a ^ b).bit_count().testgroup.test_cover_hash_parity.py(106 cases) compares live against it, so an imagehash release that changes output fails CI instead of drifting.cover_hash_golden.jsonpins today's hashes without imagehash.imagehashrow becomescover hash, a self-test against a known answer, on an asymmetric card that is stable across Pillow's resampling filters.Parity evidence
A sweep of 2,591 hashes (every unique CBZ/CBR/CB7/CBT/PDF cover under
~/Codein original, JPEG-q25, 1/5-scale, 5%-crop and mirrored forms, plus every jpg/png/webp/gif, including ~169 real covers from codex's cover cache): 2,590 identical. The one miss is 1 bit on a mirror-symmetric Pillow APNG test fixture. There, a mathematically zero coefficient sits on an exactly-zero median, so rounding decides the bit. No comic cover differed.Test plan
make fix lint ty test: 2516 passed, 1 skipped (pre-existing)uv export --no-dev --all-extras | grep -iE 'numpy|scipy|pywavelets|imagehash'prints nothing🤖 Generated with Claude Code