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实现。
转换链路:
- WGS84(经纬度) → ECEF(地心地固直角坐标)
- 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,减少数值误差,提高效率。
- 跨带处理一定要写单元测试,我吃过这个亏。
下一章咱们会讲高精地图的数据格式和存储结构,到时候这些坐标系知识会反复用到。先把基础打牢,后面才不慌。