从边界点创建封闭多边形

5
我有一组经纬度点的数组,用于定义一个区域的边界。我想基于这些点创建一个多边形,并在地图上绘制和填充该多边形。目前,我的多边形似乎由许多连接所有点的补丁组成,但是点的顺序不正确,当我尝试填充多边形时,会得到一个奇怪的区域(请参见附图)。我对我的经纬度点(mypolyXY 数组)进行排序以便根据多边形的中心点,但我猜想这并不正确:
cent=(np.sum([p[0] for p in mypolyXY])/len(mypolyXY),np.sum([p[1] for p in mypolyXY])/len(mypolyXY))
# sort by polar angle
mypolyXY.sort(key=lambda p: math.atan2(p[1]-cent[1],p[0]-cent[0]))

我使用以下方法绘制点的位置(黑色圆圈)和多边形(彩色图案):
scatter([p[0] for p in mypolyXY],[p[1] for p in mypolyXY],2)
p = Polygon(mypolyXY,facecolor=colors,edgecolor='none')
ax.add_artist(p)

我的问题是:如何基于我的经纬度点数组关闭多边形?
更新: 我试着测试了一些绘制多边形的方法。我删除了排序程序,只使用出现在文件中的数据顺序。这似乎改善了结果,但正如@tcaswell所提到的,多边形形状仍然会相互重叠(请参见新图)。我希望能够找到一种路径/多边形程序来解决我的问题,并将所有形状或路径合并到多边形的边界内。欢迎提出建议。
更新2: 现在我有一个基于@Rutger Kassies和Roland Smith建议的脚本版本。最终我使用了org读取了Shapefile,效果还不错。它适用于标准的lmes_64.shp文件,但是当我使用更详细的LME文件时,每个LME可能由多个多边形组成,此脚本就会崩溃。我需要找到一种方法来合并相同LME名称的各个多边形才能使其正常工作。我附上我最终得到的脚本,在此感谢任何人对如何改进或使其更通用的脚本提供评论。此脚本创建多边形并提取该区域内从netcdf文件读取的数据。输入文件的网格是-180到180和-90到90。
import numpy as np
import math
from pylab import *
import matplotlib.patches as patches
import string, os, sys
import datetime, types
from netCDF4 import Dataset
import matplotlib.nxutils as nx
from mpl_toolkits.basemap import Basemap
import ogr
import matplotlib.path as mpath
import matplotlib.patches as patches


def getLMEpolygon(coordinatefile,mymap,index,first):

    ds = ogr.Open(coordinatefile)
    lyr = ds.GetLayer(0)
    numberOfPolygons=lyr.GetFeatureCount()

    if first is False:
        ft = lyr.GetFeature(index)
        print "Found polygon:",  ft.items()['LME_NAME']
        geom = ft.GetGeometryRef()

        codes = []
        all_x = []
        all_y = []
        all_XY= []

        if (geom.GetGeometryType() == ogr.wkbPolygon):
          for i in range(geom.GetGeometryCount()):

            r = geom.GetGeometryRef(i)
            x = [r.GetX(j) for j in range(r.GetPointCount())]
            y = [r.GetY(j) for j in range(r.GetPointCount())]

            codes += [mpath.Path.MOVETO] + (len(x)-1)*[mpath.Path.LINETO]
            all_x += x
            all_y += y
            all_XY +=mymap(x,y)


        if len(all_XY)==0:
            all_XY=None
            mypoly=None
        else:
            mypoly=np.empty((len(all_XY[:][0]),2))
            mypoly[:,0]=all_XY[:][0]
            mypoly[:,1]=all_XY[:][3]
    else:
        print "Will extract data for %s polygons"%(numberOfPolygons)
        mypoly=None
    first=False
    return mypoly, first, numberOfPolygons


def openCMIP5file(CMIP5name,myvar,mymap):
    if os.path.exists(CMIP5name):
        myfile=Dataset(CMIP5name)
        print "Opened CMIP5 file: %s"%(CMIP5name)
    else:
        print "Could not find CMIP5 input file %s : abort"%(CMIP5name)
        sys.exit()
    mydata=np.squeeze(myfile.variables[myvar][-1,:,:]) - 273.15
    lonCMIP5=np.squeeze(myfile.variables["lon"][:])
    latCMIP5=np.squeeze(myfile.variables["lat"][:])

    lons,lats=np.meshgrid(lonCMIP5,latCMIP5)

    lons=lons.flatten()
    lats=lats.flatten()
    mygrid=np.empty((len(lats),2))
    mymapgrid=np.empty((len(lats),2))

    for i in xrange(len(lats)):
        mygrid[i,0]=lons[i]
        mygrid[i,1]=lats[i]
        X,Y=mymap(lons[i],lats[i])
        mymapgrid[i,0]=X
        mymapgrid[i,1]=Y

    return mydata, mygrid, mymapgrid

def drawMap(NUM_COLORS):

    ax = plt.subplot(111)
    cm = plt.get_cmap('RdBu')
    ax.set_color_cycle([cm(1.*j/NUM_COLORS) for j in range(NUM_COLORS)])
    mymap = Basemap(resolution='l',projection='robin',lon_0=0)

    mymap.drawcountries()

    mymap.drawcoastlines()
    mymap.fillcontinents(color='grey',lake_color='white')
    mymap.drawparallels(np.arange(-90.,120.,30.))
    mymap.drawmeridians(np.arange(0.,360.,60.))
    mymap.drawmapboundary(fill_color='white')

    return ax, mymap, cm

"""Edit the correct names below:"""

LMEcoordinatefile='ShapefileBoundaries/lmes_64.shp'
CMIP5file='tos_Omon_CCSM4_rcp85_r1i1p1_200601-210012_regrid.nc'

mydebug=False
doPoints=False
first=True


"""initialize the map:"""
mymap=None
mypolyXY, first, numberOfPolygons = getLMEpolygon(LMEcoordinatefile, mymap, 0,first)
NUM_COLORS=numberOfPolygons
ax, mymap, cm = drawMap(NUM_COLORS)


"""Get the CMIP5 data together with the grid"""
SST,mygrid, mymapgrid = openCMIP5file(CMIP5file,"tos",mymap)

"""For each LME of interest create a polygon of coordinates defining the boundaries"""
for counter in xrange(numberOfPolygons-1):

    mypolyXY,first,numberOfPolygons = getLMEpolygon(LMEcoordinatefile, mymap,counter,first)

    if mypolyXY != None:
        """Find the indices inside the grid that are within the polygon"""
        insideBoolean = plt.mlab.inside_poly(np.c_[mymapgrid[:,0],mymapgrid[:,1]],np.c_[mypolyXY[:,0],mypolyXY[:,1]])
        SST=SST.flatten()
        SST=np.ma.masked_where(SST>50,SST)

        mymapgrid=np.c_[mymapgrid[:,0],mymapgrid[:,1]]
        myaverageSST=np.mean(SST[insideBoolean])

        mycolor=cm(myaverageSST/SST.max())
        scaled_z = (myaverageSST - SST.min()) / SST.ptp()
        colors = plt.cm.coolwarm(scaled_z)

        scatter([p[0] for p in mypolyXY],[p[1] for p in mypolyXY],2)

        p = Polygon(mypolyXY,facecolor=colors,edgecolor='none')
        ax.add_artist(p)

        if doPoints is True:

            for point in xrange(len(insideBoolean)):
                pointX=mymapgrid[insideBoolean[point],0]
                pointY=mymapgrid[insideBoolean[point],1]
                ax.scatter(pointX,pointY,8,color=colors)
                ax.hold(True)


if doPoints is True:
    colorbar()
print "Extracted average values for %s LMEs"%(numberOfPolygons)
plt.savefig('LMEs.png',dpi=300)
plt.show()

附上最终图片。感谢所有的帮助。

enter image description here 祝好,Trond


你是对的,但这不是我的问题,因为名称混淆只发生在我将代码写入Stackoverflow时。在我的程序中,变量始终被称为mypolyXY。对此很抱歉。我认为问题出在经纬度点的顺序不连续,正如Roland Smith所建议的那样。只是不确定如何解决这个问题。还是谢谢你的帮助。 - Trond Kristiansen
经过一段时间的思考,我现在明白了:您的形状会自相矛盾,这就是按角度排序无法起作用的原因。这些数据的来源是什么? - tacaswell
那个 CSV 文件中的数据不是一个有效的多边形,我怀疑它不是源文件。所有点都等同于这个 shapefile 中的点: http://www.lme.noaa.gov/index.php?option=com_content&view=article&id=177&Itemid=61 - Rutger Kassies
真的。CSV文件是从包含LME多边形的Shapefile创建的。定义Shapefile多边形的经纬度点按顺序保存到CSV文件中。我认为这将使我能够使用Matplotlib和Python重新创建多边形? - Trond Kristiansen
@TrondKristiansen:我认为你需要解析.shp文件本身。请查看我的更新答案。 - Roland Smith
3个回答

3
拥有一组点并不足够,你需要知道这些点的顺序。通常情况下,多边形的点是按顺序给出的。所以你需要从第一个点到第二个点画一条线,然后从第二个点到第三个点以此类推。
如果你的列表不是按顺序排列的,你需要额外的信息才能使其成为有序列表。
Shapefile(请参阅文档)包含了一个形状列表,如Null形状、点、折线、多边形等,还包含了Z和M(测量)坐标的变体。因此,仅仅转储这些点是不够的。你需要将它们分成不同的形状,并渲染你感兴趣的部分。在这种情况下,可能是折线或多边形。请查看上面的链接以获取这些形状的数据格式。请记住,文件的某些部分是大端字节序,而另一些部分是小端字节序。真是一团糟。
我建议使用struct模块解析二进制.shp文件,因为根据文档,单个多边形的点是有序的,并且它们形成了一个封闭的链(最后一个点与第一个点相同)。
另外,您可以尝试使用当前坐标列表中的一个点作为起点,然后在列表中寻找相同的点。这些相同点之间的所有内容应该是一个多边形。这可能不是绝对可靠的方法,但可以看看效果如何。

2

我建议使用原始的Shapefile格式,这种格式适合存储多边形。作为OGR的替代方案,您可以使用Shapely或将多边形导出为Wkt等格式。

import ogr
import matplotlib.path as mpath
import matplotlib.patches as patches
import matplotlib.pyplot as plt

ds = ogr.Open('lmes_64.shp')
lyr = ds.GetLayer(0)
ft = lyr.GetFeature(38)
geom = ft.GetGeometryRef()
ds = None

codes = []
all_x = []
all_y = []

if (geom.GetGeometryType() == ogr.wkbPolygon):
  for i in range(geom.GetGeometryCount()):

    r = geom.GetGeometryRef(i)
    x = [r.GetX(j) for j in range(r.GetPointCount())]
    y = [r.GetY(j) for j in range(r.GetPointCount())]

    codes += [mpath.Path.MOVETO] + (len(x)-1)*[mpath.Path.LINETO]
    all_x += x
    all_y += y

if (geom.GetGeometryType() == ogr.wkbMultiPolygon):
  codes = []
  for i in range(geom.GetGeometryCount()):
    # Read ring geometry and create path
    r = geom.GetGeometryRef(i)
    for part in r:
      x = [part.GetX(j) for j in range(part.GetPointCount())]
      y = [part.GetY(j) for j in range(part.GetPointCount())]
      # skip boundary between individual rings
      codes += [mpath.Path.MOVETO] + (len(x)-1)*[mpath.Path.LINETO]
      all_x += x
      all_y += y

carib_path = mpath.Path(np.column_stack((all_x,all_y)), codes)    
carib_patch = patches.PathPatch(carib_path, facecolor='orange', lw=2)

poly1 = patches.Polygon([[-80,20],[-75,20],[-75,15],[-80,15],[-80,20]], zorder=5, fc='none', lw=3)
poly2 = patches.Polygon([[-65,25],[-60,25],[-60,20],[-65,20],[-65,25]], zorder=5, fc='none', lw=3)


fig, ax = plt.subplots(1,1)

for poly in [poly1, poly2]:
    if carib_path.intersects_path(poly.get_path()):
        poly.set_edgecolor('g')
    else:
        poly.set_edgecolor('r')

    ax.add_patch(poly)

ax.add_patch(carib_patch)
ax.autoscale_view()

输入图像描述

如果您想要非常简单地处理Shapefile,请还可以检查Fiona(OGR的包装器)。


这是一个很好的建议。我测试过了,似乎可以用。然而,我仍需要访问LMEs的多边形,因为我需要使用plt.mlab.inside_poly例程在LMEs内提取数据。我能否在您的代码中定义每个多边形,例如使用以下内容:mpoly=np.empty((len(all_x),2)) mypoly=np.array((all_x,all_y), dtype=float)? - Trond Kristiansen
我已经编辑了我的示例,包括一些交集处理。 - Rutger Kassies
1
实际上非常简单。但这就是我检查 ogr.wkbPolygon 的原因,因为使用 OGR 接口时需要稍微不同地处理它们。多边形的 GeometryRef() 包含更多部分。我会更新我的答案以包含它。 - Rutger Kassies
你遇到这个问题是哪个具体的Shapefile呢?看起来像是一个多边形跨越了日期线,顺便说一下,很高兴能帮助你。不确定是否有真正的解决方法。 - Rutger Kassies
我测试了我所有的Shapefiles,这是一个持续存在的问题,包括LME_64.shp文件。虽然其他一切都完美无缺。 - Trond Kristiansen
显示剩余4条评论

2
这里有一篇关于shapefiles和basemap的博客文章:http://www.geophysique.be/2013/02/12/matplotlib-basemap-tutorial-10-shapefiles-unleached-continued/。如果你想尝试,cartopy也是一个选择。从一个shapefile中绘制数据应该相对容易:
import matplotlib.pyplot as plt

import cartopy.crs as ccrs
import cartopy.io.shapereader as shpreader

# pick a shapefile - Cartopy makes it easy to access Natural Earth
# shapefiles, but it could be anything
shapename = 'admin_1_states_provinces_lakes_shp'
states_shp = shpreader.natural_earth(resolution='110m',
                                      category='cultural', name=shapename)

.

# states_shp is just a filename to a shapefile
>>> print states_shp
/Users/pelson/.local/share/cartopy/shapefiles/natural_earth/cultural/110m_admin_1_states_provinces_lakes_shp.shp

.

# Create the mpl axes of a PlateCarree map
ax = plt.axes(projection=ccrs.PlateCarree())

# Read the shapes from the shapefile into a list of shapely geometries.
geoms = shpreader.Reader(states_shp).geometries()

# Add the shapes in the shapefile to the axes
ax.add_geometries(geoms, ccrs.PlateCarree(),
                  facecolor='coral', edgecolor='black')

plt.show()

output


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接