Реализация AI-группировки фотографий по локациям в мобильном приложении
Группировка по локациям кажется простой — у каждого фото есть GPS. Но реальная задача сложнее: координаты с разбросом 10–50 метров нужно склеить в «место» (кафе, парк, пляж), разные поездки в одно место — разделить по времени, а тысячи фото без GPS — отнести к локации по контексту.
Шаг 1: CLLocation clustering
Большинство фото с современных смартфонов содержат 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 (deprecated в API 29+, теперь через ContentResolver query с 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:
- Map-based: MKMapView с кластерными пинами. Тап на пин — галерея локации
- List-based: сортировка по дате, секции = локации. Как в Apple Photos «Воспоминания»
Для map-based: MKClusterAnnotation на iOS нативно поддерживает clustering пинов при zoom-out. На Android — Google Maps SDK ClusterManager.
Сроки
GPS-кластеризация с геокодированием и базовым UI — 1–1.5 недели. Полная реализация с разделением по поездкам, визуальной группировкой фото без GPS и картой — 3–4 недели. Стоимость рассчитывается индивидуально.







