坐标系基础: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. 知识体系总览

下面这张图,是我自己梳理的坐标系转换关系。你一看就明白:

坐标系转换关系图 WGS-84 GPS 原始坐标 GCJ-02 高德/腾讯/谷歌中国 BD-09 百度地图 加密转换 二次加密 迭代法逆转换 公式逆运算 使用建议 • 国际应用:用 WGS-84 • 国内地图:用 GCJ-02 • 百度生态:用 BD-09 ⚠ 不要混用坐标系 ⚠ 转换前确认源坐标系 我踩过的坑: 1. 高德 SDK 默认 GCJ-02 2. 百度坐标需二次转换 3. 迭代法 5 次足够 4. 别手写转换公式

这张图里,从上到下是加密方向,从下到上是逆转换方向。我建议你把它存下来,每次做定位项目前看一眼,能省不少调试时间。

6. 实战中的坐标系选择策略

说了这么多理论,到底怎么选?我给出几条实战建议:

  1. 如果你做的是全球应用——直接用 WGS-84。别碰 GCJ-02 和 BD-09,那是给自己找麻烦。
  2. 如果你做的是国内应用——用 GCJ-02。高德、腾讯都支持,兼容性最好。
  3. 如果你必须用百度地图——用 BD-09。但要注意,百度 SDK 内部可能已经做了转换,先看文档再动手。
  4. 如果你在做定位漂移优化——统一坐标系是第一步。我见过最离谱的项目,一个系统里混用了三种坐标系,定位点满天飞。

我的小技巧:

在项目初期,我会在配置文件中写死一个坐标系枚举。所有输入数据先转成统一坐标系,再往下游处理。这样后期排查漂移问题时,至少能排除坐标系不一致这个因素。

好了,坐标系基础就讲到这里。记住一句话:坐标系不对,定位白费。下一节我们聊聊定位数据预处理,那又是另一番天地了。

可可精品资料库