坐标系与空间参考:WGS84、ENU、高度系统与坐标转换
做航点规划,第一个绕不开的问题就是:这个点到底在哪儿?
你想想看,无人机在天上飞,地面站屏幕上显示一个经纬度,飞控内部算的是米制偏移,高度还有两种说法。如果这些坐标系没理清楚,轻则航线偏个几十米,重则直接撞山。我早年做测绘无人机项目时就吃过这个亏——WGS84 和局部坐标搞混了,飞机差点飞到禁飞区里去。
这一章,咱们就把坐标系这件事彻底讲透。
1. WGS84 坐标系:全球统一的“经纬度语言”
WGS84 全称 World Geodetic System 1984。说白了,它是一个地心地图坐标系——地球被抽象成一个椭球体,原点在地球质心。
我们平时说的经纬度(Latitude, Longitude)就是 WGS84 的表达方式。纬度范围 -90° 到 +90°,经度范围 -180° 到 +180°。高度部分用的是椭球高(Ellipsoidal Height),这个后面会细说。
核心要点:WGS84 是 GPS 卫星使用的原生坐标系。你从 GPS 模块读到的原始数据,就是 WGS84 经纬度 + 椭球高。
举个例子,我常用的测试点坐标:
纬度: 39.9042° N
经度: 116.4074° E
椭球高: 43.5 m
这个坐标在北京天安门附近。但注意,这个 43.5 米是椭球高,不是海拔高。两者差多少?后面会讲。
2. 局部坐标系(ENU):飞控真正“认”的坐标系
WGS84 虽然全球统一,但做航点规划时用起来很别扭。你想啊,两个航点之间距离 100 米,你用经纬度差值去算,还得考虑地球曲率,太麻烦了。
飞控内部实际用的是局部笛卡尔坐标系,最常见的就是 ENU(East-North-Up)。
- E(East):东向,单位米
- N(North):北向,单位米
- U(Up):天向,单位米
ENU 的原点通常选在起飞点或地面站位置。这样一来,所有航点都变成了相对于原点的三维偏移量。飞控算起来就是简单的加减法,效率极高。
我的习惯:做航点规划时,我会先在 WGS84 里定义好关键航点(比如起飞点、降落点),然后所有中间航点都用 ENU 偏移量来表达。这样既保留了全局坐标的绝对性,又享受了局部坐标的计算便利。
3. 高度系统:AGL 与 MSL 的“生死之差”
高度问题,是航点规划里最容易出 bug 的地方。我见过不止一次,新手把 MSL 当成 AGL 用,结果飞机在山区直接撞树。
| 高度类型 | 全称 | 基准面 | 典型用途 |
|---|---|---|---|
| MSL | Mean Sea Level | 平均海平面 | 航线规划、空域管理 |
| AGL | Above Ground Level | 正下方地面 | 避障、仿地飞行 |
| Ellipsoidal Height | 椭球高 | WGS84 椭球面 | GPS 原始输出 |
三者的关系可以用一个简单公式表达:
椭球高 ≈ MSL + 大地水准面差距(Geoid Undulation)
AGL = MSL - 地面高程(DEM 或实时测距)
我曾经踩过的坑:在山区做电力巡检航线规划时,我直接用 MSL 高度设定了航点。结果飞机飞到山脊上方时,实际 AGL 只有不到 10 米,差点撞上铁塔。后来我强制要求:所有仿地航线必须用 AGL 模式,并且叠加 DEM 数据做校验。
4. 坐标转换原理:从 WGS84 到 ENU 的“数学桥梁”
实际工作中,我们经常需要在 WGS84 和 ENU 之间来回转换。转换过程分两步:
- WGS84 → ECEF(地心地固坐标系):把经纬度转成以地球质心为原点的三维直角坐标(X, Y, Z)。
- ECEF → ENU:以某个参考点(比如起飞点)为原点,做旋转和平移,得到东、北、天三个方向的偏移量。
下面是一个 Python 实现的核心代码片段:
import math
def wgs84_to_enu(lat, lon, alt, ref_lat, ref_lon, ref_alt):
# 1. 将 WGS84 转 ECEF
a = 6378137.0 # 地球长半轴
e2 = 0.00669437999014 # 第一偏心率平方
lat_rad = math.radians(lat)
lon_rad = math.radians(lon)
ref_lat_rad = math.radians(ref_lat)
ref_lon_rad = math.radians(ref_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)
# 2. 计算参考点的 ECEF 坐标
N_ref = a / math.sqrt(1 - e2 * math.sin(ref_lat_rad)**2)
x_ref = (N_ref + ref_alt) * math.cos(ref_lat_rad) * math.cos(ref_lon_rad)
y_ref = (N_ref + ref_alt) * math.cos(ref_lat_rad) * math.sin(ref_lon_rad)
z_ref = (N_ref * (1 - e2) + ref_alt) * math.sin(ref_lat_rad)
# 3. ECEF 差值转 ENU
dx = x - x_ref
dy = y - y_ref
dz = z - z_ref
e = -math.sin(ref_lon_rad) * dx + math.cos(ref_lon_rad) * dy
n = (-math.sin(ref_lat_rad) * math.cos(ref_lon_rad) * dx
- math.sin(ref_lat_rad) * math.sin(ref_lon_rad) * dy
+ math.cos(ref_lat_rad) * dz)
u = (math.cos(ref_lat_rad) * math.cos(ref_lon_rad) * dx
+ math.cos(ref_lat_rad) * math.sin(ref_lon_rad) * dy
+ math.sin(ref_lat_rad) * dz)
return e, n, u
实际工程建议:不要自己手写转换逻辑,除非你在做底层飞控。直接用成熟的库,比如 pyproj 或 geographiclib。我团队内部统一用 pyproj,一行代码搞定转换,还自动处理了大地水准面模型。
5. 知识体系总览
下面这张图把本章的核心概念串起来了。你可以看到:WGS84 是“全球坐标”,ENU 是“局部坐标”,高度系统是“垂直基准”,转换公式是“连接桥梁”。
6. 避坑指南:坐标系相关的常见错误
- 混淆 MSL 和椭球高:在中国大陆大部分地区,大地水准面差距在 -10m 到 +30m 之间。直接用 GPS 椭球高当海拔用,误差可能超过 30 米。
- ENU 原点不一致:地面站和飞控的 ENU 原点必须严格一致。我见过一个案例,地面站用起飞点做原点,飞控用当前位置做原点,结果航线偏了 200 米。
- 忽略地球曲率:航点间距超过 10 公里时,直接用平面几何算 ENU 会有明显误差。这时候必须用大圆航线或测地线算法。
最后提醒一句:坐标系转换不是“写对公式就行”的事。不同飞控固件(PX4、ArduPilot)对高度和坐标的处理细节有差异。做跨平台移植时,一定要在仿真环境里用实际数据验证一遍。