一般来说,GPS轨迹的采样间隔越小,轨迹点越密集,越能够反映车辆行驶的实际情况。但是过度密集的轨迹点会增加计算和存储的负担,因此必要时需要进行抽稀处理。例如轨迹采样间隔为3秒时(即每隔3秒采集一个轨迹点),可以过滤掉部分轨迹点,使采样间隔变为10秒或其他满足需求的值,同时保留路径的形状特征和轨迹信息。
本文会介绍3种常用的抽稀方法:降频、滑动窗口、RDP(Ramer-Douglas-Peucker) ,已集成在AutoTrack库的【轨迹抽稀模块】
1、降频
降频即降低采样频率:例如每3个轨迹点保留1个(即跳过2个轨迹点)

python
# 若存在10个轨迹点points
remained_list = list(range(0, len(points), 3)) # [0, 3, 6, 9]
2、滑动窗口
2.1、算法说明
将理想的轨迹点平均采样间隔作为滑动窗口,窗口内最多保留1个轨迹点
【关键步骤】
- 初始化:设置理想的平均采样间隔
IdealTimeInterval,初始化累计时间误差CumulativeTimeBias为0 - 从第一个点开始依次判断,默认保留第一个点
- 对于其他点,计算与上一个点的时间差(时间戳相减),并将该时间差加到
CumulativeTimeBias上 - 若 C u m u l a t i v e T i m e B i a s ≥ I d e a l T i m e I n t e r v a l CumulativeTimeBias \ge IdealTimeInterval CumulativeTimeBias≥IdealTimeInterval,则保留该点,并更新
CumulativeTimeBias- C u m u l a t i v e T i m e B i a s = C u m u l a t i v e T i m e B i a s % I d e a l T i m e I n t e r v a l CumulativeTimeBias = CumulativeTimeBias \% IdealTimeInterval CumulativeTimeBias=CumulativeTimeBias%IdealTimeInterval
- 即
CumulativeTimeBias为与IdealTimeInterval相除的余数
- 否则,不保留该点
【优点】
- 核心逻辑是保证每个理想平均采样间隔内至多只有一个点,使实际平均采样间隔接近理想值
- 对轨迹的实际采样间隔无要求:若实际值大于理想值,则大多数点得以保留,仅剔除少量点
【举例说明】
- 初始化:
IdealTimeInterval=10,CumulativeTimeBias为0,初始点保留 - 第2个点间隔8秒:
CumulativeTimeBias=8+0<10,不保留 - 第3个点间隔6秒:
CumulativeTimeBias=6+8≥10,保留,CumulativeTimeBias=14%10=4 - 第4个点间隔12秒:
CumulativeTimeBias=12+4≥10,保留,CumulativeTimeBias=16%10=6 - 第5个点间隔3秒:
CumulativeTimeBias=3+6<10,不保留 - ...

2.2、代码示例
python
# 初始化累计误差为0,若大于期望值则更新
ideal_time_interval = 10
cumulative_time_bias = 0
# 保留第一个轨迹点
remained_list = [0]
# 轨迹点的时间戳(精确到为毫秒)列表timestamps
for i in range(len(timestamps) - 1):
interval = (timestamps[i + 1] - timestamps[i]) / 1000
# 计算cumulative_time_bias,若cumulative_time_bias >= 期望值,则保留轨迹点,并更新cumulative_time_bias
cumulative_time_bias += interval
if cumulative_time_bias >= ideal_time_interval:
remained_list.append(i + 1)
cumulative_time_bias %= ideal_time_interval
3、RDP(Ramer-Douglas-Peucker)
Ramer-Douglas-Peucker算法(简称RDP算法)是一种用于减少由线连接的点集的点数的算法。
该算法通过保留数据的基本形状来简化曲线或多边形。例如在地理信息系统(GIS)中简化地图数据:在保留车辆行驶GPS轨迹特征的前提下,尽量减少轨迹点的数量
3.1、算法说明
【关键步骤】
- 对于起终点之间的所有点,计算点到直线的距离并确定最大距离
- 可根据需要选择合适的距离计算方式:直线距离或者球面距离等
- 若最大距离大于距离阈值,则以这个点为界将原路径分为两部分,递归进行此操作;
- 否则,删除中间点


【算法缺陷】 - 距离阈值依赖经验,可以根据数据的局部特征(如密度)动态调整阈值(可行但是过于复杂)
- 受离群点影响大:仅考虑距离指标,会把离群点当做特征点保留下来(切记:先剔除异常点)
3.2、用于轨迹抽稀的注意事项
【距离计算】
- 基于几何投影(方法1):使用
shapely库的Point、LineString对象的属性或方法计算点到线的距离- 问题:若点不在线的范围内(比如噪点,见下图),则得到的距离是点到线起点或终点的距离
- 基于三角形(方法2):基于海伦公式(根据三条边长计算面积)确定点到线的距离(已知边长求高)
- 需先判断是否构成三角形,若三点共线,则距离为0
- 当点不在线的范围内时,方法1计算的距离一般比方法2计算的距离大( d 3 1 > d 3 2 d_3^1 > d_3^2 d31>d32),对于确定间距最大的点一般无影响(轨迹点3到起终点连线的距离最大);当点在线的范围内时,两种计算方式的结果基本相等

【重投影】
将要删除的点进行重投影(投影到该点两侧最近的要保留的点构成的连线上),以投影点的坐标替代原坐标(轨迹点数量不变)
- 确定投影点:使用
shapely库的Point、LineString对象的属性或方法确定点在线上的投影点 - 投影点的时间戳、速度可直接使用原有轨迹点的值,但是航向角需更新
- 计算投影点航向角:可以将连线的角度与正北方向的夹角作为投影点的航向角

3.3、代码示例
完整代码详见:AutoTrack库【轨迹抽稀模块】
python
# 轨迹抽稀
def cal_projection_distance(left_p, right_p, other_p):
# 实现方式1:
# 构造shapely库的Point、LineString对象,使用函数得到点到线的距离
# Point.distance获取点到线的最小距离:若点在线的范围内,返回投影距离;若不在范围内,则大概率返回点到线的端点的距离(受线形影响)
# points_utm = [self.trans_4326.transform(lng, lat) for lng, lat in [left_p, right_p, other_p]]
# line_utm = LineString(points_utm[:-1])
# h_1 = Point(points_utm[-1]).distance(line_utm)
# 实现方式2:
# 计算点之间的距离,根据海伦公式(Heron's formula)计算三角形的面积(若共线则为0),根据面积计算某条边的高
a = cal_haversine_dis(left_p, other_p)
b = cal_haversine_dis(right_p, other_p)
c = cal_haversine_dis(left_p, right_p)
if a + b > c and b + c > a and c + a > b:
s = (a + b + c) / 2
h_2 = 2 * np.sqrt(s * (s - a) * (s - b) * (s - c)) / c
else:
h_2 = 0
# 【说明】当点不在线的范围内时(比如噪点),根据投影计算的距离比根据三角形计算的数值大,对于确定间距最大的点无影响;
# 当点在线的范围内时,两种计算方式的结果基本相等
# print(f'根据投影计算的距离为{round(h_1,2)},根据三角形计算的距离为{round(h_2,2)}')
return h_2
def rdp_core(points, left, right):
if right - left < 2:
return [left, right]
if np.all(points[left] == points[right]):
return [left, right]
max_dis = 0.0
max_i = 0
for i in range(left + 1, right):
distance = cal_projection_distance(
points[left], points[right], points[i]
)
if distance > max_dis:
max_i = i
max_dis = distance
if max_dis > self.core_param:
results1 = rdp_core(points, left, max_i)
results2 = rdp_core(points, max_i, right)
return results1[:-1] + results2
else:
return [left, right]
# 轨迹点points(经纬度坐标)
remained_list = rdp_core(points, 0, len(points) - 1)
# 重投影:找到被剔除点,使用投影坐标替换原坐标
simplified = []
updated_info = []
for left, right in zip(remained_list, remained_list[1:]):
if left + 1 == right:
continue
# 找到被剔除点,使用投影坐标替换原坐标
for other in range(left + 1, right):
# 投影坐标系转换为平面坐标系
points_utm = [trans_4326.transform(lng, lat) for lng, lat in [points[left], points[right], points[other]]]
line_utm = LineString(points_utm[:-1])
# 对于line.project(point),若点的投影点在线的起点之前,则返回0;若在线的终点之后,则返回线的长度;若在线上,则返回起点到投影点之间的长度
# line.interpolate(line.project(point)),若点的投影点在线的起点之前,则返回起点
point_pro = line_utm.interpolate(
line_utm.project(Point(points_utm[-1]))
)
x, y = point_pro.x, point_pro.y
# 平面坐标系转换为投影坐标系
lng, lat = trans_32648.transform(x, y)
# 记录要重投影的点的索引
simplified.append(other)
# 记录投影点坐标
updated_info.append([lng, lat])
# 计算航向角(根据两点的坐标计算)
bearing = cal_bearing(*points[left], *points[right])
updated_info[-1].append(bearing)
# 轨迹点数量不变
remained_list = list(range(len(points)))