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
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.
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);
}
}
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)
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) )
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]