第二章:数据采集与清洗——GPS坐标采集原理、常见数据噪声与清洗策略

大家好,我是老张。今天咱们聊聊轨迹回放里最基础、也最容易被忽视的一环——数据采集与清洗。

说实话,我见过太多人一上来就搞算法、调参数,结果回放出来的轨迹歪歪扭扭,根本没法看。为什么?因为源头数据就有问题。你想想看,GPS信号在市区高楼间跳来跳去,采集到的坐标本身就带着一堆噪声。你不处理它,后面再怎么优化也是白搭。

这一章,我就把我在多个项目中踩过的坑、总结的经验,一次性讲清楚。

2.1 GPS坐标采集原理——你得知道它怎么来的

GPS定位,说白了就是卫星给你发信号,你的设备根据信号到达的时间差算出距离,然后通过三颗以上的卫星交会出你的位置。嗯,原理不复杂,但实际用起来问题一堆。

我习惯把GPS采集分成三个层次:

  • 原始NMEA数据:GPS模块输出的标准协议,比如$GPGGA、$GPRMC这些。里面包含经纬度、海拔、速度、卫星数量等信息。
  • 解析后的坐标点:从NMEA里提取出经纬度、时间戳,形成最基础的轨迹点。
  • 经过校正的坐标:有些设备会做差分校正(比如RTK),精度能到厘米级。但大多数民用设备,精度也就3-10米。

我在项目中遇到过最头疼的事:同一个设备,在开阔地带精度2米,进了隧道直接丢星,出来之后坐标跳了50米。这种数据,你不清洗根本没法用。

核心要点:GPS采集的原始数据,包含大量冗余信息和噪声。我们真正需要的,是时间戳 + 经纬度 + 精度因子这三个核心字段。其他信息,比如卫星编号、信噪比,在清洗阶段基本用不上。

2.2 常见数据噪声——漂移、丢点、跳变

做轨迹回放这么多年,我总结出三类最常见的噪声:

噪声类型 表现 典型原因
漂移 静止时坐标来回跳动,幅度几米到几十米 多路径效应、卫星信号反射
丢点 时间序列中出现大段空白 信号遮挡、设备休眠、采样间隔不均
跳变 相邻两点距离异常大(比如瞬间移动几百米) 重新捕获卫星、坐标系统切换

为什么会这样?我举个例子。你在高架桥下走,GPS信号被桥体反射,设备收到的是反射后的信号,算出来的位置就偏了。这就是典型的漂移。而跳变,往往发生在设备从隧道出来,重新搜到卫星的那一刻——它以为自己瞬间飞出去了。

注意:漂移和跳变是两种不同的噪声。漂移是连续的、小幅度的抖动;跳变是离散的、大幅度的突变。处理策略完全不同,千万别混为一谈。

2.3 数据清洗策略——去重、滤波、插值

好了,知道了问题在哪,接下来就是怎么处理。我个人的清洗流程分三步走:

2.3.1 去重——把废话删掉

很多设备在静止时会重复上报同一个坐标。比如你停车等红灯,设备每秒钟上报一次,但坐标几乎没变。这些重复点对轨迹回放没有任何价值,反而会拖慢处理速度。

我的做法很简单:

def remove_duplicates(points, distance_threshold=0.5):
    """
    去除重复点:相邻两点距离小于阈值,则保留第一个
    points: [(timestamp, lat, lon), ...]
    """
    cleaned = [points[0]]
    for i in range(1, len(points)):
        dist = haversine(points[i-1][1:], points[i][1:])
        if dist > distance_threshold:
            cleaned.append(points[i])
    return cleaned

这里有个小技巧:阈值不要设得太小。0.5米对于民用GPS来说已经算很近了,设成0.1米反而会把正常抖动都当成重复点。

2.3.2 滤波——把噪声抹平

去重之后,剩下的就是真正的轨迹点了。但漂移还在。这时候就需要滤波。

我常用的两种滤波方法:

  • 中值滤波:取窗口内坐标的中位数,能有效去除孤立漂移点。适合处理跳变。
  • 卡尔曼滤波:基于运动模型预测下一位置,再与实际观测值融合。适合处理连续漂移。

说实话,卡尔曼滤波听起来高大上,但实际用起来参数调不好反而更糟。我建议新手先从简单的中值滤波开始。

def median_filter(points, window_size=5):
    """
    中值滤波:对经纬度分别做中值平滑
    """
    lats = [p[1] for p in points]
    lons = [p[2] for p in points]
    
    filtered_lats = []
    filtered_lons = []
    
    half = window_size // 2
    for i in range(len(points)):
        start = max(0, i - half)
        end = min(len(points), i + half + 1)
        filtered_lats.append(sorted(lats[start:end])[len(lats[start:end])//2])
        filtered_lons.append(sorted(lons[start:end])[len(lons[start:end])//2])
    
    return [(points[i][0], filtered_lats[i], filtered_lons[i]) for i in range(len(points))]

经验之谈:窗口大小选3或5就够了。窗口太大,轨迹会变得过于平滑,丢失真实转弯细节。我曾经在一个项目中用了窗口11,结果回放出来的轨迹把直角弯都抹成了圆弧,客户直接投诉。

2.3.3 插值——把丢的点补回来

丢点问题,说白了就是时间序列不完整。比如设备每5秒上报一次,但中间有10秒没数据。这时候就需要插值。

我习惯用线性插值:

def linear_interpolate(points, target_interval=1.0):
    """
    线性插值:按目标时间间隔补全缺失点
    """
    if len(points) < 2:
        return points
    
    result = []
    for i in range(len(points) - 1):
        t1, lat1, lon1 = points[i]
        t2, lat2, lon2 = points[i+1]
        
        result.append(points[i])
        
        # 如果间隔大于目标间隔,插入中间点
        gap = t2 - t1
        if gap > target_interval * 1.5:
            num_insert = int(gap / target_interval) - 1
            for j in range(1, num_insert + 1):
                ratio = j / (num_insert + 1)
                t = t1 + ratio * gap
                lat = lat1 + ratio * (lat2 - lat1)
                lon = lon1 + ratio * (lon2 - lon1)
                result.append((t, lat, lon))
    
    result.append(points[-1])
    return result

注意:插值只适用于短时间丢点(比如几秒到十几秒)。如果丢了几分钟甚至更久,线性插值就不靠谱了。这时候我建议直接标记为「数据缺失段」,不要强行补。

2.4 本章知识体系总览

下面这张图,是我自己总结的数据采集与清洗的完整流程。你可以把它当作一个检查清单:

数据采集与清洗流程 GPS坐标采集 原始NMEA数据 → 解析 识别噪声类型 漂移 / 丢点 / 跳变 数据清洗 去重 → 滤波 → 插值 去重 距离阈值法 滤波 中值滤波 / 卡尔曼滤波 插值 线性插值 / 标记缺失 清洗后的高质量轨迹数据 时间连续、坐标平滑、无重复 注意:清洗顺序不能乱。先去重,再滤波,最后插值。 如果先插值再滤波,会把噪声也插进去,越插越乱。

2.5 避坑指南——我曾经踩过的坑

最后,分享几个我亲身经历过的教训:

  • 不要迷信高精度设备:我曾经花大价钱买了RTK设备,以为数据就不用清洗了。结果在树荫下,照样漂移。精度再高的设备,也架不住信号遮挡。
  • 时间戳一定要对齐:不同设备的时间戳格式可能不一样,有的用Unix时间戳,有的用字符串。清洗前先统一成一种格式,否则后面插值会乱套。
  • 保留原始数据:清洗后的数据虽然好用,但原始数据别删。万一清洗策略有问题,你还能回退重来。我习惯在文件名上加个_raw后缀,永远保留一份。

一句话总结:数据清洗不是锦上添花,而是雪中送炭。没有干净的原始数据,再牛的算法也救不了你的轨迹回放。

好了,这一章就到这里。记住:采集是基础,清洗是关键。把这两步做扎实了,后面的轨迹回放才能顺风顺水。

可可精品资料库