简介:针对1∶500、1∶1 000、1∶2 000比例尺基础地理信息要素数据转换生产基础地理实体数据,这份技术规程面向基础地理信息专业人员和测绘生产管理人员,是新型基础测绘与实景三维中国建设中可实际参照的作业指导文件。内容覆盖范围、引用文件、术语和定义、基本要求及作业流程,详细规定了时空基准、数据格式、精度与粒度要求,并从源数据收集分析、预处理、映射转换到语义化处理、质量控制、成果归档形成完整闭环。尤其针对二维基础地理实体数据转换生产,提供了映射表制作、属性调整、实体关系获取等关键环节的实操方法;对于组合和聚合实体的生产,则需另行参考其他技术文件。压缩包内为1个PDF文档,共924KB,适合打印后对照作业。目前已有204人学习下载。读者可依据该规程快速建立转换生产的基本流程框架和作业要点,规范各工序操作,显著提高基础地理实体数据生产效率和成果质量,为城市规划、建设管理及自然资源监测提供可靠数据支撑。
1. 大比例尺要素转实体为什么让测绘项目组集体挠头:从“要素逻辑”到“实体逻辑”
手里的1:500、1:1 000、1:2 000基础地理信息数据,是按“要素”逻辑组织的——等高线是等高线、房屋面是房屋面、路灯点是路灯点,按图层和分类码躺在库里。但实景三维和新型基础测绘要的是“地理实体”逻辑——每栋楼是一个带唯一身份编码、几何边界、语义属性的对象,能挂业务数据、能单独引用。基础地理信息数据转换这个环节,就是要在这两套逻辑之间搭一座桥。桥搭不好,后面的三维场景构建、实体单体化、业务系统挂接全得返工。这套技术规程解决的核心问题,是把转换流程从“老师傅手工操作”变成可批量、可追溯、可检查的生产管线。适合正在上实景三维项目、需要消化存量DLG资源、做新型基础测绘转型的技术人员。
2. 转换前的数据体检与坐标系底盘:三个必查项不然后面全白干
2.1 坐标系与投影参数统一:高斯-克吕格3度带对不齐,转换结果就是一堆废坐标
数据转换的第一步不是写转换程序,而是给源数据做体检。我接手过的某城市级项目,1:2 000 DLG分幅存储,每幅图理论上都是2000国家大地坐标系、高斯-克吕格投影、按3度分带。但实际一查,有七幅图用的还是旧坐标系参数,有两幅图的带号跟相邻图幅不一致。如果直接进转换管线,实体之间的拓扑关系全是错的,跨图幅拼接出来的道路会是一条断成好几截的错位线。所以体检第一项:逐图幅读取坐标系信息,核对基准、投影类型、分带方式。
用GDAL/OGR写一个批量检查脚本很省事,不用打开GIS界面挨个看。
from osgeo import ogr def check_crs(shp_path): """检查单个shapefile的坐标系,输出关键参数""" ds = ogr.Open(shp_path, 0) layer = ds.GetLayer(0) srs = layer.GetSpatialRef() auth = srs.GetAuthorityCode(None) # 返回EPSG代码 name = srs.GetName() # 返回坐标系名称 print(f"文件: {shp_path}") print(f"EPSG: {auth}, 名称: {name}") # 高斯-克吕格投影的核心参数:中央经线所在带号 proj = srs.GetProjParm("central_meridian", default=None) if proj: print(f"中央经线: {proj}") ds = None # 遍历目录下所有shp,逐幅输出 import glob for f in glob.glob(r"D:\landuse\*.shp"): check_crs(f)这段脚本的核心逻辑是:先拿SRS对象,再分别取EPSG代码和投影参数。central_meridian参数对高斯-克吕格很关键,如果一批图幅的中央经线不统一,说明带号混用,需要提前做投影换带。GetAuthorityCode(None)取的是EPSG编号,4490是CGCS2000地理坐标系,4547这类是CGCS2000的高斯投影。跑完脚本发现投影参数不一致的图幅,我一般先统一到目标带号再继续,不然后面的拼接和生产全是无效功。
2.2 图层完整性检查:缺图层不是小事,映射表都没法完整建
大比例尺DLG的标准组织方式是按《基础地理信息要素分类与代码》分层,控制点、居民地、交通、管线、水系、植被、地貌这些大类各有各的图层。但存量数据经常出现图层缺失——某一批次数据里没有管线层、另一个批次缺少控制点层。映射表是按全量图层设计的,少一个图层,转换结果的实体类型就少一大类。第二个体检项是列出每个数据集的图层清单,跟标准清单对比。
import geopandas as gpd from osgeo import ogr def list_layers(gdb_path): """列出文件地理数据库中所有要素类""" ds = ogr.Open(gdb_path, 0) names = [] for i in range(ds.GetLayerCount()): layer = ds.GetLayer(i) names.append(layer.GetName()) return names # 用GeoPandas快速读取第一个图层的属性列 gdf = gpd.read_file(r"D:\landuse\building.shp") print("属性字段:", list(gdf.columns))实际生产里,我会建一个标准图层清单文本文件,跑完脚本之后用集合差集算出缺了哪些图层。注意一点:有些项目的缺层不是真的没采集,而是数据入库时合并到了别的大图层里。这种情况要先确认再补映射,别急着认定数据缺失。检查完图层,还要看每个图层里的要素数量是不是合理范围,一个面积不小的测区里只有三条房屋记录,大概率是数据导出了问题。
2.3 属性结构摸底:要素代码字段的完整性和类型直接影响实体编码
要素到实体的转换里,属性质量决定了实体编码能不能挂得上。最关键的两个字段:要素分类码(比如表征建筑物、道路、水系的那一串代码)和要素名称或功能说明。分类码字段如果为空或类型是浮点型而不是字符串,后续做代码映射时会大量匹配失败。
体检第三项:统计关键字段的空值率、分布、类型。
import pandas as pd df = gpd.read_file(r"D:\landuse\building.shp").drop(columns="geometry") # 统计要素分类码字段的空值和取值分布 col = "GB_CODE" # 国家标准分类代码字段 print("空值数量:", df[col].isna().sum()) print("唯一值数量:", df[col].nunique()) print(df[col].value_counts().head(20))这个脚本输出的核心信息有两个:空值数量告诉你哪些要素没有分类码,这些要素在映射阶段会被丢进“待人工处理”池;唯一值数量和分布告诉你源数据里的编码用的是哪个版本。最常见的翻车情况是:转换脚本按新编码表做映射,结果源数据里混着旧码,映射表查不到就全部落到未知类别。这种情况脚本不会报错,但转换结果里的未知类实体比例会突然飙升,不擅长排查的话很难定位。
三个体检项跑完大概小半天。我把这三个脚本串成一个pre_check.py,每次接数据先跑一遍,输出一份体检报告到Excel,再定后面的转换策略。这一步省不了,我踩过一次大的:跳过体检直接转,结果因为坐标带号混用,整片区域的道路实体全部发生了偏移,返工了三周。从那以后,体检脚本成了接数据后的第一道工序。
3. 要素分类码与实体编码的映射关系表:建表的逻辑与三个边界取舍
3.1 映射表的结构:两套编码体系之间靠什么对齐
要素分类码遵循的是一套面向“要素”的编码体系,实体编码遵循的是面向“对象”的编码体系。两者在分类层级和语义粒度上不完全对齐。建映射表的第一步,是把两套码表的分类层级拆开,逐层对应。我习惯把映射表建成CSV,字段包括:要素分类码、要素名称、实体分类码、实体名称、映射类型、转换规则备注。实际结构类似这样:
| 要素分类码 | 要素名称 | 实体分类码 | 实体名称 | 映射类型 | 备注 |
|---|---|---|---|---|---|
| 410201 | 房屋面 | 230101 | 房屋实体 | 1:1 | 直接对应 |
| 510101 | 道路边线 | 320201 | 道路实体 | 2:1 | 双边线合成道路面再转实体 |
| 110103 | 等高线 | 810102 | 等高线实体 | 1:1 | 含DEM辅助信息 |
映射表不是一次性建完的,至少迭代两轮。第一轮按码表逐条对应,第二轮根据实际数据里的编码分布补漏——比如数据里出现了码表里没写到的扩展码,就要补充映射或者把它归并到上一级。还有一个容易忽略的点:实体编码体系通常比要素编码更粗一层,多个相近的要素码可能归到同一个实体码,这个时候要在备注里把合并规则写清楚,避免后期换人维护时靠猜。
3.2 一对多与多对一的取舍:合并规则写在转换规则里
编码对齐里面最容易出问题的是多对一和一对多。多对一最常见:多栋附属建筑合成一个院落实体。这种情况下,转换脚本要做的是几何合并加属性融合,融合原则是主实体属性优先、附属实体属性降级保存到扩展字段。比如某个院落有主楼、门房、车库三个面要素,合并后房屋实体只保留主楼的名称和功能属性,门房和车库的名称拼进备注字段。
线要素拆成多个实体的情况也有,但大比例尺地形图里不多见。我一般建议能不拆就不拆,除非一条长线要素跨了多个行政区或明显跨越了实体边界。拆分时要注意,实体ID不能跟源要素ID共用,否则一条要素对应多个实体时ID会重复。
import csv def load_mapping(csv_path): """加载映射表,返回两个字典:按要素码查实体码、按实体码查要素码列表""" fwd = {} rev = {} with open(csv_path, encoding="utf-8-sig") as f: for row in csv.DictReader(f): fwd[row["要素分类码"]] = { "实体分类码": row["实体分类码"], "映射类型": row["映射类型"], "备注": row["备注"] } rev.setdefault(row["实体分类码"], []).append(row["要素分类码"]) return fwd, rev这段代码的作用是把映射表读进来,转成程序运行时的内存字典。utf-8-sig是处理带BOM的Excel导出CSV时的一个坑,不用它会在读第一列时多出乱码字符。返回的两个字典分别支撑正向映射和反向校验——反向校验用来检查实体编码是否被多个要素码共用,以及有没有实体码没被任何要素码对应上。表里每一行都对应一类转换逻辑,程序在运行时查字典比反复读Excel要稳得多。
3.3 映射表落地后的校验:避免漏映射和重复映射
映射表建完之后,一定要跑一条校验逻辑:拿源数据里所有出现过的要素分类码去查映射表,统计查不到的数量和占比。查不到的占比超过5%,说明映射表没建全,这时候不要急着做转换,先把这些码找出来人工确认。我常用的校验代码是拿DataFrame直接跑:
def validate_mapping(df, fwd): """输入要素表DataFrame和映射字典,输出未映射的比例""" codes = df["GB_CODE"].dropna().unique() missing = [c for c in codes if c not in fwd] total = len(codes) print(f"源数据编码总数: {total}") print(f"未映射编码数: {len(missing)}") print(f"未映射比例: {len(missing) / total:.2%}") for c in missing[:20]: print(f" 缺映射: {c}") return missing这里有个细节容易被忽略:dropna()之后还要看空值数量。如果空值数量特别多而你又没处理,映射校验看起来没问题,实际上一大堆要素会在转换管线里因为查不到实体码而走默认分支。默认分支如果设成“跳过”,转换结果就会悄悄少一批实体;如果设成“归入未分类”,实体编码会不合法。这两个结果都不好看,所以脚本里我会把空值单独统计并打印出来。
映射表这一步的核心产出是一张带校验通过的CSV表和一个mapping.py模块。后续整个转换管线都依赖这张表,表不对,转换结果全不对。
4. 自动化转换生产流程:DLG到地理实体的四段式处理管线
4.1 几何预处理:拓扑清理与几何修复
源DLG数据常年积累,几何问题不少:面要素未闭合、线要素有悬挂点、重复坐标点、自相交多边形。这些问题不清理,后面的实体构建和入库环节都会冒出来。几何预处理的顺序一般是:先修复无效几何,再删除重复要素,最后做拓扑检查。用shapely做几何修复是目前最常见的方式,效率也不错。
from shapely.validation import make_valid def repair_geometry(geom): """修复shapely几何对象,返回有效版本""" if geom is None: return None if not geom.is_valid: return make_valid(geom) return geommake_valid的作用是把无效几何按OGC规则转换为有效几何,处理自相交的时候会把相交区域拆成多个多边形,返回的可能是一个MultiPolygon。这个行为要特别注意:如果后续逻辑假设面实体一定是单个Polygon,那么MultiPolygon进来就会出问题。所以修复完之后我习惯加一句判断:如果是MultiPolygon,按面积保留最大子面,其余子面存进扩展字段,或者允许实体由多面构成,取决于实体分类的设计。
几何预处理的另外两个环节是删重复要素和拓扑检查。重复要素按“几何全等加分类码相同”判定,用shapely的equals_exact即可。拓扑检查一般做“面要素之间压盖检测”和“线要素悬挂点检测”,这一步不一定是转换的必需环节,但建议在转换前跑一遍,把明显的拓扑错误标记出来,让转换程序硬处理容易产生一堆预料外的结果。
4.2 实体构建:面闭合、线连通、点归属的判定逻辑
几何处理完,进入实体构建阶段。实体构建按几何类型分三条路:
面实体要求闭合环且面积大于阈值。面积阈值我用的是0.5平方米,小于这个的碎面直接丢弃并写入日志。这个阈值不是固定的,1:500比例尺和1:2 000比例尺要分开设,比例尺越大,需要保留的小地物越多。
线实体要求连通。一条道路实体如果由多段线要素组成,转换时要按属性匹配后合并成一条完整线。点实体要求有明确的定位点。灯杆、井盖这类点实体在转换中主要是挂属性,几何本身不太容易出问题,但它可能落入其他面实体内部。这种情况要不要做点面叠加分析,取决于实体语义:井盖落在道路面内是正常的,路灯落在房屋面内则可能是采集错误。
def build_polygon_entity(geom, threshold=0.5): """从多边形几何构建面实体,低于面积阈值的返回None""" if geom.is_empty: return None if geom.geom_type == "Polygon" and geom.area < threshold: return None return { "geometry": geom, "entity_type": "polygon", "status": "valid", "area": geom.area, }这里的参数threshold我单独拎出来,方便不同比例尺下调整。实际项目里我把它放进配置文件,1:500出图用0.2,1:1 000用0.3,1:2 000用0.5,每个项目用一套参数,避免来回改代码。调试阶段建议把丢弃的要素落成一份“未转换清单”,方便追查哪些碎面被阈值过滤了。
4.3 属性挂接与实体唯一标识码生成
实体构建完,要给每个实体生成唯一标识码。编码规则通常由项目所在地区的实体编码标准定,一般结构是:行政区划代码 + 实体大类码 + 顺序流水号。
def generate_entity_id(district_code, entity_category, seq): """生成实体唯一标识码 district_code: 6位行政区划代码 entity_category: 4位实体大类编码 seq: 顺序号,从1开始自动补零 """ return f"{district_code}{entity_category}{seq:08d}" # 示例 print(generate_entity_id("110100", "2301", 42)) # 输出: 110100230100000042流水号按实体类别分别计数,避免全局一个大流水号造成同一个实体内多个子对象无法区分。属性挂接这块,源要素的属性字段需要按映射规则复制到实体属性表。复制时会遇到字段名冲突——源数据里可能有多套字段命名习惯,比如房屋面积有的叫AREA,有的叫MJ,有的叫BUILD_AREA。我一般建一个字段映射字典,把不同命名统一。
提示:字段映射字典在管线入口处一次性执行,不要在每个处理步骤里分别改字段名,否则很容易漏掉某一支数据流。
field_alias = { "AREA": "area", "MJ": "area", "BUILD_AREA": "area", "GB_CODE": "feature_code", }这个字典在转换管线的入口处一次性作用,把所有源数据字段对齐到统一schema。做这一步的好处是,整个管线只用写一套属性处理逻辑,不用针对不同批次的数据写分支。字段映射关系最好也落一份CSV,跟映射表放一起管理,方便后期审计。
4.4 跨图幅实体的拼接与合并
分成标准图幅存储的数据,实体可能被图幅裁切线切碎。一条路跨两幅图,转换之前是两段线要素,转换以后如果按各自要素生成实体,就会是两个道路实体,同一个实体的语义被拆开了。解决思路是转换前做一次“跨图幅预匹配”。我习惯的做法是加载相邻图幅数据后,用“几何相邻 + 属性关键字段相同”两重条件判定。几何相邻用touches或者缓冲求交所判,属性关键字段一般选实体名称、要素分类码、功能等级这类实义字段。
def merge_road_segments(gdf_a, gdf_b, attr_keys): """合并两个相邻图幅中的道路要素 attr_keys: 判定同一条道路的关键属性字段列表 """ merged = [] for i, row_a in gdf_a.iterrows(): for j, row_b in gdf_b.iterrows(): if row_a["GB_CODE"] == row_b["GB_CODE"] and \ row_a["NAME"] == row_b["NAME"]: geom_a, geom_b = row_a.geometry, row_b.geometry if geom_a.distance(geom_b) < 0.01: merged.append(row_a) # 合并后删除b中的对应行 return merged这段代码的逻辑是个简化示例,实际项目里我会用GeoPandas的sjoin做空间连接加速。但核心思路就是:关键属性匹配加空间邻近判定。合并完之后的实体ID以先到的那一半为准,另一半的要素ID存进别名字段。这是血泪教训,不加别名会导致后面查原始要素得翻图幅边界那张图,麻烦得很。
5. 避坑手册:转换生产中的五个高频翻车现场
转换生产从不是一把梭就能跑通的。下面这五条坑,是我在多次项目交付里真实踩过的,几乎每一条都让项目多干了两周以上的返工活。每条按“现象、原因、解决”三层说清楚,对照自己的数据排查最快。
5.1 面实体全部退化成线:转换结果里“楼”全没了
现象:跑完转换,实体统计时发现房屋实体数量为0;查日志没有报错,但输出结果里全是线实体。
原因:源数据的房屋面几何是“非闭合”状态,线首尾没接上,程序把它读成LineString而不是Polygon。转换代码对LineString的实体类型分配是“线实体”,于是所有本应是面的房屋全部变成了线,看起来就像楼凭空消失了。
解决:进入实体构建之前强制做闭合检查,逻辑是:几何类型为LineString但要转换成的实体分类码对应面实体的,先尝试闭合修复,再走下一步。
def close_ring(line_geom, tolerance=0.05): """尝试将线收尾闭合为面,容差范围内自动闭合""" coords = list(line_geom.coords) if coords[0] != coords[-1] and line_geom.length < tolerance: coords.append(coords[0]) from shapely.geometry import Polygon return Polygon(coords)这个坑我是在模拟项目X上踩到的,某个批次的房屋面在处理时全部被判定为无效几何,转换管线走了默认分支,房子全没了。从那以后,我在管线里加了一道防守逻辑:无效几何先尝试修复,修复不了才丢弃,而且每丢弃一条都写日志。
5.2 图层合并时机太早,属性表全空了
现象:转换后的实体几何正确,但属性表里关键字段大面积为空。
原因:转换管线设计时先合并了所有图层,再统一做要素到实体的映射。但两个图层合并不光拼接要素,字段结构也不同——房屋层有BUILD_ID,水系层没有这个字段,合并后生成的新表里BUILD_ID全成空值。映射脚本按统一字段名取值,自然拿不到值。
解决:改成“逐图层转换再合并实体”的顺序。先按图层分别构建实体,再把实体层合并。合并实体层时,各图层不同的属性字段人工归纳合并。这个坑的直观表现是,属性完整率检查在最后一周才发现,返工成本巨大。现在我的习惯是拿到数据先看属性结构,再定管线顺序。
5.3 跨图幅道路被拆成了多个实体
现象:转换后的道路实体数量明显偏多,同一条主干道在图幅边界处断开成两三个实体。
原因:转换管线没有做跨图幅预匹配,每个图幅独立构建实体并生成自己的实体ID。图幅边界处,道路被裁切成了两段线,两段线的属性字段基本一致但分属不同图幅,没有进行合并。
解决:在图幅拼接阶段做一次属性匹配预合并,把同名的连续道路合并成一条。合并条件最简单直接的一条:同一道路名称、同一道路等级。如果源数据里道路名称本来就缺失,那就退一步,用“几何端点距离小于阈值 + 道路等级一致”来判定。工程上我一般把这两种判定规则做成可配置,优先用名称,名称缺失再走几何判定。
5.4 高程信息缺失导致实体高度全为0
现象:转换结果里房屋实体和道路实体的高程属性全是0,给三维场景用的是统一默认高度,场景里建筑看起来全部贴在地皮上。
原因:大比例尺DLG很多是二维数据,本就没有高度字段。转换脚本读不到高度,默认填0。三维场景渲染时高度属性直接用0,没有任何视觉修正,整个场景的立体感完全出不来。
解决:转换时把高度字段设为可空,不要默认填0。三维护展阶段再叠加高程模型或人工补测。同时加一个标识字段说明该实体高度来源——是实测、插值还是默认,来源字段在验收时经常被重点关注。通用做法是写一个高程后处理脚本,把DEM插值结果回填到没有高程值的实体上,不在转换管线里强行补。
5.5 新旧编码标准混合使用,映射表查不到编码
现象:转换日志里未知实体码比例突然达到20%以上,而且分布集中在某几个区域范围。
原因:存量数据是由多个时期生产的,不同时期用了不同版本的要素分类标准,同一种地物在不同批次里编码差一位甚至完全不同。映射表按新标准建,旧数据全落空。
解决:在映射表里为旧编码单独建一行对应关系,标注“旧标准”,转换时打印警告日志。更稳的做法是先把源数据里所有未映射的编码清单导出来,对照旧标准手工确认映射关系,确认后再二次跑管线。不要试图在程序里自动猜测编码含义,那是个无底洞。
6. 质量检查与验收:用统计脚本给转换结果兜底
6.1 数量比对:实体数明显小于要素数时先别慌
转换完成后的第一个检查项是数量比对。一般情况下,实体数量会小于要素总数,因为多个要素合并成一个实体的情况存在。但如果数量差异超过预期(比如要素10万个,实体只有1万个),说明多对一的合并规则可能写错了。我一般会把实体数量和要素数量做成一张对比表,按实体大类分类统计,差异超过20%就先查映射表和合并规则,再往下走。
| 检查项 | 参考指标 | 说明 |
|---|---|---|
| 空几何率 | 0 | 实体必须都有几何形态 |
| 实体标识码空值率 | 0 | 每个实体必须有唯一ID |
| 实体分类码空值率 | ≤1% | 超过需要在问题时清单里说明原因 |
| 关键属性完整率 | ≥99% | 按实体类别分别统计 |
| 坐标范围 | 与预计范围一致 | 防止投影错误或坐标带混用 |
6.2 属性完整率与空几何率统计
直接比较数量不够,更可靠的是属性完整率和空几何率。
def qa_stats(gdf, key_fields): """统计实体数据的空几何率和关键字段完整率""" total = len(gdf) empty_geom = gdf.geometry.is_empty.sum() print(f"实体总数: {total}") print(f"空几何数: {empty_geom}") for field in key_fields: null_count = gdf[field].isna().sum() print(f"字段 {field} 空值率: {null_count / total:.2%}")字段列表里,实体标识码、实体分类码、几何来源、数据来源是必查的四个。空几何率为0是硬指标,实体标识码空值率为0是硬指标,实体分类码空值率允许低于1%但需要在问题清单里说明原因。跑完这个脚本,问题实体能直接定位到具体的行,省去手工翻库的时间。
6.3 坐标范围校验:防止投影错误漏到验收环节
最后一个检查是坐标范围。转换过程中如果有人动过坐标系参数,实体的坐标范围会跟预计范围对不上。用简单的extent统计就能发现。
def check_extent(gdf, expected_extent): """检查实体数据的空间范围是否在预期范围内 expected_extent: (xmin, ymin, xmax, ymax) 四种参数 """ xmin, ymin, xmax, ymax = gdf.total_bounds exp_xmin, exp_ymin, exp_xmax, exp_ymax = expected_extent check = (xmin >= exp_xmin and ymin >= exp_ymin and xmax <= exp_xmax and ymax <= exp_ymax) print(f"实际范围: {xmin:.2f}, {ymin:.2f}, {xmax:.2f}, {ymax:.2f}") print(f"范围检查: {'通过' if check else '失败'}")坐标范围检查对于大项目尤其重要,因为有多个作业组并行做分区转换,最后合并到一起时某个分区的坐标偏了很难发现。跑完这个脚本,合并后的数据直接显示每个分区的实际范围,一眼就能看出来。从那以后,我每次接转换任务都先跑一遍体检脚本,做完转换再跑一遍这三个检查,全部通过才敢往库里导。这三个脚本加在一起不到300行,但养成了习惯之后至少帮我避开了三四次大型返工。希望帮到你——尤其是正准备大规模转换存量数据的时候,这套流程真的能当后悔药用。
本文还有配套的精品资源,点击获取