web-dev-qa-db-fra.com

Comment calculer la zone d'un polygone sur la surface de la Terre à l'aide de Python?

Le titre dit essentiellement tout. J'ai besoin de calculer la zone à l'intérieur d'un polygone sur la surface de la Terre à l'aide de Python. Calculer la zone enfermée par un polygone arbitraire sur la surface de la Terre En disant quelque chose à ce sujet, mais reste vague sur les détails techniques:

Si vous voulez faire cela avec une saveur plus "SIG", vous devez sélectionner une unité de mesure de votre région et trouver une projection appropriée qui préserve la zone (pas toutes les tâches). Depuis que vous parlez de calculer un polygone arbitraire, j'utiliserais quelque chose comme une projection de la superficie égale de Lambert Azimuthal. Définissez l'origine/le centre de la projection pour être le centre de votre polygone, projetez le polygone au nouveau système de coordonnées, puis calculez la zone à l'aide de techniques planes standard.

Alors, comment puis-je faire cela dans Python?

31
andreas-h

Disons que vous avez une représentation de l'état du Colorado au format Geojson

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

Toutes les coordonnées sont la longitude, la latitude. Vous pouvez utiliser PYPROJ pour projeter les coordonnées et galbé pour trouver la zone de tout polygone projeté:

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")

C'est une projection à la zone égale centrée sur la zone d'intérêt. Maintenant, faites une nouvelle représentation de Geojson projetée, transformez-vous en un objet géométrique galbé et prenez la zone:

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

C'est une approximation très étroite à la zone interrogée. Pour des fonctionnalités plus complexes, vous devrez échantillonner le long des bords, entre les sommets, pour obtenir des valeurs précises. Toutes les réserves ci-dessus sur les datelines, etc., s'appliquent. Si vous êtes intéressé uniquement dans la zone, vous pouvez traduire votre fonctionnalité de la ligne de données avant la projection.

34
sgillies

Le moyen le plus simple de le faire (à mon avis), est de projeter des choses dans une projection (une très simple) de la zone égale et d'utiliser l'une des techniques planes habituelles pour calculer la zone.

Tout d'abord, je vais supposer qu'une terre sphérique est suffisamment proche pour vos besoins, si vous posez cette question. Sinon, alors vous devez reproduire vos données à l'aide d'une ellipsoïde appropriée, auquel cas vous allez utiliser une bibliothèque de projection réelle (tout utilise Proj4 dans les coulisses, ces jours-ci) tels que le python Liaisons à GDAL/OGR ou (beaucoup plus amical) pyproj .

Cependant, si vous êtes d'accord avec une terre sphérique, il est assez simple de le faire sans aucune bibliothèque spécialisée.

La projection la plus simple sur la zone égale à calculer est une Projection sinusoïdale . Fondamentalement, vous venez de multiplier la latitude par la longueur d'un degré de latitude et de la longitude par la longueur d'un degré de latitude et du cosinus de la latitude.

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

Ok ... maintenant, tout ce que nous avons à faire est de calculer la zone d'un polygone arbitraire dans un avion.

Il y a un certain nombre de façons de le faire. Je vais utiliser ce qui est probablement le plus courant ici.

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

J'espère que cela vous dirigera dans la bonne direction, de toute façon ...

24
Joe Kington

Un peu en retard peut-être, mais voici une méthode différente, utilisant le théorème de Girard. Il stipule que la zone d'un polygone de grands cercles est R ** 2 fois la somme des angles entre les polygones moins (N-2) * Pi où n est le nombre de coins.

Je pensais que cela mériterait d'être affiché, car il ne s'appelle pas d'autres bibliothèques que des numéros, et c'est une méthode très différente de celle des autres. Bien sûr, cela ne fonctionne que sur une sphère, il y aura donc une certaine inexactitude lors de l'application à la Terre.

Tout d'abord, je définis une fonction pour calculer l'angle de roulement du point 1 le long d'un grand cercle au point 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)

Maintenant, je peux utiliser ceci pour trouver les angles, puis la zone (dans les suivants, les lons et les lats devraient bien sûr être spécifiés, et ils doivent être dans le bon ordre. De plus, le rayon de la sphère doit être spécifié.)

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

Avec les coordonnées du Colorado donné dans une autre réponse et avec le rayon de terre 6371 km, je reçois que la zone soit 268930758560.74808

6
sulkeh

Parce que la terre est une surface fermée, un polygone fermé dessiné sur sa surface crée [~ # ~] deux [~ # ~ ~] zones polygonales. Vous devez également définir lequel est à l'intérieur et qui est à l'extérieur!

La plupart des temps, les gens vont traiter avec de petits polygones, et il est donc "évident", mais une fois que vous avez des choses de la taille des océans ou des continents, vous feriez mieux de vous assurer que vous obtenez cela de manière appropriée.

En outre, rappelez-vous que les lignes peuvent aller de (-179,0) à (+179,0) de deux manières différentes. L'un est beaucoup plus long que l'autre. Encore une fois, surtout, vous ferez l'hypothèse qu'il s'agit d'une ligne qui va de (-179,0) à (-180,0) qui est (+180,0), puis à (+179,0), mais une Jour ... ce ne sera pas.

Traiter la lat-de long comme un système de coordonnées simples (x, y), voire de négliger le fait que toute projection de coordonnées va avoir des distorsions et des pauses, peut vous faire échouer fort sur les sphères.

4
Spacedman

Ou simplement utiliser une bibliothèque: 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

... retourne la zone dans des mètres carrés.

3
Ikar Pohorský

Voici une solution qui utilise basemap, au lieu de pyproj et shapely, pour la conversion de coordonnées. L'idée est la même que suggérée par @sgillies. Notez que j'ai ajouté le 5ème point pour que le chemin soit une boucle fermée.

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

Le résultat est de 268993.609651 en km ^ 2.

3
Jason