簡體   English   中英

如何使用python計算地球表面多邊形的面積?

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

標題基本上說明了一切。 我需要使用 Python 計算地球表面多邊形內的面積。 計算地球表面上任意多邊形所包圍的區域說明了這一點,但在技術細節上仍然含糊不清:

如果您想以更“GIS”的風格執行此操作,那么您需要為您的區域選擇一個測量單位並找到一個合適的投影來保留區域(並非所有人都這樣做)。 由於您正在談論計算任意多邊形,因此我會使用 Lambert Azimuthal Equal Area projection 之類的東西。 將投影的原點/中心設置為多邊形的中心,將多邊形投影到新的坐標系,然后使用標准平面技術計算面積。

那么,我如何在 Python 中做到這一點?

假設您有一個以 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

這是一個非常接近被調查區域的近似值。 對於更復雜的特征,您需要沿頂點之間的邊緣進行采樣,以獲得准確的值。 以上關於日期變更線等的所有警告均適用。 如果您只對區域感興趣,則可以在投影之前將您的要素從日期線轉換出來。

最簡單的方法(在我看來)是將事物投影到(一個非常簡單的)等面積投影中,並使用一種常用的平面技術來計算面積。

首先,如果您要問這個問題,我將假設球形地球足夠接近您的目的。 如果不是,那么您需要使用適當的橢球重新投影數據,在這種情況下,您將需要使用實際的投影庫(現在一切都在幕后使用 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

無論如何,希望這將為您指明正確的方向...

也許有點晚了,但這里有一種不同的方法,使用 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)

現在我可以用它來找到角度,然后是面積(在下面,當然應該指定 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 公里,我知道該區域是 268930758560.74808

或者干脆使用一個庫: 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

...以平方米為單位返回面積。

這是一個使用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 為單位)。

更新:底圖已被棄用,因此您可能需要首先考慮替代解決方案。

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

此外,根據這個討論,吉拉德定理(蘇克的答案)似乎在某些情況下沒有給出准確的結果,例如“由從極到極的 30º 月牙包圍並以本初子午線和 30ºE 為界的區域”(見在這里)。

更精確的解決方案是直接在球體上執行線積分 下面的比較表明這種方法更精確。

像所有其他答案一樣,我應該提到我們假設一個球形地球的警告,但我認為對於非關鍵目的來說這已經足夠了。

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

我在那里球面幾何包中寫了一個更明確的版本(並且有更多的參考和待辦事項......)。

數值比較

科羅拉多州將作為參考,因為之前的所有答案都在其區域進行了評估。 其精確的總面積為 104,093.67 平方英里(來自美國人口普查局,第 89 頁,另見此處),或 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 投影進行比較沒有意義。 直觀上需要更少的計算。 另一方面,三角函數可能是計算密集型的。

因為地球是一個封閉的表面,所以在其表面上繪制的封閉多邊形會創建兩個多邊形區域。 您還需要定義哪個在里面,哪個在外面!

大多數時候人們會處理小多邊形,所以這是“顯而易見的”,但一旦你擁有海洋或大陸大小的東西,你最好確保你以正確的方式得到它。

另外,請記住,線可以以兩種不同的方式從 (-179,0) 變為 (+179,0)。 一個比另一個長得多。 同樣,大多數情況下您會假設這是一條從 (-179,0) 到 (-180,0) 的線,即 (+180,0) 然后到 (+179,0),但是一個天……不會。

將 lat-long 視為簡單的 (x,y) 坐標系,甚至忽略任何坐標投影都會出現扭曲和中斷的事實,可能會讓您在球體上大失所望。

我知道 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 km2,或報告的正確值 (269601.367661 km2) 的 -0.17%。

根據 Yellows 的斷言,直接積分更精確。

但是 Yellows 使用地球半徑 = 6378 137 m,這是 WGS-84 橢球半長軸,而 Sulkeh 使用 6371 000 m。

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

假設科羅拉多地區的真實值(來自美國人口普查局)是 269601367661 平方米,那么 Sulkeh 方法與地面實況的變化為:-0,025%,優於線積分法的 -0.07。

所以到目前為止,Sulkeh 的提議似乎更加精確。

為了能夠對解進行數值比較,假設地球是球形的,所有計算都必須使用相同的地球半徑。

這是一個 Python 3 實現,其中該函數將獲取 lats 和 long 的元組對列表,並返回投影多邊形中包含的區域。它使用 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 Sq。 山。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM