☰
GPS轨迹降噪:先分型再处理,别让平滑毁掉转弯与急刹
2026/11/2 2:08:03 网站建设 项目流程

简介:面向GPS轨迹清洗场景的Python代码包,适合需要处理定位漂移、异常点的开发者与数据分析人员,重点解决轨迹噪点影响后续分析与位置服务精度的问题。包内共3个文件,均为py脚本,压缩包约7KB,分别承担高德猎鹰与百度鹰眼接口的封装调用,以及基于轨迹点间距离分布的降噪主流程,结构轻量,便于嵌入现有数据处理流程。已有153人学习。读者可从中获得两类在线地图API的上传与清洗思路、距离阈值判定和异常点剔除的实现方法,以及从网络请求、返回数据解析到结果输出的完整串联方式,也可据此调整阈值与平滑策略,形成适合自身业务的降噪方案,对智能交通、物流跟踪、户外运动记录等场景均有参考价值。

1. GPS 轨迹降噪:为什么“先平滑”往往是最差的第一步

GPS 轨迹降噪这件事,最容易被忽略的一点是:把一条抖动的轨迹抹平,和把一条轨迹修正对,是两件完全不同的事。前者只需要一个低通滤波器,后者要求你判断每一个点到底错在哪一步。市面上不少 GPS 轨迹降噪 API 和 Python 算法包,默认行为都是先平滑再输出,结果是一条看起来顺滑、但转弯被削圆、急刹被抹平、路口被拉成弧线的轨迹。如果后面要拿它算里程、算停留时长、做地图匹配,这些被“美化”掉的细节恰恰是唯一有价值的部分。所以真正该先问的不是“用哪个滤波器”,而是“这个异常点违反了哪条物理约束”。

2. 噪点分型:跳点、漂移与丢点的物理来源不同,判据也必须不同

轨迹降噪里最常见的失败,是把所有异常点当成同一类东西,然后上一个统一的阈值。实际处理过数据就会知道,一条轨迹里至少混着三种性质完全不同的坏点,它们的成因、可观测特征和正确的处理方式都不一样。混在一起处理的结果通常是两头不讨好:阈值放得松,噪声没去掉;放得紧,真实的急转弯和停车起步全被当成噪声删了。所以动手写代码之前,先给噪点分型。

2.1 从多径效应到 accuracy 字段:误差是怎么写进坐标里的

接收机拿到的是卫星的伪距和载波相位,位置解算是把这些观测方程做最小二乘或者卡尔曼解算出来的。误差链路大致是:卫星钟差和星历误差、电离层与对流层延迟、多径反射、接收机噪声,最后通过卫星几何分布(DOP 值)放大成水平位置误差。开阔地带水平精度通常能到 3~5 米,城市峡谷里几十米很常见,高架桥下或者隧道口出来那一两秒,几百米的偏差也不是没有。

有一条线索经常被浪费掉,就是定位结果附带的水平精度字段(Android 上叫 accuracy,iOS 上叫 horizontalAccuracy)。它本质上是接收机或者融合算法给出的置信半径,大致可以理解成“真实位置大概率落在这个圆里”。这个值在开阔地带是几米,进了楼宇密集区会突然跳到 50 米以上。用这个字段做第一层筛选,成本比任何纯几何阈值都低:精度半径已经 80 米的点,它的坐标基本没有参考价值。

另一个容易被忽略的事实是,手机端拿到的往往不是纯卫星解,而是融合定位的产物。WiFi、基站、蓝牙信标甚至惯性传感器都会参与,融合算法在几种来源之间切换时,会出现“整体瞬移到另一个位置,过几秒又跳回来”的现象。这种跳变的幅度经常远超卫星误差,而且 accuracy 字段不一定同步变大。这就是纯靠精度字段过滤仍然会漏的原因,也是很多人第一次处理手机轨迹时感到困惑的地方。

2.2 三种噪点形态的可观测特征

跳点是最直观的一种:单个点或者连续两三个点飞出去,前后的点都在正常轨迹上。它的可观测特征是前后相连的两段速度异常,但首尾两点之间跨越它之后的距离是正常的。如果只看速度,会把跳点前后的正常点也标脏,这就是后面要讲的连锁误杀。

漂移是另一种:一段连续的点整体偏离真实位置,速度看上去很正常(因为相邻点之间距离很小),但整段的位置是错的。静止状态下的漂移最典型,车停在楼下,轨迹却在周围画一个直径几十米的毛线团。这种点用速度阈值完全筛不掉,它的瞬时速度很低,甚至比真实移动还低。

丢点严格来说不算噪点,它是采样中断:隧道里没信号,出来之后时间戳跳了几十秒,两个点之间的直线距离几百米。如果直接用距离除以时间算速度,会得到一个看起来还挺合理的值,从而把这段瞬移当成真实移动保留下来。丢点必须单独识别,判据是时间间隔本身,不是距离。

把这三类分开之后,处理顺序就清楚了:先按时间间隔切段,再按精度字段和物理约束剔除跳点,最后用聚类处理漂移段。这个顺序不是随便排的,切段放最前面是因为一旦时间轴不可信,后面所有基于 dt 的计算都是空中楼阁。

2.3 Haversine 距离与速度阈值:第一道物理闸门

计算两点间距离不要用经纬度差直接开方,那是把度当成米用,在纬度 30 度附近经度方向会差出十几个百分点。标准做法是 Haversine 公式,它在小距离上的数值稳定性比余弦定理好,而且不需要开三次方。

import math EARTH_RADIUS_M = 6371008.8 # WGS84 平均半径,单位米 def haversine(lat1, lon1, lat2, lon2): """返回两点间的大圆距离,单位米。适用于 1km 以内的轨迹尺度。""" phi1 = math.radians(lat1) phi2 = math.radians(lat2) dphi = math.radians(lat2 - lat1) dlam = math.radians(lon2 - lon1) a = math.sin(dphi / 2) ** 2 + \ math.cos(phi1) * math.cos(phi2) * math.sin(dlam / 2) ** 2 # min(1.0, a) 是防止浮点误差让 a 略微超过 1,导致 asin 域错误 return 2 * EARTH_RADIUS_M * math.asin(math.sqrt(min(1.0, a)))

有了距离,就能把“速度”这个物理量算出来。速度阈值法的核心假设只有一条:无论是人、车还是别的什么,相邻采样点之间的位移不可能超过某个上限。这个上限按交通方式分档,步行取 3 m/s,骑行取 10 m/s,城市道路取 35 m/s,高速取 45 m/s,各留一点余量就行。分档的目的不是提高精度,是避免用一个过宽的阈值把慢速场景下的噪声全放过去。

def filter_spikes(points, max_speed=45.0, max_accuracy=60.0, max_gap_s=30.0): """ 单向扫描剔除跳点。 points: dict 列表,每项含 lat / lon / ts(秒级时间戳)/ accuracy(米) max_speed: 允许的最大瞬时速度,单位 m/s max_accuracy: 精度半径上限,超过则直接丢弃 max_gap_s: 相邻点最大时间间隔,超过则视为丢点、断开重连 """ kept = [] for p in points: acc = p.get('accuracy') # 精度半径已知且过大,坐标不值得信任,直接丢弃 if acc is not None and acc > max_accuracy: continue if not kept: kept.append(p) continue prev = kept[-1] dt = p['ts'] - prev['ts'] # 时间戳重复或倒序,无法计算速度,跳过 if dt <= 0: continue # 采样中断:断开重连,不清洗,交给下游分段处理 if dt > max_gap_s: kept.append(p) continue d = haversine(prev['lat'], prev['lon'], p['lat'], p['lon']) if d / dt <= max_speed: kept.append(p) return kept

这段代码里几个参数都不是随手写的。max_accuracy 取 60 米,是因为城市环境下 30 到 50 米很常见,卡太严会把正常点也丢光;max_gap_s 取 30 秒,对应一般手机定位的采样容错,超过这个间隔说明中间有中断,这时候拿它算速度没有意义,正确动作是断开而不是丢弃。

要注意的是,这个方法的效果高度依赖“参照点是对的”。如果参照点本身就是一个跳点,那么它后面的正常点会因为与之距离过远而被误删,一个坏点能带走后面一串好点。这是单向扫描的固有缺陷,解决办法要么是双向扫描取交集,要么是先做一次中值处理再算速度。

3. 局部一致性:中值滤波、航向突变与驻留聚类

上一章的物理阈值能干掉大部分离谱的跳点,但剩下那些“幅度不大但持续偏”的点,绝对阈值就无能为力了。这时候要换一个思路:不判断单点的绝对合理性,而是判断它跟邻居是否一致。局部一致性是轨迹降噪里最通用的一类判据,中值滤波、航向角差分、空间聚类本质上都是这个思路的不同变体,区别只在于“邻居”是按时间取还是按空间取。

3.1 中值滤波为什么比均值滤波更适合轨迹序列

滑动平均是最容易想到的平滑手段,也是最容易毁掉轨迹的一种。它在数学上就是一个低通滤波器,会把所有高频成分压掉,包括噪声,也包括真实的急转弯。轨迹在路口拐弯时,坐标序列呈现的是一个阶跃式的变化,均值滤波会把这个阶跃抹成一段斜坡,拐角被削成一个圆弧,转弯半径凭空变大十几米。

中值滤波不一样。它取窗口内的排序中位数,只要窗口里坏点不超过一半,输出就不会被坏点拉偏;同时它的 breakdown point 是 50%,意味着它能容忍接近一半的污染。更关键的是,排序统计量天然保持阶跃:一个窗口里如果 3 个点在马路这边、2 个点在马路那边,中值取到的仍然是马路这边,阶跃不会被抹平。这就是它在轨迹处理里比均值更适合的根本原因。

def median_smooth(points, window=5): """ 对经度、纬度分量分别做滑动中值,保留轨迹的阶跃特征。 window: 窗口长度,必须是奇数,5 表示前后各取 2 个邻居 """ if window % 2 == 0: raise ValueError('window 必须为奇数') half = window // 2 n = len(points) out = [] for i in range(n): lo = max(0, i - half) hi = min(n, i + half + 1) lats = sorted(p['lat'] for p in points[lo:hi]) lons = sorted(p['lon'] for p in points[lo:hi]) mid = len(lats) // 2 q = dict(points[i]) q['lat'] = lats[mid] q['lon'] = lons[mid] out.append(q) return out

窗口取 5 是一个常见的折中:能容忍 2 个连续坏点,同时对真实的加减速响应还算及时。取 3 只能容忍 1 个坏点,噪声稍密就顶不住;取 9 以上虽然更稳,但会把短距离的变道、绕行直接吃掉。窗口长度这个参数没有普适最优解,它取决于采样率和噪声密度,换个数据源就得重调。

这里有个容易被忽略的细节:直接对经纬度分量做中值,在高纬度地区会有畸变。经度一度对应的实际距离随纬度按余弦收缩,在纬度 60 度的地方只有赤道的一半。窗口小的时候影响不大,窗口一大,经度方向的中值和纬度方向的中值就不在同一个物理尺度上了。稳妥的做法是先投影到局部平面(比如以轨迹首点为原点的东-北坐标系)再处理,最后转回经纬度。

中值滤波还有一个明确的边界:它处理不了整段漂移。如果连续十几个点整体偏了二十米,窗口内的中值也是偏的,输出仍然是偏的。这类问题必须用空间聚类来解决。

3.2 航向角二阶差分:抓住折返型跳点

速度阈值对“原地折返”这类跳点不够敏感。设想一个点飞到了侧向十米外又回来,相邻两段的速度可能都在合理范围内,距离判据放它过去,但轨迹形状上明显多了一个尖刺。这时候看角度比看距离有效。

方位角(bearing)的计算要注意公式选择。用平面近似atan2(dlon, dlat)在近距离上误差可以接受,但它没有考虑经度收缩,在高纬度会偏。标准做法是球面公式:

def bearing(p1, p2): """p1 指向 p2 的方位角,0~360 度,正北为 0,顺时针增大""" lat1 = math.radians(p1['lat']) lat2 = math.radians(p2['lat']) dlon = math.radians(p2['lon'] - p1['lon']) x = math.sin(dlon) * math.cos(lat2) y = math.cos(lat1) * math.sin(lat2) - \ math.sin(lat1) * math.cos(lat2) * math.cos(dlon) return (math.degrees(math.atan2(x, y)) + 360.0) % 360.0 def turn_angle(b1, b2): """两个方位角之间的最小夹角,范围 0~180 度""" d = abs(b1 - b2) % 360.0 return d if d <= 180.0 else 360.0 - d

拿到转角之后,判定不能一刀切。低速时转 150 度很正常,路口掉头、停车场里挪车、人在原地转圈都会出现这种角度;高速时转 60 度就基本可以确定是噪点,因为 60 km/h 的车不可能在 1 秒内拐这么大弯。所以阈值应该跟速度挂钩,我一般这么设:速度高于 15 m/s 时阈值取 60 度,低于 3 m/s 时取 150 度,中间线性插值。这条规则不需要精确的物理模型,它只是把“开得快就拐不了急弯”这个常识编码进去。

还要加一个段长门限。如果相邻两点之间的距离小于 2 米,方位角基本是噪声主导,算出来的角度会在 0 到 360 之间随机跳。这种情况下算出来的转角没有任何意义,直接跳过比强行判定更靠谱。

3.3 驻留漂移的密度聚类判别

静止漂移是速度判据和角度判据都搞不定的一类:点与点之间距离很小、速度很低、航向乱转但转角也未必超阈值。它的本质特征是“空间上高度聚集、时间上持续很久”,这正好对应密度聚类的适用场景。

DBSCAN 的思路是:以每个点为中心画一个半径 eps 的圆,圆内点数达到 min_samples 就标为核心点,核心点之间互相连通形成一个簇;不属于任何簇的点标为噪声。用在轨迹上要加一个时间维度判据,否则缓慢移动的一小段也会被当成停留。

def _neighbors(points, i, eps_m): """返回第 i 个点在 eps_m 半径内的所有点索引""" pi = points[i] out = [] for j, pj in enumerate(points): if j == i: continue if haversine(pi['lat'], pi['lon'], pj['lat'], pj['lon']) <= eps_m: out.append(j) return out def find_stationary_clusters(points, eps_m=30.0, min_samples=10, min_duration_s=120): """ 识别静止漂移簇。 eps_m: 邻域半径(米),应大于该场景下的典型定位误差 min_samples: 核心点的最小邻居数,和采样率相关 min_duration_s: 簇的时间跨度下限,用来区分“真停留”和“缓慢通过” 返回: (labels, clusters)。labels[i] 为 -1 表示噪声点; clusters 是 {簇编号: {center_lat, center_lon, start_ts, end_ts}} """ visited = [False] * len(points) labels = [-1] * len(points) clusters = {} cluster_id = 0 for i in range(len(points)): if visited[i]: continue visited[i] = True neigh = _neighbors(points, i, eps_m) if len(neigh) + 1 < min_samples: continue # 标记为噪声,留待后续处理 labels[i] = cluster_id seeds = list(neigh) k = 0 while k < len(seeds): j = seeds[k] if not visited[j]: visited[j] = True n2 = _neighbors(points, j, eps_m) if len(n2) + 1 >= min_samples: seeds.extend(n2) if labels[j] == -1: labels[j] = cluster_id k += 1 members = [idx for idx, lb in enumerate(labels) if lb == cluster_id] span = points[max(members)]['ts'] - points[min(members)]['ts'] if span >= min_duration_s and len(members) >= min_samples: clusters[cluster_id] = { 'center_lat': sum(points[m]['lat'] for m in members) / len(members), 'center_lon': sum(points[m]['lon'] for m in members) / len(members), 'start_ts': points[min(members)]['ts'], 'end_ts': points[max(members)]['ts'], 'count': len(members), } else: # 时间跨度不够,说明只是缓慢通过,退回噪声点 for m in members: labels[m] = -1 cluster_id += 1 return labels, clusters

判据的价值全在min_duration_s上。不加这个条件,一个 30 秒内缓慢通过斑马线的行人也会被聚成一个簇,然后被压缩成“停留 30 秒”,里程和时长全错。取 120 秒是因为大多数业务场景里,短于两分钟的停留不值得单独记录。

识别出簇之后怎么处理,取决于下游用途。如果目的是算停留点,就把簇内所有点替换成中心点加时间区间,轨迹点数会大幅减少;如果目的是算里程,就把簇内点全部丢弃,只保留进出两个点。这两种处理出来的结果差别很大,选之前要想清楚。

还有一点,eps_m应该显著大于该场景下的典型定位误差。误差 10 米的地段用 15 米做邻域,正常移动的点也会被聚到一起;反过来用 100 米做邻域,一条 50 米长的街就被压缩成一个点了。经验值是按误差的 2 到 3 倍取。

4. 卡尔曼滤波:它解决的是状态估计,不是“看起来更顺”

前面三章都是“剔除”,把坏点扔掉。但扔掉之后轨迹会变稀疏,而且有些点数不多、幅度不大的噪声靠阈值剔不干净。这时候才轮到卡尔曼滤波登场。关于它的一个常见误解是把它当成高级平滑器,实际上它输出的不是“平滑后的观测”,而是“对真实状态的估计”,这两者在转弯和急加减速时会明显分叉。

4.1 匀速模型的状态方程与观测方程

轨迹处理最常用的是二维匀速(CV)模型。状态向量取x = [px, py, vx, vy]^T,前两个是位置、后两个是速度,都在局部平面坐标系里,单位是米和米每秒。状态转移矩阵按时间间隔 dt 展开:

F = [[1, 0, dt, 0], [0, 1, 0, dt], [0, 0, 1, 0], [0, 0, 0, 1]]

观测矩阵 H 只观测位置,不观测速度,因为 GPS 直接给出的就是坐标:

H = [[1, 0, 0, 0], [0, 1, 0, 0]]

这个模型的核心假设是“采样间隔内速度不变”。在 1 Hz 采样下,相邻点间隔 1 秒,匀速近似基本成立;但如果采样间隔拉长到 10 秒,中间可能经历了一次完整的加速和减速,匀速假设就崩了。这就是为什么采样率越低,卡尔曼滤波的效果越差,而很多人把原因归到“参数没调好”上。

4.2 过程噪声 Q 与观测噪声 R 的物理含义

Q 和 R 这两个矩阵是调参的全部战场,但它们的含义其实很直观。Q 表示“模型有多不可信”,R 表示“观测有多不可信”。两者的比值决定了滤波器的行为倾向:Q/R 大,滤波器更信观测,输出贴近原始轨迹,噪声滤不掉;Q/R 小,滤波器更信模型,输出平滑但滞后严重,转弯会被拉成一段弧线。

这里有一个很实用的技巧:R 不要设成常数,直接用 accuracy 字段动态给。R = [[acc^2, 0], [0, acc^2]],精度差的点观测噪声大方差,滤波器自然会压低它的权重。这比手工调一个固定的 R 要稳得多,因为它自动适应了不同路段的信号质量。这是我在踩过几次坑之后才养成的习惯,之前一直用固定 R,进城区就翻车。

Q 的确定要稍微费点劲。物理上它对应“加速度的功率谱密度”,反映车辆实际的加减速能力。私家车在城市道路上的加速度大概 1~3 m/s²,高速上不超过 2 m/s²,所以 Q 取 0.5 到 2 之间是合理的。取 10 以上等于告诉滤波器“模型完全不可信”,那它退化成直接输出观测值;取 0.01 以下等于说“车绝对匀速”,转弯时滤波器会顽固地沿着旧方向走,形成明显的滞后。

4.3 局部平面上的二维 CV 实现与失效边界

卡尔曼滤波要在平面直角坐标里做,经纬度不能直接线性运算。如果直接对经纬度做线性运算,方位角和距离的几何关系会混乱。做法是选轨迹首点作为原点,用局部切平面近似把经纬度转成东向、北向米:

import numpy as np def to_local_xy(lat0, lon0, lat, lon): """以 (lat0, lon0) 为原点的局部切平面近似,返回 (东向米, 北向米)""" x = math.radians(lon - lon0) * EARTH_RADIUS_M * math.cos(math.radians(lat0)) y = math.radians(lat - lat0) * EARTH_RADIUS_M return x, y def to_latlon(lat0, lon0, x, y): """局部平面坐标转回经纬度""" lat = lat0 + math.degrees(y / EARTH_RADIUS_M) lon = lon0 + math.degrees(x / (EARTH_RADIUS_M * math.cos(math.radians(lat0)))) return lat, lon class CVKalman: """二维匀速卡尔曼滤波器,状态 [x, y, vx, vy]""" def __init__(self, q=1.0): self.q = q self.x = np.zeros((4, 1)) self.P = np.eye(4) * 100.0 # 初始协方差给大一点,表示开局不确定 self.H = np.array([[1, 0, 0, 0], [0, 1, 0, 0]], dtype=float) self.inited = False def _F(self, dt): return np.array([[1, 0, dt, 0], [0, 1, 0, dt], [0, 0, 1, 0], [0, 0, 0, 1]], dtype=float) def _Q(self, dt): # 连续白噪声加速度模型离散化,q 为加速度功率谱密度 dt2, dt3, dt4 = dt * dt, dt ** 3, dt ** 4 q = self.q return q * np.array([ [dt4 / 4, 0, dt3 / 2, 0], [0, dt4 / 4, 0, dt3 / 2], [dt3 / 2, 0, dt2, 0], [0, dt3 / 2, 0, dt2], ], dtype=float) def step(self, z_x, z_y, r_var, dt): """z_x, z_y: 观测位置;r_var: 观测方差(由 accuracy 平方得到)""" if not self.inited: self.x = np.array([[z_x], [z_y], [0.0], [0.0]]) self.inited = True return z_x, z_y F = self._F(dt) Q = self._Q(dt) R = np.eye(2) * r_var # 预测 self.x = F @ self.x self.P = F @ self.P @ F.T + Q # 更新 z = np.array([[z_x], [z_y]]) S = self.H @ self.P @ self.H.T + R K = self.P @ self.H.T @ np.linalg.inv(S) self.x = self.x + K @ (z - self.H @ self.x) self.P = (np.eye(4) - K @ self.H) @ self.P return float(self.x[0, 0]), float(self.x[1, 0])

代码里的r_var建议用accuracy^2,没有 accuracy 字段就按场景给个常数,开阔地 25(即 5 米标准差),城区 400(20 米)。

这个实现有两个明确的失效边界,必须提前知道。第一是长时间静止:车停着不动,观测位置一直在小范围内漂,滤波器会逐渐把速度估计成一个小数值,然后开始“匀速漂移”,输出的位置会缓慢地往一个方向爬。解决办法是检测到连续多个点的位移小于阈值时,强制把速度状态归零,并且重置协方差。第二是采样间隔突变:dt 从 1 秒突然变成 30 秒,预测步把不确定性放得太大,更新时几乎完全信观测,等于没滤。稳妥的处理是在 dt 超过阈值时直接重置滤波器,把它当作一段新轨迹的起点。

5. 排查与避坑:GPS 降噪链路上最容易翻车的五处细节

算法本身不难,难的是那些不写进教科书的细节。下面五条都是实际处理数据时踩过的,每条按现象、原因、解决三段说清楚,能省掉不少返工。

5.1 距离算错:把经纬度差直接当米用

现象:在纬度 40 度的城市测一段 1 公里直线,算出来的里程是 1.3 公里左右;换成南方的数据,误差又变成另一个方向。换成 Haversine 之后数值正常了。

原因:经纬度是角度不是长度。纬度一度大约 111 公里,这个还算稳定;经度一度对应的距离是111 * cos(纬度)公里,纬度 40 度时只有 85 公里,纬度 60 度时只剩 55 公里。直接对两个方向的差值做欧氏距离,等于给经度方向乘了一个固定的错误系数,误差随纬度单调放大。

解决:所有距离统一走 Haversine 或者先投影到局部平面再算欧氏距离。投影的好处是后续的角度、聚类、滤波都能在同一个平面里做,不用反复转来转去;坏处是投影在轨迹尺度超过几十公里时会失真。城市级轨迹用投影,跨城轨迹用 Haversine 逐段算。

5.2 坐标系混用:基准偏移被当成漂移

现象:某段轨迹在地图上整体偏移了几百米,形状是对的,位置是错的。降噪算法把这段判成了大片漂移点,一通操作之后轨迹被压成了一个点。

原因:数据源用的坐标系和算法假设的不一致。国际上通用的是 WGS84,但国内地图服务常用的是经过非线性偏移的坐标系,两者在同一个位置能差出几百米。这种偏移是空间位置的系统性偏差,不是随机的定位噪声,用任何基于“局部一致性”的判据都识别不出来,因为它整段都一致地偏。

解决:在数据入口处统一坐标系,明确标注来源,转换只做一次。如果上游给的字段里没写坐标系,先拿几个已知地标点对一下,对不上就别急着跑降噪。转换的时候注意非线性偏移不是简单的加减常数,需要专门的转换函数,而且来回转换会有精度损失,尽量不要反复转。

5.3 时间戳乱序:滤波器的 dt 变成负数

现象:卡尔曼滤波跑完,轨迹在某几个位置出现了明显的倒退,或者某个点的位置突然跳到几百米外。日志里看 dt 有负值。

原因:原始数据的时间戳不保证单调递增。多源融合、离线补传、批量导入都会让点乱序。dt 为负时,状态转移矩阵里的 dt 也是负的,等于让模型“倒着预测”,位置和速度都会算错。更隐蔽的是 dt 恰好等于 0 的情况,此时 Q 矩阵全零,协方差不再增长,滤波器会变得越来越“自信”,最后完全不响应新的观测。

解决:进算法之前先按 ts 排序,排序后去重(相同时间戳只保留 accuracy 最好的那个)。排序这步看起来平淡,但它是所有时序算法的前提。另外建议加一个断言,dt 超过 5 倍中位采样间隔就报警,这往往意味着数据里混进了别的轨迹段。

5.4 单向扫描的连锁误杀:一个跳点带走一串好点

现象:一段 200 点的轨迹,清洗完只剩 60 个点,中间有一大段连续被删,但那段实际是正常行驶。

原因:单向扫描的参照点是“上一个保留的点”。如果某个跳点因为某些原因被保留了(比如它的 accuracy 字段恰好正常、速度也刚好在阈值内),那么它后面的正常点与它之间的距离会很大,速度超标,于是被删;删完之后参照点还是那个跳点,下一个正常点又被删,连锁反应一路传下去。

解决:两条路。一是双向扫描,正向跑一遍、反向跑一遍,取两次都保留的点作为最终结果,跳点前后至少有一侧会被正确判断。二是先做一次中值平滑再算速度

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询