地心地固坐标系(ECEF)与站心坐标系(ENU)的转换
1. 概述
我在《大地经纬度坐标与地心地固坐标的的转换》这篇文章中已经论述了地心坐标系的概念。我们知道,基于地心坐标系的坐标都是很大的值,这样的值是不太方便进行空间计算的,所以很多时候可以选取一个站心点,将这个很大的值变换成一个较小的值。以图形学的观点来看,地心坐标可以看作是世界坐标,站心坐标可以看作局部坐标。
站心坐标系以一个站心点为坐标原点,当把坐标系定义为X轴指东、Y轴指北,Z轴指天,就是ENU(东北天)站心坐标系。这样,从地心地固坐标系转换成的站心坐标系,就会成为一个符合常人对地理位置认知的局部坐标系。同时,只要站心点位置选的合理(通常可选取地理表达区域的中心点),表达的地理坐标都会是很小的值,非常便于空间计算。

2. 原理
令选取的站心点为P,其大地经纬度坐标为\((B_p,L_p,H_p)\),对应的地心地固坐标系为\((X_p,Y_p,Z_p)\)。地心地固坐标系简称为ECEF,站心坐标系简称为ENU。
2.1. 平移
通过第一节的图可以看出,ENU要转换到ECEF,一个很明显的图形操作是平移变换,将站心移动到地心。根据站心点P在地心坐标系下的坐标\((X_p,Y_p,Z_p)\),可以很容易推出ENU转到ECEF的平移矩阵:
\begin{bmatrix}
1&0&0&X_p\\
0&1&0&Y_p\\
0&0&1&Z_p\\
0&0&0&1\\
\end{bmatrix}
\]
反推之,ECEF转换到ENU的平移矩阵就是T的逆矩阵:
\begin{bmatrix}
1&0&0&-X_p\\
0&1&0&-Y_p\\
0&0&1&-Z_p\\
0&0&0&1\\
\end{bmatrix}
\]
2.2. 旋转
另外一个需要进行的图形变换是旋转变换,其旋转变换矩阵根据P点所在的经度L和纬度B确定。这个旋转变换有点难以理解,需要一定的空间想象能力,但是可以直接给出如下结论:
- 当从ENU转换到ECEF时,需要先旋转再平移,旋转是先绕X轴旋转\((\frac{pi}{2}-B)\),再绕Z轴旋转\((\frac{pi}{2}+L)\)
- 当从ECEF转换到ENU时,需要先平移再旋转,旋转是先绕Z轴旋转\(-(\frac{pi}{2}+L)\),再绕X轴旋转\(-(\frac{pi}{2}-B)\)
根据我在《WebGL简易教程(五):图形变换(模型、视图、投影变换)》提到的旋转变换,绕X轴旋转矩阵为:
\begin{bmatrix}
1&0&0&0\\
0&cosθ&-sinθ&0\\
0&sinθ&cosθ&0\\
0&0&0&1\\
\end{bmatrix}
\]
绕Z轴旋转矩阵为:
\begin{bmatrix}
cosθ&-sinθ&0&0\\
sinθ&cosθ&0&0\\
0&0&1&0\\
0&0&0&1\\
\end{bmatrix}
\]
从ENU转换到ECEF的旋转矩阵为:
\tag{1}
\]
根据三角函数公式:
sin(π/2-α)=cosα\\
cos(π/2+α)=-sinα\\
cos(π/2-α)=sinα\\
\]
有:
\begin{bmatrix}
-sinL&-cosL&0&0\\
cosL&-sinL&0&0\\
0&0&1&0\\
0&0&0&1\\
\end{bmatrix}
\tag{2}
\]
\begin{bmatrix}
1&0&0&0\\
0&sinB&-cosB&0\\
0&cosB&sinB&0\\
0&0&0&1\\
\end{bmatrix}
\tag{3}
\]
将(2)、(3)带入(1)中,则有:
\begin{bmatrix}
-sinL&-sinBcosL&cosBcosL&0\\
cosL&-sinBsinL&cosBsinL&0\\
0&cosB&sinB&0\\
0&0&0&1\\
\end{bmatrix}
\tag{4}
\]
而从ECEF转换到ENU的旋转矩阵为:
\tag{5}
\]
旋转矩阵是正交矩阵,根据正交矩阵的性质:正交矩阵的逆矩阵等于其转置矩阵,那么可直接得:
\begin{bmatrix}
-sinL&cosL&0&0\\
-sinBcosL&-sinBsinL&cosB&0\\
cosBcosL&cosBsinL&sinB&0\\
0&0&0&1\\
\end{bmatrix}
\tag{6}
\]
2.3. 总结
将上述公式展开,可得从ENU转换到ECEF的图形变换矩阵为:
\begin{bmatrix}
1&0&0&X_p\\
0&1&0&Y_p\\
0&0&1&Z_p\\
0&0&0&1\\
\end{bmatrix}
\begin{bmatrix}
-sinL&-sinBcosL&cosBcosL&0\\
cosL&-sinBsinL&cosBsinL&0\\
0&cosB&sinB&0\\
0&0&0&1\\
\end{bmatrix}
\]
而从ECEF转换到ENU的图形变换矩阵为:
\begin{bmatrix}
-sinL&cosL&0&0\\
-sinBcosL&-sinBsinL&cosB&0\\
cosBcosL&cosBsinL&sinB&0\\
0&0&0&1\\
\end{bmatrix}
\begin{bmatrix}
1&0&0&-X_p\\
0&1&0&-Y_p\\
0&0&1&-Z_p\\
0&0&0&1\\
\end{bmatrix}
\]
3. 实现
接下来用代码实现这个坐标转换,选取一个站心点,以这个站心点为原点,获取某个点在这个站心坐标系下的坐标:
#include <iostream>
#include <eigen3/Eigen/Eigen>
#include <osgEarth/GeoData>
using namespace std;
const double epsilon = 0.000000000000001;
const double pi = 3.14159265358979323846;
const double d2r = pi / 180;
const double r2d = 180 / pi;
const double a = 6378137.0; //椭球长半轴
const double f_inverse = 298.257223563; //扁率倒数
const double b = a - a / f_inverse;
//const double b = 6356752.314245; //椭球短半轴
const double e = sqrt(a * a - b * b) / a;
void Blh2Xyz(double &x, double &y, double &z)
{
double L = x * d2r;
double B = y * d2r;
double H = z;
double N = a / sqrt(1 - e * e * sin(B) * sin(B));
x = (N + H) * cos(B) * cos(L);
y = (N + H) * cos(B) * sin(L);
z = (N * (1 - e * e) + H) * sin(B);
}
void Xyz2Blh(double &x, double &y, double &z)
{
double tmpX = x;
double temY = y ;
double temZ = z;
double curB = 0;
double N = 0;
double calB = atan2(temZ, sqrt(tmpX * tmpX + temY * temY));
int counter = 0;
while (abs(curB - calB) * r2d > epsilon && counter < 25)
{
curB = calB;
N = a / sqrt(1 - e * e * sin(curB) * sin(curB));
calB = atan2(temZ + N * e * e * sin(curB), sqrt(tmpX * tmpX + temY * temY));
counter++;
}
x = atan2(temY, tmpX) * r2d;
y = curB * r2d;
z = temZ / sin(curB) - N * (1 - e * e);
}
void TestBLH2XYZ()
{
//double x = 113.6;
//double y = 38.8;
//double z = 100;
//
//printf("原大地经纬度坐标:%.10lf\t%.10lf\t%.10lf\n", x, y, z);
//Blh2Xyz(x, y, z);
//printf("地心地固直角坐标:%.10lf\t%.10lf\t%.10lf\n", x, y, z);
//Xyz2Blh(x, y, z);
//printf("转回大地经纬度坐标:%.10lf\t%.10lf\t%.10lf\n", x, y, z);
double x = -2318400.6045575836;
double y = 4562004.801366804;
double z = 3794303.054150639;
//116.9395751953 36.7399177551
printf("地心地固直角坐标:%.10lf\t%.10lf\t%.10lf\n", x, y, z);
Xyz2Blh(x, y, z);
printf("转回大地经纬度坐标:%.10lf\t%.10lf\t%.10lf\n", x, y, z);
}
void CalEcef2Enu(Eigen::Vector3d& topocentricOrigin, Eigen::Matrix4d& resultMat)
{
double rzAngle = -(topocentricOrigin.x() * d2r + pi / 2);
Eigen::AngleAxisd rzAngleAxis(rzAngle, Eigen::Vector3d(0, 0, 1));
Eigen::Matrix3d rZ = rzAngleAxis.matrix();
double rxAngle = -(pi / 2 - topocentricOrigin.y() * d2r);
Eigen::AngleAxisd rxAngleAxis(rxAngle, Eigen::Vector3d(1, 0, 0));
Eigen::Matrix3d rX = rxAngleAxis.matrix();
Eigen::Matrix4d rotation;
rotation.setIdentity();
rotation.block<3, 3>(0, 0) = (rX * rZ);
//cout << rotation << endl;
double tx = topocentricOrigin.x();
double ty = topocentricOrigin.y();
double tz = topocentricOrigin.z();
Blh2Xyz(tx, ty, tz);
Eigen::Matrix4d translation;
translation.setIdentity();
translation(0, 3) = -tx;
translation(1, 3) = -ty;
translation(2, 3) = -tz;
resultMat = rotation * translation;
}
void CalEnu2Ecef(Eigen::Vector3d& topocentricOrigin, Eigen::Matrix4d& resultMat)
{
double rzAngle = (topocentricOrigin.x() * d2r + pi / 2);
Eigen::AngleAxisd rzAngleAxis(rzAngle, Eigen::Vector3d(0, 0, 1));
Eigen::Matrix3d rZ = rzAngleAxis.matrix();
double rxAngle = (pi / 2 - topocentricOrigin.y() * d2r);
Eigen::AngleAxisd rxAngleAxis(rxAngle, Eigen::Vector3d(1, 0, 0));
Eigen::Matrix3d rX = rxAngleAxis.matrix();
Eigen::Matrix4d rotation;
rotation.setIdentity();
rotation.block<3, 3>(0, 0) = (rZ * rX);
//cout << rotation << endl;
double tx = topocentricOrigin.x();
double ty = topocentricOrigin.y();
double tz = topocentricOrigin.z();
Blh2Xyz(tx, ty, tz);
Eigen::Matrix4d translation;
translation.setIdentity();
translation(0, 3) = tx;
translation(1, 3) = ty;
translation(2, 3) = tz;
resultMat = translation * rotation;
}
void TestXYZ2ENU()
{
double L = 116.9395751953;
double B = 36.7399177551;
double H = 0;
cout << fixed << endl;
Eigen::Vector3d topocentricOrigin(L, B, H);
Eigen::Matrix4d wolrd2localMatrix;
CalEcef2Enu(topocentricOrigin, wolrd2localMatrix);
cout << "地心转站心矩阵:" << endl;
cout << wolrd2localMatrix << endl<<endl;
cout << "站心转地心矩阵:" << endl;
Eigen::Matrix4d local2WolrdMatrix;
CalEnu2Ecef(topocentricOrigin, local2WolrdMatrix);
cout << local2WolrdMatrix << endl;
double x = 117;
double y = 37;
double z = 10.3;
Blh2Xyz(x, y, z);
cout << "ECEF坐标(世界坐标):";
Eigen::Vector4d xyz(x, y, z, 1);
cout << xyz << endl;
cout << "ENU坐标(局部坐标):";
Eigen::Vector4d enu = wolrd2localMatrix * xyz;
cout << enu << endl;
}
void TestOE()
{
double L = 116.9395751953;
double B = 36.7399177551;
double H = 0;
osgEarth::SpatialReference *spatialReference = osgEarth::SpatialReference::create("epsg:4326");
osgEarth::GeoPoint centerPoint(spatialReference, L, B, H);
osg::Matrixd worldToLocal;
centerPoint.createWorldToLocal(worldToLocal);
cout << fixed << endl;
cout << "地心转站心矩阵:" << endl;
for (int i = 0; i < 4; i++)
{
for (int j = 0; j < 4; j++)
{
printf("%lf\t", worldToLocal.ptr()[j * 4 + i]);
}
cout << endl;
}
cout << endl;
osg::Matrixd localToWorld;
centerPoint.createLocalToWorld(localToWorld);
cout << "站心转地心矩阵:" << endl;
for (int i = 0; i < 4; i++)
{
for (int j = 0; j < 4; j++)
{
printf("%lf\t", localToWorld.ptr()[j * 4 + i]);
}
cout << endl;
}
cout << endl;
double x = 117;
double y = 37;
double z = 10.3;
osgEarth::GeoPoint geoPoint(spatialReference, x, y, z);
cout << "ECEF坐标(世界坐标):";
osg::Vec3d out_world;
geoPoint.toWorld(out_world);
cout << out_world.x() <<'\t'<< out_world.y() << '\t' << out_world.z() << endl;
cout << "ENU坐标(局部坐标):";
osg::Vec3d localCoord = worldToLocal.preMult(out_world);
cout << localCoord.x() << '\t' << localCoord.y() << '\t' << localCoord.z() << endl;
}
int main()
{
//TestBLH2XYZ();
cout << "使用Eigen进行转换实现:" << endl;
TestXYZ2ENU();
cout <<"---------------------------------------"<< endl;
cout << "通过OsgEarth进行验证:" << endl;
TestOE();
}
这个示例先用Eigen矩阵库,计算了坐标转换需要的矩阵和转换结果;然后通过osgEarth进行了验证,两者的结果基本一致。运行结果如下:
4. 参考
- 站心坐标系和WGS-84地心地固坐标系相互转换矩阵
- Transformations between ECEF and ENU coordinates
- GPS经纬度坐标WGS84到东北天坐标系ENU的转换
- 三维旋转矩阵;东北天坐标系(ENU);地心地固坐标系(ECEF);大地坐标系(Geodetic);经纬度对应圆弧距离
地心地固坐标系(ECEF)与站心坐标系(ENU)的转换的更多相关文章
- GNSS学习笔记--坐标转换
GNSS 坐标转换 GNSS计算主要涉及三个坐标系,地心地固坐标系,地理坐标系和站心坐标系.这里主要介绍一下三个坐标的含义和转换公式. 地心地固坐标系如图X,Y,Z表示 (ECEF坐标系),以地心O为 ...
- RTKLIB源码解析(一)——单点定位(pntpos.c)
RTKLIB源码解析(一)--单点定位(pntpos.c) 标签: GNSS RTKLIB 单点定位 [TOC] pntpos int pntpos (const obsd_t *obs, int n ...
- 利用Hackrf One进行GPS定位欺骗制作超级跑马机
0×00 驾校的困惑 现行规定要求每个学员都必须在驾校练习够规定的学时,才能参加考试,在每台教练车上都安装有计时计程终端,学员刷卡刷指纹后开始累计里程.但是目前中国的很多驾校,存在车少人多的情况,假设 ...
- ECEF和大地坐标系的相互转化
在阅读 RTKLIB的源码时,发现了ECEF和大地坐标系的相互转换的函数,大地坐标系(φ,λ,h)转成ECEF(X,Y,Z)与所看书籍(GPS原理与接收机,谢刚,电子工业出版社)的公式是一样的,而EC ...
- ArcGIS中的坐标系定义与转换 (转载)
原文:ArcGIS中的坐标系定义与转换 (转载) 1.基准面概念: GIS中的坐标系定义由基准面和地图投影两组参数确定,而基准面的定义则由特定椭球体及其对应的转换参数确定,因此欲正确定义GIS系统坐 ...
- fbx 模型转换 export
最近在做自定义类型到fbx的转换 有关polygon的理解 vertex,normal,color等信息,是离散的放置的,对fbx里面的mesh加了控制点(vertex)信息之后, 需要再设置poly ...
- PROJ.4学习——坐标系转换
PROJ.4学习——坐标系转换 前言 PROJ可以做任从最简单的投影到许多参考数据非常复杂的转换.PROJ最初是作为地图投影工具开发的,但随着时间的推移,它已经发展成为一个强大的通用坐标转换引擎,可以 ...
- GIS中的坐标系定义与转换
GIS中的坐标系定义与转换 青岛海洋地质研究所 戴勤奋 2002-3-27 14:22:47 ----------------------------------------------------- ...
- Arcgis:坐标系统极其转换
1. ArcGIS中的坐标系统 ArcGIS中预定义了两套坐标系统,地理坐标系(Geographic coordinate system)和投影坐标系(Projectedcoordinate syst ...
随机推荐
- 连接共享打印机失败错误代码0x80070035
局域网内共享打印机非常方便,但是在连接中经常遇到问题,其中出现错误代码0x80070035的概率非常之高! 1.必须确保有关打印功能的相关服务都处于自动启动状态,重点检查TCP/IP NetBIOS ...
- EL表达式学习(一)
一.初次接触EL表达式: <%@ page language="java" import="java.util.*" pageEncoding=" ...
- [转]dd大牛的《背包九讲》
P01: 01背包问题 题目 有N件物品和一个容量为V的背包.第i件物品的费用是c[i],价值是w[i].求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大. 基本思路 这是最 ...
- java.lang.NullPointerException at org.apache.jsp.index_jsp._jspInit(index_jsp.java:40)
做JSP页面时,出现如下错误,把如上面依赖下面一个去除即可
- 源码解析Spring AOP的加载与生效
本次博主主要进行Spring AOP这里的解析,因为在工作中使用后,却不知道背后的实现原理并在使用的过程中发现了一些认知缺陷,所以决定写这么一篇文章以供大家参考参考,进入正题. 本次博主使用了@Asp ...
- SpringBoot2.x+mybatis plus3.x集成Activit7版本
最近在写一个开源项目ruoyi-vue-pro,暂时负责Activiti7工作流的搭建,接这个任务一个原因,是比较好奇Activiti7版本与先前的5.6版本究竟有什么区别,因为先前在工作当中,最开始 ...
- VS Code闪现,巨头纷纷入局的Web IDE缘何崛起?
我发了,我装的. 就在前几天,微软简短的发布了Visual Studio Code for the Web 的公告,而没过一阵,这则公告就被删除了,现在点经相关内容已经是404状态了.虽然公告的内容已 ...
- chrome插件开发学习(一)
两个不错的网址: 360chrome插件开发文档:http://open.chrome.360.cn/extension_dev/manifest.html 图灵 chrome插件开发于应用 电子书: ...
- CSP 2021 游记
\(\text{Day -INF}\) 看见了 \(\text{SCP2021}\) 的报名通知,想着应该教练会让我们统一报名,就没放在心上 然后-- 然后过了二十多天教练根本没有提报名的事情,搞得我 ...
- Vue Abp vNext用户登录(Cookie)
因为Abp vNext没找到Vue的模板,网上也没找到相关vNext的例子,只能自己试着写写,asp.net core abp vue都是刚学不久,所以很粗糙也可能有错误的地方,如果您看到请指正,谢谢 ...