001Notes

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
27 sections

Perceptual hashing answers a narrower question than semantic search:

Is this probably another version of the same picture?

That sounds simple until five files sit beside one another: the original photograph, a byte-for-byte copy, a lower-quality JPEG export, a crop, and a different photograph of the same car. A person may casually call the first four “the same image.” A computer needs a measurable definition.

same bytes?yes
same global appearance?yes
same local structure?yes
same subject?yes

An exact copy agrees under every definition. The other cases pull those definitions apart.

Exact hashing, perceptual hashing, embeddings, and ORB each define same differently. This tutorial builds those definitions from pixels upward, calculates them by hand, and checks them against controlled images and the actual ClusterLens code.

ClusterLens’s production PyQt release remains clustering-only. The duplicate-search subsystem described here is experimental repository code, which makes it a useful object to inspect rather than a finished feature to advertise.


1. A file is not the picture you see

When you open car.jpg, you see a car. The computer first sees bytes:

FF D8 FF E0 00 10 4A 46 49 46 ...

A JPEG decoder turns those compressed bytes into a grid of pixels. An RGB pixel contains red, green, and blue channel values. A two-by-two image might decode as:

PositionVisible colourRGB values
top-leftred[255, 0, 0]
top-rightgreen[0, 255, 0]
bottom-leftblue[0, 0, 255]
bottom-rightwhite[255, 255, 255]

Decoded image shape

height×width×3 colour channels

A 1200×800 image contains 2,880,000 channel values before hashing begins.

What does a single pixel become in grayscale?

Grayscale is not “delete two numbers and keep one.” A decoder combines red, green, and blue according to how strongly they contribute to perceived brightness. A useful approximation is:

RGB to luminance

L0.299R + 0.587G + 0.114B

Green contributes most to this brightness estimate; blue contributes least.

For the red pixel [255,0,0], the luminance is approximately 0.299×255 = 76. For green [0,255,0], it is approximately 150. Both were fully saturated colours, yet green becomes much brighter in grayscale. Once RGB becomes one number, the original hue cannot be reconstructed.

Controlled car fixture as decoded RGB
Three values per pixel preserve colour and brightness.
1250 × 1250 × 3

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.

Two files can contain different bytes and decode to almost the same pixels. JPEG quality 85 and quality 95 choose different compression approximations. Resizing changes the pixel count. PNG and JPEG use different formats. An exact hash reads bytes, so every change matters. A perceptual hash decodes the image and intentionally forgets detail.

1file bytesFF D8 …
2RGB pixelsH×W×3
3grayscaleH×W
4small structure9×8 / 32×32
5fingerprint64 bits

The 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

SHA-256 maps any file to 256 bits. One of its intended properties is the avalanche effect: a tiny input change should create a large, unpredictable output change.

original.jpg      → 7c41…e9a2
one byte changed  → b903…1f77

That is excellent for asking “Are these files byte-for-byte identical?” It is intentionally bad for asking “Do these pictures still look alike?”

The experiment below uses one visible source and deterministic edits. Re-exporting changes SHA-256, while the perceptual fingerprints can remain close.

“Avalanche” does not mean that the visible picture changed dramatically. It describes how a cryptographic digest responds to input bytes. The digest should not reveal that two inputs were nearly the same; such a relationship would weaken its security role.

original SHA-256
JPEG-export SHA-256
XOR
changed digest bits
total digest bits256
digest disagreement

The image edit is visually mild, but the measured SHA-256 digest changes across roughly half its bit positions.

OriginalControlled original image
Hamming distance/ 64
Derived editControlled transformed image
same SHA-256?no
CLIP cosine
case

The distance shown here was computed from the source files, not estimated from the preview.

ToolIts meaning of “same”What it cannot prove
SHA-256identical file bytesvisual equivalence after re-encoding
perceptual hashsimilar coarse appearancebyte identity or semantic identity
embeddingnearby learned visual conceptsduplicate identity
ORBmatching local landmarksglobally identical image

An exact hash can prove byte identity. A perceptual hash can only propose a candidate.

A complete four-file example

Assume a folder contains an original A.jpg, an ordinary file copy B.jpg, a lower-quality JPEG export C.jpg, and a different photograph D.jpg of the same red car.

SHA-256 groups only A and B because their bytes match. C decodes to almost the same scene, but its encoder chose different bytes. D contains the same subject category but a different moment, camera position, and pixel grid.

PairExact hashPerceptual hashEmbeddingAppropriate conclusion
A–Bequalclose or equalcloseexact file duplicate
A–Cdifferentusually closeclosenear-duplicate candidate
A–Ddifferentdepends on compositionoften closerelated, not proven duplicate

A single score cannot express all three relationships. The final system is therefore a pipeline rather than one universal hash.


3. From an 8×8 bit grid to Hamming distance

ClusterLens asks ImageHash for hash_size=8. The result contains 8×8 yes/no decisions.

Fingerprint width

8×8=64 bits÷4 bits per hex digit=16 hex digits

Hexadecimal is only compact printing. It does not add information.

To compare fingerprints, XOR each bit pair. XOR is 1 exactly where the bits differ. Then count the ones.

Toggle any candidate bit:

A
B
XOR
differing bits
normalized distance
agreement

XOR identifies the positions; Python’s bit_count() counts them.

Hamming distance

dH(a,b)=Σi=1n(ai⊕bi)=popcount(a XOR b)

For 64-bit hashes, the distance ranges from 0 to 64.

Distance zero means equal fingerprints, not necessarily equal pixels or bytes. Billions of possible images are being squeezed into only (2^{64}) fingerprint values. Collisions are unavoidable.

Complete hexadecimal dry run

Use one-byte fingerprints so every bit fits on screen:

A = B4 hexadecimal = 1011 0100 binary
B = A6 hexadecimal = 1010 0110 binary

                       1011 0100
XOR                    1010 0110
                       ---------
                       0001 0010

The XOR contains two 1 bits, so the Hamming distance is 2. ClusterLens performs the same operation on all 64 bits at once: parse the hex string as an integer, XOR it with the stored integer, and call bit_count().

Ordinary subtraction would describe the wrong geometry. 0000 and 8000 are numerically far apart but differ in one bit. 7fff and 8000 are numerically adjacent but disagree in every bit. Hamming distance measures fingerprint decisions.


4. Information is discarded before the first bit appears

pHash, dHash, and wHash differ, but the implementations used by ClusterLens share two important choices: convert to grayscale and aggressively shrink or decompose the image.

Grayscale replaces three colour channels with luminance. Two differently coloured shapes with similar brightness can become indistinguishable. Shrinking removes small text, texture, and compression noise. That creates robustness and false matches at the same time.

Imagine describing a poster as “dark top, bright face in the centre, title near the bottom.” The description survives a JPEG export, but another poster may fit it too. Perceptual hashing succeeds and fails for the same reason: it remembers a small description.

This is an information bottleneck. A full image contains millions of channel values; the final fingerprint contains 64 decisions. The algorithm cannot compress that much without merging many visually distinct inputs. The engineering question is whether it tends to merge differences the product wants to ignore.

available informationmillions of RGB values
representation can answercolour, texture, layout, tiny detail
representation cannot recovercompressed file bytes

Decoding exposes pixels but already forgets how those pixels were encoded in the original file.

Two failure directions now have names:

  • too sensitive: an edit the user considers harmless changes too many bits;
  • not sensitive enough: two meaningfully different images collapse to nearby bits.

The transformation experiment measures the first. Negative image pairs and threshold false positives measure the second. Both must be tested because improving one can worsen the other.


5. dHash remembers brightness direction

dHash means difference hash. For an 8×8 fingerprint, ImageHash converts to grayscale, resizes to 9 columns by 8 rows, and compares each pixel with the pixel immediately to its right. Nine columns create eight horizontal gaps; eight rows times eight gaps gives 64 bits.

Exact ImageHash comparison

br,c=1ifI(r,c+1)>I(r,c)else0

It stores local brightness direction, not the brightness values.

For one row:

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.

The toy row teaches the operation. The next artifact performs it on the actual car fixture. Select a row to inspect its nine measured brightness values and eight resulting decisions.

Actual car fixture reduced to the nine by eight dHash brightness matrix
Nearest-neighbour enlargement of the actual 9×8 matrix.

Each output bit belongs to the gap between two adjacent cells, not to either pixel by itself.

The previous article described the opposite comparison direction. Either convention can form a consistent fingerprint, but a tutorial must match its implementation. ImageHash uses pixels[:, 1:] > pixels[:, :-1]: right brighter than left.

Because [10,14] and [100,140] create the same bit, moderate brightness changes often survive. A flip reverses gradients, while a crop relocates structure. dHash is fast and transparent; it is not crop- or rotation-invariant.

What dHash forgot

From [10,14,13,20,19], dHash stores only 1010. It cannot reconstruct whether the first rise was four brightness units or forty. It cannot recover colour or know whether an edge belonged to a bonnet, invoice, or face.

This explains two behaviours at once. JPEG noise may change values without changing comparison directions; unrelated layouts can accidentally share directions after both shrink to 9×8. Robustness and collision risk are consequences of the same information loss.


6. pHash describes broad spatial frequencies

Frequency means how rapidly brightness changes across space. A smooth sky changes slowly; alternating black and white stripes change quickly. A photograph is a weighted mixture of many such patterns.

Low frequencies create broad bands. Increase frequency to create fine detail.

2D DCT intuition

C(u,v)=α(u)α(v)ΣxΣyI(x,y)cosu(x)cosv(y)

I(x,y) is brightness. u,v choose a pattern. A large coefficient says that pattern explains much of the image.

The real ImageHash pHash path is:

  1. resize to 32×32 because 8 × highfreq_factor 4 = 32;
  2. convert to grayscale;
  3. apply a DCT across rows and columns;
  4. keep the top-left 8×8 low-frequency block;
  5. calculate its median;
  6. write 1 for coefficients above the median.
Car fixture at the selected pHash stage
1 · decoded source
Start with the decoded photograph

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.

JPEG noise often changes fine high-frequency detail more than broad composition, so pHash commonly survives recompression. Cropping and flipping rearrange composition and can change many bits. The transformation lab lets us test that statement instead of accepting it as folklore.

Tiny pHash thresholding dry run

The real block is 8×8. Use four teaching coefficients:

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

If recompression changes them to [79,11,-3,7], their median relationships may remain 1100. If a crop rearranges frequency energy, several decisions move. The DCT does not recognize cars; it compresses spatial energy.

ImageHash’s phash retains the top-left block, including its DC or broad brightness coefficient, and thresholds by the block median. Other published pHash variants may exclude DC or use an average. They are related algorithms, but not necessarily the function ClusterLens calls.

The heatmap is scaled for human viewing: a bright square means a large log-magnitude coefficient relative to this block. That display scaling is not part of the hash. The bit decision still uses the signed numerical coefficient and the recorded median. Separating visualization from computation prevents a common tutorial mistake—quietly changing the data so that it looks clearer.


7. wHash decomposes structure at multiple scales

A wavelet transform splits an image into one coarse approximation and several detail regions:

RegionIntuition
LLbroad approximation
LHone orientation of detail
HLthe other orientation
HHdiagonal/fine detail

Then LL can be decomposed again, like stepping closer to the picture.

LL
approximation
LH
detail
HL
detail
HH
detail

Level one separates broad structure from three orientations of detail.

The labels describe the filters, not four smaller crops. LL combines low-frequency behaviour horizontally and vertically. LH, HL, and HH retain different changes between neighbouring regions. Recursing into LL asks the overview question again at a coarser scale.

ImageHash’s default wHash uses Haar wavelets, removes the maximum-level LL component, takes a lower-scale coefficient block, compares values with their median, and emits bits.

One Haar split by hand

Start with [10,14,20,24]. Pairwise averages describe coarse content and differences describe detail:

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

A two-dimensional Haar transform performs analogous horizontal and vertical operations, producing LL/LH/HL/HH. pHash asks which cosine patterns explain the image; wHash asks how averages and detail behave across scales.

dHashbrightness directions
pHashDCT composition
wHashwavelet structure
Actual low wavelet coefficient block for the controlled car fixture
The displayed block is a visualization of the coefficients later thresholded into wHash bits.

All three methods process the same edited source and still produce different distances because they ask different visual questions.

HashWhat it remembersTypical weakness
dHashhorizontal brightness directionflip, crop, rotation
pHashlow-frequency compositioncrop and layout collision
wHashmulti-scale wavelet structurecoarse-structure collision

No hash wins everywhere. The correct comparison is a measured transformation matrix on images resembling the intended collection.


8. Hash size changes capacity and thresholds

An 8×8 hash contains 64 bits. A 16×16 hash contains 256. That may sound like a storage setting, but it changes the measuring instrument itself.

Imagine describing a painting with a grid of sticky notes. A 4×4 grid gives sixteen broad observations: “dark here, bright there.” A 16×16 grid gives 256 smaller observations. The finer grid can distinguish details the coarse grid merges together, but it can also notice harmless changes the coarse grid ignores. More bits therefore mean more capacity, not automatically more correctness.

width64 bits
hex characters16
same-source mean
different-image mean
same source
different image

Distances grow with bit width, so an absolute threshold cannot be copied unchanged.

Compare different widths

δH=dHnumber of bits

Distance 8 is 12.5% disagreement for 64 bits but only 3.125% for 256 bits.

More bits do not repair invariances the algorithm never had. They only increase the capacity of that representation.

This distinction matters. If a horizontal flip reverses the brightness directions used by dHash, asking dHash for 256 bits does not make it flip-invariant. It records the wrong kind of evidence at greater resolution. In the same way, a higher-resolution thermometer still cannot measure air pressure.

GridBitsDensely packed bytesPrinted hex characters
4×41624
8×864816
16×162563264

One million packed 64-bit fingerprints need about 8 MB for the fingerprint payload; 256-bit fingerprints need about 32 MB. Real SQLite rows also contain paths, indexes, pages, and invalidation metadata. Changing hash width changes the representation, threshold scale, persisted format, and compatibility contract.

The normalized distance is useful when comparing widths. A raw distance of eight means eight disagreeing decisions in both cases, but its importance is different:

64-bit hash:   8 / 64  = 12.50% disagreement
256-bit hash:  8 / 256 =  3.125% disagreement

That does not prove that equal percentages have equal meaning across algorithms. It prevents the most obvious unit error. Thresholds still have to be measured on the intended hash family and the intended image transformations.


9. A Hamming threshold is policy, not truth

A candidate passes when its Hamming distance is at most a threshold, conventionally written as the Greek letter tau, τ.

Candidate rule

keep candidatewhendH(query, candidate) ≤ τ

The hash computes the distance. The product owner chooses τ and therefore chooses the trade-off.

Think of an airport security gate. The scanner produces measurements; a policy decides which measurements trigger inspection. Calling a candidate “duplicate” at distance eight is not a new fact discovered by mathematics. It is the consequence of choosing a gate at eight.

The controlled experiment labels every derived edit of the same source as positive and pairs of different base images as negative. This deliberately counts a crop or flip as the same origin even when a global hash cannot recognize it.

true positives
false positives
false negatives
true negatives
precision
recall
review candidates

Raise the threshold and watch recovered edits and false alarms move together.

Before reading the totals, inspect a single labelled pair. “Actual” comes from the experiment label; “decision” comes from the selected hash and threshold.

First measured image
Second measured image
actual relationship
measured distance
gate decision
confusion cell

A confusion matrix is just many pair-level decisions counted together.

Threshold quality

precision=TPTP+FPrecall=TPTP+FN

Cleanup may prioritize precision; exploratory review may accept more false alarms for recall.

There is no universal “0–4 is definitely duplicate” range. The useful threshold changes with algorithm, width, edits, data, and downstream review cost.

Reading a confusion matrix

The four names become much easier if read as two questions:

  1. Did the gate predict positive—did it send the pair to duplicate review?
  2. Was the pair actually positive according to the experiment label?
NameGate saidLabel saidPlain-language meaning
true positiveduplicate candidatesame sourceuseful item entered the queue
false positiveduplicate candidatedifferent sourcereviewer receives a false alarm
false negativenot a candidatesame sourcea wanted duplicate was missed
true negativenot a candidatedifferent sourceunrelated pair stayed out

Suppose a run gives TP=70, FP=10, FN=20, and TN=50:

precision = 70 / (70 + 10) = 0.875
recall    = 70 / (70 + 20) = 0.778

About 87.5% of the review queue is useful, while 77.8% of known same-source edits were recovered. Lowering the threshold may improve precision while harming recall. The interactive lab uses the actual controlled ledger, but the interpretation is identical.

Labels also define the result. This experiment calls a horizontal flip the same source. A tool concerned only with accidental JPEG exports could exclude flips and crops from its positives and obtain a different best threshold. Metrics inherit the experimenter’s definition of correctness.


10. The SQLite index is a cache with an invalidation rule

ClusterLens stores image_path, three readable hex hashes, three eight-byte u64 values, mtime_ns, and file_size. The u64 form makes XOR/popcount direct. The time and size ask whether a stored fingerprint still describes the current file.

SQLite is not performing vector search here. It is a durable notebook. One row says, in effect: “When this path had this modification time and this byte size, these were its fingerprints.” On the next scan, ClusterLens can consult the note instead of reopening and hashing an unchanged image.

filesystemcar-01.webpmtime=100 · 2.1 MB
comparisonno row
SQLitecompute and insert

The first scan has no cached row, so all fingerprints are calculated.

The service enables WAL mode, batches upserts, reuses unchanged rows, and prunes missing paths. Derived data without invalidation eventually becomes misinformation.

WAL means write-ahead log. Instead of rewriting the main database page immediately for every change, SQLite records changes in a log that can later be checkpointed. For this local application it permits readers and a writer to coexist more comfortably. It does not change the meaning of a fingerprint; it changes how safely and efficiently the cache is maintained.

Why keep hex and u64 forms?

The string b4 is convenient in logs. The same bits packed as a number are convenient for scanning:

hex:       b4
integer:   180
bits:      10110100

ClusterLens writes the 64-bit value as eight big-endian bytes and reads it with the same byte order. Mixing byte order would silently compare rearranged fingerprints. mtime_ns and size are inexpensive change detectors, not cryptographic content proofs; they are a practical local-cache compromise.

The current query reads the selected u64 column and scans it in Python:

Selected-backend scan

T(N)N × one 64-bit XOR/popcount=O(N)

The operation is cheap; a million Python iterations are still not free. Filtering the folder reduces N.

O(N) means that doubling the eligible rows roughly doubles the work. Each comparison is tiny, yet millions of tiny Python operations still consume time. This measured benchmark repeats a real 64-bit XOR and bit_count() over the generated fingerprint set; it is a machine-specific observation, not a universal speed promise.

measured base rows
measured elapsed
linear estimate
passing comparisons

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

ClusterLens stores all three families but the request chooses phash, dhash, or whash:

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)

The old article incorrectly took the minimum of three distances. Combining families requires a separate policy: OR/minimum improves recall, AND/maximum improves strictness, and voting requires calibration. None is the current query path.

For distances pHash=4, dHash=15, wHash=13, and threshold 8, minimum/OR accepts because one family passes. Maximum/AND rejects because two fail. A two-of-three vote also rejects. None is mathematical truth; each defines a different product policy. ClusterLens avoids hiding that choice by selecting and recording one backend.

Try those policies directly. This is a hypothetical dry run, deliberately separate from the actual single-backend ClusterLens path.

OR / any passes
AND / all pass
vote / two pass
actual requestone selected backend

Changing the combination rule changes the candidate set without changing a single hash bit.

scanned0 / 5
kept0
best distance

Step through a full XOR/threshold scan before reranking.


12. Candidate generation is not ranking

The hash threshold is a cheap gate. Embeddings then order survivors by dot product with the normalized query embedding. If hash matches exist, ClusterLens restricts scoring to them. If none exist, the current experimental code falls back to embedding search over eligible records and labels the reason as image similarity.

The gate restricts membership; embedding scores change order.

That fallback broadens discovery but is not hash-confirmed duplicate evidence. Two different Ferraris may have similar embeddings because they express the same concept. Useful ranking is not deletion proof.

Here is the distinction in ordinary terms. A nightclub door policy decides who may enter; it does not decide the order in which people stand once inside. The hash threshold is the door. The embedding score orders the admitted candidates. ORB can later adjust that order using local correspondence.

The following ledger uses the generated car-01 query and measured transformations. Select a stage to see which columns are legally available at that moment.

At the gate, only the selected pHash distance and threshold determine membership.


13. ORB compares local landmarks

Global hashes summarize the whole frame. ORB instead detects repeatable keypoints, estimates orientation across an image pyramid, constructs binary descriptors around them, and matches descriptors between images. A crop may move the global summary while preserving the corner of a headlight.

Those terms need unpacking:

  • A keypoint is an image location with a distinctive neighbourhood, often a corner or textured junction. A blank sky is a poor keypoint because many nearby patches look the same.
  • An image pyramid contains smaller copies of the image. Detecting across scales helps the same headlight corner survive when one file is resized.
  • Orientation estimates which way the local patch points. ORB rotates its description into that orientation so modest rotation is less disruptive.
  • A descriptor is a compact binary description of brightness comparisons around one keypoint. ClusterLens receives 32 bytes, or 256 bits, per retained ORB keypoint.

The keypoint picture below is an actual OpenCV run. The circles are not objects the model recognized. They mark locally distinctive locations at different scales.

Actual ORB keypoints drawn over the controlled car fixture
Actual detected keypoints; circle size reflects detection scale.
1 · Find distinctive locations

Corners and textured junctions are easier to find again than smooth regions.

retained keypoints
descriptor matrix

Follow one local observation from detection to matching.

ClusterLens asks for at most 600 features per grayscale image. BFMatcher uses Hamming distance with crossCheck=True: a match survives only when both descriptors select one another as their best match.

Without cross-check, query descriptor Q7 might choose candidate descriptor C2 even though C2 prefers a completely different query descriptor. With cross-check, a pair survives only if Q7 → C2 and C2 → Q7. This removes some ambiguous matches, though repeated windows can still fool both directions.

Actual ORB correspondences
The preview draws the best 40 for readability; the score uses all cross-checked matches.
query descriptors
candidate descriptors
matches
average distance
ORB score

OpenCV produced these values with the same settings and formula as ClusterLens.

Coverage

match ratio=cross-checked matchesmax(query descriptors, candidate descriptors)

Descriptor quality

distance score=max(0, 1−average Hamming distance96)

ClusterLens heuristic

ORB=0.6(match ratio)+0.4(distance score)

These weights are a heuristic, not calibrated probabilities.

The code does not fit a homography or run RANSAC. Mutual nearest descriptors can still be geometrically inconsistent. ORB here is a reranking signal, not verification proof.

Complete ORB score dry run

Suppose the query has 100 descriptors, the candidate has 80, and 60 pairs survive cross-check. Their average binary distance is 24.

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

Coverage penalizes matching only a small part of the available landmarks. Average distance rewards the quality of surviving pairs. Repeated windows, tiles, or text can still create mutually nearest but spatially inconsistent matches. Homography plus RANSAC would test whether matches agree on one geometric transformation; this implementation does not.


14. Score fusion can reverse a ranking

When ORB is enabled:

Current final score

final=0.8(embedding)+0.2(ORB)

A can overtake B when local structure compensates for its weaker embedding.

The next control changes a weight that the current code does not expose. It is a calibration experiment: α=1 trusts only the embedding, while α=0 trusts only ORB.

final=α(embedding)+(1−α)(ORB)

The production heuristic corresponds to α=0.80. Moving this slider teaches sensitivity; it does not alter ClusterLens.

Embeddings can be negative while ORB is clamped to [0,1]. A production weight should be calibrated on labelled review judgements; 0.8/0.2 is not universal.

With the default controls, A receives 0.8(0.75)+0.2(0.90)=0.780. B begins with the stronger embedding but receives 0.8(0.82)+0.2(0.20)=0.696. A wins because the product definition makes local correspondence worth 20% of relevance. Keeping component scores visible makes that judgement inspectable.


15. Review evidence is not a deletion command

The review builder removes the query itself and sorts by perceptual distance and reranked score. It can export a decision:

{
  "keeper_path": "query.jpg",
  "duplicate_path": "candidate.jpg",
  "action": "trash",
  "hash_distance": 0,
  "source_files_changed": false
}

trash records an intention. The export does not move either source file.

A responsible review screen should help a person answer two separate questions: “Are these the same underlying item?” and “If so, which file should survive?” Similarity alone cannot choose the keeper. Resolution, file size, capture time, edit history, path, and user intent may matter. A smaller JPEG can be the duplicate while the larger original is the keeper; in another workflow the edited JPEG may be the valuable deliverable.

Keeper
keeper · car-01
Candidate
candidate · JPEG quality 35

No decision recorded. Source files remain untouched.

The experimental UI calls selected perceptual-hash distance zero “Exact.” That means equal selected 64-bit fingerprints, not equal file bytes. A safe label is “identical pHash” unless SHA-256 or byte comparison was also performed.

For the displayed pair, the actual pHash distance is zero while SHA-256 differs. That single counterexample is enough to disprove the claim “pHash distance zero means exact file.” The correct evidence ladder is:

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

CaseWhy a global hash can failUseful additional evidence
cropcontent moves between global cellsORB/local features
rotation or fliplayout changesrotation-aware matching
watermarknew contrast structuremasking and review
colour-only editthese hashes discard huecolour hash
template/screenshotmany files share coarse layoutOCR/content model
burst photosclose composition, distinct momenttime and sharpness
tiny thumbnaildetail disappearsdimensions and source quality

A false negative is the same source above the threshold. A false positive is a different source below it. Raising the threshold trades one for the other. Adding another signal changes available evidence; it does not make the original hash certain.

Use the measured gallery below to inspect failures rather than memorizing the table. pass at 8 counts how many of the twelve controlled same-source transformations entered the queue for the selected edit and backend. A low count means many false negatives under this experiment’s definition of “same source.”

Original controlled fixture
original
Transformed controlled fixture
representative distance
mean across 12 images
range
pass at τ=8

The failure is predictable from what this fingerprint preserves and discards.

Diagnose the cause, not just the number

For a crop, pixels are not only removed: remaining content is rescaled into new global cells. dHash adjacency, pHash frequency composition, and wHash coarse layout can all change. Local features are a sensible additional signal because some headlight corners may still exist.

For a horizontal flip, all content survives but left and right exchange positions. That is why semantic embeddings can remain very similar while a layout-sensitive hash changes sharply. For a watermark, the original layout remains but a new high-contrast structure is inserted. The appropriate remedy therefore depends on the edit; “increase the threshold” is not a universal repair.


17. Complete dry run

The final lab follows one actual query through the complete stack. Unlike the earlier policy examples, its distances and scores are loaded from the reproducible evidence artifact. At every stage it also states the strongest claim the evidence permits.

stage1 / 7
working set
claim allowed

Begin with file identities. There are no hashes, scores, candidates, or decisions yet.

The seven stages are:

  1. discover and freeze file identities;
  2. reuse or compute indexed fingerprints;
  3. choose one backend and threshold;
  4. XOR/popcount eligible candidates;
  5. rerank survivors with embeddings;
  6. optionally blend ORB evidence;
  7. show evidence and record a human decision without changing source files.

Notice how the claim becomes stronger only when new evidence arrives. Discovery proves a file was found. A pHash gate supports “candidate under pHash policy.” An embedding supports learned visual similarity. ORB supports local descriptor correspondence. None of those, individually or combined, authorizes deletion. The final human step records intent; a separate, explicitly authorized file operation would still be required to mutate the collection.

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

The main lesson is not “pHash finds duplicates.” It is:

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.

This page renders loading…, generated loading… with ImageHash loading… and OpenCV loading….

Primary references

  1. ImageHash source: pHash, dHash, and wHash implementations
  2. ImageHash documentation and hash-size behaviour
  3. NIST Secure Hash Standard (FIPS 180-4)
  4. OpenCV ORB class reference
  5. OpenCV binary feature matching tutorial
  6. Rublee et al.: ORB, an efficient alternative to SIFT or SURF
Diagram