本文提供的方法仅供客户端在没有太高的曲线精度要求的情况下优化的一种思路, 如果要求太高的话可以采用百度地图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
, 根据余弦定理:
由此可以计算出
β
的值:
- 如果
β
<30º
, 将 B 点的坐标修改为 A 点的坐标; - 如果
β
<60º
, 将 B 点的坐标修改为 A 点和 B 点的中点的坐标; - 如果
β
<ratio
, 将 B 点的坐标修改为 A 点、 B 点和C 点的的中点的坐标;
第1. 2方法可以将三角形处理成一条直线, 3方法处理后可以增大 β
, 降低锯齿; 通过多次调整ratio
的值, 可以消除大部分的锯齿;
在实际的使用中还出现了另外一个问题, 对于轨迹中出现的形如 "Z" 字的形状, 如下图, B和C两点总是在 AD连线的两边; 理想的状态总是想将这四个点连成一条直线, 但是经过以上的方法的处理以后, 得到的曲线是一条弯曲的曲线;
这种情况比较难以确定angle_b
和angle_c
的大小, 根据上面的经验, 按照平均每个角 90º
来处理(不要问我怎么来的, 我也不知道怎么来的, 哈哈哈), 那么两角之和小于 180º
的话, 就需要进行处理, 下面是处理的代码
- (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;
}
好了, 通过以上的处理, 基本可以满足客户端对轨迹的粗略纠偏; 如果大家还有其他思路的话, 可以一起探讨.