一种对空间对象(Spatial Object)关系的划分是距离关系(Distance Relations)、拓扑关系(Topological Relations)和角度关系(Directional Relations)。
距离关系和角度关系是显然的,如对象A距离对象B500米,这就是一种距离关系,对象A在对象B的西边,这就是一种角度关系。而A在B的内部,这算是一种拓扑关系。在空间分析中经常使用的空间连接工具中两个图层间要素的关系往往就是拓扑关系,如相交(intersect),包含(contain),接触(touch)等等。而拓扑关系之所以称为拓扑关系,是因为这种关系在两个几何在同时进行任意弯曲、拉伸、缩小等连续变形下是保持不变的(如一张纸上有一个墨点,无论如何揉搓这张纸墨点依然在纸上),而拓扑学研究几何在拓扑变换下保持不变的性质。
本文从代数拓扑的对几何的基本认知入手,对四交模型、九交模型和OGC维度拓展的九交模型进行介绍,并对PostGIS、QGIS、shapely中的相关操作进行演示,帮助不熟悉有关知识的同学建立正确的知识框架。
代数拓扑是一个研究表达几何配置及其关系的符号的代数操作的几何学分支。(Algebraic topology is a branch of geometry that deals with the algebraic manipulation of symbols that represent geometric configurations and their relationships to one another.)
代数拓扑空间数据模型基于cell,不同空间维度下的cell有不同的定义:
接下来给出几个基础的定义:
\(\partial{C}=(\cup_{i=1}^{x}{\partial{A_i}})-(\cup_{i=1}^{x}\cup_{j=i+1}^{x}(\partial A_i \cap \partial A_j))\) \(C^\circ = (\cup_{i=1}^{x}{\bar A_i})-\partial C\) \(C^-=\cap_{i=1}^{x}{A_i^-}\)
至此,interior,boundary和exterior被严格区分,以下的表述会大量用到这三个名词。此外,以下的cell均指cell complexes。
为了将所有cell置于统一的universe下讨论,基础的拓扑结构需要满足一下两条完备性定理:
此外,这里还假设,任意cell的闭包严格位于通用的universe之内,如$A\subset R^2$
嵌入空间的维数和cell的维数之差被定义为codimension。如对于3维空间中的2-cell而言,其codimension为1;对于2维空间中的1-cell而言,其codimension也为1;2维空间中的2-cell的codimension则为0。
接下来介绍在代数拓扑视角下的GIS和地图制图中常见的点(point)、线(line)、面(region)的对应的cell 复形子集。这些复形是同质n维(homogeneously n-dimension)的,且没有被分割为非空且不相连的部分。
四交模型(4-intersection model)是Egenhofer等提出的,使用内部和边界是否交集为空,进行空间对象关系判断的模型。九交模型(9-intersection model)在此基础上,额外引入外部,进行空间关系的判断。已有研究证明,在codimension=0,即嵌入空间和几何对象的维数相等时,四交模型和九交模型有相同的结果;在codimension>0时,即嵌入空间和几何对象的维数不相等时,九交模型可以提供更多的细节信息,这些信息在判断相等关系时至关重要。
对于空间对象$A$和$B$,四交模型矩阵可以表达为:
\[I_4(A,B)= \left( \begin{matrix} A^\circ\cap B^\circ & A^\circ\cap \partial B\\ \partial A \cap B^\circ & \partial A \cap \partial B \end{matrix} \right)\]矩阵元素为二者的交集($\emptyset$或$\neg{\emptyset}$),如对于region A 和 region B,
\[I_4(A,B)= \left( \begin{matrix} \neg\emptyset & \neg\emptyset\\ \emptyset & \emptyset \end{matrix} \right)\]表示,region B 包含于 region A。
九交模型的矩阵可以表达为:
\[I_9(A,B)= \left( \begin{matrix} A^\circ\cap B^\circ & A^\circ\cap \partial B & A^\circ\cap B^-\\ \partial A \cap B^\circ & \partial A \cap \partial B & \partial A\cap B^-\\ A^- \cap B^\circ & A^- \cap \partial B & A^- \cap B^- \end{matrix} \right)\]理论上使用九交模型,有$2^9=512$种空间关系,在$R^2$,可以证明,仅有一小部分可以在空间对象间被实现。
基于维度拓展的九交模型(Dimensionally Extended Nine-Intersection Model, DE-9IM)是由OGC”Simple Features for SQL”定义计算空间几何拓扑关系的矩阵模型,其同样基于外部、内部和边界,在$R^2$使用-1,0,1,2表示几何$A$和$B$各部分相交集合的维度(即-1表示二者交集为空,0表示二者交集为点集,1表示二者交集为线集,2表示二者交集为面集)。进一步地,以$F$表示-1,以$T$表示0,1或2,以$*$表示-1,0,1或2。基于该模型定义了8种空间关系,也就是QGIS等软件中空间连接时要求选择的八种要素几何的空间关系,即:equals(相等),disjoint(相离),intersects(相交),touches(接触),crosses(交叉),within(在内部),contains(包含),overlaps(重叠)。可以对DE-9IM进行如下形式的表达:
\[DE-9IM(A,B)= \left( \begin{matrix} dim(A^\circ\cap B^\circ) & dim(A^\circ\cap \partial B) & dim(A^\circ\cap B^-)\\ dim(\partial A \cap B^\circ) & dim(\partial A \cap \partial B) & dim(\partial A\cap B^-)\\ dim(A^- \cap B^\circ) & dim(A^- \cap \partial B) & dim(A^- \cap B^-) \end{matrix} \right)\]其中,$dim(x)$表示计算$x$的空间维度,其值域为${-1,0,1,2}$。
这里首先对OGC定义几何类型及其对应的内部、边界和外部进行总结
则对于几何$A$和几何$B$,OGC定义的$A$与$B$的8种拓扑关系有如下结果:
这里以北大深研院为背景简单创建了一个线图层和一个面图层,面图层有7个面,线图层有4条线,二者的CRS均为EPSG:4547
,如下图所示,面图层以fid
字段进行标注,线图层以'L'
拼接fid
字段进行标注。以下fid=1
的面要素对应的几何被称为P1
,fid=1
的线要素对应的几何被称为L1
,按照这种命名方式,将通过多种方式将完成如下任务:
以下SQL语句是对上述问题的求解:
select b.fid from (select * from simple_polygon where fid=1) a,(select * from simple_polygon where fid!=1) b where st_contains(a.geom, b.geom);
select b.fid from (select * from simple_polygon where fid=4) a, (select * from simple_polygon where fid!=4) b where st_intersects(a.geom, b.geom);
select b.fid from (select * from simple_polygon where fid=1) a, simple_line b where st_crosses(b.geom, a.geom);
select b.fid from (select * from simple_line where fid=3) a, (select * from simple_polygon where fid!=3) b where st_intersects(a.geom, b.geom);
select st_touches(a.geom,b.geom) p5p1, st_touches(a.geom,c.geom) p5l1 from (select * from simple_polygon where fid=5) a, (select * from simple_polygon where fid=1) b, (select * from simple_line where fid=1) c;
执行结果如下:
让我们来检查一下执行结果的正确性:
在QGIS中用pyqgis
进行问题的求解:
lineLayer = QgsProject.instance().mapLayersByName('simple_line')[0]
polygonLayer = QgsProject.instance().mapLayersByName('simple_polygon')[0]
# 1
P1 = polygonLayer.getFeature(1)
idList = []
for polygonFeature in polygonLayer.getFeatures():
if polygonFeature['fid'] != P1['fid'] and P1.geometry().contains(polygonFeature.geometry()):
idList.append(polygonFeature['fid'])
print(f"P1包含(contains)的其他面有:{idList}")
# 2
P4 = polygonLayer.getFeature(4)
idList = []
for polygonFeature in polygonLayer.getFeatures():
if polygonFeature['fid'] != P4['fid'] and P4.geometry().intersects(polygonFeature.geometry()):
idList.append(polygonFeature['fid'])
print(f"P4相交(intersects)的其他面有:{idList}")
# 3
idList = []
for lineFeature in lineLayer.getFeatures():
if lineFeature.geometry().crosses(P1.geometry()):
idList.append(lineFeature['fid'])
print(f"穿过(crosses)P1的线有:{idList}")
# 4
L3 = lineLayer.getFeature(3)
idList = []
for lineFeature in lineLayer.getFeatures():
if lineFeature['fid'] != L3['fid'] and lineFeature.geometry().intersects(L3.geometry()):
idList.append(lineFeature['fid'])
print(f"与L3相交(intersects)的其他线有:{idList}")
# 5
P5 = polygonLayer.getFeature(5)
L1 = lineLayer.getFeature(1)
print(f'P5与P1和L1的接触关系分别是{P5.geometry().touches(P1.geometry())},{P5.geometry().touches(L1.geometry())}')
运行结果如下图所示,与PostGIS的运行结果完全一致。
geopandas
依赖于shapely
,使用其读取空间数据并进行判断:
import geopandas
polygonGdf = geopandas.read_file(r'F:\shchyan\a.gpkg', layer='simple_polygon')
polygonGdf['fid'] = list(range(1, polygonGdf.shape[0]+1))
lineGdf = geopandas.read_file(r'F:\shchyan\a.gpkg', layer='simple_line')
lineGdf['fid'] = list(range(1, lineGdf.shape[0]+1))
# 1.
containsResult = polygonGdf[polygonGdf.geometry.within(polygonGdf[polygonGdf['fid'] == 1].geometry.iloc[0]) & (polygonGdf.fid != 1)]['fid'].values.tolist()
print(f"1. P1包含的其他面:{containsResult}")
# 2.
intersectsResult = polygonGdf[polygonGdf.geometry.intersects(polygonGdf[polygonGdf['fid'] == 4].geometry.iloc[0]) & (polygonGdf.fid != 4)]['fid'].values.tolist()
print(f"2. P4相交的其他面:{intersectsResult}")
# 3.
crossesResult = lineGdf[lineGdf.geometry.crosses(polygonGdf[polygonGdf['fid'] == 1].geometry.values[0])]['fid'].values.tolist()
print(f"3. 穿过P1的线:{crossesResult}")
# 4.
intersectsLinesResult = lineGdf[lineGdf.geometry.intersects(lineGdf[lineGdf['fid'] == 3].geometry.iloc[0]) & (lineGdf['fid'] != 3)]['fid'].values.tolist()
print(f"4. 与L3相交的其他线:{intersectsLinesResult}")
# 5.
p5TouchesP1 = polygonGdf[polygonGdf['fid'] == 5].geometry.values[0].touches(polygonGdf[polygonGdf['fid'] == 1].geometry.values[0])
p5TouchesL1 = polygonGdf[polygonGdf['fid'] == 5].geometry.values[0].touches(lineGdf[lineGdf['fid'] == 1].geometry.values[0])
print(f"5. P5与P1和L1分别接触吗?{p5TouchesP1},{p5TouchesL1}")
运行结果如下图所示,与PostGIS的运行结果也完全一致。
在QGIS环境主要使用Processing Toolbox
的Join attributes by location (summary)
工具对43000+个网格对700000个建筑进行分割,统计每个网格内包含(contains)的分割后的建筑数量,直接使用下图所示的pipeline进行处理:
对count的统计进行可视化,局部结果:
显然,它统计错误了。使用geopandas进行类似的操作也会出现类似的问题,这里不再截图示意。经过测试小数据集上没有出现此问题,具体原因未知。
如果添加一个buffer操作,使添加面积字段后的几何进行负值的缓冲区生成再进行空间连接,则得到:
处理结果:
此时统计正确了。
本推文从代数拓扑对几何的内部、边界和外部的定义入手,对GIS的四交模型、九交模型和维度拓展的九交模型进行了较为详细的介绍,并使用PostGIS、QGIS和shapely进行了示例演示,值得注意的是在大数据集上目前的QGIS和shapely似乎存在着一些问题,如果遇到这种特殊情况需要灵活处理。
此外,值得指出的是,除了代数拓扑,还可以根据目的从其他角度对GIS中的几何进行定义,但大体最终会落到点、线、面上。
之所以专门写一下这个,是因为有同学之前在问我空间连接时关系怎么选。写本推文的初衷也是帮助对空间连接参数陌生的同学正确地使用空间连接工具,顺便对GIScience学科的知识进行了解,希望可以帮助到有需要的同学捏!
ヾ(•ω•`)o就先这样结束啦!下一篇写点什么好呢?(如果有机会的话)