关于几何:如何使用python计算地球表面上多边形的面积?

How to calculate the area of a polygon on the earth's surface using python?

标题基本上说明了一切。 我需要使用Python计算地球表面上的多边形内部的面积。 计算地球表面上任意多边形所围成的面积虽然可以说明这一点,但是在技术细节上仍然含糊不清:

If you want to do this with a more
"GIS" flavor, then you need to select
an unit-of-measure for your area and
find an appropriate projection that
preserves area (not all do). Since you
are talking about calculating an
arbitrary polygon, I would use
something like a Lambert Azimuthal
Equal Area projection. Set the
origin/center of the projection to be
the center of your polygon, project
the polygon to the new coordinate
system, then calculate the area using
standard planar techniques.

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


假设您以GeoJSON格式表示了科罗拉多州

1
2
3
4
5
6
7
{"type":"Polygon",
"coordinates": [[
   [-102.05, 41.0],
   [-102.05, 37.0],
   [-109.05, 37.0],
   [-109.05, 41.0]
 ]]}

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

1
2
3
4
5
6
7
8
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几何对象,并采用以下区域:

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

这是与被调查区域非常接近的近似值。对于更复杂的特征,您需要沿顶点之间的边缘进行采样,以获取准确的值。上面有关日期变更线等的所有警告均适用。如果您仅对区域感兴趣,则可以在投影之前将特征从日期线移开。


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

首先,如果您要问这个问题,我将假设球形地球足够接近您的目的。如果没有,那么您需要使用适当的椭球体重新投影数据,在这种情况下,您将要使用实际的投影库(如今,所有东西都在幕后使用proj4),例如与GDAL / OGR的python绑定或(更为友好的)pyproj。

但是,如果您对球形地球还可以,那么无需任何专门的库就可以很简单地完成此操作。

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

1
2
3
4
5
6
7
8
9
10
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

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

有很多方法可以做到这一点。我将在这里使用最常见的一种。

1
2
3
4
5
6
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

无论如何,希望这会指引您正确的方向...


或者只是使用一个库:https://github.com/scisco/area

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

...以平方米为单位返回面积。


也许有点晚了,但这是使用吉拉德定理的另一种方法。它指出,大圆多边形的面积为R ** 2乘以多边形之间的夹角之和减去(N-2)* pi,其中N是拐角数。

我认为这值得一提,因为它不依赖于numpy之外的任何其他库,并且它是与其他库完全不同的方法。当然,这仅适用于球体,因此将其应用于地球时会存在一些误差。

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

1
2
3
4
5
6
7
8
9
10
11
12
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)

现在,我可以使用它来找到角度,然后是面积(在下面,当然应该指定经度和纬度,并且它们的顺序应正确。此外,还应指定球体的半径。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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为单位。


由于地球是封闭表面,因此在其表面绘制的封闭多边形会创建两个多边形区域。您还需要定义哪个在内部,哪个在外部!

大多数情况下,人们会处理小的多边形,因此这是"显而易见的",但是一旦您拥有了海洋或大洲般大小的东西,则最好确保以正确的方式进行处理。

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

将经纬度视为简单的(x,y)坐标系,甚至忽略了任何坐标投影都会变形和断裂的事实,可能会使您在球体上大失所望。


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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

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

1
2
3
4
calc_area(lis_lats_lons = [(-102.05, 41.0),
 (-102.05, 37.0),
 (-109.05, 37.0),
 (-109.05, 41.0)])

输出面积268952044107.4342 Sq。山


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

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

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

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

到目前为止,苏尔凯的提议似乎更为精确。

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


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

而且,根据此讨论,在某些情况下,吉拉德定理(苏尔克的答案)似乎没有给出准确的结果,例如"由极点到极点的30o弧度所包围的区域,并由本初子午线和30oE包围"这里)。

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

像所有其他答案一样,我应该提到一个假设,即我们假设一个球形地球,但是我认为对于非关键性目的而言,这就足够了。

Python实现

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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

我在那里的球面几何软件包中编写了一个更明确的版本(以及更多参考文献和TODO ...)。

数值比较

科罗拉多州将作为参考,因为先前的所有答案均在其所在地区进行了评估。其精确的总面积为104,093.67平方英里(来自美国人口普查局,第89页,另请参见此处),或269601367661平方米。我没有找到USCB实际方法的资料,但我认为它是基于对地面实际测量值的总和,或使用WGS84 / EGM2008进行的精确计算。

1
2
3
4
5
6
7
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投影进行比较将没有意义。直观上,需要更少的计算。另一方面,三角函数可能是计算密集型的。