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的平移矩阵:

\[T =
\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的逆矩阵:

\[T^{-1} =
\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确定。这个旋转变换有点难以理解,需要一定的空间想象能力,但是可以直接给出如下结论:

  1. 当从ENU转换到ECEF时,需要先旋转再平移,旋转是先绕X轴旋转\((\frac{pi}{2}-B)\),再绕Z轴旋转\((\frac{pi}{2}+L)\)
  2. 当从ECEF转换到ENU时,需要先平移再旋转,旋转是先绕Z轴旋转\(-(\frac{pi}{2}+L)\),再绕X轴旋转\(-(\frac{pi}{2}-B)\)

根据我在《WebGL简易教程(五):图形变换(模型、视图、投影变换)》提到的旋转变换,绕X轴旋转矩阵为:

\[R_x =
\begin{bmatrix}
1&0&0&0\\
0&cosθ&-sinθ&0\\
0&sinθ&cosθ&0\\
0&0&0&1\\
\end{bmatrix}
\]

绕Z轴旋转矩阵为:

\[R_z =
\begin{bmatrix}
cosθ&-sinθ&0&0\\
sinθ&cosθ&0&0\\
0&0&1&0\\
0&0&0&1\\
\end{bmatrix}
\]

从ENU转换到ECEF的旋转矩阵为:

\[R = {R_z(\frac{pi}{2}+L)}\cdot{R_x(\frac{pi}{2}-B)}
\tag{1}
\]

根据三角函数公式:

\[sin(π/2+α)=cosα\\
sin(π/2-α)=cosα\\
cos(π/2+α)=-sinα\\
cos(π/2-α)=sinα\\
\]

有:

\[R_z(\frac{pi}{2}+L) =
\begin{bmatrix}
-sinL&-cosL&0&0\\
cosL&-sinL&0&0\\
0&0&1&0\\
0&0&0&1\\
\end{bmatrix}
\tag{2}
\]
\[R_x(\frac{pi}{2}-B) =
\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)中,则有:

\[R =
\begin{bmatrix}
-sinL&-sinBcosL&cosBcosL&0\\
cosL&-sinBsinL&cosBsinL&0\\
0&cosB&sinB&0\\
0&0&0&1\\
\end{bmatrix}
\tag{4}
\]

而从ECEF转换到ENU的旋转矩阵为:

\[R^{-1} = {R_x(-(\frac{pi}{2}-B))} \cdot {R_z(-(\frac{pi}{2}+L))}
\tag{5}
\]

旋转矩阵是正交矩阵,根据正交矩阵的性质:正交矩阵的逆矩阵等于其转置矩阵,那么可直接得:

\[R^{-1} =
\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的图形变换矩阵为:

\[M = T \cdot R =
\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的图形变换矩阵为:

\[M^{-1} = R^{-1} * T^{-1} =
\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. 参考

  1. 站心坐标系和WGS-84地心地固坐标系相互转换矩阵
  2. Transformations between ECEF and ENU coordinates
  3. GPS经纬度坐标WGS84到东北天坐标系ENU的转换
  4. 三维旋转矩阵;东北天坐标系(ENU);地心地固坐标系(ECEF);大地坐标系(Geodetic);经纬度对应圆弧距离

地心地固坐标系(ECEF)与站心坐标系(ENU)的转换的更多相关文章

  1. GNSS学习笔记--坐标转换

    GNSS 坐标转换 GNSS计算主要涉及三个坐标系,地心地固坐标系,地理坐标系和站心坐标系.这里主要介绍一下三个坐标的含义和转换公式. 地心地固坐标系如图X,Y,Z表示 (ECEF坐标系),以地心O为 ...

  2. RTKLIB源码解析(一)——单点定位(pntpos.c)

    RTKLIB源码解析(一)--单点定位(pntpos.c) 标签: GNSS RTKLIB 单点定位 [TOC] pntpos int pntpos (const obsd_t *obs, int n ...

  3. 利用Hackrf One进行GPS定位欺骗制作超级跑马机

    0×00 驾校的困惑 现行规定要求每个学员都必须在驾校练习够规定的学时,才能参加考试,在每台教练车上都安装有计时计程终端,学员刷卡刷指纹后开始累计里程.但是目前中国的很多驾校,存在车少人多的情况,假设 ...

  4. ECEF和大地坐标系的相互转化

    在阅读 RTKLIB的源码时,发现了ECEF和大地坐标系的相互转换的函数,大地坐标系(φ,λ,h)转成ECEF(X,Y,Z)与所看书籍(GPS原理与接收机,谢刚,电子工业出版社)的公式是一样的,而EC ...

  5. ArcGIS中的坐标系定义与转换 (转载)

    原文:ArcGIS中的坐标系定义与转换 (转载) 1.基准面概念:  GIS中的坐标系定义由基准面和地图投影两组参数确定,而基准面的定义则由特定椭球体及其对应的转换参数确定,因此欲正确定义GIS系统坐 ...

  6. fbx 模型转换 export

    最近在做自定义类型到fbx的转换 有关polygon的理解 vertex,normal,color等信息,是离散的放置的,对fbx里面的mesh加了控制点(vertex)信息之后, 需要再设置poly ...

  7. PROJ.4学习——坐标系转换

    PROJ.4学习——坐标系转换 前言 PROJ可以做任从最简单的投影到许多参考数据非常复杂的转换.PROJ最初是作为地图投影工具开发的,但随着时间的推移,它已经发展成为一个强大的通用坐标转换引擎,可以 ...

  8. GIS中的坐标系定义与转换

    GIS中的坐标系定义与转换 青岛海洋地质研究所 戴勤奋 2002-3-27 14:22:47 ----------------------------------------------------- ...

  9. Arcgis:坐标系统极其转换

    1. ArcGIS中的坐标系统 ArcGIS中预定义了两套坐标系统,地理坐标系(Geographic coordinate system)和投影坐标系(Projectedcoordinate syst ...

随机推荐

  1. JFrame显示刷新

    1 import java.awt.BorderLayout; 2 import java.awt.Font; 3 import java.awt.event.ActionEvent; 4 impor ...

  2. go进阶--测试

    目录 1.单元测试 1.1项目结构 1.2 源代码文件 1.3 单元测试 1.4 执行测试 1.5 单元测试总结 2.性能测试 2.1 项目结构 2.2 源码文件 2.3 测试文件 2.4 执行测试 ...

  3. mysql 8.0 1405的坑

    因版本差异:password  可能对应:authentication_string字段 不行的话再尝试一下,并且修改下密码类型 ALTER USER 'root'@'%' IDENTIFIED WI ...

  4. docker《三》单机部署项目容器,nginx负载均衡

    接着<二> 创建一个网段(和二在一个网段) docker network create --subnet=172.19.0.0/24 pro-net docker run -d --nam ...

  5. nacos在nginx下集群以及数据库问题

    持久化mysql时指定数据库编辑application.properties spring.datasource.platform=mysql db.num=1 db.url.0=jdbc:mysql ...

  6. 「萌新指南」SOA vs. 微服务:What’s the Difference?

    实话实说,在我还没有实习之前,我是连 SOA 是啥都不知道的,只听说过微服务,毕竟微服务实在太火了,想不知道都难,我觉得实习的时候肯定也是微服务,进组之后发现是 SOA 架构,当时都懵了,看了很多文档 ...

  7. centos7 修改时间

    2021-07-28 查看日志时发现系统时间不正确,故修改时间 # 查看当前系统时间 date # 修改当前系统时间 date -s "2021-7-28 17:03:00" # ...

  8. C++之常指针,指针常量,函数指针,const用法总结

    1.const char *p,char const *p,char * const p 对于C++而言,没有const * 修饰符,所以,const只可以修饰类型或者变量名.因而const char ...

  9. Java中Scanner用法总结

    最近在做OJ类问题的时候,经常由于Scanner的使用造成一些细节问题导致程序不通过(最惨的就是网易笔试,由于sc死循环了也没发现,导致AC代码也不能通过...),因此对Scanner进行了一些总结整 ...

  10. springMVC学习总结(一) --springMVC搭建

    springMVC学习总结(一) --springMVC搭建 搭建项目 1.创建一个web项目,并在项目中的src文件夹下创建一个包com.myl.controller. 2.添加相应jar包 3.在 ...