AI-групування фотографій за локаціями в мобільних додатках
Групування за локаціями здається простим — у кожного фото є GPS. Але реальне завдання складніше: координати з розкидом 10–50 метрів потрібно склеїти в "місце" (кафе, парк, пляж), різні поїздки в одне місце — розділити за часом, а тисячі фото без GPS — віднести до локації за контекстом.
Крок 1: CLLocation кластеризація
Більшість фото з сучасних смартфонів містять GPS в EXIF. На iOS читаємо через PHAsset.location:
let fetchOptions = PHFetchOptions()
let photos = PHAsset.fetchAssets(with: .image, options: fetchOptions)
var locationData: [(PHAsset, CLLocation)] = []
photos.enumerateObjects { asset, _, _ in
if let location = asset.location {
locationData.append((asset, location))
}
}
Для Android: ExifInterface з TAG_GPS_LATITUDE / TAG_GPS_LONGITUDE, або MediaStore MediaColumns.LATITUDE (застаріло в API 29+, тепер через ContentResolver запит з MediaStore.Images.Media.LATITUDE).
Кластеризація GPS-координат — знов DBSCAN, але в метричному просторі (відстань гаверсинуса):
func haversineDistance(_ a: CLLocationCoordinate2D, _ b: CLLocationCoordinate2D) -> Double {
let R = 6371000.0 // метри
let dLat = (b.latitude - a.latitude) * .pi / 180
let dLon = (b.longitude - a.longitude) * .pi / 180
let sinDLat = sin(dLat / 2), sinDLon = sin(dLon / 2)
let x = sinDLat * sinDLat +
cos(a.latitude * .pi / 180) * cos(b.latitude * .pi / 180) * sinDLon * sinDLon
return R * 2 * atan2(sqrt(x), sqrt(1 - x))
}
Радіус кластера eps = 200 метрів добре працює для міської зйомки. Для туристичних поїздок краще 500–1000 метрів.
Розділення за часом: одне місце, різні поїздки
Користувач був у Барселоні тричі. GPS-кластер один, але візити — різні. Розділіть за часовими проміжками в кластері:
func splitByTimeGap(assets: [PHAsset], maxGapHours: Double = 12) -> [[PHAsset]] {
let sorted = assets.sorted { $0.creationDate! < $1.creationDate! }
var groups: [[PHAsset]] = [[sorted[0]]]
for i in 1..<sorted.count {
let gap = sorted[i].creationDate!.timeIntervalSince(sorted[i-1].creationDate!) / 3600
if gap > maxGapHours {
groups.append([sorted[i]])
} else {
groups[groups.count - 1].append(sorted[i])
}
}
return groups
}
12-годинний розрив — стандартно. Можна адаптувати: в межах одного міста 6 годин достатньо, для різних днів поїздки — 24 години.
Зворотне геокодування: координати → назва місця
У нас кластер з центроїдом (lat, lon). Потрібна людська назва: "Барселона, Іспанія" або "Центральний парк, Нью-Йорк".
Apple MapKit — CLGeocoder().reverseGeocodeLocation(). Безплатно, але обмежено для пакетних запитів. Не перевищуйте 1 запит на секунду.
Google Places API — платно, але повертає назву закладу (кафе, готель), а не просто вулицю:
func reverseGeocode(coordinate: CLLocationCoordinate2D) async throws -> PlaceName {
let location = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
let placemarks = try await CLGeocoder().reverseGeocodeLocation(location)
guard let placemark = placemarks.first else { throw GeoError.noResult }
return PlaceName(
city: placemark.locality,
country: placemark.country,
name: placemark.name
)
}
Кешируйте результати геокодування — одні й ті ж координати можуть зустрічатися у сотень фото. Словник [String: PlaceName] з ключем "\(lat.1)_\(lon.1)" (округлення до 1 знака ≈ 11 км, достатньо для групування по міству).
Фото без GPS: візуальна класифікація
15–30% фото не містять GPS (старі фото, зйомка в приміщенні при відключеній геолокації). Для них — Vision VNClassifyImageRequest + словник категорій з геосемантикою.
Якщо фото класифіковано як "beach", "mountain", "cityscape" — показуйте в секції "Природа" або "Міста" без конкретної адреси. Якщо є часова мітка і рядом є інші фото з GPS — привласніть найближчому кластеру за часом (+/- 1 година).
UI: відображення
Два паттерни для UI:
- На основі карти: MKMapView з кластерними пінами. Тап на піна — галерея локації
- На основі списку: сортування за датою, секції = локації. Як Apple Photos "Спогади"
Для map-based: MKClusterAnnotation на iOS нативно підтримує кластеризацію пінів при zoom-out. На Android — Google Maps SDK ClusterManager.
Часові рамки
GPS-кластеризація з зворотним геокодуванням та базовим UI — 1–1,5 тижні. Повна реалізація з розділенням за поїздками, візуальною групуванням фото без GPS та картою — 3–4 тижні. Вартість розраховується індивідуально.







