DEV Community

ZoomApps
ZoomApps

Posted on

Finding duplicate photos on iPhone without uploading a single one

Most photo cleaners send your library somewhere or lock the useful part behind a weekly subscription. We wanted the opposite for our app, Fast Cleaner: everything runs on the iPhone, and the free tier covers a normal daily cleanup. Here is how the duplicate finder works and what we learned shipping it to 50 languages.

1. One embedding per photo with Apple Vision

VNGenerateImageFeaturePrintRequest returns a feature vector for an image. Two photos of the same scene end up close to each other, even with small changes in framing or exposure.

let request = VNGenerateImageFeaturePrintRequest()
let handler = VNImageRequestHandler(cgImage: thumb, options: [:])
try handler.perform([request])
let print = request.results?.first as? VNFeaturePrintObservation
Enter fullscreen mode Exit fullscreen mode

We request a 160Ɨ160 thumbnail from PhotoKit instead of the full image. Vision resamples to its own input size anyway, and the smaller request loads about 30% faster. Embeddings are cached on the device, so the second scan only processes new photos.

2. Compare only what can be similar

Comparing every photo with every other one is O(n²). On a 50,000-photo library that is far too slow. We bucket photos by the month they were taken and compare pairs inside a bucket. Duplicates and burst-like series are almost always taken close in time.

var d: Float = 0
try a.computeDistance(&d, to: b)
if d <= 0.3 { unionFind.union(i, j) }
Enter fullscreen mode Exit fullscreen mode

A distance threshold of 0.3 gave the best balance between "same moment" and "similar but different". Union-find turns the pairs into groups.

3. Pick the best shot, offer the rest

Inside each group a simple weighted score picks the photo to keep:

  • face capture quality (VNDetectFaceCaptureQualityRequest): 0.35
  • sharpness, Laplacian variance on luminance: 0.30
  • resolution, log-scaled: 0.15
  • open eyes and smile from face landmarks: 0.15
  • newer date as a tiebreak: 0.05

Photos without faces get a neutral face score, so landscapes are not penalised.

4. Things that bit us

  • Memory. Batches of 50 images inside autoreleasepool, otherwise large libraries run out of memory.
  • The simulator lies. In the iOS simulator Vision returned the same vector for every image, so all distances were 0 and every photo landed in one group. We now detect "collapsed" embeddings (different sizes, distance exactly 0) and skip such buckets.
  • Main thread. Reading the photo library during view rendering caused watchdog kills on big libraries. All PhotoKit reads moved off the main thread.
  • Deletion safety. Deletions go through PhotoKit, so everything lands in Recently Deleted for 30 days.

Pricing without a weekly plan

Scanning, the report, swipe-to-clean and up to 300 MB of cleanup a day are free. Pro removes the limit and adds video compression: $1.99/month (first month $0.99), $9.99/year or $19.99 once.

Fast Cleaner is on the App Store: https://apps.apple.com/app/id6772155675 — more at https://zoomapps.kz/fastcleaner/

Top comments (0)