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

43
标题已经说了一切。我需要使用Python计算地球表面多边形内部的面积。 计算地球表面任意多边形所包含的区域 对此有所涉及,但在技术细节方面仍然模糊:

如果您想要更具“GIS”风格的方法,则需要选择用于测量面积的单位,并找到一个保留面积(并非所有投影都能)的适当投影。由于您要计算任意多边形,因此建议使用诸如Lambert等面积方位投影。将投影的中心/原点设置为多边形的中心,将多边形投影到新坐标系统中,然后使用标准平面技术计算其面积。

那么,我该如何在Python中实现这个功能呢?

如果您选择一个保持面积的投影,多边形的边将不再是直线。 - John La Rooy
10个回答

43

假设你拥有一个以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

这是与测量区域非常接近的近似值。 对于更复杂的功能,您需要沿边缘,顶点之间进行采样,以获得精确的值。 所有关于日期线等的警告都适用。 如果您只对面积感兴趣,可以在投影之前将要素移开日期线。


6
严格来讲,这个 GeoJSON 应该有第五个结束点 [-102.05, 41.0]。规范有时候对这些事情描述得不太明确。 - Brad Koch
7
那个面积的结果是什么单位?是平方米还是平方英寸? - jyf1987
4
如果有其他人也想知道,这是指平方米。你可以谷歌搜索“科罗拉多州面积”,得到104,185平方英里。将其转换为平方米,结果大致相同。 - spanishgum
1
这对于可能不是简单四边形的多边形同样有效吗? - Ali
所以,基本上我可以使用任何等面积投影来转换坐标,并使用格林定理计算面积,对吗?如果是这样,那么人们还可以选择使用“basemap”来进行投影(例如,“aea”,“cea”)。在格林定理方面,一个一行代码 area=np.abs(0.5*np.sum(y[:-1]*np.diff(x) - x[:-1]*np.diff(y))) 就可以让你不需要使用 shapely 模块。 - Jason
显示剩余2条评论

27
在我看来,最简单的方法是将事物投影到(非常简单的)等面积投影中,并使用通常的计算面积的平面技术之一。首先,如果你提出这个问题,我会假设一个球形地球足以满足你的需求。如果不是这样,那么你需要使用适当的椭球体重新投影你的数据,在这种情况下,你将需要使用实际的投影库(现在所有东西都在后台使用proj4),例如GDAL/OGR的python绑定或者(更友好的)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

好的...现在我们需要计算平面内任意多边形的面积。

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

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

希望这能为您指明正确的方向...无论如何。

请注意,使用正弦投影线条会变形。最好在点之间沿着大圆进行插值,以便您的投影多边形不会变形。 - Spacedman
2
@spacedman - 当然!我只是想展示一个简单的近似值。它并不打算完全准确。多边形边缘的扭曲只有在他试图计算具有非常大的边缘和少量顶点的多边形时才会有影响。 - Joe Kington
非常感谢你提供的重新投影函数,你解救了我一整天的迷茫!顺带一提,使用重新投影后,你还可以使用Python的shapely包来计算面积,方法是shapely.geometry.Polygon(list(zip(x, y))).area,以得到平方米为单位的面积。 - Ali

11

或者简单使用一个库: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

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


1
我喜欢这种方法让我可以直接使用纬度/经度点,并且考虑到地球的曲率,而不需要将点投影到笛卡尔坐标系上。我将此方法与标准的Green定理进行比较,并将结果与Google上不同州的面积进行比较,发现这个库提供了最准确的结果。谢谢! - blaylockbk
2
据我所知,该库计算大地面积时假定地球是一个半径为已定义为“WGS84_RADIUS = 6378137”的完美球体。请牢记这一点。 - rysqui

9
可能有点晚了,但是这里有一种不同的方法,使用Girard定理。该定理指出,由大圆构成的多边形的面积是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)

现在我可以使用这个来找到角度,然后计算面积(在下面的内容中,经纬度当然应该被指定并且它们应该按正确的顺序。此外,球体的半径应该被指定)。
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公里,我得到该区域面积为268930758560.74808


我尝试了您的方法,得到了负值。此外,它的绝对值 (139013699.103) 与我使用“网格”方法得到的值 (809339.212) 相差很大。该方法是从一个矩形墨卡托网格中获取多边形内所有网格单元,并将网格单元面积相加。每个单元格的面积是其纵向和经向间隔的乘积(从纬度、经度转换为公里)。 - Jason
你能澄清一下你的评论吗?你所提到的数字是从哪里来的?我再次尝试了我发布的代码,它给出了报告的结果... - sulkeh
我使用matplotlib的contour函数在一个轮廓上应用了它,事实上我有几十个这样的轮廓,每个都使用您的方法报告负面积。 - Jason
好的。我仍然不明白你提到的数字是从哪里来的。如果你能更具体一些,我很乐意纠正任何错误。你能否提供一个产生负面积的经度/纬度示例? - sulkeh
我在这里导出了一个轮廓坐标 https://pastebin.com/2fMkFgvu ,也许你可以试一下。我注意到的一件事是,如果我通过取每5个点来对135个点的轮廓进行子采样(x=x[::5]; y=y[::5]),我可以得到一个接近“真实”值的正值。 - Jason
是的,我使用你的例子也得到了负面积。我相信原因是计算角度时出现了舍入误差,因为这些点非常接近。这就是为什么如果进行子采样,它会起作用的原因。因此,在高精度下它不是一种稳健的方法,这并不令人感到意外。不过,感谢你指出了这一点。 - sulkeh

6
这里有一个使用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平方千米。

更新:Basemap已被弃用,因此您可能需要首先考虑其他替代方案。


请澄清一下,计算面积所使用的方程式来源是什么? - blaylockbk
1
@blaylockbk 这是格林公式 - Jason

6
您可以直接在球体上计算面积,而不是使用等面积投影。此外,根据 这个讨论,Girard's 定理(sulkeh 的答案)在某些情况下并不能给出准确的结果,例如“由极点到极点,由本初子午线和东经30度分界的30度弓形所围起来的区域”(请参见此处)。更精确的解决方案是在球体上执行线积分。以下比较表明这种方法更为精确。
像所有其他答案一样,我应该提到的一点是,我们假设地球是球形的,但我认为对于非关键性目的而言,这已足够。

Python 实现

这是一个使用线积分和 Green's 定理的 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

我写了一个更加明确的版本(包括更多参考文献和待办事项……)在sphericalgeometry包的这里

数值比较

科罗拉多州将成为参考,因为所有先前的答案都是在其面积上进行评估的。它的精确总面积为104,093.67平方英里(来自美国人口普查局第89页,也可参见此处),即269601367661平方米。我没有找到美国人口普查局实际方法的来源,但我认为它基于在地面上测量或使用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投影进行比较是没有意义的。 直观地说,需要进行更少的计算。 另一方面,三角函数可能会占用大量计算资源。

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


我现在更喜欢这个解决方案,因为它不依赖于任何额外的包。Basemap已被弃用。 - Jason
科罗拉多州的边界不是一个完美的正方形,西侧有一个小的“凸起”,这是在边界建立时由于不完美的测量所致。人口普查区可能会考虑到这一点,这就是为什么所有这里的地区都会稍微低估它的原因。 - Shawn

4

我知道,回答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%的误差。

4
因为地球是一个封闭的表面,所以在其表面上画出的封闭多边形会创建两个多边形区域。您还需要定义哪个是内部,哪个是外部!
大多数时候人们处理的是小多边形,因此这是“显而易见的”,但一旦你有像海洋或大陆那样大的东西,你最好确保你把它弄对了。
另外,请记住,线可以从(-179,0)到(+179,0)以两种不同的方式。其中一种比另一种长得多。再次说明,大多数情况下,您会假设这是一条从(-179,0)到(-180,0)(+180,0),然后到(+179,0)的线,但总有一天...它不会是这样的。
将纬度和经度视为简单的(x,y)坐标系统,甚至忽略任何坐标投影都将具有扭曲和断裂的事实,可能会使您在球体上失败。

非常正确!我只是在我的答案中举了一个最基本的例子……它甚至都没有试图处理穿越本初子午线(0-360)或反子午线(-180到180)的情况。 - Joe Kington
21
是的,当地球还是平的时候,生活简单多了。 - Spacedman

0

根据Yellows的说法,直接积分更精确。

但是Yellows使用的地球半径为6378 137米,这是WGS-84椭球体的半长轴,而Sulkeh使用的是6371 000米。

在Sulkeh的方法中使用半径为6378 137米,则得到269533625893平方米。

假设科罗拉多州面积的真实值(来自美国人口普查局)为269601367661平方米,则Sulkeh方法与实际值的偏差为:-0.025%,比线积分法的-0.07%更好。

因此,Sulkeh的提议似乎是迄今为止更精确的。

为了能够对解决方案进行数字比较,并假设地球是一个球体,所有计算必须使用相同的地球半径。


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平方米。


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