尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

S-57电子海图ENC文件格式解析:从二进制结构到要素属性剖解

S-57电子海图ENC文件格式解析:从二进制结构到要素属性剖解 从事电子海图和相关船舶数据处理的朋友手里多多少少都会碰到一些后缀特别怪异的文件xxx.000、CATALOG.031、xxx.001。我第一次拿到 ENCElectronic Navigational Chart电子航海图数据时双击打不开拖进 QGIS 是乱码最后被同事提醒“这玩意儿是 S-57 封装的”才意识到自己面对的不是普通 GIS 数据而是一套在航海领域沿用了二十多年的国际标准。S-57 是 IHO国际海道测量组织发布的《数字海道测量数据传输标准》而 ENC 就是按照 S-57 标准生产出来的成品电子海图。说人话就是S-57 是“格式和语法的规定”ENC 是“用这套规定写出来的文件”。这篇文章我就把一个 ENC 文件放到工作台上从二进制头到要素属性一层一层拆开给你看顺便把这些年踩过的坑、用过的方法也一并交代清楚。这篇文章适合三类人看一是 GIS 开发或数据处理工程师需要将 ENC 接入自有系统二是航海、海事、海洋测绘相关专业的在校生或测试人员总被 S-57 文件弄得一头雾水三是做船舶航行系统集成、想搞懂电子海图底层结构的开发者。1. 拆解前的认知准备S-57 和 ENC 到底是怎么组织的在打开文件之前我建议你先建立一个整体框架。很多人一上来就急着转格式结果遇到各种报错根本原因是没搞懂 S-57 的底层模型。S-57 不是一个单纯的“图层文件夹”它描述世界的方式可以概括为一句话现实世界的每个地理实体都被拆成了“特征”和“空间”两部分分开放再通过指针关联。1.1 S-57 的世界模型特征记录与空间记录S-57 把电子海图里的信息抽象成两类记录Feature Record特征记录描述“这是什么”比如一座灯标、一片水深区、一个碍航物。特征记录里存放对象类标识OBJL、属性如水深值、灯质、以及指向空间记录的指针。Spatial Record空间记录描述“它在哪里”是点、线、面这些几何元素。空间记录里存放坐标、拓扑关系、以及指向特征记录的指针。打个比方Feature Record 相当于户口本上的“张三职业灯标”Spatial Record 相当于地图上的那个坐标点“灯标在 30.123456°N, 122.654321°E”。两者分开存通过 FSPT 和 VRPT 这类指针互相指认。一个 Feature 可以挂多个 Spatial 要素一个 Spatial 要素也可以被多个 Feature 引用这就是为什么我们不能像打开普通 Shapefile 那样简单地理解 ENC 文件。特征记录又细分为 Geo地理、Meta元数据、Cartographic制图、Collection集合等子类型ENC 中常见的是 Geo 和 Meta。元记录描述数据的来源、质量、覆盖范围、比例尺等信息比如 M_QUAL、M_COVR它们是理解数据可信度的关键但经常被新手忽略。1.2 为什么 S-57 用 ISO/IEC 8211 封装数据接着回答一个常见疑问为什么 S-57 不用 XML 或者直接上 PostGIS而选择 ISO/IEC 8211 这种看起来很老的二进制结构ISO/IEC 8211 是一个数据描述文件规范早在 1994 年就被 S-57 采纳。它的核心思想是“记录-字段-子字段”的流式结构。每条记录由 Leader记录头、Directory目录区和 Field Area字段区构成。记录头告诉你这条记录总长多少、目录区从哪开始目录区告诉你每个字段的名字、长度、位置字段区才真正存放数据。这种结构的好处是流式读取不需要把整个文件载入内存一次读一条记录就能处理对海图这种动辄几十上百 MB 的文件非常友好。自描述文件开头有一条特殊的 DDRData Descriptive Record数据描述记录里面定义了后续每条数据记录DR的字段结构。等于说每个文件自带一张“说明书”。容错性好某一条记录损坏时可以跳过它继续处理后续记录。这在航海数据交换场景中很实用。理解“自描述”这一点特别重要。很多解析库包括 GDAL 的 S57 驱动之所以能处理不同生产者、不同版本的海图靠的就是先从 DDR 读出字段规则再去解析后续 DR而不是硬编码一套属性表。1.3 拆解前需要建立的 5 个心智模型在看具体文件之前请先在脑子里建立这 5 个概念对象类Object Class用 3 位数字编号来区分实体类型比如 30 是水深区38 是碍航物42 是灯标46 是陆地。GDAL 有时会把它们映射为惊讶的大写图层名比如 DEPARE、OBSTRN。属性Attribute每个对象类带一组属性。水深区带 DRVAL1最小水深、DRVAL2最大水深灯标带 LITCHR灯质、COLOUR颜色等。属性编码有枚举值也有自由值。空间原型Primitive只有三种——点Node、线Edge、面Face。注意 S-57 的面不是坐标闭合环而是由若干条边围成的区域这在转换时容易导致拓扑问题。拓扑TopologyS-57 使用链节点拓扑边与边在节点处相连面引用边来组成边界。如果没有正确的拓扑处理逻辑你直接拿到几何后会发现面是碎的。比例尺和用途Usage同一片海域可能存在不同比例尺的 ENC如 Overview、Coastal、Approach、Harbor各文件是独立封装的揣测哪个图层属于哪个展示比例很重要。有了这 5 个认知你再去解剖文件会有一种“看懂了藏宝图”的感觉。2. 文件内部的“器官”记录类型、对象类和属性配对现在我们把 ENC 文件放到解剖台上看看它内部到底长什么样。这里我以一个典型的海图数据文件为例它可能包含水深、岸线、航行标志、碍航物、港口设施等对象类。GDAL 打开后你可能会看到这些图层DEPARE、COALNE、DEPCNT、LIGHTS、OBSTRN、SOUNDG、BOYSAW、WRECKS 等。这不是巧合而是 S-57 规定的对象类映射对应的默认图层名。2.1 一条空间记录里到底装了什么一条典型的空间记录比如一条边会包含这些信息RCNMRecord Name记录类别标记能区分是特征记录还是空间记录是向量边还是孤立节点。RCIDRecord Identifier记录唯一标识在一个数据集内不会重复。VRIDVector Record Identifier空间记录自己的 ID 和版本号用于拓扑引用。PRIMPrimitive空间原型1 表示点2 表示线3 表示面。这个字段对解析几何至关重要。SG2D2D Coordinate一组二维坐标单位是 1×10⁻⁷ 度即度的小数点后 7 位。也就是说 S-57 的坐标不是经纬度的小数而是一个整数。转换时除以 10,000,000 得到经纬度。VRPTVector Record Pointer指向构成这条边或这个面的其他空间记录。FSPTFeature to Spatial Pointer指回特征记录的指针。这里要注意S-57 中坐标使用整数存储很大程度上是为了在 90 年代提高计算效率和压缩体积。GDAL 和很多解析库已经帮你做好了转换但如果你自己写解析器一定要记得这个单位换算否则坐标会偏到十万八千里外。2.2 文件名与 CATALOG.031航海数据里的“档案袋”一个 ENC 交换集通常包含多个文件其中一个重要的文件叫CATALOG.031它是整个交换集的目录文件列出了所有单元文件、更新文件、版本号、发行日期、数据所属海图编号等信息。打个比方把 ENC 交换集想象成一个快递包裹CATALOG.031 就是包裹里的发货单负责说明箱子里有几件东西、分别是什么、当前是第几版。主数据文件通常命名为GB5A0001.000这种格式其中前两位代表生产国代码中间的字母和数字代表海图编号和航行区域.000表示基础数据文件。更新文件是.001、.002这样的递增编号。注意更新文件不是整体替换文件而是增量。你必须从基础文件开始按编号顺序依次应用更新才能拿到最新状态。很多人因为只拷贝了.000而漏了更新文件导致海图内容和官方公告对不上这在航海场景里是会出大事的。CATALOG.031本身也遵循 S-57 的封装规则同样可以被 GDAL 读取。在自动化处理多张海图时我会先扫描 CATALOG.031确认版本和文件列表再决定加载哪些文件这个习惯能避免很多数据一致性错误。2.3 对象类和属性怎么配对一张快速查阅表S-57 标准里定义了约 170 多个对象类和 180 多个属性但日常最常用到的就那么几十个。我把自己经常用的几个列出来对象类图层名中文含义常见属性空间形态DEPARE水深区DRVAL1最小水深、DRVAL2最大水深、QUAPOS位置精度面SOUNDG单点水深DEPTH深度值、TECSOU测量方式点OBSTRN碍航物QUASOU性质、WATLEV水下状态、EXPSOU是否存在可疑水深点/面WRECKS沉船QUASOU、WATLEV、CATWRK沉船类别点/面LIGHTS航标灯LITCHR灯质、COLOUR灯色、SECTORS扇形范围、LITVIS灯标可见距离点COALNE海岸线CATCOA岸线类型线DEPCNT等深线VALDCO等深值线M_QUAL数据质量元数据CATZOC置信度等级、POSACC位置精度面这些属性有些是枚举码比如 CATZOC 用 A1、A2、B、C、D 表示不同可信度等级有些是数值比如 DEBTH。实际从 GDAL 读取时不同版本驱动展示的属性名可能略有差异最准确的办法是直接查ogrinfo -al的输出。2.4 一组实际属性值示例我在最近处理的一份 Harbor 比例尺 ENC 里取了一条 LIGHTS 要素的属性大致长这样不同文件字段略有差异以你自己数据为准OBJECT: LIGHTS OBJL: 42 PRIM: 1 LNAM: ...长名称标识 LITCHR: Fl(2)G 5s COLOUR: 3 SECTORS: 045/120 LITVIS: 18这个信息翻译过来就是一座灯质为“联闪 2 次绿光、周期 5 秒”的灯标灯光可见扇形角度从 45 度到 120 度射程 18 海里。这类信息在船舶航行时直接决定航路规划所以属性解析必须准确。你要特别留意枚举码和文本混存的属性比如 COLOUR 在标准里是枚举但某些生产商的软件会输出英文名所以清洗数据时最好先归一化。3. 实操把一个 ENC 文件真正拆开理论铺垫够了现在开始动手。我用的是最常见的 GDAL/OGR 工具链配合一些二进制查看工具和开源海图显示软件。整个过程分四步先看二进制头再用 OGR 看图层再用 Python 批量解析属性最后用可视化软件验证。3.1 第一步用 hexdump 看二进制头打开终端执行hexdump -C -n 256 ENC.000 | head -30你会看到类似这样的输出00000000 31 32 33 34 35 36 34 32 35 32 31 30 20 20 20 20 |123456425210 | 00000010 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 | |开头几个字节是 ASCII 数字其实就代表这条记录的长度和目录区信息。ISO 8211 的记录 Leader 是 24 字节里面用字符格式记录了“这段记录的总长度”“目录区每个字段名占几位”“每个字段长度占几位”等。你不需要手工解析这些只要知道这种设计是为了让读取程序能按固定规则跳过目录区找到真正的字段数据。如果往后面继续 dump你会看到一些偶尔可读的 ASCII 片段比如DEPARE、DRVAL1这是因为对象类名和属性名在字段区里以字符形式存在。但大部分坐标、指针都是不可读的二进制。如果你要开发自己的解析器建议参考 GDAL 的开源实现而不是从零开始写 ISO 8211 的底层逻辑这玩意儿远看是格式近看是深坑。3.2 第二步用 GDAL/OGR 把图层和属性拉出来如果你的环境里装了 GDAL没有的话直接pip install gdal或者用 OSGeo4W第一步先用ogrinfo看图层列表ogrinfo -so ENC.000输出大致是这样的INFO: Open of ENC.000 using driver S57 successful. 1: DEPARE (3D Polygon) 2: COALNE (3D Line String) 3: DEPCNT (3D Line String) 4: LIGHTS (3D Point) 5: OBSTRN (3D Point) 6: SOUNDG (3D Point) 7: WRECKS (3D Point)看到没有所有图层名都是简写。3D Polygon、3D Point里的第三个维度通常是水深高度或高度但到底代表什么要看属性不能想当然。接着查看某个具体图层的字段结构ogrinfo -al -so ENC.000 DEPARE你会看到字段列表里面通常包含OBJECT、OBJL、PRIM、LNAM、DRVAL1、DRVAL2等。要注意的是GDAL 的 S57 驱动在读取特征记录时会尝试把属性名称翻译成带语义的名字但并非 100% 完整。如果你发现某些枚举属性变成了纯数字别慌去查 S-57 标准附录 A 的表格对应关系即可。3.3 第三步用 Python 批量解剖所有对象类写个小脚本循环读取所有图层和要素一次拿到全部属性的频次和字段分布。我经常用类似下面的代码from osgeo import ogr ds ogr.Open(ENC.000, 0) if ds is None: print(无法打开文件) exit(1) for layer_index in range(ds.GetLayerCount()): layer ds.GetLayerByIndex(layer_index) print(f图层: {layer.GetName()}, 要素数: {layer.GetFeatureCount()}) feature layer.GetNextFeature() if feature: feature_defn layer.GetLayerDefn() field_names [] for i in range(feature_defn.GetFieldCount()): field_names.append(feature_defn.GetFieldDefn(i).GetName()) print(字段:, , .join(field_names)) geom feature.GetGeometryRef() if geom: print(几何类型:, geom.GetGeometryName()) ds None如果你的数据源是多个文件可以把整个目录都遍历一遍顺便统计每个对象类出现的次数和数据覆盖范围。我当时做海图数据质量检查时就用这套脚本快速发现了好几个图层中水深属性为空的情况为后续手工排查缩小了范围。如果你更喜欢用 Fiona也可以这样读取import fiona with fiona.open(ENC.000, layerDEPARE) as src: for feature in src: props feature[properties] print(props.get(DRVAL1), props.get(DRVAL2))这里我特别提醒一句直接用ogr2ogr把 ENC 转成 GeoJSON/Shapefile 是最快的方式但也是最容易出问题的方式。因为 S-57 面向对象的模型和 GeoJSON 这种简单要素模型并不完全兼容转换后可能出现面要素被拆断、属性丢失、单要素多点集合变形等问题。如果需要精确保留语义建议使用 PostGIS 的ogr2ogr导入到数据库保留属性更可靠如果只是做可视化快速浏览GeoJSON 完全够用。3.4 第四步用海图显示软件验证拆解结果数据拆完必须拿“人眼”验证一遍。我常用两种方式OpenCPN免费开源航海软件可以直接加载 ENC 目录。它会自动读取 CATALOG.031、应用 S-52 显示规则把数据渲染成真正符合航海习惯的海图。如果 OpenCPN 里显示正常说明文件基本没问题。7Cs HydroView或其他 S-57 查看器适合快速查看属性和几何的对应关系能直接选中某个要素看它挂在哪个对象类、带什么属性。这一步看起来“不够硬核”但实际能帮你发现很多程序读不出来的问题。比如有一次我解析出的某个碍航物坐标和 OpenCPN 里显示的位置明显差了几百米最终查出是源文件里使用的基准面并非 WGS84单纯按整数缩放换算会出错。如果只盯着代码输出这种问题很难暴露。4. 拆解中常见的坑和排查实录这是我最想让你认真看的一部分。解剖 ENC 文件技术上不难难的是处理数据和业务理解之间那些隐形的“暗雷”。4.1 图层空、要素少的真相有次我拿到一个 Harbor 比例尺的 ENC打开后 DEPARE 图层居然一个要素都没有。当时第一反应是文件损坏但 OpenCPN 能正常显示。后来仔细看 CATALOG.031 才发现该数据集的实际覆盖范围是以港口入口为主水深区被拆成了相邻的另一个单元文件而不是我手里这份。这种问题在自动批量处理时特别常见。建议接到 ENC 文件后先看 CATALOG.031 和元数据图层如 M_COVR、M_QUAL再去看具体的地理要素。不要一开始就假设单文件包含全部信息。航海数据是以“单元cell”为单位组织的一个完整海图可能横跨多个 cell。4.2 水深、障碍物、灯标数据对不上的原因S-57 中“特征-空间”是分离的在某些生产软件里两个不同的特征可能指向同一个空间记录。转成标准 GIS 数据后这种“多对一”关系可能被展开成重复几何也可能被丢弃具体表现是水深层里出现一个点但不知道该点属于碍航物还是沉船。遇到这种情况建议先检查对象的OBJL字段确认对象类编码。如果对象类和实际业务语义不符往往是因为源文件里存在“复合特征”或“多用途要素”。我在处理沉船数据时经常发现同一条沉船同时出现在 OBSTRN 和 WRECKS 两个图层它们的坐标一致但属性侧重不同。这两类数据在生成海图时一个用于一般警告一个用于沉船专项显示。做数据融合时如果不做去重下游系统会重复播报同一碍航物。4.3 更新文件.001到底怎么叠很多新人拿到.000和.001两个文件想省事就只加载.000。但航海数据是强时效性的漏掉更新文件等于拿旧图跑船。正确做法是先读取 CATALOG.031确认有哪些更新文件然后按顺序应用。GDAL 的 S57 驱动本身不自动帮你应用增量更新你需要用海图生产软件或者专门的更新工具先做“增量合并”合并后的完整 ENC 才能用于后续 GIS 分析。我自己写自动化脚本时采用了一个笨但可靠的办法先把基础文件.000和更新文件.001、.002拷贝到一个临时目录让支持增量合并的工具生成一个新的基础文件然后再统一处理。千万别手动去改属性来模拟更新后果比不更新还可怕。4.4 坐标系与单位的“暗雷”S-57 标准默认地理坐标系是 WGS84但实际生产中你可能会遇到本地基准面海图尤其是一些老版本数据和部分浅吃水区域的测量数据。坐标单位是 1×10⁻⁷ 度水深单位是米灯标射程单位是海里这些在文档里都写得很清楚但很多人解析时习惯性认为“所有数值都是米”结果把灯标射程和深度混在一起参与计算。建议在代码里维护一张“属性单位表”识别到具体属性名后按单位转换。另外大部分 S-57 水深值以正值表示深度比如 DRVAL220.5 表示最大水深 20.5 米。但部分海域测量基准面不同水深值可能以负值表示读取时务必结合CHDST深度基准面等元数据字段判断。这个坑我在处理南海某区域的港口图时踩过当时把水深全部当成正值参与航道扫测差点出错。4.5 拓扑错误和面要素缺失S-57 的面不是“坐标环”而是“边集合”。某些生产软件在导出时会包含未正确闭合的边导致 GDAL 无法构建完整多边形表现为要素存在但几何为 NULL或者面只有边界线没有内部填充。处理这类数据时我会先用 QGIS 的“检查几何有效性”工具跑一遍找出无效要素再用ogr2ogr的-makevalid参数尝试修复。修复后必须目检因为自动修复可能把真实的空洞如港池、船坞误删。我的经验是对 ENC 做任何几何处理之前先备份原始文件。这类数据的精度要求极高宁可多用几分钟备份也不能用错误几何污染下游系统。5. 从 S-57 走向 S-100解剖完之后的几点体会解剖完一个 ENC 文件后你会发现 S-57 的设计其实相当精妙它在 90 年代的技术条件下用最小的存储代价完成了一个全球海图数据交换系统的搭建。但它的缺点也很明显属性体系封闭、拓扑处理复杂、GML 和 Web 服务时代并不友好。于是就有了新一代标准 S-100 以及基于它的 S-101 ENC。S-100 采用基于地理信息标准的通用数据模型支持 XML/GML、要素目录可扩展、产品规格按需定制。未来你会接触到越来越多以.h5或 GML 形式发布的海道数据但现役船舶设备和大量存量系统仍在使用 S-57 格式这个过渡期会持续很长。所以掌握 S-57 不是“学老古董”而是在为理解海事数据建模思路打底子。最后分享一个我个人的实操习惯无论处理多少个 ENC 文件我都会生成一份“数据拆解报告”按对象类统计要素数量、属性缺失率、坐标范围、更新版本号。这样哪怕半年后再回来处理同一批数据也能一眼看出变化。做海图数据这件事慢就是快稳就是赢。
返回列表