◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SONNET 4.5 — Retires on the Claude API on 11/30, 34 days left. If your app calls it, check the migration target●SDK 58 ISSUE — Early reports include Android widgets not refreshing on a schedule and Stripe linking statically●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SONNET 4.5 — Retires on the Claude API on 11/30, 34 days left. If your app calls it, check the migration target●SDK 58 ISSUE — Early reports include Android widgets not refreshing on a schedule and Stripe linking statically●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
Articles/App Dev
◇ App Dev/2026-07-29Advanced

The same wallpaper appeared twice in my catalog — measuring perceptual hash thresholds across 324 images

Four approaches to image catalog deduplication measured end to end: SHA-256, dHash, a hue signature, and a block signature. dHash alone scored 0.9% precision. Here is why perceptual hashes collapse on smooth images, and how the ground truth turned out to be the thing that was wrong.

image processingperceptual hashingdHashdeduplicationasset managementNode.jssharpindie development43

✦ Premium Article

I was scrolling my own wallpaper app one morning when my thumb stopped.

The same artwork appeared twice, a dozen rows apart.

One came from the original launch batch, the other from a later import. Different filenames, different upload dates, so nothing in the catalog table had ever flagged it. When you maintain a few thousand image assets as an indie developer, this kind of drift accumulates quietly.

Nobody had complained in an App Store review. It still felt wrong that a paying user would scroll past the same picture twice.

Reviewing several thousand images by hand was not an option, so I built something to find them mechanically.

The design I sketched at the start was wrong twice over. Here is what the measurements actually showed.

Building a verification set I could re-run

Experimenting directly on the production catalog would have left me with no way to check my own results. Instead I synthesized a set with the same characteristics.

160 source images at 1290×2796, the iPhone portrait resolution — 40 each of gradients, blurred blobs, stripes, and noise clouds. That mix mirrors what a wallpaper catalog tends to contain.

From those I generated five kinds of derivative:

DerivativeCountGround truth
Re-encoded at JPEG quality 92 → 7454Duplicate
Resized 1290×2796 → 828×179232Duplicate
Brightness raised 6%23Duplicate
Cropped 2% on each edge, scaled back15Duplicate
Hue rotated 80 degrees40Distinct

324 images in total. Labeling the hue variants as distinct is the pivot of the whole exercise. In a wallpaper catalog the color variant is the product, and merging it would delete inventory.

Everything below was measured on Node 22 with sharp (via libvips).

An earlier version of this article gave the sharp version as "8.18.3". That was a mix-up with libvips: sharp itself is on the 0.3x line, and the 8.x number belongs to libvips. Version numbers are what let you reproduce a result, so I did not want to leave a wrong one standing.

SHA-256 gets you almost nothing

I started with a SHA-256 over the raw file bytes. A few lines of code, and it seemed reasonable to sweep the exact matches first.

It found 3 of the 124 real duplicates. A recall rate of 2.4%.

In hindsight the result is obvious. Re-encoding a JPEG changes every byte. So does resizing, brightness adjustment, and cropping. Byte equality only catches the case where the identical file was uploaded twice — and an upload-time guard had already been handling that case for years.

What remained in the catalog was precisely the category exact hashing cannot see: visually identical, byte-wise different.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦SHA-256 byte matching found 3 of 124 real duplicates — a 2.4% recall rate. The measured breakdown shows exactly which transformations defeat exact hashing and why re-encoding alone is enough
✦dHash at a Hamming threshold of 10 scored 97.5% recall against 0.9% precision, with 16,570 false pairs. The cause: 80 of 160 source images collapsed to an identical, information-free hash. A popcount gate brings false positives to zero
✦Complete implementation of a three-band classifier (auto-merge, human review, distinct) with 96.9% precision in the auto band, zero missed duplicates, and 20.6 ms per image across 324 files
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

◇ App Dev2026-10-26
Your 5% Rollout Says 99.76% Crash-Free. How Many Users Until You Can Trust It?
At 5% rollout, a crash-free rate rests on few users and the interval is wide. A Wilson-interval script that says proceed, wait or halt, plus simulated numbers on how often you halt a good release.
◇ App Dev2026-09-10
Your First EAS Workflow in a Rork Repo, and the Alert That Goes Missing Exactly When You Need It
Putting two files into .eas/workflows in a repo exported from Rork, and why a notification wired with needs stays silent on exactly the nights it fails — with the output of a small local checker I actually ran.
◇ App Dev2026-08-30
After Bumping targetSdk to 36, Which Part of Your Dependencies Should You Actually Read
I opened six real dependency AARs and counted what they contribute to my manifest. Not one declares targetSdkVersion, and twelve permissions arrive that I never wrote. Here is what to check before you submit.