☰
基础地理要素转实体数据实战:从DLG到基础地理实体的核心流程
2026/10/14 12:30:40 网站建设 项目流程

简介:面向基础地理信息专业技术人员的一份正式技术规程,规定了1∶500、1∶1 000、1∶2 000比例尺基础地理信息要素数据转换为基础地理实体数据的技术路径。文件涵盖范围、引用文件、术语定义、基本要求、作业流程、源数据收集与预处理、映射转换、语义化处理、质量控制和成果归档等关键环节,并明确了时空基准、数据格式、精度与粒度要求。该PDF文档共1个文件,压缩包约924KB,内容结构完整,尤其适合测绘单位、数据生产人员和技术管理人员在新型基础测绘与实景三维中国建设中参考使用。当前已有204人学习下载。借助该技术规程,可快速了解二维基础地理实体转换生产的完整流程、质检方法和验收要求,作为项目作业依据能有效减少资料整理时间;组合与聚合地理实体数据生产还需另行查阅其他专项技术文件。

1. 把1∶500、1∶1 000、1∶2 000基础地理信息要素数据转成基础地理实体数据,不是改名,是要换一套数据模型

把1∶500、1∶1 000、1∶2 000基础地理信息要素数据转换成基础地理实体数据,听起来只像换个输出格式,但真正下过生产任务的人都知道,这是从“画图思维”切到“实体思维”的一次重构。要素数据是一张张按层拆开的图,房屋、道路、水系经常被分幅、分层拆成几十个独立对象;实体数据要求的是现实世界里一个完整对象,房子是一栋带唯一编码的房子,道路是一条能关联养护、通行信息的完整道路。某公司在模拟项目X里最初直接改字段名交付,结果在“实体唯一性、语义完整性、关系一致性”这三项上被卡了一个月。这篇不逐条解读规程条文,按生产顺序讲清楚实体化到底转什么、怎么转、参数怎么定、坑在哪里。

2. 从要素图层到实体模型:先搞清差在哪,再动手

2.1 要素是“画出来的”,实体是“认出来的”

大比例尺基础地理信息要素数据,也就是我们常说的 DLG,是分层组织的。1∶500、1∶1 000、1∶2 000 数据里通常有水系、居民地、交通、管线、境界、地貌、植被这些大类,每个大类下面又按分类代码拆成小类。这类数据的核心是“表达地形”,所以一条路会按图幅切成好几段,一段河可能同时有面要素和线要素,一栋楼会因为楼层、材质差异被分割成多个相邻多边形。这些分割在制图上没有错,但在实体化场景里就麻烦了,因为一个现实对象被拆成了多个制图对象。

基础地理实体数据要回答的是“现实世界里有什么”。一个实体必须携带唯一标识、语义类型、空间几何、属性信息、生命周期,还要能表达和其他实体的关系。你拿到一份 DLG,看到十个相邻多边形都写了同一个门牌号,人眼能认出这是一栋楼,但数据库认不出来。实体化转换要做的是把这些碎片化的几何重组成一个对象,再给这个对象一个稳定 ID、一个语义分类,并把原来分散在要素上的属性归纳到实体上。

举个例子,一栋长条形楼房被一条消防通道分隔成了三段。在 1∶1 000 的 DLG 里,这三个多边形各自独立,属性完全一致,甚至分属两个图幅。实体化时必须按门牌和房屋结构把它们合并成一个建筑实体,消防通道那部分要不要保留洞口,取决于实体模型允不允许带洞。这些几何决策直接影响后边的空间分析,比如算建筑面积、做日照分析,所以不能靠纯dissolve一气呵成。

2.2 第一步:建一张“要素-实体”映射字典,比写代码更重要

我见过不少团队一上来就写脚本,把原始图层直接转出来,结果属性表里出现大量“空语义、Other 类型、无关联对象”的实体。根子不在代码,而在于缺少一张“要素-实体”映射字典。这张字典是实体化转换的基线,所有合并规则、属性映射、关系生成都从这里来。

映射字典至少要有这些字段:原始图层名、原始要素分类代码、几何类型、目标实体类型、合并关键字、处理动作、备注。合并关键字很关键,比如房屋实体用“门牌号+名称”合并,道路实体用“道路名称+等级”合并,水系实体用“河流名+方向”合并。没有合并关键字,两栋相邻房屋可能因为几何相接被错误合并成一栋。

下面是一个简化后的映射关系示例:

原始要素层原始分类代码(示例)目标实体类型转换动作说明
居民地层310101建筑实体按门牌/名称合并相邻面多部件合并为一个实体,消除公共边
居民地层310201辅助建筑实体优先保留为独立实体如车棚、附属用房,不强行合并
交通层420101道路实体面转中心线或保留面1∶500 可保留面,1∶2 000 长线宜转中心线
水系层210101水系实体按名称合并面面状水系跨图幅合并
境界层610101行政区实体线要素转面边界需与相邻行政界线拓扑一致

这张表里要区分“实体要素”和“辅助要素”。有些要素不独立成实体,而是作为实体的一部分或附属信息存在,比如道路的边石线、房屋的阳台轮廓线,它们在实体模型中可能是道路实体的附属几何或属性,而不是单独实体。映射字典里把这些要素标记为“不参与实体构建”,能省掉大量后处理的麻烦。

另外,多个原始图层可以映射到同一实体类型,但处理动作不同。比如房屋的“面轮廓”和“房屋注记”都能映射到建筑实体,注记不生产几何,只负责补全名称属性。映射字典建议用表格维护,每次升级代码或增加比例尺数据源时,先更新字典再跑流程,避免脚本逻辑被临时需求改乱。

2.3 别把dissolve当成实体化

很多初步接触实体数据的人会觉得,实体化就是把同一名称的要素合并一下,dissolve一下就完了。实际上dissolve只能解决几何融合,做不了三件事:一是不会生成唯一的实体标识,二是不会把原始要素的分类代码映射成实体语义类型,三是不会生成实体之间的关系。做完dissolve的成果,本质上还是一条要素记录,只是几何变大了。

完整的实体化至少包含四步:几何预处理、实体构建与合并、语义属性映射、关系生成与校验。几何预处理解决拓扑错误和接边缝隙;实体构建解决“哪些要素属于同一个实体”;语义映射解决“这个实体是什么”;关系生成解决“这个实体和周围的实体是什么关系”。四步都跑通,数据才是实体模型。后续章节按这四步展开,每一步都会给出可用的参数和脚本思路。

3. 实体化转换的落地流程:清洗、融合、编码、属性映射,一条龙跑完

3.1 前置清洗:拓扑和接边不能偷懒

原始 DLG 数据从多个图幅、多个作业员手里汇过来,几乎一定有拓扑问题。最常见的是面要素自相交、公共边重复、图幅接边处差几个厘米。这些问题如果不处理,后面做实体合并时会生成诡异的多边形空洞和狭长碎面,质量检查直接挂掉。

我一般会先用脚本做一轮几何清洗。这里用 GeoPandas 做演示,它读写矢量数据、做空间运算都很方便。

import geopandas as gpd from shapely.validation import make_valid def clean_elements(path, layer=None, crs_target="EPSG:XXXX", min_area=0.0): gdf = gpd.read_file(path, layer=layer) if gdf.crs is None: gdf = gdf.set_crs(crs_target) else: gdf = gdf.to_crs(crs_target) # 修复自相交、线环自重叠等无效几何 gdf["geometry"] = gdf["geometry"].apply(make_valid) # 去掉空几何和无效行 gdf = gdf[~gdf.is_empty & gdf.geometry.notna()] gdf = gdf[gdf.geometry.is_valid] # 按比例尺过滤碎面,线状要素不参与面过滤 if min_area > 0: gdf = gdf[gdf.geometry.area >= min_area] return gdf

make_valid会把自相交的面改成有效多边形,但遇到节点重叠时输出的可能是 MultiPolygon,这是正常的。min_area参数是用来剔除碎面的,建议 1∶500 取 0.1 平方米,1∶1 000 取 0.2 平方米,1∶2 000 取 0.5 平方米。这个值不能拍脑袋定得太大,否则真有的小型附属设施会被误删。

接边处理比拓扑修复更麻烦。相邻图幅的两个同名要素,公共边往往不完全重合,直接dissolve会留下细缝或重叠。Shapely 里提供了snap函数,可以把一个几何的节点吸附到另一个几何的边上。

from shapely.ops import snap tol = 0.1 # 单位:米,按比例尺设置 gdf_left["geometry"] = gdf_left.geometry.snap( gdf_right.geometry.unary_union, tol ) gdf_right["geometry"] = gdf_right.geometry.snap( gdf_left.geometry.unary_union, tol )

snap是单向的,左图层向右图层吸完以后,右图层还需要向左图层再吸一次,双向吸附才能把公共边基本拉齐。容差tol的推荐值是图上 0.1 毫米对应的地面距离:1∶500 取 0.05 米,1∶1 000 取 0.1 米,1∶2 000 取 0.2 米。注意,容差不能大于相邻要素之间的最小间距,否则两个本来不挨着的物体会被硬吸到一起。

3.2 实体合并与唯一标识分配

几何清洗完成后,开始做实体合并。合并的核心是先按“实体类型 + 合并关键字”分组,再对同一组内的要素做几何融合。合并关键字通常是名称、门牌号、道路编号这类稳定属性。

entity_gdf = clean_gdf.dissolve( by=["ENTITY_TYPE", "KEY_NAME"], aggfunc={ "SRC_DATE": "max", # 数据生产日期取最新 "SRC_SCALE": "min", # 比例尺分母取最小,即高精度优先 "SRC_ID": list # 保留所有原始要素ID,便于追溯 }, as_index=False )

这里aggfunc里的规则要谨慎:SRC_DATE取最大值,表示数据源时间越新越好;SRC_SCALE取最小值,表示 1∶500 优先于 1∶1 000 优先于 1∶2 000;SRC_ID转成列表,是为了保留“这个实体是从哪些原始要素来的”,后面质检和回溯都要用。

dissolve之后,每个实体在几何上是一个多边形,但可能出现一个实体内部带岛洞的情况。这个在真实地形里不一定错,比如建筑围合成天井。所以不要强行把所有洞都填掉,先判断洞的面积和成因。如果洞是接边缝隙产生的伪洞,面积通常很小,可以删除;如果是真实的院落空间,要保留。

接下来分配实体唯一标识。有一种做法是直接生成 UUID,好处是全局不冲突,坏处是没有可读性,甲方验收时很难核对。

import uuid entity_gdf["ENTITY_ID"] = [ f"ENT{uuid.uuid4().hex[:24].upper()}" for _ in range(len(entity_gdf)) ]

常见生产做法是用“行政区划代码 + 实体类型代码 + 顺序号”拼一段固定长度的字符串。顺序号按实体进入库的顺序生成,同一个实体的几何更新时,顺序号不能变。所以更稳妥的做法是:先用一个内部字段存“实体候选键”,比如“实体类型+代表点坐标格网+名称”,保证同一个实体每次合并都能算出同一个候选键,入库时再用候选键生成正式标识。这样就不会因为今天少合并一个部件,明天多合并一个部件,实体 ID 就变掉。

3.3 属性映射和时间戳:强制暴露未映射编码

几何变成实体以后,属性不能直接照搬原始图层。原始 DLG 的分类代码表达的是制图分类,比如“310101”可能代表“一般房屋”,但在实体模型里,你要的是“建筑实体”“建筑附属设施”“公共设施”这类更接近业务语义的分类。属性映射要建一张代码映射字典。

code_map = { "310101": "BUILDING", "310102": "BUILDING", "310201": "AUX_BUILDING", "420101": "ROAD", "210101": "WATER_AREA", } def map_entity_type(code): code_str = str(code).strip() # 先匹配完整代码,再回退到前四位 if code_str in code_map: return code_map[code_str] if code_str[:4] in code_map: return code_map[code_str[:4]] return "UNKNOWN" entity_gdf["ENTITY_TYPE"] = entity_gdf["CODE"].apply(map_entity_type)

我习惯在脚本里把“未映射编码”作为硬校验,而不是默默赋一个OTHER。否则数据量大时根本不知道哪些编码漏掉了。

all_codes = set(entity_gdf["CODE"].astype(str)) mapped_codes = set(code_map.keys()) unmapped = sorted(c for c in all_codes if c not in mapped_codes) if unmapped: raise SystemExit(f"存在未映射编码: {unmapped[:20]}")

这里要注意,有些原始数据的分类代码带扩展位,比如前四位是“3101”,后面两位是细分码。映射字典应先匹配完整代码,匹配不上再回退前四位。回退逻辑必须可配置,不能所有图层都回退,否则会丢失细分语义。

时间戳字段同样要在这一步补上。基础地理实体要求记录实体的起始时间、终止时间。生产转换场景下,通常把原始要素的数据生产日期作为实体起始时间,终止时间留空,表示“当前存在”。如果原始要素里已经有采集时间、更新时间,优先用这些时间字段。

entity_gdf["START_TIME"] = entity_gdf["DEM_PRODUCT_DATE"] entity_gdf["END_TIME"] = None entity_gdf["DATA_SOURCE"] = "DLG@1:1000"

DATA_SOURCE字段要保留,它能告诉你这个实体来自哪个比例尺、哪批生产成果。后面如果有多个比例尺数据源,这个字段是排查“几何不统一”问题的关键线索。

4. 多尺度融合与关系构建:高精度优先规则不能拍脑袋

4.1 同一实体的多要素合并:先定优先级,再动手

一个地区手里往往同时有 1∶500、1∶1 000、1∶2 000 三套 DLG,它们比例尺不同、现势性不同、属性详略也不同。实体化的时候,同一个现实实体可能在三套数据里都出现过。这时如果直接把三套数据全部append再dissolve,结果一定乱套。

我给生产团队定的规则很简单:几何以高精度数据为主,属性以现势性新和属性全的数据为主,低比例尺数据只做补充。具体优先级是这样:

数据源几何采用属性贡献备注
1∶500主几何完整属性覆盖范围内优先使用
1∶1 000次选几何补充属性1∶500缺失区域使用
1∶2 000不参与几何名称、代码校验现势性最新时可反补属性

执行时先按“实体语义类型 + 名称/门牌 + 空间位置”做初步匹配,把三套数据里指向同一实体的要素找出来。空间位置匹配通常用质心距离阈值,阈值可以取两套数据的中误差之和再乘 1.5。比如 1∶500 数据中误差 0.2 米,1∶1 000 数据中误差 0.5 米,那阈值大约取 1.05 米。这个参数不能套用所有场景,所以我在脚本里把它单独放到配置文件里,每条实体对应一个匹配距离。

优先级确定后,几何上取 1∶500 的实体面,但要用 1∶1 000 和 1∶2 000 的面做边界修正。我一般不对低精度面做大的边界替换,因为低精度数据在局部可能比高精度更“准确”,但这种情况属于特例,不建议在批处理里自动处理。更稳的办法是:把高精度几何作为基础,用低精度几何做“公共边吸附”和“节点微调”,但保留高精度的整体形状。

4.2 实体间关系的构建与校验

基础地理实体数据不是一堆孤立对象,实体之间的关系是它区别于传统 DLG 的重要特征。关系类型一般包括空间包含关系、聚合组成关系、连通关系。空间包含关系容易做错,因为很多人拿空间连接一跑,输出的关系记录爆炸式增长。

以建筑实体和宗地实体的关系为例,一个建筑必须坐落在一块宗地内,可以用空间包含关系表达。

building_in_parcel = building_gdf.sjoin( parcel_gdf, how="left", predicate="covered_by" ) relation_gdf = building_in_parcel[ building_in_parcel["PARCEL_ID"].notna() ].copy() relation_gdf["RELATION_TYPE"] = "SPATIAL_COVERED_BY" relation_gdf = relation_gdf.rename(columns={ "ENTITY_ID": "CHILD_ID", "PARCEL_ID": "PARENT_ID" }) relation_gdf = relation_gdf[["CHILD_ID", "PARENT_ID", "RELATION_TYPE"]]

predicate="covered_by"比within合适,因为建筑边线刚好和宗地边界重合时,within会漏判,而covered_by允许边界重合。参数选择上,空间连接结果必须做唯一性检查,一个建筑实体出现在多个宗地里是不合理的;如果出现了,要回查原始图形是否跨界。

连通关系通常发生在道路实体、水系实体上。两条道路要在端点上相接,通过距离容差判断。这种关系在数据模型里往往是关系表里的一条记录,而不是在几何上做真正的拓扑打断。生成连通关系时要注意:一个复杂路口处有多条道路相交,关系表里会产生 N 条关系,数量没问题,但必须保证关系表里没有重复对。

关系表的外键完整性也要检查。关系表里出现的PARENT_ID和CHILD_ID,必须能在实体表里找到对应实体。我在生产脚本里固定加这一条:

valid_parent = set(entity_gdf["ENTITY_ID"]) valid_child = set(entity_gdf["ENTITY_ID"]) rel_check = ( relation_gdf["PARENT_ID"].isin(valid_parent) & relation_gdf["CHILD_ID"].isin(valid_child) ) if not rel_check.all(): bad = relation_gdf[~rel_check] relation_gdf = relation_gdf[rel_check] print(f"清理无效关系 {len(bad)} 条")

关系冗余也是个坑。一个建筑实体可能同时和宗地、建筑群、行政区都有空间包含关系,这些关系都能生成,但实体模型如果只需要“最近一级父对象”,那就要在父对象类型上做取舍。我的做法是:关系表再加一个RELATION_CLASS字段,区分“行政归属”“土地权属”“设施隶属”,这样同一对实体之间可以有多条不同类别的关系,但每个类别下只能保留一条。

5. 转换避坑与排查:五个高频坑,每个都有返工教训

5.1 坑一:合并后的实体出现内部空洞,质检拓扑挂掉

现象是实体化和合并后,建筑实体内部出现一些面积很小的三角形或细长多边形空洞,拓扑检查把这些标记成了面内环异常。原因是原始 DLG 里相邻要素的公共边坐标不完全一致,比如左右两个图幅的同一段边线,一个节点多、一个节点少,dissolve时算法把差值区域留成了孔洞。

处理办法是在dissolve之前,对源要素统一执行buffer(0)或make_valid,让公共边先拓扑归并;合并完成后,再按空洞面积阈值做清理。面积小于实体的 0.5%,且空洞周长大于总周长的 30%,基本可以判定是伪洞,直接按外环重建。但如果是天井、采光井这类真实洞口,就不能删。所以清理前要先看一眼空洞的位置,别让自动化把有意义的内部空间删掉。

5.2 坑二:未映射分类编码被静默赋成 UNKNOWN,语义完整性不达标

现象是入库时质检报告显示某类实体语义类型缺失率高达 20%,但生产人员坚持说脚本跑过了。原因很简单,脚本里用了code_map.get(code, "UNKNOWN"),所有没匹配上的编码都被静默变成了UNKNOWN,没有报错,也没有输出清单。

解决方法是把“未映射编码”当错误处理,强制中断生产进程。如果你不想中断,那就一定要生成一个unmapped_codes.txt,列出所有没映射的原始编码,并派专人逐个确认是补映射规则还是真不属于实体对象。我现在的做法是:首轮跑数据时打开strict=True,宁可中断三轮,也不交付一批带着一堆UNKNOWN的实体数据。编码映射字典本身也要留变更记录,谁加的映射、哪天改的,都要能追溯。

5.3 坑三:实体标识在生产过程中前后不一致,入库主键冲突

现象是同一栋建筑,第一次合并生成的 ID 是ENT10001,第二次因为多合并了一个附属用房,整个组被打散,重新生成后 ID 变了,入库更新时主键冲突,甲方库里出现一栋楼两个 ID 的脏数据。

解决思路是 ID 生成必须和几何合并顺序解耦。不能在dissolve之后按顺序编号,因为只要输入顺序或合并分组变一下,顺序号就全变。我用的办法是先用“实体类型 + 稳定的位置格网 + 名称散列”算一个候选键,候选键在合并前后保持不变;正式入库时再按候选键查重,有则沿用旧 ID,无则分配新 ID。这样即便属性更新、几何微调,ID 也稳定。

附带一个坑:多作业员并行生产时,如果各自按顺序生成 ID,几乎必然冲突。必须划分配号段,比如 A 作业员用 5000 开头,B 作业员用 6000 开头,或者都用基于候选键的散列值。

5.4 坑四:多比例尺数据接边处同一个实体出现双份

现象是 1∶500 和 1∶1 000 数据的重叠区域里,同一栋楼生成了两个实体,几何略微错位,属性各有一部分。原因是流程里没有做“跨数据源同名实体匹配”,只按图层分别实体化,再把三份结果直接追加。

解决方法是先做实体级空间匹配。判定两个实体指向同一个现实对象的三条规则:实体语义类型一致、名称或门牌一致、质心距离小于前面说的匹配阈值。满足三条规则就标记为同一实体,再按高精度优先原则选择主几何,把低精度实体的属性合并进来,低精度实体记录标记为“已融合”,不进入正式成果。这个步骤必须在建关系表之前跑,否则关系表里也会重复生成两套关系。

5.5 坑五:面积、长度属性是手抄的,实体几何更新后数值全乱

现象是入库后统计实体面积,和原始 DLG 里属性表的面积差了一大截。原因是生产脚本把原始属性表的AREA字段直接搬到了实体属性表里,没有在实体几何合并完成后重新计算。dissolve之后的实体几何,面积理论上等于原要素面积之和减去公共边消除的误差,但如果不重新计算,这个误差就永久留在数据里。

解决方法是合并后强制重算所有派生几何量。面积字段用entity_gdf.geometry.area统一刷新,总线状实体长度也要重新计算。对 MultiPolygon 实体,还要特别说明面积是不是各构件的面积之和。很多质检工具对“实体面积与原要素面积差异超过一定比例”会标错,所以这一步省不得。

6. 用质检批处理收尾:交付前先让数据自己过一遍

6.1 检查项怎么设:必填、唯一、有效、成关系

前面五章说的坑,光靠人盯是盯不过来的。我现在养成的习惯是交付前跑一个本地质检脚本,把成果数据里最关键的几类问题一次性扫出来。

def run_quality_check(entity_gdf, relation_gdf=None): issues = [] # 必填字段空值检查 required = ["ENTITY_ID", "ENTITY_TYPE", "START_TIME", "GEOM_ID"] for col in required: null_count = entity_gdf[col].isna().sum() if null_count > 0: issues.append(f"{col} 空值 {null_count} 条") # ID唯一性检查 dup_ids = entity_gdf["ENTITY_ID"].duplicated().sum() if dup_ids: issues.append(f"ENTITY_ID 重复 {dup_ids} 条") # 几何有效性检查 invalid_count = (~entity_gdf.geometry.is_valid).sum() if invalid_count: issues.append(f"无效几何 {invalid_count} 条") # 关系表外键一致性 if relation_gdf is not None: parent_set = set(entity_gdf["ENTITY_ID"]) bad_rel = ( ~relation_gdf["PARENT_ID"].isin(parent_set) | ~relation_gdf["CHILD_ID"].isin(parent_set) ).sum() if bad_rel: issues.append(f"关系表无效外键 {bad_rel} 条") return issues

这个脚本不求全,但能在半小时内把最容易导致验收退回的问题暴露出来。检查结果直接写成一个 CSV 报告,随成果一起发给甲方,至少能让验收的人看到你做过自检。

6.2 报告留档,另存一个“后悔药”

质检报告我一般会放到成果目录下的QA/文件夹里,文件名带生产批次时间。这里有一条血泪经验:早期项目里质检报告只输出到控制台,结果质询时拿不出记录,只能重新跑数据。现在所有质检脚本都必须把结果落盘,哪怕是空结果,也要保留一个空报告文件。

从 1∶500、1∶1 000、1∶2 000 要素数据到基础地理实体数据,真正难的不是某个算法,而是把“画图成果”重新理解成“现实对象”。那些容易翻车的环节,几乎都出在映射规则不完整、ID 不稳定、关系表冗余这些看起来很小的流程上。把清洗、合并、编码、关系、质检做成一条固定流水线,每批次都留痕,后面再返工也有后悔药可吃。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询