19

I'm trying to use the shapely.geometry.Polygon module to find the area of polygons but it performs all calculations on the xy plane. This is fine for some of my polygons but others have a z dimension too so it's not quite doing what I'd like.

Is there a package which will either give me the area of a planar polygon from xyz coordinates, or alternatively a package or algorithm to rotate the polygon to the xy plane so that i can use shapely.geometry.Polygon().area?

The polygons are represented as a list of tuples in the form [(x1,y1,z1),(x2,y2,z3),...(xn,yn,zn)].

Mark Rotteveel
  • 100,966
  • 191
  • 140
  • 197
Jamie Bull
  • 12,889
  • 15
  • 77
  • 116

7 Answers7

23

Here is the derivation of a formula for calculating the area of a 3D planar polygon

Here is Python code that implements it:

#determinant of matrix a
def det(a):
    return a[0][0]*a[1][1]*a[2][2] + a[0][1]*a[1][2]*a[2][0] + a[0][2]*a[1][0]*a[2][1] - a[0][2]*a[1][1]*a[2][0] - a[0][1]*a[1][0]*a[2][2] - a[0][0]*a[1][2]*a[2][1]

#unit normal vector of plane defined by points a, b, and c
def unit_normal(a, b, c):
    x = det([[1,a[1],a[2]],
             [1,b[1],b[2]],
             [1,c[1],c[2]]])
    y = det([[a[0],1,a[2]],
             [b[0],1,b[2]],
             [c[0],1,c[2]]])
    z = det([[a[0],a[1],1],
             [b[0],b[1],1],
             [c[0],c[1],1]])
    magnitude = (x**2 + y**2 + z**2)**.5
    return (x/magnitude, y/magnitude, z/magnitude)

#dot product of vectors a and b
def dot(a, b):
    return a[0]*b[0] + a[1]*b[1] + a[2]*b[2]

#cross product of vectors a and b
def cross(a, b):
    x = a[1] * b[2] - a[2] * b[1]
    y = a[2] * b[0] - a[0] * b[2]
    z = a[0] * b[1] - a[1] * b[0]
    return (x, y, z)

#area of polygon poly
def area(poly):
    if len(poly) < 3: # not a plane - no area
        return 0

    total = [0, 0, 0]
    for i in range(len(poly)):
        vi1 = poly[i]
        if i is len(poly)-1:
            vi2 = poly[0]
        else:
            vi2 = poly[i+1]
        prod = cross(vi1, vi2)
        total[0] += prod[0]
        total[1] += prod[1]
        total[2] += prod[2]
    result = dot(total, unit_normal(poly[0], poly[1], poly[2]))
    return abs(result/2)

And to test it, here's a 10x5 square that leans over:

>>> poly = [[0, 0, 0], [10, 0, 0], [10, 3, 4], [0, 3, 4]]
>>> poly_translated = [[0+5, 0+5, 0+5], [10+5, 0+5, 0+5], [10+5, 3+5, 4+5], [0+5, 3+5, 4+5]]
>>> area(poly)
50.0
>>> area(poly_translated)
50.0
>>> area([[0,0,0],[1,1,1]])
0

The problem originally was that I had oversimplified. It needs to calculate the unit vector normal to the plane. The area is half of the dot product of that and the total of all the cross products, not half of the sum of all the magnitudes of the cross products.

This can be cleaned up a bit (matrix and vector classes would make it nicer, if you have them, or standard implementations of determinant/cross product/dot product), but it should be conceptually sound.

Tom Smilack
  • 2,057
  • 15
  • 20
  • Thanks, Tom. I'd found that page and also some sample code for applying Stoke's theorem to a 2D polygon but was having trouble making it work for 3D. Your implementation looks good to me. I'm just adapting it to work with the way my data's structured which is [(x1,y1,z1),(x2,y2,z2),...]. – Jamie Bull Sep 28 '12 at 16:02
  • The `area` function should be the same. `cross_product_magnitude` would change to `x = a[1] * b[2] - a[2] * b[1]` etc. – Tom Smilack Sep 28 '12 at 16:06
  • Yes, I've got that - but it's throwing out results that are way too large. Do I need to move the shape so one vertex is at the origin? – Jamie Bull Sep 28 '12 at 16:15
  • You shouldn't have to. I think I messed up somewhere, I'll look into it. – Tom Smilack Sep 28 '12 at 16:31
  • Did my edit not work then? It works in my code here. Also, your code there is still missing the abs(). I've added another answer with the edited code, and using numpy for the cross-product. – Jamie Bull Sep 28 '12 at 18:50
  • Actually my edit didn't get the right result on your tests. It worked for walls in my application but not for sloping surfaces. Yours is working just fine. I just need to add `abs()` when calling it as I'm summing up all the surface areas and some of the areas come out negative: `poly_area = abs(area(poly))`. – Jamie Bull Sep 28 '12 at 19:10
  • Added `abs` to my answer for completeness. It has to do with Stokes' Theorem and how areas can be signed depending on the direction you integrate around the perimeter. – Tom Smilack Sep 28 '12 at 19:59
  • `unit_normal` and `det` are not needed. It is enough to get `norm(total)` (much simpler to compute; `norm` should be written). – sancho.s ReinstateMonicaCellio Apr 26 '16 at 18:20
  • 1
    Why is the unit normal computed via determinant? Can’t you just do a cross product of the polygon first two edges + normalization ? – DolphinDream Dec 05 '18 at 23:51
8

This is the final code I've used. It doesn't use shapely, but implements Stoke's theorem to calculate the area directly. It builds on @Tom Smilack's answer which shows how to do it without numpy.

import numpy as np

#unit normal vector of plane defined by points a, b, and c
def unit_normal(a, b, c):
    x = np.linalg.det([[1,a[1],a[2]],
         [1,b[1],b[2]],
         [1,c[1],c[2]]])
    y = np.linalg.det([[a[0],1,a[2]],
         [b[0],1,b[2]],
         [c[0],1,c[2]]])
    z = np.linalg.det([[a[0],a[1],1],
         [b[0],b[1],1],
         [c[0],c[1],1]])
    magnitude = (x**2 + y**2 + z**2)**.5
    return (x/magnitude, y/magnitude, z/magnitude)

#area of polygon poly
def poly_area(poly):
    if len(poly) < 3: # not a plane - no area
        return 0
    total = [0, 0, 0]
    N = len(poly)
    for i in range(N):
        vi1 = poly[i]
        vi2 = poly[(i+1) % N]
        prod = np.cross(vi1, vi2)
        total[0] += prod[0]
        total[1] += prod[1]
        total[2] += prod[2]
    result = np.dot(total, unit_normal(poly[0], poly[1], poly[2]))
    return abs(result/2)
Jamie Bull
  • 12,889
  • 15
  • 77
  • 116
  • I am looking to implement this solution but what is not clear is why the unit_normal function implements the first 3 points of the polygon. poly is a list of 3d points i.e., a list of tuples as posted in the original question. or is the response applicabel to a 3-point polygon only? thanks – Kostas Markakis Feb 04 '20 at 09:41
  • 1
    From what I remember, the unit normal vector is the same for any three (non-colinear) points on a polygon, we can just take the first three points and calculate it from that – Jamie Bull Feb 04 '20 at 14:11
1

#pythonn code for polygon area in 3D (optimised version)

def polygon_area(poly):
    #shape (N, 3)
    if isinstance(poly, list):
        poly = np.array(poly)
    #all edges
    edges = poly[1:] - poly[0:1]
    # row wise cross product
    cross_product = np.cross(edges[:-1],edges[1:], axis=1)
    #area of all triangles
    area = np.linalg.norm(cross_product, axis=1)/2
    return sum(area)



if __name__ == "__main__":
    poly = [[0+5, 0+5, 0+5], [10+5, 0+5, 0+5], [10+5, 3+5, 4+5], [0+5, 3+5, 4+5]]
    print(polygon_area(poly))
  • Doesn't work with this "L" shaped polygon [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 0.7071067811865475, 0.7071067811865476], [2.0, 0.7071067811865475, 0.7071067811865476], [2.0, 1.414213562373095, 1.4142135623730951], [0.0, 1.414213562373095, 1.4142135623730951]] Are you sure this will work for concave polygons? – ZachV Apr 21 '22 at 18:47
0

The area of a 2D polygon can be calculated using Numpy as a one-liner...

poly_Area(vertices) = np.sum( [0.5, -0.5] * vertices * np.roll( np.roll(vertices, 1, axis=0), 1, axis=1) )
raras
  • 1
0

Fyi, here is the same algorithm in Mathematica, with a baby unit test

ClearAll[vertexPairs, testPoly, area3D, planeUnitNormal, pairwise];
pairwise[list_, fn_] := MapThread[fn, {Drop[list, -1], Drop[list, 1]}];
vertexPairs[Polygon[{points___}]] := Append[{points}, First[{points}]];
testPoly = Polygon[{{20, -30, 0}, {40, -30, 0}, {40, -30, 20}, {20, -30, 20}}];
planeUnitNormal[Polygon[{points___}]] :=
  With[{ps = Take[{points}, 3]},
   With[{p0 = First[ps]},
    With[{qs = (# - p0) & /@ Rest[ps]},
     Normalize[Cross @@ qs]]]];
area3D[p : Polygon[{polys___}]] :=
  With[{n = planeUnitNormal[p], vs = vertexPairs[p]},
   With[{areas = (Dot[n, #]) & /@ pairwise[vs, Cross]},
    Plus @@ areas/2]];
area3D[testPoly]
Reb.Cabin
  • 5,426
  • 3
  • 35
  • 64
  • The `planeUnitNormal` computation is not robust in case the first three points are colinear. A smarter algorithm would pick three points that are not colinear (testing by `pairwise[...,Cross]=!=0` and throw if it can't find three. – Reb.Cabin Apr 08 '14 at 17:58
  • 1
    @reb-cabin why throw? If every triple of points is collinear, then the answer is zero. – Don Hatch Jan 25 '19 at 07:03
0

Same as @Tom Smilack's answer, but in javascript

//determinant of matrix a
function det(a) {
  return a[0][0] * a[1][1] * a[2][2] + a[0][1] * a[1][2] * a[2][0] + a[0][2] * a[1][0] * a[2][1] - a[0][2] * a[1][1] * a[2][0] - a[0][1] * a[1][0] * a[2][2] - a[0][0] * a[1][2] * a[2][1];
}
//unit normal vector of plane defined by points a, b, and c
function unit_normal(a, b, c) {
  let x = math.det([
    [1, a[1], a[2]],
    [1, b[1], b[2]],
    [1, c[1], c[2]]
  ]);
  let y = math.det([
    [a[0], 1, a[2]],
    [b[0], 1, b[2]],
    [c[0], 1, c[2]]
  ]);
  let z = math.det([
    [a[0], a[1], 1],
    [b[0], b[1], 1],
    [c[0], c[1], 1]
  ]);
  let magnitude = Math.pow(Math.pow(x, 2) + Math.pow(y, 2) + Math.pow(z, 2), 0.5);
  return [x / magnitude, y / magnitude, z / magnitude];
}
// dot product of vectors a and b
function dot(a, b) {
  return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
}
// cross product of vectors a and b
function cross(a, b) {
  let x = (a[1] * b[2]) - (a[2] * b[1]);
  let y = (a[2] * b[0]) - (a[0] * b[2]);
  let z = (a[0] * b[1]) - (a[1] * b[0]);
  return [x, y, z];
}

// area of polygon poly
function area(poly) {
  if (poly.length < 3) {
    console.log("not a plane - no area");
    return 0;
  } else {
    let total = [0, 0, 0]
    for (let i = 0; i < poly.length; i++) {
      var vi1 = poly[i];
      if (i === poly.length - 1) {
        var vi2 = poly[0];
      } else {
        var vi2 = poly[i + 1];
      }
      let prod = cross(vi1, vi2);
      total[0] = total[0] + prod[0];
      total[1] = total[1] + prod[1];
      total[2] = total[2] + prod[2];
    }
    let result = dot(total, unit_normal(poly[0], poly[1], poly[2]));

    return Math.abs(result/2);
  }

}
Michael
  • 1,627
  • 1
  • 19
  • 29
0

Thanks for detailed answers, But I am little surprised there is no simple answer to get the area.

So, I am just posting a simplified approach for calculating area using 3d Coordinates of polygon or surface using pyny3d.

#Install pyny3d as:
pip install pyny3d

#Calculate area
import numpy as np
import pyny3d.geoms as pyny

coords_3d = np.array([[0,  0, 0],
                           [7,  0, 0],
                           [7, 10, 2],
                           [0, 10, 2]])
polygon = pyny.Polygon(coords_3d)
print(f'Area is : {polygon.get_area()}')
sandeepsign
  • 539
  • 6
  • 11