Perceptual Hashing And Duplicate Review
On this page27
A from-scratch visual tutorial on exact hashes, dHash, pHash, wHash, thresholds, indexing, embedding reranking, ORB, and safe duplicate review.
Article details
- Status
- Building Publicly
- Subcategory
- ClusterLens
- Last reviewed
- 2 Sept 2026
- Prerequisites
- No image-processing or binary-math knowledge required
Is this probably another version of the same picture?
An exact copy agrees under every definition. The other cases pull those definitions apart.
1. A file is not the picture you see
FF D8 FF E0 00 10 4A 46 49 46 ...
| Position | Visible colour | RGB values |
|---|---|---|
| top-left | red | [255, 0, 0] |
| top-right | green | [0, 255, 0] |
| bottom-left | blue | [0, 0, 255] |
| bottom-right | white | [255, 255, 255] |
Decoded image shape
A 1200×800 image contains 2,880,000 channel values before hashing begins.
What does a single pixel become in grayscale?
RGB to luminance
Green contributes most to this brightness estimate; blue contributes least.

The picture is still visually rich. No perceptual fingerprint exists yet.
Move through the actual preprocessing intermediates. Every step makes later comparisons cheaper by making some earlier questions impossible to answer.
FF D8 …H×W×3H×W9×8 / 32×3264 bitsThe file has not been decoded yet. Exact hashes operate here; perceptual hashes continue through the pipeline.
2. Exact hashes and perceptual hashes want opposite things
original.jpg → 7c41…e9a2
one byte changed → b903…1f77
The image edit is visually mild, but the measured SHA-256 digest changes across roughly half its bit positions.


The distance shown here was computed from the source files, not estimated from the preview.
| Tool | Its meaning of “same” | What it cannot prove |
|---|---|---|
| SHA-256 | identical file bytes | visual equivalence after re-encoding |
| perceptual hash | similar coarse appearance | byte identity or semantic identity |
| embedding | nearby learned visual concepts | duplicate identity |
| ORB | matching local landmarks | globally identical image |
A complete four-file example
| Pair | Exact hash | Perceptual hash | Embedding | Appropriate conclusion |
|---|---|---|---|---|
| A–B | equal | close or equal | close | exact file duplicate |
| A–C | different | usually close | close | near-duplicate candidate |
| A–D | different | depends on composition | often close | related, not proven duplicate |
3. From an 8×8 bit grid to Hamming distance
Fingerprint width
Hexadecimal is only compact printing. It does not add information.
Toggle any candidate bit:
XOR identifies the positions; Python’s bit_count() counts them.
Hamming distance
For 64-bit hashes, the distance ranges from 0 to 64.
Complete hexadecimal dry run
A = B4 hexadecimal = 1011 0100 binary
B = A6 hexadecimal = 1010 0110 binary
1011 0100
XOR 1010 0110
---------
0001 0010
4. Information is discarded before the first bit appears
Decoding exposes pixels but already forgets how those pixels were encoded in the original file.
5. dHash remembers brightness direction
Exact ImageHash comparison
It stores local brightness direction, not the brightness values.
pixels: 10 14 13 20 19
right > left? 1 0 1 0
bits: 1 0 1 0
Start with nine brightness measurements. No comparison has been made yet.

Each output bit belongs to the gap between two adjacent cells, not to either pixel by itself.
What dHash forgot
6. pHash describes broad spatial frequencies
Low frequencies create broad bands. Increase frequency to create fine detail.
2D DCT intuition
I(x,y) is brightness. u,v choose a pattern. A large coefficient says that pattern explains much of the image.

It still contains colour, fine texture, and its original spatial resolution.
Step through the exact intermediate evidence rather than treating “apply DCT” as a black box.
Tiny pHash thresholding dry run
coefficients: 80, 12, -4, 8
sorted: -4, 8, 12, 80
median: 10
80 > 10 → 1 12 > 10 → 1
-4 > 10 → 0 8 > 10 → 0
fingerprint: 1100
7. wHash decomposes structure at multiple scales
| Region | Intuition |
|---|---|
| LL | broad approximation |
| LH | one orientation of detail |
| HL | the other orientation |
| HH | diagonal/fine detail |
approximation
detail
detail
detail
Level one separates broad structure from three orientations of detail.
One Haar split by hand
averages: [(10+14)/2, (20+24)/2] = [12, 22]
differences: [(10-14)/2, (20-24)/2] = [-2, -2]
coarsest average: (12+22)/2 = 17
coarse contrast: (12-22)/2 = -5

All three methods process the same edited source and still produce different distances because they ask different visual questions.
| Hash | What it remembers | Typical weakness |
|---|---|---|
| dHash | horizontal brightness direction | flip, crop, rotation |
| pHash | low-frequency composition | crop and layout collision |
| wHash | multi-scale wavelet structure | coarse-structure collision |
8. Hash size changes capacity and thresholds
Distances grow with bit width, so an absolute threshold cannot be copied unchanged.
Compare different widths
Distance 8 is 12.5% disagreement for 64 bits but only 3.125% for 256 bits.
| Grid | Bits | Densely packed bytes | Printed hex characters |
|---|---|---|---|
| 4×4 | 16 | 2 | 4 |
| 8×8 | 64 | 8 | 16 |
| 16×16 | 256 | 32 | 64 |
64-bit hash: 8 / 64 = 12.50% disagreement
256-bit hash: 8 / 256 = 3.125% disagreement
9. A Hamming threshold is policy, not truth
Candidate rule
The hash computes the distance. The product owner chooses τ and therefore chooses the trade-off.
Raise the threshold and watch recovered edits and false alarms move together.
A confusion matrix is just many pair-level decisions counted together.
Threshold quality
Cleanup may prioritize precision; exploratory review may accept more false alarms for recall.
Reading a confusion matrix
| Name | Gate said | Label said | Plain-language meaning |
|---|---|---|---|
| true positive | duplicate candidate | same source | useful item entered the queue |
| false positive | duplicate candidate | different source | reviewer receives a false alarm |
| false negative | not a candidate | same source | a wanted duplicate was missed |
| true negative | not a candidate | different source | unrelated pair stayed out |
precision = 70 / (70 + 10) = 0.875
recall = 70 / (70 + 20) = 0.778
10. The SQLite index is a cache with an invalidation rule
mtime=100 · 2.1 MBThe first scan has no cached row, so all fingerprints are calculated.
Why keep hex and u64 forms?
hex: b4
integer: 180
bits: 10110100
Selected-backend scan
The operation is cheap; a million Python iterations are still not free. Filtering the folder reduces N.
The estimate scales the measured loop to teach growth. It is not a second benchmark and should not be used as a service-level guarantee.
11. The exact query uses one backend
query_value = int(query_hash, 16)
candidate_value = int.from_bytes(candidate_blob, "big")
distance = (query_value ^ candidate_value).bit_count()
if distance <= max_distance:
keep(candidate)
Changing the combination rule changes the candidate set without changing a single hash bit.
Step through a full XOR/threshold scan before reranking.
12. Candidate generation is not ranking
The gate restricts membership; embedding scores change order.
At the gate, only the selected pHash distance and threshold determine membership.
13. ORB compares local landmarks

Corners and textured junctions are easier to find again than smooth regions.
Follow one local observation from detection to matching.

OpenCV produced these values with the same settings and formula as ClusterLens.
Coverage
Descriptor quality
ClusterLens heuristic
These weights are a heuristic, not calibrated probabilities.
Complete ORB score dry run
match ratio = 60 / max(100, 80) = 0.60
distance score = 1 - 24/96 = 0.75
ORB score
= 0.6(0.60) + 0.4(0.75)
= 0.36 + 0.30
= 0.66
14. Score fusion can reverse a ranking
Current final score
A can overtake B when local structure compensates for its weaker embedding.
The production heuristic corresponds to α=0.80. Moving this slider teaches sensitivity; it does not alter ClusterLens.
15. Review evidence is not a deletion command
{
"keeper_path": "query.jpg",
"duplicate_path": "candidate.jpg",
"action": "trash",
"hash_distance": 0,
"source_files_changed": false
}


No decision recorded. Source files remain untouched.
same SHA-256 / bytes → byte-identical file evidence
pHash distance 0 → identical 64-bit pHash fingerprint
small pHash distance → candidate under the chosen policy
human review → product decision with context
16. Failure modes follow the discarded information
| Case | Why a global hash can fail | Useful additional evidence |
|---|---|---|
| crop | content moves between global cells | ORB/local features |
| rotation or flip | layout changes | rotation-aware matching |
| watermark | new contrast structure | masking and review |
| colour-only edit | these hashes discard hue | colour hash |
| template/screenshot | many files share coarse layout | OCR/content model |
| burst photos | close composition, distinct moment | time and sharpness |
| tiny thumbnail | detail disappears | dimensions and source quality |
The failure is predictable from what this fingerprint preserves and discards.
Diagnose the cause, not just the number
17. Complete dry run
Begin with file identities. There are no hashes, scores, candidates, or decisions yet.
SHA-256 → proves identical bytes
perceptual hash → proposes visually similar candidates
embedding → ranks learned visual similarity
ORB → adds local correspondence
metadata → helps choose a keeper
human review → authorizes an action
Every representation forgets information. A safe duplicate tool makes that loss visible, combines independent evidence carefully, and never turns an approximate candidate into an irreversible decision without authority.
Primary references