news 2026/10/28 10:13:58

LPC_经纬海拔转XYZ

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LPC_经纬海拔转XYZ

LPC:Localization Planning and Control

输入:GNSS 输出LLH(lat, lon, h),WGS84(纬度 φ,经度 λ,大地高 h)
输出:ENU(东、北、天) / NED(北、东、地,很多无人机 / IMU 用 NED;你说的西北地很少见,行业标准是 NED)

变换链路标准:

(LLH \xrightarrow{椭球公式} ECEF(X,Y,Z) \xrightarrow{平移+旋转矩阵} ENU)
基准点:(P_0(\phi_0,\lambda_0,h_0)),也就是 ENU 原点。
注意:ENU/NED 不是地图投影,是站心切平面坐标变换(LTP)

两套方向约定:

  • ENU:E 东,N 北,U 天(向上)——Waymo、GTSAM、大多数自动驾驶定位后端
  • NED:N 北,E 东,D 地(向下)——PX4、飞控、很多 IMU 器件坐标系

NED 和 ENU 只是坐标轴重排 + U 取反:

[NED]=[01010000−1][ENU] \begin{bmatrix} N \\ E \\ D \end{bmatrix}= \begin{bmatrix} 0 & 1 & 0 \\ 1 & 0 & 0 \\ 0 & 0 & -1 \end{bmatrix} \begin{bmatrix} E \\ N \\ U \end{bmatrix}​NED​​=​010​100​00−1​​​ENU​​

前置:WGS84 椭球常数

(\begin{aligned}
a &= 6378137.0 \quad \text{半长轴(m)} \
f &= 1/298.257223563 \quad \text{扁率} \
e^2 &= 2f-f^2 \quad \text{第一偏心率平方}
\end{aligned})卯酉圈半径 (R_N = \dfrac{a}{\sqrt{1-e2\sin2\phi}})

a=6378137.0半长轴(m)f=1/298.257223563扁率e2=2f−f2第一偏心率平方 \begin{aligned} a &= 6378137.0 \quad \text{半长轴(m)} \\ f &= 1/298.257223563 \quad \text{扁率} \\ e^2 &= 2f-f^2 \quad \text{第一偏心率平方} \end{aligned}afe2​=6378137.0半长轴(m)=1/298.257223563扁率=2f−f2第一偏心率平方​

卯酉圈半径:
RN=a1−e2sin⁡2ϕ R_N = \dfrac{a}{\sqrt{1-e^2\sin^2\phi}}RN​=1−e2sin2ϕ​a​


方法一:标准完整变换(工业 / 自动驾驶首选,Waymo/GTSAM 底层实现)

步骤:

  1. LLH → ECEF(地心地固直角坐标)
  2. 求基准点 (P_0) 的 ECEF:(X_0,Y_0,Z_0)
  3. 矢量差:(\Delta X = X-X_0,\Delta Y=Y-Y_0,\Delta Z=Z-Z_0)
  4. ECEF → ENU 旋转矩阵:

(R_{enu} =
\begin{bmatrix}
-\sin\lambda_0 & \cos\lambda_0 & 0 \
-\sin\phi_0\cos\lambda_0 & -\sin\phi_0\sin\lambda_0 & \cos\phi_0 \
\cos\phi_0\cos\lambda_0 & \cos\phi_0\sin\lambda_0 & \sin\phi_0
\end{bmatrix})
(\begin{bmatrix}E\N\U\end{bmatrix} = R_{enu}
\begin{bmatrix}\Delta X \ \Delta Y \ \Delta Z\end{bmatrix})

所有角度计算:弧度!

逆变换:ENU → ECEF → LLH

方法二:局部近似公式(简易微分公式,小范围 < 1km,适合低成本小车)

不经过 ECEF,直接用经纬度差换算东、北偏移

(\begin{aligned}
\Delta E &= R_N \cos\phi_0 \cdot (\lambda-\lambda_0) \
\Delta N &= R_M \cdot (\phi-\phi_0) \
\Delta U &= h - h_0
\end{aligned})
(R_M=\dfrac{a(1-e2)}{(1-e2\sin2\phi_0){3/2}}) 子午圈半径
⚠️ 缺点:距离原点远了曲率误差明显,不能用于高精度激光 + IMU 融合

下面代码优先实现【方法一:标准 ECEF 中转版本】(工程主用),附带简易近似版本。


Python 实现(可直接运行,WGS84 LLH <-> ENU <-> NED)

import math class Wgs84Ltp: # WGS84 椭球参数 A = 6378137.0 F = 1.0 / 298.257223563 E2 = 2 * F - F * F @staticmethod def llh2ecef(lat_deg, lon_deg, h): """LLH(度,度,m) -> ECEF X,Y,Z (m)""" lat = math.radians(lat_deg) lon = math.radians(lon_deg) sin_lat = math.sin(lat) cos_lat = math.cos(lat) sin_lon = math.sin(lon) cos_lon = math.cos(lon) rn = Wgs84Ltp.A / math.sqrt(1.0 - Wgs84Ltp.E2 * sin_lat * sin_lat) x = (rn + h) * cos_lat * cos_lon y = (rn + h) * cos_lat * sin_lon z = (rn * (1 - Wgs84Ltp.E2) + h) * sin_lat return x, y, z @staticmethod def ecef2llh(x, y, z, max_iter=10): """ECEF -> LLH (lat_deg, lon_deg, h)""" lon = math.atan2(y, x) p = math.hypot(x, y) lat = math.atan2(z, p * (1 - Wgs84Ltp.E2)) for _ in range(max_iter): sin_lat = math.sin(lat) rn = Wgs84Ltp.A / math.sqrt(1.0 - Wgs84Ltp.E2 * sin_lat * sin_lat) h = p / math.cos(lat) - rn lat = math.atan2(z, p * (1 - Wgs84Ltp.E2 * rn / (rn + h))) return math.degrees(lat), math.degrees(lon), h @staticmethod def ecef2enu(x, y, z, x0, y0, z0, lat0_deg, lon0_deg): """ECEF to ENU, origin P0(lat0,lon0,h0)""" lat0 = math.radians(lat0_deg) lon0 = math.radians(lon0_deg) dX = x - x0 dY = y - y0 dZ = z - z0 s_lon0 = math.sin(lon0) c_lon0 = math.cos(lon0) s_lat0 = math.sin(lat0) c_lat0 = math.cos(lat0) e = -s_lon0 * dX + c_lon0 * dY n = -s_lat0 * c_lon0 * dX - s_lat0 * s_lon0 * dY + c_lat0 * dZ u = c_lat0 * c_lon0 * dX + c_lat0 * s_lon0 * dY + s_lat0 * dZ return e, n, u @staticmethod def enu2ecef(e, n, u, x0, y0, z0, lat0_deg, lon0_deg): """ENU back to ECEF""" lat0 = math.radians(lat0_deg) lon0 = math.radians(lon0_deg) s_lon0 = math.sin(lon0) c_lon0 = math.cos(lon0) s_lat0 = math.sin(lat0) c_lat0 = math.cos(lat0) dx = -s_lon0 * e - s_lat0 * c_lon0 * n + c_lat0 * c_lon0 * u dy = c_lon0 * e - s_lat0 * s_lon0 * n + c_lat0 * s_lon0 * u dz = c_lat0 * n + s_lat0 * u return x0 + dx, y0 + dy, z0 + dz @staticmethod def llh2enu(lat, lon, h, lat0, lon0, h0): """LLH -> ENU 主入口""" x0, y0, z0 = Wgs84Ltp.llh2ecef(lat0, lon0, h0) x, y, z = Wgs84Ltp.llh2ecef(lat, lon, h) return Wgs84Ltp.ecef2enu(x, y, z, x0, y0, z0, lat0, lon0) @staticmethod def enu2llh(e, n, u, lat0, lon0, h0): """ENU -> LLH""" x0, y0, z0 = Wgs84Ltp.llh2ecef(lat0, lon0, h0) x, y, z = Wgs84Ltp.enu2ecef(e, n, u, x0, y0, z0, lat0, lon0) return Wgs84Ltp.ecef2llh(x, y, z) @staticmethod def enu2ned(e, n, u): """ENU转NED""" return n, e, -u @staticmethod def ned2enu(n, e, d): """NED转ENU""" return e, n, -d @staticmethod def llh2enu_approx(lat, lon, h, lat0, lon0, h0): """【简易近似版本 方法二】适合小范围<1km""" lat0_r = math.radians(lat0) rn = Wgs84Ltp.A / math.sqrt(1.0 - Wgs84Ltp.E2 * math.sin(lat0_r)**2) rm = Wgs84Ltp.A * (1 - Wgs84Ltp.E2) / ((1 - Wgs84Ltp.E2 * math.sin(lat0_r)**2)**1.5) dlon = math.radians(lon - lon0) dlat = math.radians(lat - lat0) e = rn * math.cos(lat0_r) * dlon n = rm * dlat u = h - h0 return e, n, u if __name__ == "__main__": # ===== 测试样例 ===== # 基准原点 P0 lat0 = 34.76 lon0 = 113.65 h0 = 100.0 # 目标点 lat = 34.761 lon = 113.652 h = 105.0 e,n,u = Wgs84Ltp.llh2enu(lat, lon, h, lat0, lon0, h0) print(f"ENU: E={e:.3f}, N={n:.3f}, U={u:.3f}") ned = Wgs84Ltp.enu2ned(e,n,u) print(f"NED: N={ned[0]:.3f}, E={ned[1]:.3f}, D={ned[2]:.3f}") # 逆变换验证 lat2, lon2, h2 = Wgs84Ltp.enu2llh(e,n,u, lat0, lon0, h0) print(f"逆算LLH: lat={lat2:.6f}, lon={lon2:.6f}, h={h2:.3f}") # 简易近似版本对比 e_ap, n_ap, u_ap = Wgs84Ltp.llh2enu_approx(lat, lon, h, lat0, lon0, h0) print(f"近似ENU: E={e_ap:.3f}, N={n_ap:.3f}, U={u_ap:.3f}")

C++ 实现(Header + cpp,ROS/GTSAM 工程直接嵌入,无第三方依赖)

wgs84_ltp.h

#pragma once #include <cmath> struct ENU { double E, N, U; }; struct NED { double N, E, D; }; struct ECEF { double X, Y, Z; }; struct LLH { double lat; // deg double lon; // deg double h; // m }; class Wgs84Ltp { public: static constexpr double A = 6378137.0; static constexpr double F = 1.0 / 298.257223563; static constexpr double E2 = 2 * F - F * F; static ECEF llh2ecef(const LLH& llh); static LLH ecef2llh(const ECEF& ecef, int iter = 10); static ENU ecef2enu(const ECEF& p, const ECEF& origin_ecef, double lat0_deg, double lon0_deg); static ECEF enu2ecef(const ENU& enu, const ECEF& origin_ecef, double lat0_deg, double lon0_deg); static ENU llh2enu(const LLH& point, const LLH& origin); static LLH enu2llh(const ENU& enu, const LLH& origin); static NED enu2ned(const ENU& enu); static ENU ned2enu(const NED& ned); // 简易微分近似版本 static ENU llh2enu_approx(const LLH& point, const LLH& origin); };

wgs84_ltp.cpp

#include "wgs84_ltp.h" ECEF Wgs84Ltp::llh2ecef(const LLH& llh) { double lat = llh.lat * M_PI / 180.0; double lon = llh.lon * M_PI / 180.0; double slat = sin(lat); double clat = cos(lat); double slon = sin(lon); double clon = cos(lon); double rn = A / sqrt(1.0 - E2 * slat * slat); ECEF out{}; out.X = (rn + llh.h) * clat * clon; out.Y = (rn + llh.h) * clat * slon; out.Z = (rn * (1 - E2) + llh.h) * slat; return out; } LLH Wgs84Ltp::ecef2llh(const ECEF& ecef, int iter) { LLH out{}; out.lon = atan2(ecef.Y, ecef.X); double p = hypot(ecef.X, ecef.Y); double lat = atan2(ecef.Z, p * (1 - E2)); double h = 0.0; for(int i=0; i<iter; i++){ double slat = sin(lat); double rn = A / sqrt(1.0 - E2 * slat * slat); h = p / cos(lat) - rn; lat = atan2(ecef.Z, p * (1 - E2 * rn/(rn + h))); } out.lat = lat * 180.0 / M_PI; out.lon = out.lon * 180.0 / M_PI; out.h = h; return out; } ENU Wgs84Ltp::ecef2enu(const ECEF& p, const ECEF& o, double lat0_deg, double lon0_deg) { double lat0 = lat0_deg * M_PI / 180.0; double lon0 = lon0_deg * M_PI / 180.0; double dX = p.X - o.X; double dY = p.Y - o.Y; double dZ = p.Z - o.Z; double slon0 = sin(lon0); double clon0 = cos(lon0); double slat0 = sin(lat0); double clat0 = cos(lat0); ENU enu{}; enu.E = -slon0 * dX + clon0 * dY; enu.N = -slat0*clon0*dX - slat0*slon0*dY + clat0*dZ; enu.U = clat0*clon0*dX + clat0*slon0*dY + slat0*dZ; return enu; } ECEF Wgs84Ltp::enu2ecef(const ENU& enu, const ECEF& o, double lat0_deg, double lon0_deg) { double lat0 = lat0_deg * M_PI / 180.0; double lon0 = lon0_deg * M_PI / 180.0; double slon0 = sin(lon0); double clon0 = cos(lon0); double slat0 = sin(lat0); double clat0 = cos(lat0); double dX = -slon0 * enu.E - slat0*clon0*enu.N + clat0*clon0*enu.U; double dY = clon0 * enu.E - slat0*slon0*enu.N + clat0*slon0*enu.U; double dZ = clat0 * enu.N + slat0 * enu.U; ECEF out{}; out.X = o.X + dX; out.Y = o.Y + dY; out.Z = o.Z + dZ; return out; } ENU Wgs84Ltp::llh2enu(const LLH& point, const LLH& origin) { ECEF o_ecef = llh2ecef(origin); ECEF p_ecef = llh2ecef(point); return ecef2enu(p_ecef, o_ecef, origin.lat, origin.lon); } LLH Wgs84Ltp::enu2llh(const ENU& enu, const LLH& origin) { ECEF o_ecef = llh2ecef(origin); ECEF p_ecef = enu2ecef(enu, o_ecef, origin.lat, origin.lon); return ecef2llh(p_ecef); } NED Wgs84Ltp::enu2ned(const ENU& enu) { return {enu.N, enu.E, -enu.U}; } ENU Wgs84Ltp::ned2enu(const NED& ned) { return {ned.E, ned.N, -ned.D}; } ENU Wgs84Ltp::llh2enu_approx(const LLH& point, const LLH& origin) { double lat0_r = origin.lat * M_PI / 180.0; double slat0 = sin(lat0_r); double rn = A / sqrt(1.0 - E2 * slat0 * slat0); double rm = A*(1-E2) / pow(1.0 - E2 * slat0*slat0, 1.5); double dlon_r = (point.lon - origin.lon) * M_PI / 180.0; double dlat_r = (point.lat - origin.lat) * M_PI / 180.0; ENU enu{}; enu.E = rn * cos(lat0_r) * dlon_r; enu.N = rm * dlat_r; enu.U = point.h - origin.h; return enu; }

main.cpp 测试:

#include <iostream> #include "wgs84_ltp.h" int main(){ LLH origin{34.76, 113.65, 100.0}; LLH pt{34.761, 113.652, 105.0}; ENU enu = Wgs84Ltp::llh2enu(pt, origin); NED ned = Wgs84Ltp::enu2ned(enu); std::cout << "ENU E:" << enu.E << " N:" << enu.N << " U:" << enu.U << "\n"; std::cout << "NED N:" << ned.N << " E:" << ned.E << " D:" << ned.D << "\n"; LLH back = Wgs84Ltp::enu2llh(enu, origin); std::cout << "back lat:" << back.lat << " lon:" << back.lon << " h:" << back.h << "\n"; return 0; }

CMakeLists.txt(编译)

cmake_minimum_required(VERSION 3.16) project(ltp_demo) set(CMAKE_CXX_STANDARD 17) add_executable(demo main.cpp wgs84_ltp.cpp)

两种转换方法对比总结(工程选型)

表格

方案原理精度适用场景后端适配(你前面 GTSAM / 联邦滤波)
【标准 ECEF 中转】LLH→ECEF→ENU严格椭球几何,旋转矩阵变换全范围高精度自动驾驶、GNSS 后处理、激光 ICP、IMU 预积分✅首选,Waymo 同款
【局部微分近似】直接 Δlat Δlon 算 EN一阶泰勒切平面近似,忽略高阶曲率项<1km 很好,远了漂移低成本单片机、简易小车,可视化粗算❌ 不建议放进因子图优化残差

补充工程实操要点(和你之前定位链路打通)

  1. 原点选择策略
    • 固定原点:高精地图原点,整张地图一套 ENU;缺点:离原点几十公里曲率误差上来
    • 滑动原点(sliding LTP):以当前车辆 GNSS 位置动态刷新 P0,Waymo/Cruise 滑动窗口定位后端方案,永远保持车辆在原点附近,曲率误差压到极小
  2. 和 GTSAM 对接:GTSAM 自带Pose3、Point3就是 ENU 空间;把 GNSS、激光匹配、IMU 预积分全部放到 ENU 下构建因子
  3. 坐标系转换顺序:
GNSS LLH → ENU(后端优化域) → LLH → UTM/GK(地图存储、可视化)
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/28 10:13:03

PWA站点采集实战:Service Worker缓存机制拆解与动态内容完整提取方案

最近团队接了几个工业SaaS平台的数据采集需求,清一色都是PWA架构。一开始用常规的HTTP请求框架去做,要么拿到的是几天前的旧数据,要么页面骨架出来了但核心数据区域全是空的,抓下来的HTML全是缓存壳子。一开始以为是对方做了强反爬,调了代理、浏览器指纹都没用,最后抓包逐…

作者头像 李华
网站建设 2026/10/28 10:12:48

必知!Python 基于sklearn库的6个项目,解锁机器学习核心技能

基于 的 &#xff08;-Learn&#xff09;库的机器学习编程练习项目-Learn这个简称指的是中那个强大而且在很大程度上被使用的机器学习库, 它提供了丰富的算法和工具, 适用于各种各样的机器学习任务。以下是一些基于该库的机器学习编程练习项目。这些项目按照从基础到进阶的顺序…

作者头像 李华
网站建设 2026/10/28 10:12:47

Python入门难不难?新手最该学什么

“想学, 但是我们不知道从哪里开始”、“看视频的时候觉得特别简单, 可一动手去做的时候就卡住了”——这种情况几乎是每一个新手都会碰到的问题。其实, 这件事并没有像人们想象的那样难, 真正让人感到困难的地方不是语言本身, 而是你到底有没有使用恰当的学习顺序。一、先说结…

作者头像 李华
网站建设 2026/10/28 10:12:05

商品结构有没有被改:用Pillow定位局部像素差异

AI做商品图可能改变产品在画面中的轮廓、连接关系或可见细节&#xff0c;但一张差异图不能直接判断商品有没有变形。背景调整、压缩和光照变化也会造成像素差异。本文给出一个较窄的本地检查方法&#xff1a;在已经对齐的两张图片中&#xff0c;比较同一矩形区域&#xff0c;把…

作者头像 李华
网站建设 2026/10/28 10:10:16

DCF / EDCA 完整流程、参数与时延换算

DCF / EDCA 完整流程、参数与时延换算下面全部基于工业场景主流&#xff1a;5 GHz OFDM&#xff08;Wi‑Fi 5 / Wi‑Fi 6&#xff09; 基础物理层常量\begin{align} SIFS & 16\ \mu s \\ SlotTime & 9\ \mu s \end{align}SIFS&#xff1a;短帧间隔&#xff0c;专门用于…

作者头像 李华
网站建设 2026/10/28 10:09:39

Docker安装详解--基于Ubuntu环境

目录 1. Docker Server版本安装 1.1 Ubuntu 安装&#xff08;以华为云 Ubuntu 20.04 为例&#xff09; 1.2 实战经验 1.2.1 Docker 镜像源修改 1.2.2 Docker 目录修改 望读者阅读本文之后能够掌握如何安装 docker。&#xff08;使用Xshell进行操作&#xff09; 正文开…

作者头像 李华