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.
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
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.
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) }
A distance threshold of 0.3 gave the best balance between "same moment" and "similar but different". Union-find turns the pairs into groups.
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.
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/
Comments
No comments yet. Start the discussion.