001 Notes
Perceptual Hashing And Duplicate Review
Are these concepts related?
Does this image still look roughly the same after compression, resize, or small edits?
1. Exact Hash vs Perceptual Hash
same bytes -> same hash
one byte changes -> completely different hash
photo.jpg
photo-copy.jpg
same photo exported at JPEG quality 95
same photo exported at JPEG quality 85
same photo resized from 4000px to 2000px
same screenshot saved as PNG and JPEG
same-looking image -> nearby hash
different-looking image -> far hash
| Tool | Finds | Misses |
|---|---|---|
| cryptographic file hash | exact copies | resized or recompressed copies |
| pHash, dHash, wHash | near visual copies | semantic-only similarity |
| embeddings | visually or conceptually related images | proof of duplicate identity |
| ORB/local features | geometric near-duplicates | textureless images and heavy edits |
Exact hash can prove exact duplication.
Perceptual hash can propose duplicate candidates.
Embedding similarity can rerank candidates.
ORB can verify some geometric matches.
The user decides destructive action.
2. Bit Fingerprint Mental Model
image A hash: 10110010 01011001
image B hash: 10110010 01011101
A: 10110010 01011001
B: 10110010 01011101
^
Hamming distance = 1
distance = bit_count(hash_a XOR hash_b)
hash_a: 10110010
hash_b: 10110110
XOR: 00000100
3. Why Hashes Survive Small Edits
brightness structure
large shapes
local gradients
low-frequency layout
scale-level structure
small compression noise
minor pixel shifts
some texture detail
some color detail
| More detail kept | Less detail kept |
|---|---|
| fewer false positives | more robust to small edits |
| fragile to recompression | more collisions |
| closer to exact matching | closer to approximate matching |
4. dHash
1. resize image to 9 x 8
2. convert to grayscale
3. for each row, compare pixel[x] with pixel[x + 1]
4. write 1 if left pixel is brighter, else 0
5. output 8 x 8 = 64 bits
pixels: [10, 14, 13, 20, 19]
compare: 10 > 14 false -> 0
14 > 13 true -> 1
13 > 20 false -> 0
20 > 19 true -> 1
bits: 0101
0 1 0 1 1 0 0 1
1 1 0 0 0 1 1 0
...
small resizes
minor compression
similar contrast structure
simple near-duplicates
rotation
heavy crop
large local edits
watermarks
layout-preserving templates with different meaning
5. pHash
low frequency:
broad areas, large shapes, smooth gradients
high frequency:
edges, texture, noise, fine detail
1. resize image
2. convert to grayscale
3. compute frequency coefficients
4. keep low-frequency coefficients
5. compare coefficients to median or average
6. emit bits
pixels
-> broad frequency structure
-> low-frequency block
-> 64-bit fingerprint
recompressed images
resized images
same image with minor noise
same broad composition
heavy crop
large overlay text
rotation
simple images with similar layout
template collisions
6. wHash
image
-> coarse structure
-> medium structure
-> fine structure
-> compact low-scale signature
| Hash | Signal |
|---|---|
| dHash | local brightness direction |
| pHash | low-frequency composition |
| wHash | multi-scale coarse structure |
matched by pHash distance 4
matched by dHash distance 6
matched by wHash distance 5
7. Hamming Distance Thresholds
distance 0:
identical hash
distance 1-4:
very close
distance 5-8:
common near-duplicate range
distance 9-16:
broader candidate search, more false positives
distance > 16:
often too loose unless used with other filters
hash algorithm
image type
dataset size
review tolerance
whether a reranker follows
Candidate A:
pHash distance: 2
dHash distance: 4
wHash distance: 3
Candidate B:
pHash distance: 12
dHash distance: 15
wHash distance: 11
8. Index Design
| Field | Purpose |
|---|---|
| image path or image id | maps hash to file |
| mtime_ns | detects changed file |
| file_size | detects changed file |
| pHash hex | readable hash |
| dHash hex | readable hash |
| wHash hex | readable hash |
| pHash u64 | fast XOR and bit count |
| dHash u64 | fast XOR and bit count |
| wHash u64 | fast XOR and bit count |
flowchart TD
A[Scan image paths] --> B{Path in index?}
B -- no --> C[Compute pHash/dHash/wHash]
B -- yes --> D{mtime or size changed?}
D -- yes --> C
D -- no --> E[Reuse stored hashes]
C --> F[(Hash index)]
E --> F
F --> G[Prune removed paths]
simple deployment
transactional updates
easy pruning
easy inspection
works without a server
9. Querying The Index
query_hashes = compute_hashes(query_image)
for candidate in hash_index:
phash_distance = bit_count(query.phash XOR candidate.phash)
dhash_distance = bit_count(query.dhash XOR candidate.dhash)
whash_distance = bit_count(query.whash XOR candidate.whash)
best_distance = min(phash_distance, dhash_distance, whash_distance)
if best_distance <= threshold:
emit candidate
| Candidate | pHash dist | dHash dist | wHash dist | Pass threshold 8? |
|---|---|---|---|---|
| A | 2 | 4 | 3 | yes |
| B | 7 | 12 | 9 | yes, pHash |
| C | 18 | 21 | 17 | no |
| D | 5 | 6 | 5 | yes |
lowest best distance
more matching hash families
exact file hash matches first
same dimensions when useful
larger source-quality files when choosing representatives
only current folder
only selected tags
only visible filtered results
only images from the same scan session
10. Hash Prefilter Plus Rerank
flowchart LR
A[All images] --> B[Perceptual hash threshold]
B --> C[Candidate set]
C --> D[Embedding rerank]
D --> E[Optional ORB verification]
E --> F[Review group]
Among the hash candidates, which images are closest in visual or semantic vector space?
hash -> generate candidates
embedding -> rank candidates
ORB -> verify geometry where useful
human review -> approve action
11. ORB Verification
1. detect keypoints in image A
2. detect keypoints in image B
3. compute descriptors around keypoints
4. match descriptors between images
5. filter weak matches
6. optionally estimate geometry
corners
distinct texture patches
small repeatable structures
same image is cropped
same image is resized
same object appears with mild transformation
same scene has local feature overlap
image is textureless
screenshot has flat regions
patterns repeat many times
crop removes most landmarks
image is heavily edited
12. Duplicate Groups Are Review Objects
representative image
candidate images
exact hash status
perceptual hash distances
matching hash family
embedding similarity if computed
ORB match score if computed
file size
dimensions
modified time
paths
preview thumbnails
keep largest
keep newest
keep selected
open containing folder
move candidates to review folder
mark as not duplicate
hide group
rescan changed files
13. Failure Modes
| Case | Problem |
|---|---|
| crop | global structure changes |
| rotation | many hashes are orientation-sensitive |
| watermark or text overlay | structure changes |
| screenshots | flat regions collide |
| memes/templates | same layout, different meaning |
| burst photos | visually close but not duplicates |
| resized thumbnails | detail loss can create false matches |
| generated images | similar composition can collide |
near duplicate candidate
possible duplicate
matched by pHash distance 5
needs review
duplicate
safe to delete
same image
14. Performance And UI Runtime
compute hashes in worker jobs
store hashes persistently
reuse unchanged rows
use u64 XOR and bit_count
batch database writes
filter candidate paths before full scans
show progress
support cancellation
surface index size and cleanup controls
bucket by hash prefix
use multiple hash tables
restrict search to active folder/filter
parallelize candidate scanning
use native popcount/vectorized operations
add locality-sensitive hashing only after profiling
15. What Was Used, Removed, Or Deferred
pHash
dHash
wHash
SQLite-backed hash index
mtime and size invalidation
u64 XOR plus bit_count distance
duplicate candidate grouping
embedding rerank
optional ORB verification
review-first UI policy
treating perceptual hashes as semantic labels
hiding hash distance
automatic deletion from near-duplicate evidence
recomputing hashes unnecessarily
blocking the UI thread
presenting candidate matches as certain duplicates
GPU hash kernels
large-scale locality-sensitive hashing
rotation-invariant duplicate proof
fully automatic duplicate cleanup
destructive cleanup based only on embeddings
Comments