2. 坐标系基础

做无人机航点任务链,第一个绕不开的坎就是坐标系。我见过不少新手,代码写得挺漂亮,结果飞机一飞就偏到隔壁山头去了——十有八九是坐标系搞混了。

说白了,坐标系就是给地球上的点贴标签。标签贴错了,导航就乱了。今天咱们把 WGS84、ENU 和它们之间的转换彻底讲透。

WGS84:地球的“全球身份证”

WGS84 全称 World Geodetic System 1984。它是目前 GPS 使用的标准坐标系。你手机里拿到的经纬度,就是 WGS84 坐标。

它的核心参数就三个:

  • 经度(Longitude):东西方向,范围 -180° ~ 180°
  • 纬度(Latitude):南北方向,范围 -90° ~ 90°
  • 海拔高度(Altitude):相对于 WGS84 椭球面的高度,单位米

注意,这里的高度是椭球高,不是海拔高。我在项目中遇到过,有人直接用 GPS 高度当海拔,结果飞机在山区飞的时候,离地面忽高忽低。嗯,这里要区分一下:椭球高 ≠ 海拔高,中间差一个大地水准面起伏。

关键点:WGS84 是一个地心地固坐标系(ECEF),原点在地球质心,Z 轴指向北极,X 轴指向本初子午线与赤道的交点。它描述的是地球上的绝对位置。

ENU:局部坐标系,飞手的好朋友

ENU 全称 East-North-Up。它是一个局部直角坐标系,原点通常设在起飞点或某个参考点。

  • E 轴(东):指向正东方向
  • N 轴(北):指向正北方向
  • U 轴(天):指向天空,垂直于当地水平面

为什么需要 ENU?你想想看,WGS84 坐标是球面上的角度,计算两点之间的距离、方向,得用球面三角公式,麻烦得很。ENU 是平面直角坐标系,加减乘除就能算,多省事。

我个人习惯,所有航点任务链的路径规划、避障计算,都在 ENU 坐标系里做。只在最后下发指令时,才转回 WGS84 给飞控。

经验之谈:ENU 坐标系的原点选在起飞点最方便。这样飞机起飞时坐标就是 (0, 0, 0),后续所有航点都是相对于起飞点的偏移量。调试时一眼就能看出位置对不对。

坐标系转换:从球面到平面,再回来

转换的核心思路分两步:

  1. WGS84 → ECEF:把经纬高转成地心地固直角坐标
  2. ECEF → ENU:把地心坐标旋转平移成局部坐标

反过来也一样,ENU → ECEF → WGS84。

我画了一张流程图,帮你理清关系:

坐标系转换流程 WGS84 (lat, lon, alt) ECEF (X, Y, Z) ENU (E, N, U) 公式转换 旋转+平移 逆转换 核心思路 WGS84 是球面坐标,ENU 是平面坐标 通过 ECEF 作为中间桥梁完成转换

代码实现:Python 版转换工具

理论讲完了,上代码。这是我项目里一直在用的转换函数,经过多次飞行验证,精度够用。

import math

def wgs84_to_ecef(lat, lon, alt):
    """WGS84 转 ECEF"""
    a = 6378137.0          # 地球长半轴
    f = 1 / 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

def ecef_to_enu(X, Y, Z, lat0, lon0, alt0):
    """ECEF 转 ENU,参考点 (lat0, lon0, alt0)"""
    X0, Y0, Z0 = wgs84_to_ecef(lat0, lon0, alt0)

    dx = X - X0
    dy = Y - Y0
    dz = Z - Z0

    lat0_rad = math.radians(lat0)
    lon0_rad = math.radians(lon0)

    # 旋转矩阵
    E = -math.sin(lon0_rad) * dx + math.cos(lon0_rad) * dy
    N = (-math.sin(lat0_rad) * math.cos(lon0_rad) * dx
         - math.sin(lat0_rad) * math.sin(lon0_rad) * dy
         + math.cos(lat0_rad) * dz)
    U = (math.cos(lat0_rad) * math.cos(lon0_rad) * dx
         + math.cos(lat0_rad) * math.sin(lon0_rad) * dy
         + math.sin(lat0_rad) * dz)

    return E, N, U

def enu_to_wgs84(E, N, U, lat0, lon0, alt0):
    """ENU 转 WGS84"""
    lat0_rad = math.radians(lat0)
    lon0_rad = math.radians(lon0)

    # 逆旋转矩阵
    X = (-math.sin(lon0_rad) * E
         - math.sin(lat0_rad) * math.cos(lon0_rad) * N
         + math.cos(lat0_rad) * math.cos(lon0_rad) * U) + wgs84_to_ecef(lat0, lon0, alt0)[0]
    Y = (math.cos(lon0_rad) * E
         - math.sin(lat0_rad) * math.sin(lon0_rad) * N
         + math.cos(lat0_rad) * math.sin(lon0_rad) * U) + wgs84_to_ecef(lat0, lon0, alt0)[1]
    Z = (math.cos(lat0_rad) * N
         + math.sin(lat0_rad) * U) + wgs84_to_ecef(lat0, lon0, alt0)[2]

    # ECEF 转 WGS84(迭代法)
    a = 6378137.0
    f = 1 / 298.257223563
    e2 = 2*f - f*f

    p = math.sqrt(X**2 + Y**2)
    theta = math.atan2(Z * a, p * (a * (1 - f)))

    lat = math.atan2(Z + e2 * a * math.sin(theta)**3,
                     p - e2 * a * math.cos(theta)**3)
    lon = math.atan2(Y, X)
    alt = p / math.cos(lat) - a / math.sqrt(1 - e2 * math.sin(lat)**2)

    return math.degrees(lat), math.degrees(lon), alt

使用建议:把参考点 (lat0, lon0, alt0) 设为起飞点。起飞前记录一次,后续所有航点都用这个参考点做转换。我习惯在任务开始时调用一次 set_reference(),后面只管 ENU 坐标。

常见坐标系对比

坐标系 类型 原点 单位 适用场景
WGS84 球面坐标 地球质心 度、米 GPS 定位、航点存储
ECEF 直角坐标 地球质心 米 中间转换桥梁
ENU 直角坐标 局部参考点 米 路径规划、避障计算
NED 直角坐标 局部参考点 米 飞控姿态控制

NED 是北东地坐标系,飞控内部常用。不过做航点任务链,ENU 更直观——向上是正,符合直觉。

避坑指南:我曾经在转换时忘记把经纬度转成弧度,结果算出来的 ENU 坐标差了十万八千里。还有一次,参考点选错了,飞机起飞后直接往反方向飞。记住:参考点必须和起飞点一致,转换前先检查单位。

精度问题:什么时候需要高精度?

普通消费级无人机,GPS 精度在 2~5 米。用上面的公式足够了。但如果你做测绘、巡检这类高精度任务,需要考虑:

  • RTK 定位:厘米级精度,转换公式不变,但输入数据更准
  • 大地水准面模型:椭球高转海拔高,需要 EGM2008 等模型
  • 地球曲率:航点距离超过 10 公里时,ENU 的平面近似误差会变大

我个人习惯,10 公里以内的任务,直接用上面的代码。超过 10 公里,我会分段设置参考点,或者改用 UTM 投影。

好了,坐标系这块就讲这么多。记住一句话:WGS84 存数据,ENU 做计算。转换代码写一次,后面所有航点任务都能复用。