3、高精地图坐标系:WGS84坐标系、UTM投影、局部坐标系(ENU)、坐标转换实战

做高精地图这几年,我最大的感触就是——坐标系这东西,看着简单,坑是真多。

你想想看,一辆车在路上跑,它得知道自己在哪里。但「在哪里」这个问题的答案,其实取决于你用什么尺子去量。GPS给的是经纬度,地图引擎要的是米,控制算法要的是相对于车的位置。这中间,就是坐标系转换的活儿。

今天咱们就把这几个坐标系掰扯清楚。我会结合我实际项目里踩过的坑来讲,希望能帮你少走弯路。

3.1 WGS84坐标系:地球的「身份证」

WGS84,全称World Geodetic System 1984。说白了,就是给地球定了个标准模型。

地球不是完美的球体,它有点扁。WGS84用一个椭球体来近似地球,然后在这个椭球体上定义经纬度。我们平时说的「北纬39.9度,东经116.4度」,就是WGS84坐标系下的坐标。

关键点:

  • WGS84是一个地心地固坐标系(ECEF的一种)。原点在地球质心,Z轴指向北极,X轴指向本初子午线与赤道的交点。
  • 它用经纬度(Lon, Lat)和海拔高度(H)来表示位置。
  • 几乎所有消费级GPS设备(手机、车载导航)输出的原始数据都是WGS84。

重要:WGS84的经纬度是角度单位(度),不是长度单位。1度经度在地球不同纬度对应的实际距离是不一样的。赤道上1度≈111km,到了北京(北纬40度),1度经度≈85km。所以,千万别直接用经纬度算距离!

我在项目里遇到过一个问题:团队里有人直接用经纬度差值做路径规划,结果在北极附近算法直接崩了。嗯,这就是没理解WGS84本质的后果。

3.2 UTM投影:把球面压平

WGS84坐标虽然精确,但它是球面上的。我们做地图、做可视化、做路径规划,都需要在平面上操作。这就需要一个投影——把球面映射到平面。

UTM(Universal Transverse Mercator,通用横轴墨卡托投影)是目前最常用的投影方式之一。

UTM的核心思想:

  • 把地球分成60个带(Zone),每个带宽度6度经度。
  • 每个带单独做一个横轴墨卡托投影,尽量减少变形。
  • 投影后的坐标用东向(Easting)和北向(Northing)表示,单位是米。

举个例子:北京天安门广场的WGS84坐标大约是(116.397°, 39.908°)。它落在UTM 50N带。投影后的UTM坐标大约是(东向: 450000m, 北向: 4418000m)。

我的习惯:在做高精地图数据存储时,我一般会同时保留WGS84和UTM坐标。WGS84用于跨区域拼接,UTM用于局部计算。这样既保证了全局一致性,又兼顾了局部效率。

UTM的坑:

  • 每个带之间不连续。跨越UTM带时,坐标会跳变。我曾经在跨省的高速项目里,就因为没处理好带间转换,导致地图出现了一条「裂缝」。
  • UTM在赤道附近变形小,越往两极变形越大。所以高纬度地区(比如北欧、加拿大)不太适合用UTM。

3.3 局部坐标系(ENU):以「我」为中心

WGS84和UTM都是全局坐标系。但在自动驾驶中,很多时候我们只关心「车周围100米内」的东西。这时候用全局坐标反而麻烦——数字太大,精度还容易丢。

局部坐标系,也叫ENU坐标系(East-North-Up),就是一个以某个点为原点的直角坐标系。

ENU的定义:

  • 原点:你自己选,通常是车辆当前位置或地图原点。
  • X轴(东向):指向正东。
  • Y轴(北向):指向正北。
  • Z轴(天向):指向天空,垂直于地面。

说白了,ENU就是「以我为中心,东是X,北是Y,天是Z」。这个坐标系非常直观,适合做局部路径规划、障碍物检测、传感器融合。

实际应用:在自动驾驶中,激光雷达、摄像头、毫米波雷达输出的目标位置,通常都是相对于车辆自身的ENU坐标。然后通过坐标转换,把这些局部坐标映射到全局地图上。

我记得有一次调试,发现车辆明明在直道上,但地图上的轨迹却歪了。查了半天,发现是ENU坐标系的原点没对齐——车辆定位模块用的原点和地图模块用的原点差了0.5米。嗯,0.5米在高速上就是一条车道线的偏差。

3.4 坐标转换实战:从WGS84到ENU

理论说完了,咱们来点实际的。下面是我在项目中常用的坐标转换流程,用Python实现。

转换链路:

  1. WGS84(经纬度) → ECEF(地心地固直角坐标)
  2. ECEF → ENU(局部坐标)

为什么要经过ECEF?因为WGS84和ENU之间没有直接的数学公式,ECEF是它们之间的桥梁。

import math
import numpy as np

# WGS84椭球参数
a = 6378137.0          # 长半轴 (米)
f = 1 / 298.257223563  # 扁率
b = a * (1 - f)        # 短半轴 (米)
e2 = 2*f - f**2        # 第一偏心率平方

def wgs84_to_ecef(lat, lon, alt):
    """WGS84经纬度转ECEF"""
    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(ref_lat, ref_lon, ref_alt, target_ecef):
    """ECEF转ENU(以参考点为原点)"""
    ref_lat_rad = math.radians(ref_lat)
    ref_lon_rad = math.radians(ref_lon)
    
    # 参考点的ECEF坐标
    ref_x, ref_y, ref_z = wgs84_to_ecef(ref_lat, ref_lon, ref_alt)
    
    # 目标点相对于参考点的向量
    dx = target_ecef[0] - ref_x
    dy = target_ecef[1] - ref_y
    dz = target_ecef[2] - ref_z
    
    # 旋转矩阵(从ECEF到ENU)
    sin_lat = math.sin(ref_lat_rad)
    cos_lat = math.cos(ref_lat_rad)
    sin_lon = math.sin(ref_lon_rad)
    cos_lon = math.cos(ref_lon_rad)
    
    east = -sin_lon * dx + cos_lon * dy
    north = -sin_lat * cos_lon * dx - sin_lat * sin_lon * dy + cos_lat * dz
    up = cos_lat * cos_lon * dx + cos_lat * sin_lon * dy + sin_lat * dz
    
    return east, north, up

# 示例:天安门广场坐标转换
lat, lon, alt = 39.908, 116.397, 50.0  # 目标点
ref_lat, ref_lon, ref_alt = 39.9, 116.4, 50.0  # 参考点(比如车辆位置)

target_ecef = wgs84_to_ecef(lat, lon, alt)
east, north, up = ecef_to_enu(ref_lat, ref_lon, ref_alt, target_ecef)

print(f"目标点相对于参考点的ENU坐标:")
print(f"东向: {east:.2f} 米")
print(f"北向: {north:.2f} 米")
print(f"天向: {up:.2f} 米")

避坑指南:我曾经在代码里直接用经纬度差值乘以一个固定系数来估算距离,结果在高速场景下误差累积到几十米。后来老老实实用了ECEF中转,精度才上来。坐标转换没有捷径,该走的步骤一步都不能省。

3.5 总结与建议

好了,咱们把三个坐标系串一遍:

坐标系 单位 特点 适用场景
WGS84 度、米 全球统一,球面坐标 GPS原始数据、跨区域地图拼接
UTM 平面投影,分带管理 地图存储、局部路径规划
ENU 以参考点为原点,直观 传感器融合、局部控制

我个人建议,在实际项目中:

  • 数据存储用WGS84或UTM,保证全局一致性。
  • 实时计算用ENU,减少数值误差,提高效率。
  • 跨带处理一定要写单元测试,我吃过这个亏。

下一章咱们会讲高精地图的数据格式和存储结构,到时候这些坐标系知识会反复用到。先把基础打牢,后面才不慌。