本文提供的方法僅供客戶端在沒有太高的曲線精度要求的情況下優化的一種思路, 如果要求太高的話可以采用百度地圖SDK提供的軌跡糾偏的API;
首先說明一下需求的產生: 隨著現在越來越多人開始注重運動, 運動類APP也是越來越多, 而運動軌跡的記錄, 是運動類APP不可缺少的功能, 由于運動過程中 GPS 精度的變化, 不可避免的會導致定位誤差,產生軌跡漂移的現象, 盡可能的減少漂移產生的軌跡曲線鋸齒現象正是下面要解決的問題.
先來看下面兩張對圖:
從上面可以看出, 前后的效果還是比較不錯, 去除了部分漂移帶來的鋸齒, 下面談談優化的思路.
對比第一張圖的鋸齒形狀可以看出, 大部分比較明顯的鋸齒的頂點與其前后兩個點之間形成的夾角非常小, 一般來說, 角度小于120度的時候, 就會產生, 我們首先處理的就是這部分點的情況; 下面先看代碼
- (NSArray *)correctedLocationsFromLocations:(NSArray<CLLocation *> *)locations ratio:(CGFloat)ratio {
@autoreleasepool {
// 軌跡點異常校正
NSInteger count = locations.count;
NSMutableArray *tmp = [NSMutableArray arrayWithCapacity:locations.count];
for (NSInteger i = 0; i < count; i++) {
@autoreleasepool {
if (i > 0 && i < count - 1) {
CLLocation *location_a = tmp[i-1];
CLLocation *location_b = locations[i];
CLLocation *location_c = locations[i+1];
CGFloat a = fabs([location_b distanceFromLocation:location_c]);
CGFloat b = fabs([location_a distanceFromLocation:location_c]);
CGFloat c = fabs([location_a distanceFromLocation:location_b]);
CGFloat angle_b = acos(((a*a) + (c*c) - (b*b)) / (2*a*c));
if (angle_b < M_PI / 6) {
CLLocationDegrees mid_longitude = location_a.coordinate.longitude;
CLLocationDegrees mid_latitude = location_a.coordinate.latitude;
CGFloat speed = location_b.speed;
CGFloat course = location_b.course;
CGFloat horizontalAccuracy = location_b.horizontalAccuracy;
CGFloat verticalAccuracy = location_b.verticalAccuracy;
CGFloat altitude = location_b.altitude;
NSDate *timestamp = location_b.timestamp;
CLLocationCoordinate2D coor = {mid_latitude, mid_longitude};
CLLocation *loc = [[CLLocation alloc] initWithCoordinate:coor altitude:altitude horizontalAccuracy:horizontalAccuracy verticalAccuracy:verticalAccuracy course:course speed:speed timestamp:timestamp];
[tmp addObject:loc];
} else if (angle_b < M_PI / 3) {
CLLocationDegrees mid_longitude = (location_a.coordinate.longitude + location_c.coordinate.longitude) / 2;
CLLocationDegrees mid_latitude = (location_a.coordinate.latitude + location_c.coordinate.latitude) / 2;
CGFloat speed = location_b.speed;
CGFloat course = location_b.course;
CGFloat horizontalAccuracy = location_b.horizontalAccuracy;
CGFloat verticalAccuracy = location_b.verticalAccuracy;
CGFloat altitude = location_b.altitude;
NSDate *timestamp = location_b.timestamp;
CLLocationCoordinate2D coor = {mid_latitude, mid_longitude};
CLLocation *loc = [[CLLocation alloc] initWithCoordinate:coor altitude:altitude horizontalAccuracy:horizontalAccuracy verticalAccuracy:verticalAccuracy course:course speed:speed timestamp:timestamp];
[tmp addObject:loc];
} else if (angle_b < M_PI * ratio) {
CLLocationDegrees mid_longitude = (location_a.coordinate.longitude +location_b.coordinate.longitude + location_c.coordinate.longitude) / 3;
CLLocationDegrees mid_latitude = (location_a.coordinate.latitude + location_b.coordinate.latitude + location_c.coordinate.latitude) / 3;
CGFloat speed = location_b.speed;
CGFloat course = location_b.course;
CGFloat horizontalAccuracy = location_b.horizontalAccuracy;
CGFloat verticalAccuracy = location_b.verticalAccuracy;
CGFloat altitude = location_b.altitude;
NSDate *timestamp = location_b.timestamp;
CLLocationCoordinate2D coor = {mid_latitude, mid_longitude};
CLLocation *loc = [[CLLocation alloc] initWithCoordinate:coor altitude:altitude horizontalAccuracy:horizontalAccuracy verticalAccuracy:verticalAccuracy course:course speed:speed timestamp:timestamp];
[tmp addObject:loc];
} else {
CLLocation *loc = locations[i];
[tmp addObject:location_b];
}
continue;
} else {
CLLocation *loc = locations[i];
[tmp addObject:loc];
continue;
}
}
}
return [tmp copy];
}
}
這個方法需要傳入需要校正的GPS點數組 locations
和一個最大校正的角度 ratio
, 根據余弦定理:
由此可以計算出
β
的值:
- 如果
β
<30o
, 將 B 點的坐標修改為 A 點的坐標; - 如果
β
<60o
, 將 B 點的坐標修改為 A 點和 B 點的中點的坐標; - 如果
β
<ratio
, 將 B 點的坐標修改為 A 點、 B 點和C 點的的中點的坐標;
第1. 2方法可以將三角形處理成一條直線, 3方法處理后可以增大 β
, 降低鋸齒; 通過多次調整ratio
的值, 可以消除大部分的鋸齒;
在實際的使用中還出現了另外一個問題, 對于軌跡中出現的形如 "Z" 字的形狀, 如下圖, B和C兩點總是在 AD連線的兩邊; 理想的狀態總是想將這四個點連成一條直線, 但是經過以上的方法的處理以后, 得到的曲線是一條彎曲的曲線;
這種情況比較難以確定angle_b
和angle_c
的大小, 根據上面的經驗, 按照平均每個角 90o
來處理(不要問我怎么來的, 我也不知道怎么來的, 哈哈哈), 那么兩角之和小于 180o
的話, 就需要進行處理, 下面是處理的代碼
- (NSArray *)correcteZShapeFromLocations:(NSArray *)locations {
@autoreleasepool {
NSInteger count = locations.count;
if (count <= 5) {
return locations;
}
NSMutableArray *tmp = [NSMutableArray arrayWithCapacity:locations.count];
for (NSInteger i = 0; i < count - 4; i++) {
@autoreleasepool {
if (i > 0) {
{
CLLocation *location_a = tmp[i-1];
CLLocation *location_b = locations[i];
CLLocation *location_c = locations[i+1];
CLLocation *location_d = locations[i+2];
CGFloat angle_b = 0;
CGFloat angle_c = 0;
{
CGFloat a = fabs([location_b distanceFromLocation:location_c]);
CGFloat b = fabs([location_a distanceFromLocation:location_c]);
CGFloat c = fabs([location_a distanceFromLocation:location_b]);
if (a>0 && c > 0) {
angle_b = acos(((a*a) + (c*c) - (b*b)) / (2*a*c));
}
}
{
CGFloat d = fabs([location_b distanceFromLocation:location_c]);
CGFloat b = fabs([location_d distanceFromLocation:location_c]);
CGFloat c = fabs([location_d distanceFromLocation:location_b]);
if (b>0 && d>0) {
angle_c = acos(((d*d) + (b*b) - (c*c)) / (2*d*b));
}
}
CGFloat r = (angle_b + angle_c);
if (r < M_PI && r > 0) {
CLLocation *location_mid_a_d = [self midLocationFromLocation1:location_a location2:location_d];
{
CLLocationDegrees mid_longitude = (location_mid_a_d.coordinate.longitude + location_mid_a_d.coordinate.longitude) / 2;
CLLocationDegrees mid_latitude = (location_mid_a_d.coordinate.latitude + location_mid_a_d.coordinate.latitude) / 2;
CGFloat speed = location_b.speed;
CGFloat course = location_b.course;
CGFloat horizontalAccuracy = location_b.horizontalAccuracy;
CGFloat verticalAccuracy = location_b.verticalAccuracy;
CGFloat altitude = location_b.altitude;
NSDate *timestamp = location_b.timestamp;
CLLocationCoordinate2D coor = {mid_latitude, mid_longitude};
CLLocation *loc = [[CLLocation alloc] initWithCoordinate:coor altitude:altitude horizontalAccuracy:horizontalAccuracy verticalAccuracy:verticalAccuracy course:course speed:speed timestamp:timestamp];
[tmp addObject:loc];
}
{
CLLocationDegrees mid_longitude = (location_mid_a_d.coordinate.longitude + location_mid_a_d.coordinate.longitude) / 2;
CLLocationDegrees mid_latitude = (location_mid_a_d.coordinate.latitude + location_mid_a_d.coordinate.latitude) / 2;
CGFloat speed = location_c.speed;
CGFloat course = location_c.course;
CGFloat horizontalAccuracy = location_c.horizontalAccuracy;
CGFloat verticalAccuracy = location_c.verticalAccuracy;
CGFloat altitude = location_c.altitude;
NSDate *timestamp = location_c.timestamp;
CLLocationCoordinate2D coor = {mid_latitude, mid_longitude};
CLLocation *loc = [[CLLocation alloc] initWithCoordinate:coor altitude:altitude horizontalAccuracy:horizontalAccuracy verticalAccuracy:verticalAccuracy course:course speed:speed timestamp:timestamp];
[tmp addObject:loc];
}
i++;
} else {
[tmp addObject:location_b];
}
}
} else {
CLLocation *loc = locations[i];
[tmp addObject:loc];
}
}
}
CLLocation *location_1 = locations[count - 4];
CLLocation *location_2 = locations[count - 3];
CLLocation *location_3 = locations[count - 2];
CLLocation *location_4 = locations[count - 1];
[tmp addObject:location_1];
[tmp addObject:location_2];
[tmp addObject:location_3];
[tmp addObject:location_4];
return [tmp copy];
}
}
獲取兩個點中間的點:
/** 兩個點的中點 */
- (CLLocation *)midLocationFromLocation1:(CLLocation *)location1 location2:(CLLocation *)location2 {
CLLocationDegrees mid_longitude = (location1.coordinate.longitude +location2.coordinate.longitude) / 2;
CLLocationDegrees mid_latitude = (location1.coordinate.latitude + location2.coordinate.latitude) / 2;
CGFloat speed = location1.speed;
CGFloat course = location1.course;
CGFloat horizontalAccuracy = location1.horizontalAccuracy;
CGFloat verticalAccuracy = location1.verticalAccuracy;
CGFloat altitude = location1.altitude;
NSDate *timestamp = location1.timestamp;
CLLocationCoordinate2D coor = {mid_latitude, mid_longitude};
CLLocation *loc = [[CLLocation alloc] initWithCoordinate:coor altitude:altitude horizontalAccuracy:horizontalAccuracy verticalAccuracy:verticalAccuracy course:course speed:speed timestamp:timestamp];
return loc;
}
好了, 通過以上的處理, 基本可以滿足客戶端對軌跡的粗略糾偏; 如果大家還有其他思路的話, 可以一起探討.