קיבוץ קואורדינטות GPS עד למיקום המדויק?
כאשר המשתמשים שלכם מייבאים אלפי תמונות מהטלפון שלהם, מיון ידני לפי מיקום הוא מותרות שאינכם יכולים להרשות לעצמכם. אתם זקוקים לאלגוריתם שיבין היכן כל צילום צולם. 15–30% מהתמונות חסרות קואורדינטות GPS, והשאר מפוזרות עם מרווח שגיאה של עד 50 מטרים. כיצד מקבצים אותן לפי מקומות בפועל? אנו משתמשים בקיבוץ מבוסס צפיפות DBSCAN עם מרחק haversine, גיאוקודינג הפוך, וסיווג סצנה באמצעות Vision API. במשך 5 שנים יישמנו למעלה מ-50 פרויקטים למובייל שבהם השילוב הזה השיג דיוק של 97%. לדוגמה, אפליקציית נסיעות צריכה לשלב אוטומטית צילומים מאותו טיול לאלבומים, אך בשל רעש GPS או קואורדינטות חסרות, זו הופכת למשימה לא טריוויאלית. הגישה שלנו פותרת זאת ביעילות.
בעיות שאנו פותרים
פיזור קואורדינטות. נקודות GPS מאותו מיקום יכולות לסטות ב-10–50 מטרים. DBSCAN עמיד לרעש כזה ואינו דורש ציון מספר הקבוצות מראש. זה מקצץ בעלויות מיון ידני של תמונות.
מיזוג טיולים. אם ביקרתם באותו פארק שלוש פעמים בימים שונים, האלגוריתם חייב להפריד בין הביקורים. לאחר הקיבוץ, אנו ממיינים תמונות לפי זמן ומפצלים לפי סף הניתן להגדרה (ברירת מחדל 12 שעות). זה חוסך עד שבועיים של זמן פיתוח לשילוב מודול כזה.
גיאוגים חסרים. עבור צילומים ללא GPS, אנו משתמשים ב-VNClassifyImageRequest (iOS) או ML Kit (אנדרואיד). אנו מסווגים את הסצנה ומשייכים אותה לקבוצה זמנית אם התמונה צולמה בתוך שעה מקבוצה עם קואורדינטות.
מדוע DBSCAN עדיף על K-means למשימה זו?
K-means דורש ציון מספר הקבוצות מראש ורגיש לחריגים. DBSCAN מוצא אוטומטית קבוצות בצורה שרירותית ומסמן רעש. על נתונים דלילים עם אלפי נקודות, DBSCAN מהיר פי 5 מ-K-means כי הוא אינו מחשב מחדש צנטרואידים.
איסוף נתוני GPS
ב-iOS אנו קוראים מיקום דרך PHAsset.location. באנדרואיד — דרך ExifInterface עם תגי GPS, או MediaStore (בהתחשב בהתיישנות ב-API 29+). חשוב: ב-iOS 14+ עליכם לבקש הרשאה PHPhotoLibrary.requestAuthorization.
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))
}
} קיבוץ DBSCAN עם מרחק haversine
כדי לקבץ נקודות אנו משתמשים ב-DBSCAN — אלגוריתם מבוסס צפיפות שאינו דורש ציון מספר הקבוצות מראש. המרחק מחושב באמצעות נוסחת haversine כדי להתחשב בכדוריות כדור הארץ.
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))
}רדיוס הקבוצה 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)) } } מכוונן למשימה: 200 מטר לעיר, 500–1000 מטר לטיולים תיירותיים. מספר נקודות מינימלי הוא 2. עבור ספריות גדולות (>10,000 תמונות) אנו משתמשים בעיבוד מקבילי דרך 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 ומפצלים לפי פערי זמן.
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.
גיאוקודינג: קואורדינטות → שם מקום
כל קבוצה מקבלת שם קריא באמצעות גיאוקודינג הפוך. לפי תיעוד Apple, CLGeocoder חינמי אך מוגבל ל-1 בקשה/שנייה. Google Places API בתשלום אך מחזיר שמות עסקיים (קפה, מלון). אנו ממליצים להשתמש תחילה ב-CLGeocoder, ולאחר מכן ב-Google אם יש צורך.
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
)
}התוצאות נשמרות במטמון במילון עם מפתח DispatchQueue.concurrentPerform המעוגל לדיוק של 1 (דיוק ~11 ק"מ — מספיק לקיבוץ ברמת עיר).
מה לעשות עם תמונות ללא GPS?
עבור תמונות ללא קואורדינטות אנו משתמשים ב-VNClassifyImageRequest (iOS) או מקבילות. אנו מסווגים את הסצנה: חוף, הרים, עיר. אם לא ניתן לשייך תמונה לקבוצה ידועה לפי זמן, אנו מכניסים אותה לקטע נושאי ללא קישור לכתובת.
| תרחיש | שיטה | דיוק לדוגמה |
|---|---|---|
| נוף עירוני | Vision Classify | 85% |
| חוף / טבע | Vision Classify | 90% |
| תמונה עם חותמת זמן ליד קבוצת GPS | שיוך זמן (±שעה) | 95% |
ממשק משתמש: תצוגת מפה ורשימה
אנו מיישמים שני מצבים:
- מפה: MKMapView עם MKClusterAnnotation ב-iOS, ClusterManager באנדרואיד. נעצים מתקבצים/מתפרקים אוטומטית בזום. עבור SwiftUI, מפה עם נעצים זמינה דרך
creationDate. - רשימה: קטעים לפי מיקומים, ממוינים לפי תאריך התמונה הראשונה. דומה ל-Memories ב-Apple Photos. עבור ספריות גדולות (>5000 תמונות) אנו משתמשים ב-
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 }לגלילה חלקה.
דוגמה להגדרת רדיוס קיבוץ
- עיר: 200 מ', minPoints=2
- פארק: 300 מ', minPoints=3
- טיול: 500–1000 מ', minPoints=2
- פיצול לפי זמן: 6 שעות (עיר), 12 שעות (ברירת מחדל), 24 שעות (טיול)
תהליך ולוחות זמנים
- ניתוח — לימוד נתוני המקור, מבנה EXIF, דרישות ממשק משתמש.
- עיצוב — בחירת מודלי קיבוץ, גיאוקודר, סכמת מטמון.
- יישום — כתיבת מודולים לאיסוף, קיבוץ, גיאוקודינג, סיווג.
- בדיקות — אימות על ספריות תמונות אמיתיות (עד 10,000 צילומים).
- השקה — שילוב באפליקציה, הגדרת TestFlight / Firebase Distribution.
| מודול | לוח זמנים |
|---|---|
| קיבוץ בסיסי + גיאוקודינג + רשימה | 1–1.5 שבועות |
| גרסה מלאה עם מפה, פיצול טיולים וסיווג לתמונות ללא GPS | 3–4 שבועות |
התמחור מחושב באופן אישי. צרו קשר להערכת פרויקט. אנו מבטיחים תוצאות מגובות בהסמכות Apple ו-Google וחמש שנות ניסיון. קבלו ייעוץ כבר עכשיו.
מה כלול בעבודה
- קוד מקור של מודולים ב-Swift / Kotlin עם הערות.
- הגדרת מטמון וספי קיבוץ.
- שילוב עם ממשק משתמש קיים (מפה או רשימה).
- תיעוד API והגדרות.
- תמיכה טכנית למשך שבועיים לאחר המסירה.
צרו קשר — נמצא את הפתרון האופטימלי לפרויקט שלכם.







