ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

经纬度换算与坐标系转换:从度分秒到地图显示的完整指南

经纬度换算与坐标系转换:从度分秒到地图显示的完整指南

经纬度换算这件事,听起来简单,真做起来坑特别多。我见过不少拿着GPS设备跑到外业现场,回来导出一堆度分秒数据,结果往Excel里一贴全乱;也有人收到别人发来的点坐标,丢进地图软件发现位置差出几百米,第一反应是设备坏了,实际是坐标系压根没对上。这篇文章就把经纬度换算和“显示点经纬度”这条链路从头到尾拆开讲:两种格式怎么互转、四种坐标系为什么会互相漂移、每一步操作怎么做、踩过的坑有哪些。适合刚开始接触GIS的开发者、做外业采集的测绘相关从业者、无人机航测的朋友,以及任何想把自己的轨迹点准确标到地图上的人。

1. 经纬度换算是什么:先搞定格式和符号

1.1 两种主流格式

经纬度在日常生活和工程里,最常遇到的写法有两种:度分秒(DMS)和十进制度(DD)。

度分秒的例子是“东经121度28分25.32秒,北纬31度13分49.44秒”,也就是121°28'25.32"E、31°13'49.44"N。这种写法人看了很直观,但计算、入库、画图都很麻烦。

十进制度的例子是“经度121.4737,纬度31.2304”。小数点后面直接就是度数,适合程序处理。大多数GPS接收机、地图API、GIS软件内部用的都是十进制度。

很多人刚开始搞不清的就是这两者关系:同样是描述一个点,凭什么一个带小圆圈和撇号,一个带小数点?其实核心就是一个,没有高低之分,只是表示习惯不同。工程上要求所有数据统一格式,所以必须会换。

1.2 度分秒与十进制度的换算公式

换算公式我直接列出来,后面会用手算一步步过一遍。

十进制度(DD)转度分秒(DMS):

  • 整数部分就是度(D)
  • 小数部分乘60,整数部分是分(M)
  • 剩余小数再乘60,得到秒(S)

度分秒转十进制度:

  • 十进制度 = D + M/60 + S/3600

以东经121度28分25.32秒为例: 121 + 28/60 + 25.32/3600 = 121 + 0.466667 + 0.007033 = 121.4737

北纬31度13分49.44秒: 31 + 13/60 + 49.44/3600 = 31 + 0.216667 + 0.013733 = 31.2304

反过来,把121.4737还原回度分秒:

  • 度:121
  • 小数0.4737乘60得28.4220,取整数28作为分
  • 0.4220乘60得25.32,作为秒 结果就是121°28'25.32",完全对得上。

需要注意的是,这个例子特意选了一个“能除尽”的坐标,实际数据往往会有长尾巴,保留到小数点后4位左右就足够多数场景使用了,因为0.0001度大约对应11米,已经能确定一个路口的位置。

1.3 一度、一分、一秒在地面上到底有多远

这个体感特别重要。很多人换算完没有概念,不知道自己算出来的结果是不是合理。

地球赤道周长约40000公里,所以经度方向上一度大约111公里。纬度方向上一度同样是约111公里。这么算下来:

  • 一分大约是1.85公里
  • 一秒大约是30.9米
  • 0.0001度大约是11米
  • 0.00001度大约是1.1米

有了这个尺度,你就能判断一个坐标“看起来对不对”。比如有人说自己站在某路边,结果两个坐标之间差出0.1度,那就是11公里,明显中间隔了好几个街区,肯定有问题。

1.4 常用显示场景里的格式约定

不同场景的默认格式不一样,这是最容易被忽略的:

  • GPS裸机输出的NMEA句子,比如GPGGA,里面的经纬度是“度+分”的变体,格式是ddmm.mmmm。比如一个句子写成3107.4123,N,意思是31度07.4123分,不是31.074123度。
  • Excel表格和数据库一般存十进制度。
  • 某些勘察软件界面显示度分秒,但导出的文本是十进制度。
  • 地图API的坐标参数,绝大多数要求十进制度,并且经纬度顺序必须按照API文档来,有的先经度后纬度,有的先纬度后经度。

所以拿到数据第一步不是算,而是先问一句:这个坐标是什么来源,里面是什么格式。格式定了,再谈换算。

2. 坐标系换算:为什么换了格式还是显示偏了

2.1 你拿到的坐标不一定都是同一个地球

格式搞定了,位置还可能偏。原因是你手里的经纬度,可能不是在同一个坐标框架下定义的。

最常见的三种:

  • WGS84:GPS卫星直接输出的坐标基准,国际上通用的原始坐标系,很多离线地图、国际地图服务直接用这个。
  • GCJ-02:国内网络地图服务商普遍使用的加密坐标系,对WGS84做了非线性偏移,可以理解为在原始坐标上叠加了一个“随机但可计算的扰动”。原坐标经过加密后,点位被移动了几十到几百米。
  • BD-09:另一个国内地图平台在GCJ-02基础上再次加密得到的坐标系,两者又差了几百米。

你用手机GPS获取的通常是WGS84坐标,直接把WGS84坐标丢给某个国产网络地图底图,就会看到点位漂移。反过来,从某个地图API上拾取出来的坐标,如果直接当成GPS原始坐标发回设备,也会偏。这不是你操作错了,是坐标参考不一致。

2.2 几种坐标系对照表

坐标系典型来源典型去向是不是最终显示坐标
WGS84GPS设备、国际在线地图、野外采集数据分析、离线底图、国外地图服务直接显示在国际系底图上没大问题
GCJ-02国内地图平台坐标拾取、网络地图叠加国内地图服务底图需要传给国内在线地图API时使用
BD-09某个地图平台二次加密后的输出该平台自家地图只在那个平台显示时使用

如果数据要在多个平台间流转,你就得在各坐标系之间做转换。转换不是随便加减,它是一套固定的数学算法。

2.3 WGS84与GCJ-02互转算法实现

我写一个简化版本,适合日常用,跑起来不需要额外依赖。核心思路是先把WGS84坐标代入偏移函数算出GCJ-02结果,反之用迭代逼近。

import math a = 6378245.0 ee = 0.00669342162296594323 pi = math.pi def out_of_china(lng, lat): return not (72.004 <= lng <= 137.8347 and 0.8293 <= lat <= 55.8271) def transform_lat(x, y): ret = -100.0 + 2.0*x + 3.0*y + 0.2*y*y + 0.1*x*y + 0.2*math.sqrt(abs(x)) ret += (20.0 * math.sin(6.0*x*pi) + 20.0 * math.sin(2.0*x*pi)) * 2.0 / 3.0 ret += (20.0 * math.sin(y*pi) + 40.0 * math.sin(y/3.0*pi)) * 2.0 / 3.0 ret += (160.0 * math.sin(y/12.0*pi) + 320.0 * math.sin(y*pi/30.0)) * 2.0 / 3.0 return ret def transform_lng(x, y): ret = 300.0 + x + 2.0*y + 0.1*x*x + 0.1*x*y + 0.1*math.sqrt(abs(x)) ret += (20.0 * math.sin(6.0*x*pi) + 20.0 * math.sin(2.0*x*pi)) * 2.0 / 3.0 ret += (20.0 * math.sin(x*pi) + 40.0 * math.sin(x/3.0*pi)) * 2.0 / 3.0 ret += (150.0 * math.sin(x/12.0*pi) + 300.0 * math.sin(x/30.0*pi)) * 2.0 / 3.0 return ret def wgs84_to_gcj02(lng, lat): if out_of_china(lng, lat): return lng, lat d_lat = transform_lat(lng - 105.0, lat - 35.0) d_lng = transform_lng(lng - 105.0, lat - 35.0) rad_lat = lat / 180.0 * pi magic = math.sin(rad_lat) magic = 1 - ee * magic * magic sqrt_magic = math.sqrt(magic) d_lat = (d_lat * 180.0) / ((a * (1 - ee)) / (magic * sqrt_magic) * pi) d_lng = (d_lng * 180.0) / (a / sqrt_magic * math.cos(rad_lat) * pi) return lng + d_lng, lat + d_lat def gcj02_to_wgs84(lng, lat): if out_of_china(lng, lat): return lng, lat lng_tmp, lat_tmp = lng, lat for _ in range(10): mg_lng, mg_lat = wgs84_to_gcj02(lng_tmp, lat_tmp) d_lng = mg_lng - lng d_lat = mg_lat - lat if abs(d_lng) < 1e-9 and abs(d_lat) < 1e-9: break lng_tmp -= d_lng lat_tmp -= d_lat return lng_tmp, lat_tmp

这个脚本我实测下来,反复迭代几次误差能控制在很小范围内。核心原理就是把待转换坐标当作WGS84,计算它转成GCJ-02后的误差,然后用误差反向修正原坐标,反复逼近。

2.4 GCJ-02与BD-09互转算法

如果你用的地图平台是基于BD-09的,就需要在GCJ-02和BD-09之间再转一次。算法相对固定:

x_pi = math.pi * 3000.0 / 180.0 def gcj02_to_bd09(lng, lat): z = math.sqrt(lng*lng + lat*lat) + 0.00002 * math.sin(lat * x_pi) theta = math.atan2(lat, lng) + 0.000003 * math.cos(lng * x_pi) bd_lng = z * math.cos(theta) + 0.0065 bd_lat = z * math.sin(theta) + 0.006 return bd_lng, bd_lat def bd09_to_gcj02(lng, lat): x = lng - 0.0065 y = lat - 0.006 z = math.sqrt(x*x + y*y) - 0.00002 * math.sin(y * x_pi) theta = math.atan2(y, x) - 0.000003 * math.cos(x * x_pi) gcj_lng = z * math.cos(theta) gcj_lat = z * math.sin(theta) return gcj_lng, gcj_lat

注意,BD-09的官方称呼就是某平台坐标体系,它只在该平台体系里使用。除非你确定目标底图是它,否则不建议把坐标转成BD-09。

2.5 换算工具怎么选

在工具选择上,我建议按场景区分:

  • 如果只是零星转换几个点,优先用在线转换网页,又快又不用装东西。但要注意,涉密的、不允许公开的数据不要丢到网上,别贪方便。
  • 如果数据量几十上百个点,建议用代码批量处理。Python跑脚本,或者Excel套公式都对。
  • 如果日常经常做坐标转换,最好把脚本固化成一个小工具,我后文会详细说怎么固化。
  • 如果要做严谨的测绘级转换,涉及高程、投影带、七参数等,那就得用专业测绘软件,不能用简化的火星坐标算法来替代。

3. 实操:把经纬度点一步步显示到地图上

3.1 第一步:标准化原始坐标

假设你手里有三个点的坐标,如表所示:

点号原始格式原始坐标
A度分秒121°28'25.32"E,31°13'49.44"N
B度分秒121°27'58.10"E,31°14'22.08"N
C十进制度121.4789,31.2298

先统一成十进制度。A点我们已经算过,是121.4737、31.2304。B点同样算一下: 121 + 27/60 + 58.10/3600 = 121.4661 31 + 14/60 + 22.08/3600 = 31.2395

C点本身就是十进制度,不动。这样三个点全部统一到了十进制度格式。

为了不让后续步骤被负号问题干扰,顺便检查所有符号:东经北纬都是正数,没问题。

3.2 第二步:确定目标坐标系并执行换算

接下来看你打算在哪个地图软件里显示。

如果你用的是国际通用底图(比如OpenStreetMap系或者自己下载的全球影像),底图坐标系一般是WGS84,那你直接用标准化后的WGS84坐标就行,不用再做坐标系转换。

如果你用的是国内网络地图平台,底图多半是GCJ-02或者BD-09,那你就要把手里的WGS84坐标按第三节的算法转换。例如A点在WGS84下是121.4737、31.2304,转成GCJ-02后大约会变成121.4787、31.2297附近,看起来“挪”了几十米。

千万不要在WGS84底图和火星坐标底图之间混着用。这个问题的表现非常迷惑:点能显示出来,但位置明显不在实地的路口上,而是偏到马路另一侧甚至几个街区外。

3.3 第三步:在GIS/地图软件里显示点

我拿PC端的开源GIS桌面软件来举例,这个流程在绝大多数同类软件里通用:

  • 新建项目,先加载一个合适底图。
  • 添加文本图层,选择你准备的那个CSV文件。
  • 指定X字段为经度,Y字段为纬度。
  • 关键一步:设置图层的坐标系。如果你的CSV存的是WGS84十进制度,就选WGS84地理坐标系;如果已经转成了GCJ-02,也要在软件里做对应设置,或者把原始WGS84坐标用工具转换后再导入。
  • 确认后软件会把点显示到底图上。如果点没有出现,优先检查属性表,看字段名是不是被识别成字符串了。

如果是Web端显示,用JavaScript更直接。核心代码只有几行:

// 示意代码:具体API以你选的地图库为准 var map = initMap('map', { center: [31.2304, 121.4737], zoom: 15 }); // 创建一个点标记 var marker = addMarker([31.2304, 121.4737], { title: '采样点A' }); marker.bindInfo('经度:121.4737,纬度:31.2304'); marker.openInfo();

这里的initMap、addMarker是地图库的通用封装,实际编写时换成所选地图库的方法即可。关键是传入坐标时的顺序和坐标系要一致,这是绝大多数新手卡壳的地方。

3.4 第四步:批量显示多个点

只有一个点的时候,手工点一下就能看到。二三十个点的时候,手工操作就很痛苦了,这时候要么用CSV导入功能,要么在代码里循环创建标记。

批量显示的核心逻辑就是循环。你把坐标存成数组:

var points = [ [121.4737, 31.2304, 'A点'], [121.4661, 31.2395, 'B点'], [121.4789, 31.2298, 'C点'] ]; points.forEach(function (item) { var marker = addMarker([item[1], item[0]], { title: item[2] }); });

这里我把经纬度顺序明确分开,数组里先存经度后存纬度,在显示时再调换成纬度、经度,就是为了防止顺序弄反。实际项目里你这套数组如果直接给地图库,很可能就因为顺序不同导致点全跑飞,后文会专门讲这个坑。

4. 常见问题与排查技巧实录

4.1 经纬度写反:最常见的“点跑到海里”

我记得有一次帮人排查,数据是从野外设备里导出来的,在Excel里看起来“经度”一列是31开头,“纬度”一列是121开头,因为表格的表头写错了,后面所有分析全用了反的数据。在GIS软件里加载进去,点直接出现在完全不对的位置上。

排查坐标顺序有个土办法:看范围。我国大部分区域经度是73到135,纬度是3到53。如果你发现一个坐标列表里“纬度”数值在70到135之间晃动,那大概率是经纬度写反了。另外,坐标数据如果来自GPS,通常是先纬度后经度(NMEA格式),但在GeoJSON、CSV等通用格式里,主流习惯是先经度后纬度。两种体系混用时极易搞反。

4.2 负号被吃掉:南纬西经全变东北

另一个高频问题:经纬度字符串在解析时,小数和负号被截断。尤其是手持设备导出的TXT,有的字段里带负号,有的不带。一旦负号被丢掉,南纬就变成了北纬,西经就变成了东经,差出的距离会是整个半球的量级。

建议拿到原始坐标先做一次完整性检查:凡是纬度大于90,或者经度大于180,一定有问题。凡是应该出现负号的位置没见到负号,也要警惕。

4.3 把“分”当成“度”:NMEA坐标解析错误

NMEA的GPGGA、GPRMC等语句里的经纬度不是十进制度,也不是纯度分秒,而是“度分”压缩在一起。比如31度07.4123分会被写成3107.4123。很多新手直接把3107.4123当成十进制度,那必然错得离谱。

正确解析方法是取出前两位作为度,剩下的是分,然后除以60加到度上: 31 + 07.4123 / 60 = 31.1235

这种格式在车载GPS、串口模块里非常常见,解析前一定要确认协议。

4.4 坐标系混用导致的几百米偏移

这是最隐蔽的一种错误。点能显示,但位置总差那么几十到几百米。遇到这种问题,先不要怀疑底图不准,先想想坐标到底是WGS84、GCJ-02还是BD-09。

我自己排查的顺序一般是:

  • 先看原始数据的来源文档,确定它声称是什么坐标系。
  • 再拿到目标底图上已知一个点,做交叉验证。
  • 最后选择是否启用转换函数。

如果手头只有国内网络地图的坐标,又非要在WGS84底图上显示,可以先把坐标转回WGS84再画。这步不做,位置必然偏。

4.5 浮点数精度:后8位和小数点

十进制度的精度,我实际建议保留到小数点后6位,这已经到0.1米级别,对绝大多数显示场景都溢出了。但有些系统会输出十二位小数,或者反过来只保留三位小数,这两种情况都会出问题。

保留位数太少,比如经度121.474,换算回秒大概是25.32秒附近的近似值,位置偏差可能已经到几十米。保留位数太多意义不大,但要注意粘贴到Excel时别被自动截断成科学计数法。批量处理前先把列的格式设为文本,再复制数据进去。

5. 没网的时候怎么手工换算和标注

5.1 手算流程

我不鼓励手算,但在没有电脑、没有App的场合,这套方法还是能用。你需要一个计算器或者手机里的基础计算器。

把度分秒转成十进制度,按这个步骤:

  • 分除以60,得到一个小数。
  • 秒除以3600,得到另一个小数。
  • 整数度加上前面两个小数,就是十进制度。

反过来,把十进制度转成度分秒:

  • 整数部分是度。
  • 小数部分乘60,整数部分是分。
  • 新得的小数再乘60,四舍五入就是秒。

我常用的验算技巧是:算完以后把结果代回去,重新转一遍,看能不能还原。误差在0.1秒以内就算手算成功。

5.2 纸质地图上的标注换算

如果要在纸质图纸上标点,就得利用图纸边角里的经纬网格。一般地形图每隔5分或10分画一条经纬线,先判断点位落在哪个网格里,再根据网格边的距离做比例内插。

举个例子:某点落在经度网格121度25分到121度30分之间,图纸上这段距离是8厘米,点位距121度25分那根线3.2厘米,那么经度大概是121度25分 + 3.2/8 × 5分 = 121度27分。这是一种纯机械的内插做法,熟练之后精度大概在一两秒以内,对应实地几十米,够用了。

但纸质图纸要注意磁偏角、投影变形。数字化环境下直接用软件读取是更稳妥的方式。

5.3 验证步骤:用已知点反推

我每次做批量换算,都会故意塞一个已知坐标进去,比如跑外业时记录过一个路灯的位置,后来又在网络地图上确认过它的GCJ-02坐标。批量转换后看看这个已知点能不能对得上,能对得上,说明整条链路没毛病;对不上,就逐段排查。

这个习惯在测绘里叫作检核点。哪怕是简单的经纬度换算,也建议设一两个检查点,成本极低,收益极高。

6. 个人经验:这套流程我踩过的坑和固化下来的习惯

6.1 先定坐标系,再动数据

我的第一个建议,也是最重要的一个:动坐标之前,先确认三件事——数据是什么格式、数据是什么坐标系、目标软件要什么坐标系。格式没确认前不要换算,坐标系没确认前不要显示。

很多时候点错,不是数学不过关,而是“我以为”:以为GPS给的就是地图显示的坐标,以为度分秒和十进制度只是写法不同,以为所有地图平台用的坐标系都一样。这三个“以为”只要中一个,后面就全是返工。

6.2 坐标系转换工具做成常备脚本

我建议你把自己常用的转换脚本固化成一个文件,放在电脑某个固定目录。每次拿到新数据,直接调脚本批量读入、输出标准化结果,不要每次都临时在网页上敲。

我的小工具一般包含四个函数:

  • DMS转DD
  • DD转DMS
  • WGS84转GCJ-02
  • GCJ-02转WGS84

再加一个主程序,支持读取CSV文件,指定经纬度列,输出转换后的新CSV。这套东西加起来不到一百行,但帮我省了大量重复劳动。建议你也去写一套自己用的,写的过程顺便验证一遍公式有没有记错。

6.3 数据版本和原始备份

还有一个容易被忽略的问题:数据命名和版本管理。外业数据经过连续几次转换后,很容易变成“最终版”“最终版2”“真最终版”。一旦后续发现某个环节错了,你根本不知道哪个文件是原始值。

我的做法是文件命名里带坐标系和格式标记,比如“点位20241206_wgs84_dd.csv”“点位20241206_gcj02_dd.csv”,原始文件永远不覆盖,新文件加前缀或日期。这个习惯花不了几秒,但能让排查问题的时候少走很多弯路。

最后说一个我自己的心得:经纬度换算这事,90%的麻烦不是算不出来,而是格式约定和坐标参考搞不清。每次拿到坐标,先问“什么格式、什么坐标系、谁给的”这三个问题,能少踩八成坑。另外,我电脑里常年放着一个坐标转换脚本和一个Excel模板,外业数据回来第一件事就是跑标准化流程,三十秒出结果。建议你也做一套,一旦成型,后面再也不用为“点显示不出来”这种问题焦虑了。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进