How OpenCleaner finds “the same memory”
$ everything below runs on your device — nothing is uploaded
OpenCleaner's job is to group the photos of a time range into clusters of variants of the same memory: bursts, retries, near-duplicates taken seconds apart. The design has one strong bias — a wrong split costs you a few extra taps, but a wrong merge could suggest deleting a photo you wanted. When in doubt, the algorithm keeps photos apart.
┌────────────────────────────┐ │ photo library │ your selected time range └─────────────┬──────────────┘ │ PhotoKit fetch (metadata only) ▼ ┌────────────────────────────┐ │ stage 1 · time grouping │ consecutive photos chained │ gap ≤ 10 s │ while taken ≤ 10 s apart └─────────────┬──────────────┘ │ groups of ≥ 2 photos only ▼ ┌────────────────────────────┐ │ stage 2 · vision │ 512×512 thumbnail per photo │ feature prints │ → compact visual fingerprint │ (on-device neural net) │ (Apple Vision framework) └─────────────┬──────────────┘ │ distance ≤ 0.6 → same memory ▼ ┌────────────────────────────┐ │ clusters │ you pick what to keep, │ (2+ similar photos) │ the rest becomes pending └────────────────────────────┘
Stage 1 — time grouping
Photos are sorted by creation date and chained: a photo joins the
current group when it was taken within 10 seconds of the
previous one. The window is relative to the previous photo,
not the group's first — so a long burst stays one group. Groups with a
single photo are discarded immediately: a singleton can't be a
duplicate, and this keeps the expensive stage small.
Stage 2 — visual refinement
Time proximity alone isn't enough: a landscape and a restaurant menu shot seconds apart must not be offered as duplicates. Each time group is therefore split by visual similarity:
- Every candidate gets a feature print — a compact numeric fingerprint produced by Apple's Vision framework from a 512×512 thumbnail, on-device.
- Within a time group, each photo is compared to the reference of
every subcluster and joins the first one whose distance is
≤
0.6— otherwise it starts its own. - A photo that can't be analyzed (e.g. stuck in iCloud) is kept alone — it will never be suggested for deletion on missing evidence.
Tuning parameters
| parameter | value | meaning |
|---|---|---|
| timeWindow | 10 s | max gap between consecutive photos of one group |
| maxFeatureDistance | 0.6 | max fingerprint distance to count as the same memory |
| analysisSize | 512×512 | thumbnail size used for fingerprinting |
The safety nets
- Choosing photos in a cluster only marks them as pending — nothing is deleted yet, and anything can be restored.
- Finalizing shows you every pending photo one last time.
- Deletion goes through iOS itself, so photos land in “Recently Deleted” for 30 more days.
Want the exact code? It's ~150 lines:
github.com/arthurbricq/open-cleaner
— see Clusterer.swift.