几何校验与修复:为什么你的 GeoJSON 导入 GIS 后会报 self-intersection
title: 几何校验与修复:为什么你的 GeoJSON 导入 GIS 后会报 self-intersection
做数据清洗的人大多踩过这个坑:把行政区划 Shapefile 或者从 Excel 拼出来的边界转成 GeoJSON,丢进 QGIS 或 PostGIS,结果要么导入失败,要么一跑空间分析就弹 "Ring Self-intersection" 或者 "GEOS geoprocessing error"。先别急着怀疑源数据错了——十有八九是几何本身不合法。
OGC Simple Features 对"有效多边形"提了几条硬性要求,下面这几条最常被违反:
- 自相交(self-intersection):一条边穿过了另一条边,边界自己跟自己交叉。
- 环方向错误:外环应当逆时针(CCW),内环(洞)应当顺时针(CW)。老数据根本不在乎这个规矩,而 RFC 7946 规范 才正式把它写进要求。
- 重复或重合顶点:相邻两点坐标完全相同,或者两条边部分重叠。
- 未闭合环:线性环首尾点不一致。GeoJSON 规范要求环必须闭合(首点等于末点),可不少转换工具会漏掉这一步。
- 退化几何:面积为零、或者边没有长度。
shapely 实战:先校验再修复
Python 这边用 shapely 最直接。构建几何时记得加 always_xy=True,告诉 shapely 这是经纬度(EPSG:4326)地理坐标,不是平面 XY。不声明不会报错,但部分操作的语义会悄悄错。
from shapely.geometry import shape
from shapely.ops import make_valid
from shapely.validation import explain_validity
def clean_feature(feature):
geom = shape(feature["geometry"], validate=False)
if not geom.is_valid:
reason = explain_validity(geom) # 给人话原因,比 is_valid=False 有用
print(f"{feature['properties'].get('adcode')}: {reason}")
geom = make_valid(geom) # GEOS 3.8+ 覆盖模式修复
feature["geometry"] = geom.__geo_interface__
return feature
# 批量走一遍
import json
src = json.load(open("raw.geojson"))
src["features"] = [clean_feature(f) for f in src["features"]]
json.dump(src, open("clean.geojson", "w"), ensure_ascii=False)
make_valid 用的是 GEOS 3.8 引入的覆盖模式(overlay),能把自交面拆成合法的带洞面或 MultiPolygon。explain_validity 直接报出像 "Ring Self-intersection[116.39 39.90]" 这样的具体位置,定位比单纯看 is_valid 快得多。
三种方案怎么选
| 工具 | 校验 | 修复 | 适用场景 |
|---|---|---|---|
| shapely(Python) | is_valid + explain_validity | make_valid(覆盖模式) | 离线 ETL、批量清洗 |
| PostGIS | ST_IsValid + ST_IsValidReason | ST_MakeValid | 数据库内批量、SQL 流水线 |
| Turf.js(JS) | 无 is_valid;用 @turf/kinks 找自交边,features.length===0 近似判定 | @turf/rewind 调环方向 | 浏览器/Node 前端轻量校验 |
Turf 比较特别:它没有原生的 is_valid,只能靠 @turf/kinks 找出自交的线段,kinks 为空就当作合法。环方向则交给 @turf/rewind。
三个坑
1. make_valid 可能把面拆成 MultiPolygon,破坏"一个 adcode 对应一个面"的一对一假设。 后面按 adcode 前缀上卷成地市、省级边界时(见 区县边界上卷),会突然冒出多行。修复后务必检查 geometry 类型有没有从 Polygon 变成 MultiPolygon,必要时 explode 或保留原 adcode。
2. 顺序错了会出怪事。 应该先做"无效几何修复",再做 Douglas-Peucker 抽稀。抽稀在非法几何上行为未定义,要么直接抛异常,要么产出更离谱的几何。
3. 坐标系必须显式声明。 make_valid 是纯拓扑操作,不关心坐标单位。但你若顺手用 .area 看"面积",shapely 会把经纬度当平面度算,得到的是毫无意义的平方度。要看面积得走 turf.area(球面测地)或 PostGIS 的 geography 类型——环方向这一步和 RFC 7946 规范 的要求也直接相关。
把"校验 + 修复"作为 ETL 链路的独立步骤,放在导入 GIS 之前(见 Shapefile 转 GeoJSON 的 ETL 实战),能挡掉下游绝大多数 self-intersection 报错。更多格式与坐标系细节见 主站。