坐标系基础:WGS-84、GCJ-02、BD-09 与坐标转换算法
做定位算法这些年,我踩过最大的坑,就是坐标系。
你想想看,明明 GPS 信号没问题,定位点却偏了几百米。查了半天,原来是坐标系没对齐。说白了,搞定位漂移优化,第一步就得把坐标系这事整明白。
1. WGS-84 坐标系:全球通用的“原始坐标”
WGS-84 是个啥?它是美国国防部搞的一个全球大地测量系统。GPS 卫星发下来的原始数据,用的就是这套坐标系。
我习惯叫它“地球的真实坐标”。它把地球近似成一个椭球体,用经度、纬度、高度来描述一个点的位置。精度嘛,民用级大概在 2-5 米左右。
核心特点:
- 全球统一,所有 GPS 设备默认输出
- 坐标原点在地球质心
- 民用精度约 2-5 米(无差分校正时)
我在项目里遇到过,有些同学直接把 WGS-84 坐标往地图上怼,结果发现点位全飘到海里去了。嗯,这里要注意——国内的地图服务商,基本都不直接用 WGS-84。
2. GCJ-02 火星坐标系:中国特色的“加密坐标”
GCJ-02,俗称“火星坐标系”。为什么叫火星?因为 WGS-84 坐标经过它一转换,位置就偏了那么几十到几百米,像掉到了火星上一样。
这是中国国家测绘局搞的加密算法。说白了,就是给 WGS-84 坐标加了个非线性偏移。所有国内的地图服务商——高德、腾讯、谷歌中国——用的都是 GCJ-02。
避坑指南:
我曾经在对接高德地图 SDK 时,直接传了 GPS 的 WGS-84 坐标。结果呢?定位点全偏了 300 米以上。后来才发现,SDK 内部默认输入就是 GCJ-02,不需要再转一次。你想想看,这坑踩得多冤枉。
3. BD-09 百度坐标系:百度自家的“二次加密”
百度地图更狠。它在 GCJ-02 的基础上,又做了一层加密,搞出了 BD-09。说白了,就是火星坐标再加密一次。
我做过测试,WGS-84 转 BD-09,偏移量大概在 600 米左右。所以如果你用百度地图,千万别直接传 GPS 坐标。
| 坐标系 | 使用方 | 加密层级 | 相对 WGS-84 偏移 |
|---|---|---|---|
| WGS-84 | GPS 设备、国际应用 | 无加密 | 0 米 |
| GCJ-02 | 高德、腾讯、谷歌中国 | 一次加密 | 约 100-300 米 |
| BD-09 | 百度地图 | 二次加密 | 约 600 米 |
4. 坐标系转换算法:实战中的“翻译官”
搞定位漂移优化,坐标系转换是基本功。我整理了一套常用的转换逻辑,你直接拿去用。
4.1 WGS-84 转 GCJ-02
这个转换是非线性的,核心是一个查表加插值的算法。不过业界已经有成熟的开源实现,我贴一段核心代码:
// WGS-84 转 GCJ-02 核心算法(C# 示例)
public static double[] WGS84ToGCJ02(double lat, double lng)
{
double a = 6378245.0; // 地球长半轴
double ee = 0.00669342162296594323; // 偏心率平方
double dLat = TransformLat(lng - 105.0, lat - 35.0);
double dLng = TransformLng(lng - 105.0, lat - 35.0);
double radLat = lat / 180.0 * Math.PI;
double magic = Math.Sin(radLat);
magic = 1 - ee * magic * magic;
double sqrtMagic = Math.Sqrt(magic);
dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * Math.PI);
dLng = (dLng * 180.0) / (a / sqrtMagic * Math.Cos(radLat) * Math.PI);
double gcjLat = lat + dLat;
double gcjLng = lng + dLng;
return new double[] { gcjLat, gcjLng };
}
我的经验:
这段代码我用了好几年,精度在 1 米以内。但要注意,TransformLat 和 TransformLng 这两个函数内部有多项式拟合,不同版本可能略有差异。我建议你直接从开源库(比如 proj4)里拿现成的,别自己手写。
4.2 GCJ-02 转 WGS-84
这个转换是上面过程的逆运算。但因为是非线性加密,没法直接求逆。我常用的方法是迭代逼近:
// GCJ-02 转 WGS-84(迭代法)
public static double[] GCJ02ToWGS84(double gcjLat, double gcjLng)
{
double[] wgs = new double[] { gcjLat, gcjLng };
double[] temp;
double dLat, dLng;
for (int i = 0; i < 5; i++) // 迭代 5 次足够
{
temp = WGS84ToGCJ02(wgs[0], wgs[1]);
dLat = temp[0] - gcjLat;
dLng = temp[1] - gcjLng;
wgs[0] -= dLat;
wgs[1] -= dLng;
}
return wgs;
}
为什么迭代 5 次就够了?我实测过,3 次迭代精度就能到 0.5 米以内,5 次基本收敛。再多就是浪费算力了。
4.3 GCJ-02 与 BD-09 互转
百度坐标系的加密算法是公开的,转换相对简单:
// GCJ-02 转 BD-09
public static double[] GCJ02ToBD09(double gcjLat, double gcjLng)
{
double x = gcjLng, y = gcjLat;
double z = Math.Sqrt(x * x + y * y) + 0.00002 * Math.Sin(y * Math.PI);
double theta = Math.Atan2(y, x) + 0.000003 * Math.Cos(x * Math.PI);
double bdLng = z * Math.Cos(theta) + 0.0065;
double bdLat = z * Math.Sin(theta) + 0.006;
return new double[] { bdLat, bdLng };
}
注意:
BD-09 转 GCJ-02 是逆过程,公式类似但符号相反。我曾经在项目中写反了加减号,结果坐标全飞到了非洲。嗯,这种低级错误,犯一次就够了。
5. 知识体系总览
下面这张图,是我自己梳理的坐标系转换关系。你一看就明白:
这张图里,从上到下是加密方向,从下到上是逆转换方向。我建议你把它存下来,每次做定位项目前看一眼,能省不少调试时间。
6. 实战中的坐标系选择策略
说了这么多理论,到底怎么选?我给出几条实战建议:
- 如果你做的是全球应用——直接用 WGS-84。别碰 GCJ-02 和 BD-09,那是给自己找麻烦。
- 如果你做的是国内应用——用 GCJ-02。高德、腾讯都支持,兼容性最好。
- 如果你必须用百度地图——用 BD-09。但要注意,百度 SDK 内部可能已经做了转换,先看文档再动手。
- 如果你在做定位漂移优化——统一坐标系是第一步。我见过最离谱的项目,一个系统里混用了三种坐标系,定位点满天飞。
我的小技巧:
在项目初期,我会在配置文件中写死一个坐标系枚举。所有输入数据先转成统一坐标系,再往下游处理。这样后期排查漂移问题时,至少能排除坐标系不一致这个因素。
好了,坐标系基础就讲到这里。记住一句话:坐标系不对,定位白费。下一节我们聊聊定位数据预处理,那又是另一番天地了。