4. 数据预处理
各位同学,欢迎来到实战环节。前面我们聊了定位漂移的原理和整体方案,今天咱们要动真格的了——处理原始NMEA数据。
说实话,我早年刚入行时,觉得数据预处理就是“体力活”,随便写个脚本跑一跑就行。结果呢?有一次在车载项目里,因为没处理好一个异常值,导致整个轨迹回放像“鬼画符”,被客户当场质疑专业能力。从那以后,我再也不敢小看这一步。
数据预处理,说白了就是“把垃圾变成黄金”。你想想看,GPS模块吐出来的NMEA数据,里面夹杂了多少噪声、重复帧、甚至乱码?如果不做清洗,后面的定位算法再牛也白搭。
4.1 NMEA数据解析——从字符串到结构化数据
NMEA 0183协议是GPS模块最常用的输出格式。每一行以$开头,以\r\n结尾。最常见的语句是$GPGGA和$GPRMC。
我个人习惯用$GPGGA,因为它包含了经纬度、海拔、卫星数、HDOP(水平精度因子)等关键信息。解析时,我一般按逗号分割字符串,然后逐字段提取。
核心字段说明($GPGGA):
| 字段索引 | 含义 | 示例值 |
|---|---|---|
| 0 | 语句ID | $GPGGA |
| 1 | UTC时间 | 123519.00 |
| 2 | 纬度 | 4807.038 |
| 3 | 纬度方向 | N |
| 4 | 经度 | 01131.000 |
| 5 | 经度方向 | E |
| 6 | 定位质量 | 1(0=无效,1=单点,2=差分) |
| 7 | 卫星数 | 08 |
| 8 | HDOP | 1.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颗星,定位不可靠
我建议的清洗策略是“三步走”:
- 去重:基于时间戳+语句ID,保留第一条,丢弃后续重复的
- 排序:按时间戳升序排列,丢弃乱序的帧
- 过滤:只保留定位质量>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解析:把字符串变成结构体,注意纬度经度的度分格式
- 数据清洗:去重、排序、过滤无效定位
- 异常值剔除:用速度+加速度联合约束,阈值要因地制宜
- 坐标转换:WGS84→GCJ02,算法固定但精度有限