有没有针对多边形的质心查找算法的数值稳定版本?

4

假设我有一个几乎退化的2-D多边形,例如:

[[40.802,9.289],[40.875,9.394],[40.910000000000004,9.445],[40.911,9.446],[40.802,9.289]]

参考下面的图片:

enter image description here

如果我使用标准的重心算法,例如维基百科上的这个Python代码:

pts = [[40.802,9.289],[40.875,9.394],[40.910000000000004,9.445], [40.911,9.446],[40.802,9.289]]
a = 0.0
c = [0.0, 0.0]
for i in range(0,4):
    k = pts[i][0] * pts[i + 1][1] - pts[i + 1][0] * pts[i][1]
    a += k
    c = [c[0] + k * (pts[i][0] + pts[i + 1][0]), c[1] + k * (pts[i][1] + pts[i + 1][1])]
c = [c[0] / (3 * a), c[1] / (3 * a)]

我得到了 c = [-10133071.666666666, -14636692.583333334]。在其他情况下,如果 a == 0.0,我也可能会遇到除以零的情况。
理想情况下,最坏的情况是质心等于一个顶点或者在多边形内部的某个位置,并且不应使用任何任意的容差来避免这种情况。是否有一些聪明的方法来重新编写方程以使其更加数值稳定?

1
你可以尝试的一个简单方法是将整个多边形移动到原点(即减去角落的平均值)。这将为您提供更多的浮点精度。 - Nico Schertler
你的程序有误,c不应该乘以k。(但修复后,数值不稳定性仍然存在。) - user1196549
@NicoSchertler:我认为这并没有帮助,问题出在接近零的区域。 - user1196549
1
另一个常见的方法是避免使用除法,改用齐次坐标。在这样的设置中,您将获得一种接近零向量的结果,它不表示一个点。原点的齐次坐标为(0,0,1)或其任意倍数。如果您可以继续而不进行除法,则此方法很有帮助,但它只会为“该质心未定义”提供不同的表示形式,而不是您预期的多边形内部的点,因此我只是将其作为评论发布。 - MvG
@YvesDaoust 同意这不是一个保证的解决方案(这就是为什么它只是一条评论)。然而,靠近原点的增加精度可能足以表示接近零的区域。 - Nico Schertler
显示剩余4条评论
3个回答

2
当面积为零(或非常接近零,如果您无法进行精确运算)时,最好的选择可能是取一组点的周长重心。
周长重心由多边形每条边中点的加权和(权重为相应边的长度)与多边形周长之比给出。
使用精确运算,可以在此情况下计算质心。 centroid vs perimeter centroid 红点是周长重心,绿点是真正的质心 我使用Sage精确计算了质心https://cloud.sagemath.com/projects/f3149cab-2b4b-494a-b795-06d62ae133dd/files/2016-08-17-102024.sagews
人们一直在寻找一种方法来将这些点相互关联-- https://math.stackexchange.com/questions/1173903/centroids-of-a-polygon

0

我觉得这个公式很难使几乎退化的二维多边形更加稳定。问题在于面积(A)的计算依赖于减去梯形形状(参见Paul Bourke)。对于非常小的面积,你不可避免地会遇到数值精度的问题。

我看到两种可能的解决方案:

1.) 你可以检查面积,如果面积低于一个阈值,就假设多边形是退化的,并且只取最小和最大x和y值的平均值(线的中间点)

2.) 使用更高精度的浮点运算,也许类似于mpmath

顺便说一下,你的代码有一个错误。应该是:

c = [c[0] + k * (pts[i][0] + pts[i + 1][0]), c[1] + k * (pts[i][1] + pts[i + 1][1])]

然而这并没有什么区别。


0

我认为以下是计算简单多边形重心的权威C语言实现,它是由《C语言计算几何》一书作者Joseph O'Rourke编写的。

/*
    Written by Joseph O'Rourke
    orourke@cs.smith.edu
    October 27, 1995

    Computes the centroid (center of gravity) of an arbitrary
    simple polygon via a weighted sum of signed triangle areas,
    weighted by the centroid of each triangle.
    Reads x,y coordinates from stdin.  
    NB: Assumes points are entered in ccw order!  
    E.g., input for square:
        0   0
        10  0
        10  10
        0   10
    This solves Exercise 12, p.47, of my text,
    Computational Geometry in C.  See the book for an explanation
    of why this works. Follow links from
        http://cs.smith.edu/~orourke/

*/
#include    <stdio.h>

#define DIM     2               /* Dimension of points */
typedef int     tPointi[DIM];   /* type integer point */
typedef double  tPointd[DIM];   /* type double point */

#define PMAX    1000            /* Max # of pts in polygon */
typedef tPointi tPolygoni[PMAX];/* type integer polygon */

int     Area2( tPointi a, tPointi b, tPointi c );
void    FindCG( int n, tPolygoni P, tPointd CG );
int ReadPoints( tPolygoni P );
void    Centroid3( tPointi p1, tPointi p2, tPointi p3, tPointi c );
void    PrintPoint( tPointd p );

int main()
{
    int n;
    tPolygoni   P;
    tPointd CG;

    n = ReadPoints( P );
    FindCG( n, P ,CG);
    printf("The cg is ");
    PrintPoint( CG );
}

/* 
        Returns twice the signed area of the triangle determined by a,b,c,
        positive if a,b,c are oriented ccw, and negative if cw.
*/
int     Area2( tPointi a, tPointi b, tPointi c )
{
    return
        (b[0] - a[0]) * (c[1] - a[1]) -
        (c[0] - a[0]) * (b[1] - a[1]);
}

/*      
        Returns the cg in CG.  Computes the weighted sum of
    each triangle's area times its centroid.  Twice area
    and three times centroid is used to avoid division
    until the last moment.
*/
void     FindCG( int n, tPolygoni P, tPointd CG)
{
        int     i;
        double  A2, Areasum2 = 0;        /* Partial area sum */    
    tPointi Cent3;

    CG[0] = 0;
    CG[1] = 0;
        for (i = 1; i < n-1; i++) {
            Centroid3( P[0], P[i], P[i+1], Cent3 );
            A2 =  Area2( P[0], P[i], P[i+1]);
        CG[0] += A2 * Cent3[0];
        CG[1] += A2 * Cent3[1];
        Areasum2 += A2;
          }
        CG[0] /= 3 * Areasum2;
        CG[1] /= 3 * Areasum2;
    return;
}
/*
    Returns three times the centroid.  The factor of 3 is
    left in to permit division to be avoided until later.
*/
void    Centroid3( tPointi p1, tPointi p2, tPointi p3, tPointi c )
{
        c[0] = p1[0] + p2[0] + p3[0];
        c[1] = p1[1] + p2[1] + p3[1];
    return;
}

void    PrintPoint( tPointd p )
{
        int i;

        putchar('(');
        for ( i=0; i<DIM; i++) {
        printf("%f",p[i]);
        if (i != DIM - 1) putchar(',');
        }
        putchar(')');
    putchar('\n');
}

/*
    Reads in the coordinates of the vertices of a polygon from stdin,
    puts them into P, and returns n, the number of vertices.
    The input is assumed to be pairs of whitespace-separated coordinates,
    one pair per line.  The number of points is not part of the input.
*/
int  ReadPoints( tPolygoni P )
{
    int n = 0;

    printf("Polygon:\n");
    printf("  i   x   y\n");      
    while ( (n < PMAX) && 
        (scanf("%d %d",&P[n][0],&P[n][1]) != EOF) ) {
    printf("%3d%4d%4d\n", n, P[n][0], P[n][1]);
    ++n;
    }
    if (n < PMAX)
    printf("n = %3d vertices read\n",n);
    else    printf("Error in ReadPoints:\too many points; max is %d\n", 
               PMAX);
    putchar('\n');

    return  n;
}

该代码解决了书的第一版第47页上的练习12,这里有一个简短的解释在这里

Subject 2.02: How can the centroid of a polygon be computed?

The centroid (a.k.a. the center of mass, or center of gravity)
of a polygon can be computed as the weighted sum of the centroids
of a partition of the polygon into triangles.  The centroid of a
triangle is simply the average of its three vertices, i.e., it
has coordinates (x1 + x2 + x3)/3 and (y1 + y2 + y3)/3.  This 
suggests first triangulating the polygon, then forming a sum
of the centroids of each triangle, weighted by the area of
each triangle, the whole sum normalized by the total polygon area.
This indeed works, but there is a simpler method:  the triangulation
need not be a partition, but rather can use positively and
negatively oriented triangles (with positive and negative areas),
as is used when computing the area of a polygon.  This leads to
a very simple algorithm for computing the centroid, based on a
sum of triangle centroids weighted with their signed area.
The triangles can be taken to be those formed by any fixed point,
e.g., the vertex v0 of the polygon, and the two endpoints of 
consecutive edges of the polygon: (v1,v2), (v2,v3), etc.  The area 
of a triangle with vertices a, b, c is half of this expression:
            (b[X] - a[X]) * (c[Y] - a[Y]) -
            (c[X] - a[X]) * (b[Y] - a[Y]);

Code available at ftp://cs.smith.edu/pub/code/centroid.c (3K).
Reference: [Gems IV] pp.3-6; also includes code.

我没有学习过这个算法,也没有测试过它,但是乍一看,它似乎与维基百科上的算法略有不同。

来自书籍《图形宝石 IV》的代码在这里

/*
 * ANSI C code from the article
 * "Centroid of a Polygon"
 * by Gerard Bashein and Paul R. Detmer,
    (gb@locke.hs.washington.edu, pdetmer@u.washington.edu)
 * in "Graphics Gems IV", Academic Press, 1994
 */

/*********************************************************************
polyCentroid: Calculates the centroid (xCentroid, yCentroid) and area
of a polygon, given its vertices (x[0], y[0]) ... (x[n-1], y[n-1]). It
is assumed that the contour is closed, i.e., that the vertex following
(x[n-1], y[n-1]) is (x[0], y[0]).  The algebraic sign of the area is
positive for counterclockwise ordering of vertices in x-y plane;
otherwise negative.

Returned values:  0 for normal execution;  1 if the polygon is
degenerate (number of vertices < 3);  and 2 if area = 0 (and the
centroid is undefined).
**********************************************************************/
int polyCentroid(double x[], double y[], int n,
         double *xCentroid, double *yCentroid, double *area)
     {
     register int i, j;
     double ai, atmp = 0, xtmp = 0, ytmp = 0;
     if (n < 3) return 1;
     for (i = n-1, j = 0; j < n; i = j, j++)
      {
      ai = x[i] * y[j] - x[j] * y[i];
      atmp += ai;
      xtmp += (x[j] + x[i]) * ai;
      ytmp += (y[j] + y[i]) * ai;
      }
     *area = atmp / 2;
     if (atmp != 0)
      {
      *xCentroid =  xtmp / (3 * atmp);
      *yCentroid =  ytmp / (3 * atmp);
      return 0;
      }
     return 2;
     }

CGAL允许您使用精确的多精度数字类型,而不是doublefloat,以获得精确的计算结果。这将会增加执行时间开销,其思想在精确计算范例中有所描述。

一种商业实现声称使用Green定理,但我不知道它是否使用了多精度数字类型:

通过仅使用轮廓或多边形上的点应用Green定理来计算面积和质心

我认为它指的是维基百科算法,因为维基百科中的公式是Green定理的一个应用,如此处所解释。


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