从xyz坐标找到多边形的面积

19

我想使用shapely.geometry.Polygon模块来计算多边形的面积,但它只在xy平面上执行所有计算。对于我的一些多边形来说这是可以的,但其他的多边形也有一个z维度,所以它并没有完全做到我想要的。

是否有一个包可以从xyz坐标中给我提供一个平面多边形的面积,或者有一个包或算法可以将该多边形旋转到xy平面,以便我可以使用shapely.geometry.Polygon().area

这些多边形被表示为元组列表,格式如下:[(x1,y1,z1),(x2,y2,z3),...(xn,yn,zn)]


3
多边形是一个严格的二维图形。你究竟想要计算什么? - Bartlomiej Lewandowski
我正试图从顶点的“xyz”坐标中找出建筑物屋顶和墙壁的表面积。 - Jamie Bull
我还没有找到任何模块来完成这个任务,但是你可以将每个面简单地投影到 xy 平面上,并使用你一直在使用的模块进行计算。 - Bartlomiej Lewandowski
"cast down" 是什么意思? - Jamie Bull
只需将形状旋转到 z 平面上即可。 - Mark Ransom
7个回答

23

这里是计算三维平面多边形面积的公式推导过程

以下是实现该公式的Python代码:

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

为了测试它,这里有一个倾斜的10x5正方形:

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

最初的问题是我过于简化了。需要计算垂直于平面的单位向量。该面积是该向量与所有叉积之和的点积的一半,而不是所有叉积的大小之和的一半。

这可以稍微整理一下(如果您有矩阵和向量类,或者行列式/叉积/点积的标准实现,将更加简洁),但在概念上应该是正确的。


谢谢,汤姆。我已经找到了那个页面,还有一些应用斯托克斯定理到二维多边形的示例代码,但是在尝试将其应用于三维时遇到了麻烦。你的实现看起来很不错。我只是在调整它以适应我的数据结构,即[(x1,y1,z1),(x2,y2,z2),...]。 - Jamie Bull
area 函数应该保持不变。cross_product_magnitude 将变成 x = a[1] * b[2] - a[2] * b[1] 等等。 - Tom Smilack
你不应该这样做。我觉得我在某个地方搞砸了,我会调查一下。 - Tom Smilack
不需要 unit_normaldet。只需获取 norm(total)(计算更简单;应该写成 norm)。 - sancho.s ReinstateMonicaCellio
1
为什么要通过行列式计算单位法向量?难道不能只对多边形的前两条边进行叉积运算并进行归一化吗? - DolphinDream
显示剩余4条评论

8

这是我使用的最终代码。它不使用shapely,而是实现了斯托克定理直接计算面积。它基于@Tom Smilack的答案构建,展示了如何在没有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)

我想要实现这个解决方案,但不清楚的是为什么unit_normal函数实现了多边形的前三个点。poly是一个3D点列表,即原始问题中发布的元组列表。或者这个回答只适用于一个由3个点组成的多边形?谢谢。 - Kostas Markakis
1
据我所记,对于多边形上的任意三个(非共线)点,单位法向量是相同的,我们只需要取前三个点并计算即可。 - Jamie Bull

1

#优化版3D多边形面积Python代码

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

无法处理这个“L”型的多边形 [[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]] 你确定这对于凹多边形有效吗? - ZachV

0

感谢详细的回答,但我有点惊讶没有简单的方法来获取面积。

因此,我只是发布了一个使用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()}')

0

使用Numpy可以将2D多边形的面积计算为一行代码...

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

这对于三维空间中的二维多边形不起作用,例如所有点共面但是以xyz坐标引用。 - Jamie Bull

0

请注意,以下是Mathematica中相同算法的代码,并伴有简单的单元测试。

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]

如果前三个点共线,则planeUnitNormal计算不够健壮。更智能的算法会选择三个不共线的点(通过pairwise[...,Cross]=!=0测试),如果找不到三个点则会抛出异常。 - Reb.Cabin
1
@reb-cabin 为什么要抛出异常?如果每个点的三元组共线,则答案为零。 - Don Hatch

0

与Tom Smilack的答案相同,但使用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);
  }

}


"math.det" 应该改为 "det"。 - pauloz1890

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