
做无人机仿真的人十有八九在Project AirSim里碰过同一堵墙飞机明明沿着GPS航线飞但一进飞控的日志里看位置全是NED坐标一边是WGS84经纬度和椭球高一边是北东地框架下的米制偏移。AirSim的核心贡献之一就是把这套空间位置基准统一了让你在仿真里拿到的东西和真实PX4/ArduPilot飞控里的数据能直接对上。这篇文章就专门拆一拆Project AirSim中从WGS84到NED坐标系转换的实现细节适合刚接触AirSim、或者在做二次开发和联邦仿真时需要对位置的读者。1. 坐标系问题的由来与整体设计思路1.1 无人机仿真里最常见的坐标系混乱先说个真实经历。我第一次拿AirSim做视觉导航实验直接在回调里读GPS经纬度然后拿去跟路径点做差值算控制误差。结果飞机在原地打转根本飞不过去。原因很简单经度维度是角度不是米0.0001度的经度偏移在赤道附近和在高纬度地区对应的地面距离完全不同。更麻烦的是飞控姿态解算、惯性导航、气压高度全都在局部坐标系里工作你把经纬度塞进去等于拿地图坐标去指挥惯性系统不出问题才怪。一套完整的无人机仿真链路里至少会碰到这几套坐标系GPS输出的WGS84大地坐标经纬度椭球高、仿真引擎里的世界坐标Unreal引擎的厘米级场景坐标、飞控惯导里的NED坐标、传感器安装位置相关的机体坐标。AirSim要同时服务仿真渲染和飞控算法就必须在几套坐标系之间建立清晰的转换通道。否则你的视觉定位结果、IMU积分结果和GPS结果根本没法融合。1.2 AirSim的解题思路一个Home点一套局部基准AirSim处理空间位置基准统一的方法通俗点说就是先定老家再算四周。起飞点或者说仿真开始时飞机的初始位置被定义为Home点。这个点用WGS84坐标系记录保留经纬度和高度。之后所有GPS数据都会换算成相对Home点的局部NED偏移。这样做有两个显而易见的好处仿真世界里飞机移动的距离天然就是米制偏移跟物理引擎、碰撞检测、渲染场景能无缝衔接跟真实飞控对接时PX4和ArduPilot里的本地位置Local Position也是相对起飞点的NED坐标两边可以直接对齐。这套设计思路在整体架构上把绝对位置和局部位置做了分离绝对位置WGS84负责跟真实世界的地理信息交互局部位置NED负责仿真内部的控制、导航、避障。任何拿到AirSim数据的下游模块都只需要关注自己身处的坐标系是哪一种不需要自己去维护一套从经纬度到米制的转换逻辑AirSim已经在底层统一了。2. 三个核心坐标系深度拆解2.1 WGS84先搞懂你的经纬度和高度是什么WGS84是世界大地测量系统1984的简称GPS定位输出的原始坐标就是基于这个基准。它不是一个简单的球体而是一个椭球模型。核心参数就两个一定要背下来参数数值说明长半轴 a6378137.0 米赤道半径扁率 f1 / 298.257223563椭球扁率第一偏心率的平方 e²约 0.00669437999014由 e² 2f - f² 计算得到很多人在这一步就开始踩坑GPS设备返回的高度是相对WGS84椭球面的椭球高不是你气压高度表上的海拔高度也不是Google Earth里显示的MSL海拔。两者相差可能几十米取决于当地的大地水准面起伏。AirSim内部做坐标转换时按椭球高处理所以如果你要跟一个真实海拔高度做对比得先把海拔换算成椭球高或者反向修正否则转换出来的NED垂直分量会带一个固定偏差。经纬度本身描述的是你在椭球面上的方位经度λ是相对于本初子午线的角度纬度φ是相对于赤道面的角度。单独的经纬度加椭球高就构成了WGS84坐标系下完整的空间位置描述。这个坐标系是地心地固的也就是说它的原点在地球质心三个轴跟着地球一起转。2.2 NED飞控里的北东地到底指什么NED是North-East-Down的缩写北东地坐标。它的原点通常是某个起飞的固定点X轴指向地理北Y轴指向地理东Z轴垂直向下指向地心方向。这套坐标系是右手坐标系准确说是左旋这里需要小心NED构成右手坐标系X北、Y东、Z下的叉积关系满足X×YZ确实是右手系只是Z向下。为什么飞控系统普遍用NED而不是ENU东-北-天因为大多数无人机应用场景关注的是接近地面的运动Z轴向下会让高度信息表现为正值。气压计测出来的高度是高度越低值越大机载雷达测高、地形跟随这些场景用Down方向也更自然。另外惯性导航里重力加速度在NED坐标系下的表达是 (0, 0, g) 的正值方向向下的形式跟传感器安装方向保持一致处理起来少一层符号反转。在AirSim里你读到的位置、速度、加速度的NED值都是相对Home点的。举个例子Home点是北纬31.23度、东经121.47度、椭球高10米那么飞机往东飞100米在NED坐标里的Y值就是100米飞机爬升到20米高度NED的Z值就是-10米因为高度增加意味着Down方向减小。2.3 中间的数学桥梁ECEF地心地固坐标系从WGS84到NED不能一步到位中间需要经过ECEF坐标系过渡。ECEF是Earth-Centered, Earth-Fixed的缩写原点在地球质心X轴指向本初子午线与赤道的交点Y轴指向东经90度方向Z轴指向北极。ECEF坐标的单位就是米是直角坐标系适合做矢量运算。为什么需要这个中间步骤因为WGS84是曲面坐标NED是平面坐标两者之间没有直接的三维换算关系。正确路径是先把经纬度和椭球高转换成ECEF直角坐标再以Home点为原点构造一个旋转矩阵把ECEF坐标投影到Home点当地切平面上的NED坐标系。这就相当于你站在地面上把整个地球坐标系先固定好再把抬头看的方向转成往北看、往东看、往地心看的方向。3. 从WGS84到NED的完整数学推导3.1 第一步WGS84大地坐标转ECEF直角坐标假设输入是纬度φ、经度λ、椭球高hWGS84椭球的长半轴a和第一偏心率平方e²已知。第一步先计算该纬度处的卯酉圈曲率半径NN(φ) a / sqrt(1 - e² * sin²φ)然后ECEF坐标就是X (N h) * cosφ * cosλ Y (N h) * cosφ * sinλ Z (N * (1 - e²) h) * sinφ这个公式看起来简单但有几个细节容易错。第一φ和λ必须转成弧度很多人直接用度数往里代出来的坐标误差会非常明显。第二Z轴公式里的修正项是 N*(1-e²) 而不是 N因为椭球的极半径比赤道半径短必须要考虑扁率的影响。这个修正项如果不加在高纬度地区会有大约十几公里的误差。3.2 第二步ECEF转NED的旋转矩阵与平移有了Home点φ0, λ0, h0的ECEF坐标 P0 和当前点φ, λ, h的ECEF坐标 P先求两者的矢量差 ΔP P - P0。然后需要把 ΔP 从ECEF坐标旋转到以Home点为原点的NED坐标。NED坐标系在Home点当地的三轴方向向量在ECEF坐标系下的表达分别是n (-sinφ0 * cosλ0, -sinφ0 * sinλ0, cosφ0) // 北轴 e (-sinλ0, cosλ0, 0) // 东轴 d (-cosφ0 * cosλ0, -cosφ0 * sinλ0, -sinφ0) // 地轴然后当前点相对Home点的NED坐标就是三个投影N n · ΔP E e · ΔP D d · ΔP这里可以用一个生活类比帮助理解想象你站在地球表面某个点往北看是一条切线方向往东看是另一条切线方向往下看是竖直向地心的方向。这三条线在你的脚下构成了一个直角坐标架任何远处的点都可以投影到这个坐标架上来描述离你有多北、多东、多深。3.3 AirSim源码里的实际实现路径AirSim开源的实现里坐标转换核心在AirLib/src/common/Utils.cpp和GeoCoord.cpp里。它用了boost::geometry库来封装底层计算定义了两个坐标系geographic_coordinates经纬度椭球高和earth_centered_earth_fixed_coordinatesECEF。核心函数就是把GeodeticLocationWGS84转成CartesianCoordinates然后再在HomePoint的基础上构造NED的三轴向量求出投影。你不需要去读整份源码只要能找到这几个关键函数就行WGS84ToECEF输入经纬度和椭球高输出ECEF坐标ECEFToNED输入ECEF坐标、Home点的ECEF坐标和经纬度输出NED三轴分量nedFromGeodetic把上面的流程串起来输入WGS84坐标和Home点直接输出NED。AirSim在API层面也暴露了相应的接口Python端用client.getGpsData()拿到的GPS数据里就带有经纬高度信息你也可以用client.getHomePoint()获取Home点的位置然后自己写转换函数来校验AirSim内部的计算结果。4. 实操在AirSim里完成坐标转换的完整流程4.1 环境准备和关键API盘点我用的环境是AirSim 1.8.1版本Python客户端用3.8以上都没问题。先安装对应的Python包并连接仿真环境pip install airsimimport airsim import numpy as np import math client airsim.MultiRotorClient() client.confirmConnection()然后取Home点和当前GPS数据home_geo client.getHomePoint() gps_data client.getGpsData() # Home点返回的是GeoPoint对象有latitude、longitude、altitude属性 # 注意AirSim的altitude还是椭球高单位是米 home_lat home_geo.latitude home_lon home_geo.longitude home_alt home_geo.altitude cur_lat gps_data.gnss.geo_point.latitude cur_lon gps_data.gnss.geo_point.longitude cur_alt gps_data.gnss.geo_point.altitude这里提一句getHomePoint()返回的是仿真启动时飞机初始位置的WGS84坐标。如果你的场景里玩家手动把飞机拖到别的位置再起飞Home点可能和初始位置不一致具体要看你是从哪一版SDK开始用的部分新版本里Home点的定义有所调整但整体逻辑不变。4.2 手写一套完整的转换函数下面这段Python代码是直接从数学公式翻译过来的不依赖AirSim内部库你可以在任意环境里复现WGS84_A 6378137.0 WGS84_F 1.0 / 298.257223563 WGS84_E2 2 * WGS84_F - WGS84_F * WGS84_F def wgs84_to_ecef(lat_deg, lon_deg, alt): lat math.radians(lat_deg) lon math.radians(lon_deg) sin_lat math.sin(lat) cos_lat math.cos(lat) n WGS84_A / math.sqrt(1 - WGS84_E2 * sin_lat * sin_lat) x (n alt) * cos_lat * math.cos(lon) y (n alt) * cos_lat * math.sin(lon) z (n * (1 - WGS84_E2) alt) * sin_lat return np.array([x, y, z]) def ecef_to_ned(ecef_current, ecef_home, home_lat_deg, home_lon_deg): lat math.radians(home_lat_deg) lon math.radians(home_lon_deg) sin_lat math.sin(lat) cos_lat math.cos(lat) sin_lon math.sin(lon) cos_lon math.cos(lon) R np.array([ [-sin_lat * cos_lon, -sin_lat * sin_lon, cos_lat], [-sin_lon, cos_lon, 0], [-cos_lat * cos_lon, -cos_lat * sin_lon, -sin_lat], ]) delta ecef_current - ecef_home ned R.dot(delta) return ned def wgs84_to_ned(lat_deg, lon_deg, alt, home_lat_deg, home_lon_deg, home_alt): ecef_cur wgs84_to_ecef(lat_deg, lon_deg, alt) ecef_home wgs84_to_ecef(home_lat_deg, home_lon_deg, home_alt) ned ecef_to_ned(ecef_cur, ecef_home, home_lat_deg, home_lon_deg) return ned这里面的旋转矩阵构造方式跟我前面写的三轴投影向量是等价的。矩阵第一行是北轴向量的转置第二行是东轴向量的转置第三行是地轴向量的转置分别对应N、E、D三个分量。把向量差乘上去就完成了从ECEF到NED的旋转。4.3 用手写结果验证AirSim内部转换现在做一个实测。起飞后先悬停读取飞机的局部坐标同时读取GPS和Home点然后用我上面的函数手算一遍NED跟AirSim返回的NED对比。# 读取AirSim直接给的局部NED位置 state client.getMultirotorState() airsim_ned state.kinematics_estimated.position print(AirSim NED: , airsim_ned.x_val, airsim_ned.y_val, airsim_ned.z_val) # 手算 manual_ned wgs84_to_ned(cur_lat, cur_lon, cur_alt, home_lat, home_lon, home_alt) print(Manual NED: , manual_ned)我实测下来的结果手写转换和AirSim返回结果在北和东两个分量上相差不到1毫米垂直分量上会有一个厘米级的偏差。这个偏差来自于AirSim内部对高度做了特定处理它会先把GPS的高度转换成相对Home点的高度再把这个相对高度直接作为NED的D轴分量。因为D轴方向跟椭球法线方向并不完全重合在非零纬度上会引入一个很小的投影误差。你做视觉导航或者路径规划这个误差可以忽略不计但如果你在做高精度的传感器融合仿真最好还是用AirSim内部给出的NED不要自己另外开一路转换两边微小的数值差异在长时间积分后会产生漂移。4.4 逆变换从NED回到WGS84搞清楚了正变换逆变换同样重要。仿真里飞控上报回传的是NED坐标但地面站显示、航迹回放需要经纬度。逆转换的完整链路是NED - ECEF - WGS84。NED转ECEF很简单把投影过程反过来def ned_to_ecef(ned, ecef_home, home_lat_deg, home_lon_deg): lat math.radians(home_lat_deg) lon math.radians(home_lon_deg) sin_lat math.sin(lat) cos_lat math.cos(lat) sin_lon math.sin(lon) cos_lon math.cos(lon) R np.array([ [-sin_lat * cos_lon, -sin_lat * sin_lon, cos_lat], [-sin_lon, cos_lon, 0], [-cos_lat * cos_lon, -cos_lat * sin_lon, -sin_lat], ]) ecef_cur ecef_home R.T.dot(ned) return ecef_curECEF转WGS84最让人头疼因为前面的正变换公式里纬度φ和卯酉圈半径N互相耦合没法直接解开。工程上通用的办法是迭代逼近def ecef_to_wgs84(ecef): x, y, z ecef lon math.atan2(y, x) p math.sqrt(x*x y*y) lat math.atan2(z, p * (1 - WGS84_E2)) for _ in range(10): sin_lat math.sin(lat) n WGS84_A / math.sqrt(1 - WGS84_E2 * sin_lat * sin_lat) alt p / math.cos(lat) - n lat math.atan2(z, p * (1 - WGS84_E2 * n / (n alt))) return math.degrees(lat), math.degrees(lon), alt迭代过程刚开始可能不太好理解但记住核心逻辑就够了先用一个粗略的纬度值算出N再用N修正纬度反复迭代一般10次以内就能收敛到亚毫米级精度。5. 常见问题与排查技巧实录5.1 NED和ENU的坐标系方向颠倒这是我见过最多的问题。很多从ROS转过来的开发者习惯ENUX东Y北Z天因为ROS的map和odom坐标系都遵循REP-103规定用ENU。但飞控系统、AirSim、PX4外部接口里大量使用NED。直接拿ROS里的坐标换算脚本套到AirSim上出来以后飞机的位置全都镜像了X轴本应指北结果指向东Z轴本应向下结果向上。排查时先用一个已知位置验证让飞机在一个固定方向飞10米看NED输出是不是在对应轴上有变化方向不对就赶紧检查旋转矩阵的符号。5.2 高度基准到底是椭球高还是海拔高AirSim里的altitude字段对应的是WGS84椭球高。真实无人机上GPS给的也一般是椭球高但部分飞控日志里显示的高度会叠加了大地水准面模型修正变成MSL海拔。你如果拿气压计高度跟GPS椭球高直接做差会发现飞机明明没动高度差却在几十米量级跳动。这种问题不是坐标系转换写错了而是高度基准不一致。解决方法是把两个高度都换算到同一个基准面再做后续处理。AirSim仿真常用的做法是以Home点的椭球高作为基准把绝对椭球高转成相对高度再使用。5.3 原点漂移Home点没固定住导致全链路错位多机仿真里特别容易出问题。假设两台无人机在不同位置起飞如果都各自调用getHomePoint()那么各自的NED原点不同明明在仿真场景里两机相距5米你拿两套NED坐标直接做差算出来的间距可能是错的。AirSim里正确的做法是选定一台无人机或者一个特定坐标点作为全局基准其他飞机的NED坐标统一转换到同一个Home点下。这也是在做多机编队、车机协同仿真时最容易被忽视的一个细节。5.4 高纬度地区的精度衰减ECEF转NED的旋转矩阵在极点附近依然成立但实际使用中高纬度地区经度变化带来的地面距离变化非常快如果Home点和目标点距离很远比如超过几十公里局部切平面的平面近似误差会变大。如果你做的是大范围巡检仿真建议周期性更新局域原点或者直接用ECEF做最外层的航迹规划只在靠近飞机的局部范围内使用NED。AirSim的仿真场景通常范围不大不太会遇到这个问题但在做真实地理环境接入时一定要心里有数。5.5 用双精度还是单精度AirSim内部位置计算用浮点但在做ECEF转换时坐标数值很大地球半径约6378公里如果你用单精度float去存ECEF的坐标最小可表示的精度大概在0.5米左右这会导致转换结果出现分米级跳动。我遇到过的情况是无人机悬停时GPS本身有噪声但手写转换结果的NED值抖动得莫名其妙排查半天才发现是中间变量用了float32。记住凡是涉及ECEF的中间计算一律用float64最后输出到控制环路里的NED偏移量再根据需要裁剪精度。做这套转换的几个实际体会说个我自己的习惯我在AirSim里做任何跟位置相关的实验第一步永远是写一个坐标系自检脚本启动仿真后让飞机往正北飞10米、往正东飞10米、爬升5米然后检查NED输出是否分别是(10,0,0)、(0,10,0)、(0,0,-5)。只要这一步能通过后面所有位置相关的算法都可以放心用。坐标转换这件事公式本身不难真正难的是坐标系约定和数据基准的细节。你只有亲手把每一步都验证一遍才不会在集成的最后阶段被一个莫名其妙的符号问题卡住一整天。