如何使用python计算地球表面多边形的面积?

时间:2011-01-13 15:26:30

标签: python geometry geolocation geospatial

标题基本上都说明了一切。我需要使用Python计算地球表面多边形内的区域。 Calculating area enclosed by arbitrary polygon on Earth's surface说了些什么,但对技术细节仍然含糊不清:

  

如果你想用更多的东西做这件事   “GIS”味道,那么你需要选择   您所在地区的计量单位   找到一个合适的投影   保留区域(不是全部)。自从你   正在谈论计算   任意多边形,我会用   像Lambert Azimuthal这样的东西   等面积投影。设置   投影的原点/中心   多边形的中心,项目   多边形到新坐标   系统,然后使用计算面积   标准平面技术。

那么,我该如何在Python中做到这一点?

10 个答案:

答案 0 :(得分:32)

假设您以GeoJSON格式表示科罗拉多州的状态

{"type": "Polygon", 
 "coordinates": [[
   [-102.05, 41.0], 
   [-102.05, 37.0], 
   [-109.05, 37.0], 
   [-109.05, 41.0]
 ]]}

所有坐标均为经度,纬度。您可以使用pyproj投影坐标,使用Shapely查找任何投影多边形的区域:

co = {"type": "Polygon", "coordinates": [
    [(-102.05, 41.0),
     (-102.05, 37.0),
     (-109.05, 37.0),
     (-109.05, 41.0)]]}
lon, lat = zip(*co['coordinates'][0])
from pyproj import Proj
pa = Proj("+proj=aea +lat_1=37.0 +lat_2=41.0 +lat_0=39.0 +lon_0=-106.55")

这是一个以感兴趣区域为中心并将其包围的等面积投影。现在制作新的投影GeoJSON表示,变成一个Shapely几何对象,然后取区域:

x, y = pa(lon, lat)
cop = {"type": "Polygon", "coordinates": [zip(x, y)]}
from shapely.geometry import shape
shape(cop).area  # 268952044107.43506

这与调查区域非常接近。对于更复杂的特征,您需要沿顶点之间的边缘采样,以获得准确的值。以上关于日期线等的所有警告均适用。如果您只对区域感兴趣,可以在投影前将您的功能从日期线转换。

答案 1 :(得分:23)

最简单的方法(在我看来)是将事物投射到(一个非常简单的)等面积投影中,并使用一种常用的平面技术来计算面积。

首先,如果你问这个问题,我会假设球形地球足够接近你的目的。如果没有,那么你需要使用适当的椭球重新投影你的数据,在这种情况下你将要使用一个实际的投影库(这些天在幕后使用proj4),例如python绑定到{{3或者(更友好的)GDAL/OGR

但是,如果你对球形地球没问题,那么没有任何专门的库就可以很容易地做到这一点。

要计算的最简单的等面积投影是pyproj。基本上,你只需将纬度乘以一个纬度的长度,经纬度乘以纬度的长度和纬度的余弦。

def reproject(latitude, longitude):
    """Returns the x & y coordinates in meters using a sinusoidal projection"""
    from math import pi, cos, radians
    earth_radius = 6371009 # in meters
    lat_dist = pi * earth_radius / 180.0

    y = [lat * lat_dist for lat in latitude]
    x = [long * lat_dist * cos(radians(lat)) 
                for lat, long in zip(latitude, longitude)]
    return x, y

好的......现在我们要做的就是计算平面中任意多边形的面积。

有很多方法可以做到这一点。我将在这里使用sinusoidal projection

def area_of_polygon(x, y):
    """Calculates the area of an arbitrary polygon given its verticies"""
    area = 0.0
    for i in range(-1, len(x)-1):
        area += x[i] * (y[i+1] - y[i-1])
    return abs(area) / 2.0

希望无论如何,这将指向正确的方向......

答案 2 :(得分:6)

或许有点晚了,但这是一种不同的方法,使用吉拉德定理。它指出大圆多边形的面积是多边形之间角度之和的R ** 2倍减去(N-2)* pi,其中N是角的数量。

我认为这是值得发布的,因为它不依赖于任何其他库而不是numpy,并且它是一种与其他库完全不同的方法。当然,这仅适用于球体,因此将其应用于地球时会有一些不准确之处。

首先,我定义了一个函数来计算从点1沿大圆到第2点的方位角:

import numpy as np
from numpy import cos, sin, arctan2

d2r = np.pi/180

def greatCircleBearing(lon1, lat1, lon2, lat2):
    dLong = lon1 - lon2

    s = cos(d2r*lat2)*sin(d2r*dLong)
    c = cos(d2r*lat1)*sin(d2r*lat2) - sin(lat1*d2r)*cos(d2r*lat2)*cos(d2r*dLong)

    return np.arctan2(s, c)

现在我可以用它来找到角度,然后是区域(在下面,当然应该指定lons​​和lats,它们应该是正确的顺序。另外,应该指定球体的半径。)

N = len(lons)

angles = np.empty(N)
for i in range(N):

    phiB1, phiA, phiB2 = np.roll(lats, i)[:3]
    LB1, LA, LB2 = np.roll(lons, i)[:3]

    # calculate angle with north (eastward)
    beta1 = greatCircleBearing(LA, phiA, LB1, phiB1)
    beta2 = greatCircleBearing(LA, phiA, LB2, phiB2)

    # calculate angle between the polygons and add to angle array
    angles[i] = np.arccos(cos(-beta1)*cos(-beta2) + sin(-beta1)*sin(-beta2))

area = (sum(angles) - (N-2)*np.pi)*R**2

在另一个回复中给出科罗拉多坐标,并且地球半径为6371 km,我得到的区域是268930758560.74808

答案 3 :(得分:4)

因为地球是一个封闭的表面,在其表面上绘制的闭合多边形会产生 TWO 多边形区域。你还需要定义哪一个在里面,哪一个在外面!

大多数时候人们会处理小的多边形,所以它很“明显”,但是一旦你拥有了大洋或大陆的东西,你最好确保你能够正确地进行这种操作。

另外,请记住,行可以通过两种不同的方式从(-179,0)到(+179,0)。一个比另一个长得多。同样,大多数情况下,你会假设这是一条从(-179,0)到(-180,0)的线,即(+ 180,0)然后是(+179,0),但是一天......它不会。

像一个简单的(x,y)坐标系统一样处理lat-long,甚至忽略任何坐标投影都会有扭曲和断裂的事实,这会让你在球体上失败。

答案 4 :(得分:3)

或者只是使用图书馆:https://github.com/scisco/area

from area import area
>>> obj = {'type':'Polygon','coordinates':[[[-180,-90],[-180,90],[180,90],[180,-90],[-180,-90]]]}
>>> area(obj)
511207893395811.06

...以平方米的形式返回该区域。

答案 5 :(得分:2)

以下是使用basemap而不是pyprojshapely进行坐标转换的解决方案。这个想法与@sgillies建议的相同。请注意,我添加了第5个点,以便路径是闭环。

import numpy
from mpl_toolkits.basemap import Basemap

coordinates=numpy.array([
[-102.05, 41.0], 
[-102.05, 37.0], 
[-109.05, 37.0], 
[-109.05, 41.0],
[-102.05, 41.0]])

lats=coordinates[:,1]
lons=coordinates[:,0]

lat1=numpy.min(lats)
lat2=numpy.max(lats)
lon1=numpy.min(lons)
lon2=numpy.max(lons)

bmap=Basemap(projection='cea',llcrnrlat=lat1,llcrnrlon=lon1,urcrnrlat=lat2,urcrnrlon=lon2)
xs,ys=bmap(lons,lats)

area=numpy.abs(0.5*numpy.sum(ys[:-1]*numpy.diff(xs)-xs[:-1]*numpy.diff(ys)))
area=area/1e6

print area

结果是268993.609651,单位为km ^ 2.

答案 6 :(得分:0)

您可以直接在球面上计算面积,而不是使用等面积投影。

此外,根据this discussion,在某些情况下,吉拉德定理(苏尔克的答案)似乎无法给出准确的结果,例如“从极点到极点的30º月牙所包围的区域,并且以素数为界子午线和30ºE”(请参见here)。

一种更精确的解决方案是直接在球体上执行线积分。下面的比较表明此方法更为精确。

与所有其他答案一样,我应该指出我们假设是球形地球,但我认为对于非关键目的,这已经足够了。

Python实现

这是一个使用行积分和格林定理的Python 3实现:

def polygon_area(lats, lons, radius = 6378137):
    """
    Computes area of spherical polygon, assuming spherical Earth. 
    Returns result in ratio of the sphere's area if the radius is specified.
    Otherwise, in the units of provided radius.
    lats and lons are in degrees.
    """
    from numpy import arctan2, cos, sin, sqrt, pi, power, append, diff, deg2rad
    lats = np.deg2rad(lats)
    lons = np.deg2rad(lons)

    # Line integral based on Green's Theorem, assumes spherical Earth

    #close polygon
    if lats[0]!=lats[-1]:
        lats = append(lats, lats[0])
        lons = append(lons, lons[0])

    #colatitudes relative to (0,0)
    a = sin(lats/2)**2 + cos(lats)* sin(lons/2)**2
    colat = 2*arctan2( sqrt(a), sqrt(1-a) )

    #azimuths relative to (0,0)
    az = arctan2(cos(lats) * sin(lons), sin(lats)) % (2*pi)

    # Calculate diffs
    # daz = diff(az) % (2*pi)
    daz = diff(az)
    daz = (daz + pi) % (2 * pi) - pi

    deltas=diff(colat)/2
    colat=colat[0:-1]+deltas

    # Perform integral
    integrands = (1-cos(colat)) * daz

    # Integrate 
    area = abs(sum(integrands))/(4*pi)

    area = min(area,1-area)
    if radius is not None: #return in units of radius
        return area * 4*pi*radius**2
    else: #return in ratio of sphere total area
        return area

我在sphericalgeometrythere中编写了一个更为明确的版本(以及更多参考和TODO ...)。

数值比较

科罗拉多州将作为参考,因为所有先前的回答都在其区域进行了评估。其精确的总面积为104,093.67平方英里(从US Census Bureau,第89页,另请参阅here)或269601367661平方米。我没有找到USCB实际方法的资料,但我认为它是基于对地面实际测量值的总和,或使用WGS84 / EGM2008进行的精确计算。

Method                 | Author     | Result       | Variation from ground truth
--------------------------------------------------------------------------------
Albers Equal Area      | sgillies   | 268952044107 | -0.24%
Sinusoidal             | J. Kington | 268885360163 | -0.26%
Girard's theorem       | sulkeh     | 268930758560 | -0.25%
Equal Area Cylindrical | Jason      | 268993609651 | -0.22%
Line integral          | Yellows    | 269397764066 | **-0.07%**

结论:使用直接积分更为精确。

性能

我还没有对不同的方法进行基准测试,将纯Python代码与已编译的PROJ投影进行比较将没有意义。直观上需要更少的计算。另一方面,三角函数可能需要大量计算。

答案 7 :(得分:0)

根据Yellows的断言,直接积分更为精确。

但是,黄人使用的地球半径= 6378 137m,这是WGS-84椭圆形半长轴,而苏尔克使用的地球半径为6371 000 m。

在Sulkeh'方法中使用半径= 6378 137 m,得到269533625893平方米。

假定科罗拉多州面积的真实值(来自美国人口普查局)为269601367661平方米,那么与Sulkeh'方法的地面真实值相比,其变化是-0.025%,优于使用线积分法的-0.07。

因此Sulkeh的提议到目前为止似乎更为精确。

为了能够在假设球形地球的情况下对解决方案进行数值比较,所有计算都必须使用相同的地面半径。

答案 8 :(得分:0)

这是Python 3的实现,该函数将获取一组经纬度的元组对,并返回包含在投影多边形中的面积。它使用pyproj投影坐标,然后使用Shapely查找面积任何投影的多边形

def calc_area(lis_lats_lons):

import numpy as np
from pyproj import Proj
from shapely.geometry import shape


lons, lats = zip(*lis_lats_lons)
ll = list(set(lats))[::-1]
var = []
for i in range(len(ll)):
    var.append('lat_' + str(i+1))
st = ""
for v, l in zip(var,ll):
    st = st + str(v) + "=" + str(l) +" "+ "+"
st = st +"lat_0="+ str(np.mean(ll)) + " "+ "+" + "lon_0" +"=" + str(np.mean(lons))
tx = "+proj=aea +" + st
pa = Proj(tx)

x, y = pa(lons, lats)
cop = {"type": "Polygon", "coordinates": [zip(x, y)]}

return shape(cop).area 

对于一组经度/纬度样本,其面积值接近于所测得的近似值

calc_area(lis_lats_lons = [(-102.05, 41.0),
 (-102.05, 37.0),
 (-109.05, 37.0),
 (-109.05, 41.0)])

哪个输出面积268952044107.4342平方山

答案 9 :(得分:0)

我知道 10 年后回答有一些优势,但对于今天看这个问题的人来说,提供更新的答案似乎是公平的。

pyproj 直接计算面积,不需要调用 shapely:

# Modules:
from pyproj import Geod
import numpy as np

# Define WGS84 as CRS:
geod = Geod('+a=6378137 +f=0.0033528106647475126')

# Data for Colorado (no need to close the polygon):
coordinates = np.array([
[-102.05, 41.0], 
[-102.05, 37.0], 
[-109.05, 37.0], 
[-109.05, 41.0]])
lats = coordinates[:,1]
lons = coordinates[:,0]

# Compute:
area, perim = geod.polygon_area_perimeter(lons, lats)

print(abs(area))  # Positive is counterclockwise, the data is clockwise.

结果是:269154.54988400977 平方公里,或报告的正确值 (269601.367661 平方公里) 的 -0.17%。