3. 坐标系与地图投影

做无人机地面站开发,第一个绕不开的坎就是坐标系。我见过不少新手,航线画得挺漂亮,一飞出去飞机直接往反方向跑——原因就是坐标系搞错了。说白了,你在地图上点一个点,这个点到底是经纬度还是平面坐标?地图是平的,地球是圆的,这两者怎么换算?今天咱们就把这事彻底讲清楚。

3.1 为什么需要坐标系?

无人机飞控不认识地图,它只认坐标。GPS 模块给出来的是经纬度,但飞控算航线、算偏航距离,用的却是平面直角坐标。你想想看,如果直接用经纬度算两点距离,在赤道附近还行,到了高纬度地区,经度1度的实际距离会缩水一半。所以必须有一套转换机制。

我个人习惯把坐标系分成三层:

  • 地理坐标系:WGS84,也就是 GPS 用的经纬度
  • 投影坐标系:UTM,把球面展开成平面
  • 局部坐标系:以起飞点为原点的东北天坐标系,飞控真正用的

这三层之间,每一层都有对应的转换公式。地面站的任务就是把用户在地图上点的经纬度,最终转成飞控能理解的局部坐标。

核心原则:地面站里所有航线点都以 WGS84 经纬度存储,只在计算时临时转成平面坐标。这样无论你在哪个区域飞行,数据都是全球统一的。

3.2 WGS84:全球通用的经纬度基准

WGS84 全称 World Geodetic System 1984。它定义了一个椭球体模型,用来近似描述地球的形状。地球不是完美的球体,赤道半径约 6378.137 公里,极半径约 6356.752 公里,差了 21 公里多。WGS84 把这个椭球参数固定下来,全球所有 GPS 设备都按这个标准输出。

经纬度就是在这个椭球上的位置描述:

  • 纬度:从赤道向北/向南的角度,范围 -90° 到 +90°
  • 经度:从本初子午线向东/向西的角度,范围 -180° 到 +180°
  • 海拔:相对于 WGS84 椭球面的高度,不是海平面

这里有个坑。我记得有一次做项目,用户反馈无人机高度显示不准,排查了半天才发现是海拔基准搞混了。WGS84 海拔是椭球高,而气压计测的是海平面高,两者之间差一个大地水准面起伏。所以在地面站里显示高度时,一定要标注清楚用的是哪种基准。

注意:WGS84 经纬度用度分秒表示时,精度要控制在小数点后6位,对应约 0.1 米的定位精度。地面站里存储经纬度,建议用 double 类型,别用 float,否则累积误差会让你航线偏出几米。

3.3 UTM 投影:把球面拍平

UTM 全称 Universal Transverse Mercator。它把地球表面切成 60 个投影带,每个带宽度 6 度经度。每个带内用横轴墨卡托投影,把椭球面展开成平面。这样每个带内都有一个独立的平面直角坐标系,单位是米。

UTM 坐标有两个关键参数:

  • 东向偏移:从中央经线往东的距离,中央经线设为 500,000 米
  • 北向偏移:从赤道往北的距离,南半球再加 10,000,000 米

为什么中央经线要设成 500,000 米?因为投影后中央经线以西的区域会出现负值,加一个偏移量保证所有坐标都是正数。这个设计很巧妙,但如果你在做跨带航线规划时忘了处理带号,就会出大问题。

我建议地面站里做 UTM 转换时,一定要记录当前所在的投影带号。如果航线跨越两个带,需要分段处理,或者改用其他投影方式。

特性 WGS84 经纬度 UTM 平面坐标
单位 度 (°) 米 (m)
适用范围 全球统一 每个投影带内 (6°经度宽)
距离计算 复杂,需球面三角 直接勾股定理
面积计算 不准确 准确
飞控支持 原生支持 需转换后使用

3.4 经纬度与平面坐标的转换

转换公式看起来复杂,但实际用起来就三步。我习惯在代码里封装成两个函数:latLonToUtm 和 utmToLatLon。核心逻辑如下:

// 伪代码:经纬度转 UTM
function latLonToUtm(lat, lon) {
    // 1. 计算所在投影带号
    zone = floor((lon + 180) / 6) + 1;
    
    // 2. 计算中央经线
    centralMeridian = (zone - 1) * 6 - 180 + 3;
    
    // 3. 执行横轴墨卡托投影计算
    // 这里涉及椭球参数、比例因子、子午线收敛角等
    // 实际工程中建议直接调用 proj4 或 GeoTools 库
    
    return { easting, northing, zone };
}

// 伪代码:UTM 转经纬度
function utmToLatLon(easting, northing, zone) {
    // 反向计算,从平面坐标反推经纬度
    // 同样建议使用成熟库
    return { lat, lon };
}

实际项目中,我从来不会手写这些公式。Proj4 库已经非常成熟,C++、Python、JavaScript 版本都有。在地面站里,我一般用 proj4js 做前端转换,后端用 proj4 做批量处理。你只需要定义好源坐标系和目标坐标系:

// JavaScript 示例:使用 proj4js
const proj4 = require('proj4');

// 定义 WGS84 和 UTM 50N 的投影参数
proj4.defs('EPSG:4326', '+proj=longlat +datum=WGS84 +no_defs');
proj4.defs('EPSG:32650', '+proj=utm +zone=50 +datum=WGS84 +units=m +no_defs');

// 转换:经纬度 -> UTM
const utmCoord = proj4('EPSG:4326', 'EPSG:32650', [116.4, 39.9]);
console.log(`东向: ${utmCoord[0].toFixed(2)}m, 北向: ${utmCoord[1].toFixed(2)}m`);

// 反向:UTM -> 经纬度
const lonLat = proj4('EPSG:32650', 'EPSG:4326', [500000, 4500000]);
console.log(`经度: ${lonLat[0].toFixed(6)}°, 纬度: ${lonLat[1].toFixed(6)}°`);

经验之谈:我建议在地面站里缓存最近使用的投影定义。用户在一个区域飞行时,投影带不会频繁变化。每次转换都重新初始化投影对象,性能开销不小,尤其是处理几百个航点的时候。

3.5 知识体系总览

下面这张图把本章的核心逻辑串起来了。从 GPS 卫星到飞控,数据流经过三次坐标变换,每一步都有明确的数学基础。

坐标系与地图投影知识体系 GPS 卫星 WGS84 经纬度 (lat, lon, alt) UTM 平面坐标 (E, N, Zone) 局部东北天坐标 (x, y, z) 飞控导航计算 关键转换 • 投影带号计算 • 中央经线确定 • 椭球参数代入 • 比例因子修正 • 子午线收敛角 数据流方向:GPS → 经纬度 → UTM → 局部坐标 → 飞控

3.6 地面站里的坐标处理实践

在地面站开发中,坐标处理有几个关键点需要注意。我踩过的坑,列出来供你参考:

  1. 精度问题:经纬度用 double 存储,UTM 坐标用 float 就够了。因为 UTM 坐标范围大,float 的 7 位有效数字足够覆盖到厘米级精度。
  2. 跨带处理:航线跨越两个 UTM 带时,建议统一转换到其中一个带内计算。或者改用兰勃特投影,它在大区域投影时变形更均匀。
  3. 海拔基准:地面站里显示的高度,一定要标注是椭球高还是正高。我习惯在 UI 上用不同颜色区分,避免用户误判。
  4. 性能优化:如果航线有上千个航点,每次拖动地图都重新计算投影,界面会卡顿。建议用 Web Worker 做异步计算,或者缓存转换结果。

避坑指南:我曾经遇到过一个 bug,用户在北纬 80 度以上区域飞行,UTM 转换结果突然跳变。排查后发现是极地区域的投影变形过大,UTM 在 84 度以上就不适用了。极地飞行建议改用 UPS 通用极球面投影。

3.7 坐标系选择建议

不同场景下,坐标系的选择策略不一样。我总结了一个简单的对照表:

应用场景 推荐坐标系 原因
全球航线规划 WGS84 经纬度 无投影变形,全球统一
局部区域测绘 UTM 距离面积计算准确
城市低空飞行 局部东北天 飞控计算效率最高
极地飞行 UPS 避免 UTM 极地变形
跨带长航线 兰勃特等角圆锥 中纬度区域变形均匀

嗯,这里要注意一点:地面站里给用户展示地图时,用的通常是 Web Mercator 投影(也就是 Google Maps 用的那种)。但航线计算时,一定要转成 UTM 或局部坐标。Web Mercator 虽然看着舒服,但它不是等距投影,算出来的距离误差很大。

我个人习惯在地面站里维护一个坐标转换服务层。所有模块都通过这个服务层获取坐标,不直接调用 proj4。这样如果以后要换投影库,或者增加新的坐标系支持,只需要改这一个地方。

坐标系这东西,说难不难,说简单也不简单。关键是理解每个坐标系的设计初衷和适用范围。你只要记住:经纬度是存储格式,UTM 是计算格式,局部坐标是飞控格式。这三者之间的转换,就是地面站坐标处理的核心。