4. 数据预处理

各位同学,欢迎来到实战环节。前面我们聊了定位漂移的原理和整体方案,今天咱们要动真格的了——处理原始NMEA数据。

说实话,我早年刚入行时,觉得数据预处理就是“体力活”,随便写个脚本跑一跑就行。结果呢?有一次在车载项目里,因为没处理好一个异常值,导致整个轨迹回放像“鬼画符”,被客户当场质疑专业能力。从那以后,我再也不敢小看这一步。

数据预处理,说白了就是“把垃圾变成黄金”。你想想看,GPS模块吐出来的NMEA数据,里面夹杂了多少噪声、重复帧、甚至乱码?如果不做清洗,后面的定位算法再牛也白搭。

4.1 NMEA数据解析——从字符串到结构化数据

NMEA 0183协议是GPS模块最常用的输出格式。每一行以$开头,以\r\n结尾。最常见的语句是$GPGGA和$GPRMC。

我个人习惯用$GPGGA,因为它包含了经纬度、海拔、卫星数、HDOP(水平精度因子)等关键信息。解析时,我一般按逗号分割字符串,然后逐字段提取。

核心字段说明($GPGGA):

字段索引含义示例值
0语句ID$GPGGA
1UTC时间123519.00
2纬度4807.038
3纬度方向N
4经度01131.000
5经度方向E
6定位质量1(0=无效,1=单点,2=差分)
7卫星数08
8HDOP1.0
9海拔46.9
// 伪代码示例:解析$GPGGA
char *token = strtok(buffer, ",");
int fieldIndex = 0;
while (token != NULL) {
    switch (fieldIndex) {
        case 0: // 语句ID,跳过
            break;
        case 1: // UTC时间
            utc_time = atof(token);
            break;
        case 2: // 纬度(ddmm.mmmm格式)
            lat_dd = (int)(atof(token) / 100);
            lat_mm = atof(token) - lat_dd * 100;
            latitude = lat_dd + lat_mm / 60.0;
            break;
        case 4: // 经度(dddmm.mmmm格式)
            lon_ddd = (int)(atof(token) / 100);
            lon_mm = atof(token) - lon_ddd * 100;
            longitude = lon_ddd + lon_mm / 60.0;
            break;
        // ... 其他字段
    }
    token = strtok(NULL, ",");
    fieldIndex++;
}

避坑指南:我曾经在解析纬度时,直接用了atof(),结果发现北纬和南纬没区分。后来加了方向字段判断,才搞定。记住:纬度方向N/S、经度方向E/W,一定要处理符号!

4.2 数据清洗——剔除“脏数据”

解析完NMEA,你会发现数据里藏着不少“垃圾”。比如:

  • 重复帧:同一时刻收到多条相同语句
  • 乱序帧:时间戳不递增
  • 无效定位:定位质量字段为0
  • 卫星数不足:少于4颗星,定位不可靠

我建议的清洗策略是“三步走”:

  1. 去重:基于时间戳+语句ID,保留第一条,丢弃后续重复的
  2. 排序:按时间戳升序排列,丢弃乱序的帧
  3. 过滤:只保留定位质量>0且卫星数≥4的数据

注意:千万别把“HDOP过大”的数据直接扔掉。HDOP大说明水平精度差,但有时在隧道出口、高楼间,这是唯一可用的数据。我一般设个阈值(比如HDOP<5),超过的标记为“低置信度”,而不是直接删除。

4.3 异常值剔除——别让“野点”毁了轨迹

数据清洗之后,还有一类更隐蔽的问题——异常值。比如,GPS突然跳了100米,然后又跳回来。这种“野点”如果不处理,轨迹会像“心电图”。

我常用的方法是“速度+加速度联合约束”:

  • 速度约束:相邻两点距离/时间差,如果速度超过200km/h(约55m/s),视为异常
  • 加速度约束:相邻三点计算加速度,如果超过5m/s²,视为异常
// 伪代码:异常值剔除
bool isOutlier(Point p1, Point p2, Point p3) {
    double v1 = distance(p1, p2) / (p2.time - p1.time);
    double v2 = distance(p2, p3) / (p3.time - p2.time);
    double acc = (v2 - v1) / (p3.time - p1.time);
    
    if (v2 > 55.0) return true;  // 速度超过200km/h
    if (abs(acc) > 5.0) return true; // 加速度超过5m/s²
    return false;
}

个人经验:阈值别设太死。我在高速项目里,把速度阈值放宽到250km/h,因为测试车确实能跑到那个速度。而在步行场景,阈值降到30km/h就够了。你想想看,走路突然“瞬移”30米,肯定是异常。

4.4 坐标转换——WGS84转GCJ02

终于到了坐标转换。国内的地图服务(高德、腾讯)都使用GCJ02坐标系,而GPS模块输出的是WGS84。两者之间有个非线性偏移,大概在几百米到一千米不等。

转换算法是公开的,但要注意:GCJ02是加密坐标系,转换公式是固定的。我直接贴出核心代码:

// WGS84转GCJ02(简化版)
void wgs84_to_gcj02(double wgs_lat, double wgs_lon, 
                    double *gcj_lat, double *gcj_lon) {
    const double a = 6378245.0;      // 长半轴
    const double ee = 0.00669342162296594323; // 偏心率平方
    
    double dlat = transform_lat(wgs_lon - 105.0, wgs_lat - 35.0);
    double dlon = transform_lon(wgs_lon - 105.0, wgs_lat - 35.0);
    
    double radlat = wgs_lat / 180.0 * M_PI;
    double magic = sin(radlat);
    magic = 1 - ee * magic * magic;
    double sqrtmagic = sqrt(magic);
    
    dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * M_PI);
    dlon = (dlon * 180.0) / (a / sqrtmagic * cos(radlat) * M_PI);
    
    *gcj_lat = wgs_lat + dlat;
    *gcj_lon = wgs_lon + dlon;
}

重要提醒:转换后的坐标精度大约在1-5米。如果你做的是车道级定位,这个误差可能不够。我曾在高架桥项目里,因为转换误差导致车辆“跑”到对面车道,后来改用RTK+GCJ02融合才解决。

4.5 本章知识体系

为了让你更直观地理解整个流程,我画了一张图:

数据预处理流程 NMEA解析 $GPGGA/$GPRMC 数据清洗 去重/排序/过滤 异常值剔除 速度/加速度约束 坐标转换 WGS84→GCJ02 输出:清洗后的GCJ02坐标 可用于地图匹配/轨迹平滑 整个流程环环相扣,每一步都影响最终定位精度

嗯,到这里,数据预处理的核心内容就讲完了。你可能会问:“这些步骤的顺序能调换吗?”我的建议是:先解析,再清洗,然后剔除异常,最后做坐标转换。因为坐标转换会引入微小误差,如果先转换再清洗,可能会把正常点误判为异常。

总结一下:

  • NMEA解析:把字符串变成结构体,注意纬度经度的度分格式
  • 数据清洗:去重、排序、过滤无效定位
  • 异常值剔除:用速度+加速度联合约束,阈值要因地制宜
  • 坐标转换:WGS84→GCJ02,算法固定但精度有限