坐标系与空间参考:地理坐标系(WGS84)、局部坐标系(NED/ENU)、坐标转换原理

做航线规划,第一个绕不开的问题就是:飞机到底在哪儿?

你可能会说,GPS 告诉我不就完了?嗯,GPS 确实能告诉你经纬度,但航线规划不是只靠经纬度就能搞定的。我刚开始做无人机系统那会儿,就吃过这个亏——拿着 WGS84 坐标直接算距离,结果偏差大得离谱。

今天咱们就把坐标系这件事彻底讲清楚。说白了,就是搞清楚两套坐标系:地理坐标系和局部坐标系,以及它们之间怎么互相转换。

2.1 地理坐标系:WGS84

WGS84 是目前全球最通用的地理坐标系。GPS 卫星发下来的原始数据,就是 WGS84 坐标。

它用三个值来描述一个点:纬度(Latitude)、经度(Longitude)、高度(Altitude)。

  • 纬度:赤道为 0°,南北极各 90°。北正南负。
  • 经度:本初子午线为 0°,东正西负,范围 -180° ~ 180°。
  • 高度:通常指椭球高,不是海拔高。这个区别很重要,后面会讲。

核心概念:WGS84 是一个椭球模型

地球不是完美的球体,而是赤道略鼓、两极略扁的椭球。WGS84 定义了这个椭球的参数:长半轴 a = 6378137 m,扁率 f = 1/298.257223563。

所有 GPS 坐标,都是在这个椭球面上定义的。

我记得有一次做测绘项目,客户给的航点坐标是 WGS84 格式,但他们的地面站软件默认用的是海拔高。结果飞机飞过去,高度差了将近 50 米。后来一查,是椭球高和海拔高的转换没做。

避坑指南:椭球高 ≠ 海拔高

WGS84 的高度是相对于椭球面的。而海拔高是相对于大地水准面的。两者之间的差值叫大地水准面差距(Geoid Undulation),不同地区差异很大,从 -100m 到 +100m 都有可能。

我曾经在一个山区项目里,直接用 GPS 高度做仿地飞行,结果飞机差点撞山。从那以后,我每次都会检查高度基准。

2.2 局部坐标系:NED 与 ENU

WGS84 坐标适合描述全球位置,但不适合做局部计算。你想想看,用经纬度算两点之间的距离,要涉及球面三角学,计算量很大。而且,飞机飞行的姿态控制、速度控制,都是在局部坐标系里完成的。

局部坐标系有两种主流选择:NED 和 ENU。

特性 NED(北-东-地) ENU(东-北-天)
X 轴 指向北(North) 指向东(East)
Y 轴 指向东(East) 指向北(North)
Z 轴 指向地心(Down) 指向天顶(Up)
常用领域 航空、无人机姿态控制 机器人、自动驾驶、测绘
Z 值方向 向下为正(高度越低值越大) 向上为正(高度越高值越大)

我个人习惯在无人机航线规划中使用 NED 坐标系。原因很简单:PX4 和 ArduPilot 的底层姿态控制都是基于 NED 的。你用 ENU 算出来的航点,最后还是要转成 NED 才能发给飞控。

但如果你做的是地面机器人或者自动驾驶,ENU 更常见。没有绝对的对错,看你的应用场景。

2.3 坐标转换原理

坐标转换,说白了就是两件事:平移和旋转。

从 WGS84 转到局部坐标系,一般分两步走:

  1. WGS84 → ECEF(地心地固坐标系):把经纬度转成三维直角坐标。
  2. ECEF → 局部坐标系(NED/ENU):以某个参考点为原点,做旋转和平移。

为什么中间要插一个 ECEF?因为 WGS84 是球面坐标,不能直接做线性变换。ECEF 是直角坐标系,原点在地心,X 轴指向本初子午线与赤道交点,Z 轴指向北极。

2.3.1 WGS84 → ECEF

公式如下(我直接给你代码,比公式更直观):

import math

def wgs84_to_ecef(lat, lon, alt):
    # WGS84 参数
    a = 6378137.0          # 长半轴
    f = 1.0 / 298.257223563  # 扁率
    e2 = 2*f - f*f         # 第一偏心率平方

    lat_rad = math.radians(lat)
    lon_rad = math.radians(lon)

    N = a / math.sqrt(1 - e2 * math.sin(lat_rad)**2)

    x = (N + alt) * math.cos(lat_rad) * math.cos(lon_rad)
    y = (N + alt) * math.cos(lat_rad) * math.sin(lon_rad)
    z = (N * (1 - e2) + alt) * math.sin(lat_rad)

    return x, y, z

这段代码我用了很多年,基本没出过问题。唯一要注意的是:alt 必须是椭球高,不是海拔高。

2.3.2 ECEF → NED

这一步需要选一个参考点。通常选起飞点或者地面站的位置作为原点。

def ecef_to_ned(ref_lat, ref_lon, ref_alt, target_ecef):
    # 参考点转 ECEF
    ref_ecef = wgs84_to_ecef(ref_lat, ref_lon, ref_alt)

    # 计算差值
    dx = target_ecef[0] - ref_ecef[0]
    dy = target_ecef[1] - ref_ecef[1]
    dz = target_ecef[2] - ref_ecef[2]

    # 旋转矩阵(从 ECEF 到 NED)
    lat_rad = math.radians(ref_lat)
    lon_rad = math.radians(ref_lon)

    # 注意:这个矩阵是把 ECEF 的向量旋转到 NED 坐标系
    north = -math.sin(lat_rad)*math.cos(lon_rad)*dx \
            -math.sin(lat_rad)*math.sin(lon_rad)*dy \
            + math.cos(lat_rad)*dz

    east  = -math.sin(lon_rad)*dx + math.cos(lon_rad)*dy

    down  = -math.cos(lat_rad)*math.cos(lon_rad)*dx \
            -math.cos(lat_rad)*math.sin(lon_rad)*dy \
            -math.sin(lat_rad)*dz

    return north, east, down

小技巧:如果你只需要做短距离(几公里以内)的航线规划,可以简化计算——直接用经纬度差值乘以对应的弧长系数(1°纬度 ≈ 111km,1°经度 ≈ 111km * cos(lat))。

但超过 10 公里,我建议你还是老老实实用 ECEF 转换,否则误差会累积到不可接受的程度。

2.4 知识体系总览

下面这张图把整个坐标系体系串起来了。你可以看到,WGS84 是源头,ECEF 是桥梁,NED/ENU 是最终的工作坐标系。

坐标系与空间参考知识体系 坐标系与空间参考知识体系 WGS84 地理坐标系 纬度 / 经度 / 椭球高 ECEF 地心地固坐标系 X / Y / Z 直角坐标 NED 局部坐标系 北 / 东 / 地 航空、无人机姿态控制 ENU 局部坐标系 东 / 北 / 天 机器人、自动驾驶、测绘 核心转换路径 WGS84 → ECEF → NED/ENU(以参考点为原点)

2.5 实际项目中的坐标系选择建议

做航线规划这么多年,我总结了几条经验:

  • 短距离(< 5km):直接用经纬度差值乘以弧长系数,够用。省去 ECEF 转换的麻烦。
  • 中距离(5km ~ 50km):用 ECEF 做精确转换,参考点选起飞点。
  • 长距离(> 50km):分段规划。每段选一个局部参考点,避免单点转换带来的累积误差。
  • 高度敏感场景:务必确认高度基准。如果是海拔高,需要查当地的大地水准面差距,做修正。

又一个坑:有些飞控的航点协议里,高度字段默认是相对起飞点的高度(相对高),不是椭球高也不是海拔高。我曾经在导入第三方航点文件时,没注意这个细节,结果飞机起飞后直接往地里钻。

所以,每次做坐标转换,都要搞清楚:输入是什么基准?输出是什么基准?

嗯,坐标系这部分就讲到这里。内容不多,但都是基本功。你只要把 WGS84、ECEF、NED/ENU 这三者的关系理清楚,后面的航线规划就会顺畅很多。