【问题标题】:How do I efficiently determine if a polygon is convex, non-convex or complex?如何有效地确定多边形是凸面、非凸面还是复面?
【发布时间】:2025-12-25 12:55:06
【问题描述】:

来自XFillPolygon 的手册页:

  • 如果shape复杂,则路径可能会自相交。请注意,路径中的连续重合点不会被视为自相交。

  • 如果shape,则对于多边形内的每一对点,连接它们的线段不会与路径相交。如果客户知道,指定 Convex 可以提高性能。如果为非凸形路径指定凸形,则图形结果未定义。

  • 如果shape非凸,则路径不会自相交,但形状不是完全凸的。如果客户知道,指定 Nonconvex 而不是 Complex 可能会提高性能。如果您为自相交路径指定 非凸,则图形结果未定义。

我在填充XFillPolygon 时遇到性能问题,正如手册页所建议的,我要采取的第一步是指定多边形的正确形状。为了安全起见,我目前正在使用 Complex

是否有一种有效的算法来确定多边形(由一系列坐标定义)是凸的、非凸的还是复杂的?

【问题讨论】:

标签: algorithm geometry polygon computational-geometry xlib


【解决方案1】:

你可以让事情变得比礼物包装算法更容易......当你有一组没有任何特定边界的点并且需要找到凸包时,这是一个很好的答案。

相比之下,考虑多边形不是自相交的情况,它由列表中的一组点组成,其中连续点形成边界。在这种情况下,确定多边形是否凸面要容易得多(而且您也不必计算任何角度):

对于多边形的每一对连续边(每个三元组点),计算由指向这些点的边以升序定义的向量的叉积的 z 分量。取这些向量的叉积:

 given p[k], p[k+1], p[k+2] each with coordinates x, y:
 dx1 = x[k+1]-x[k]
 dy1 = y[k+1]-y[k]
 dx2 = x[k+2]-x[k+1]
 dy2 = y[k+2]-y[k+1]
 zcrossproduct = dx1*dy2 - dy1*dx2

如果叉积的 z 分量全部为正或全部为负,则多边形是凸的。否则多边形是非凸的。

如果有 N 个点,请确保计算 N 个叉积,例如请务必使用三元组 (p[N-2],p[N-1],p[0]) 和 (p[N-1],p[0],p[1])。


如果多边形是自相交的,那么it fails the technical definition of convexity即使它的方向角都在同一个方向,在这种情况下,上述方法不会产生正确的结果。

【讨论】:

  • 如果我错了,请纠正我,但是对于某些复杂的多边形,这不会失败吗?例如 [[1 3] [9 7] [7 9] [7 2] [9 6] [1 8]]]
  • 令人惊讶的错误答案,所有这些赞成票。 self-intersecting loop 会以出色的成绩通过这个算法。
  • 我已经更新了这个答案。评论者是正确的,它没有解决复杂的情况,但它仍然有价值。
  • 它只解决了部分问题,这是真的。这就是为什么它没有被接受。其他人显然已经发现了这个问题并且能够保证他们没有复杂的情况,因此发现这个答案很有用。
  • 有点困惑如何为 N 个点(如四边形)执行此操作。你关于 N 点的最后一段有点难以理解。
【解决方案2】:

当您搜索“确定凸多边形”时,此问题现在是 Bing 或 Google 中的第一项。然而,没有一个答案是足够好的。

(现已删除)answer by @EugeneYokota 通过检查是否可以将一组无序的点制成凸多边形来工作,但这不是 OP 所要求的。他要求一种方法来检查给定的多边形是否是凸的。 (计算机科学中的“多边形”通常被定义为 [如XFillPolygon documentation] 中的二维点的有序数组,连续点与边相连,最后一个点与第一个点相连。)此外,礼品包装在这种情况下,算法对于n 点的时间复杂度为O(n^2) - 这比解决此问题的实际需要大得多,而问题要求一种有效的算法。

@JasonS's answer,以及遵循他的想法的其他答案,接受star polygons,例如pentagram 或@zenna 评论中的那个,但星形多边形不被认为是凸的。作为 @plasmacel 在评论中指出,如果您事先知道多边形不是自相交的,这是一个很好的方法,但如果您没有这些知识,它可能会失败。

@Sekhat's answer 是正确的,但它也具有O(n^2) 的时间复杂度,因此效率低下。

@LorenPechtel's added answer 在她的编辑之后是最好的,但它是模糊的。

具有最佳复杂度的正确算法

我在这里介绍的算法具有O(n) 的时间复杂度,正确地测试了多边形是否是凸的,并且通过了我对其进行的所有测试。这个想法是遍历多边形的边,注意每边的方向和连续边之间方向的有符号变化。这里的“有符号”表示向左为正,向右为负(或相反),正前方为零。这些角度被归一化为负 pi(不包括)和 pi(包括)之间。 求和所有这些方向改变角度(也就是偏转角度)加在一起将导致正负一圈(即 360 度)对于凸多边形,而星状多边形(或自相交环)将具有不同的总和(n * 360 度,对于 n 整体转向,对于所有偏转角都具有相同符号的多边形)。所以我们必须检查方向改变角度的总和是正负一圈。我们还检查方向变化角度是否全部为正或全部为负且不反转(pi 弧度),所有点都是实际的 2D 点,并且没有连续的顶点是相同的。 (最后一点值得商榷——您可能希望允许重复顶点,但我更喜欢禁止它们。)这些检查的组合可以捕获所有凸多边形和非凸多边形。

这里是 Python 3 的代码,它实现了该算法并包括一些次要的效率。由于注释行和避免重复点访问所涉及的簿记,代码看起来比实际更长。

TWO_PI = 2 * pi

def is_convex_polygon(polygon):
    """Return True if the polynomial defined by the sequence of 2D
    points is 'strictly convex': points are valid, side lengths non-
    zero, interior angles are strictly between zero and a straight
    angle, and the polygon does not intersect itself.

    NOTES:  1.  Algorithm: the signed changes of the direction angles
                from one side to the next side must be all positive or
                all negative, and their sum must equal plus-or-minus
                one full turn (2 pi radians). Also check for too few,
                invalid, or repeated points.
            2.  No check is explicitly done for zero internal angles
                (180 degree direction-change angle) as this is covered
                in other ways, including the `n < 3` check.
    """
    try:  # needed for any bad points or direction changes
        # Check for too few points
        if len(polygon) < 3:
            return False
        # Get starting information
        old_x, old_y = polygon[-2]
        new_x, new_y = polygon[-1]
        new_direction = atan2(new_y - old_y, new_x - old_x)
        angle_sum = 0.0
        # Check each point (the side ending there, its angle) and accum. angles
        for ndx, newpoint in enumerate(polygon):
            # Update point coordinates and side directions, check side length
            old_x, old_y, old_direction = new_x, new_y, new_direction
            new_x, new_y = newpoint
            new_direction = atan2(new_y - old_y, new_x - old_x)
            if old_x == new_x and old_y == new_y:
                return False  # repeated consecutive points
            # Calculate & check the normalized direction-change angle
            angle = new_direction - old_direction
            if angle <= -pi:
                angle += TWO_PI  # make it in half-open interval (-Pi, Pi]
            elif angle > pi:
                angle -= TWO_PI
            if ndx == 0:  # if first time through loop, initialize orientation
                if angle == 0.0:
                    return False
                orientation = 1.0 if angle > 0.0 else -1.0
            else:  # if other time through loop, check orientation is stable
                if orientation * angle <= 0.0:  # not both pos. or both neg.
                    return False
            # Accumulate the direction-change angle
            angle_sum += angle
        # Check that the total number of full turns is plus-or-minus 1
        return abs(round(angle_sum / TWO_PI)) == 1
    except (ArithmeticError, TypeError, ValueError):
        return False  # any exception means not a proper convex polygon

【讨论】:

  • 这里有一个有点相关但更简单的方法,不需要三角函数:math.stackexchange.com/questions/1743995/…
  • @plasmacel:这种方法,就像 JasonS 的回答一样,接受五角星形或 zenna 评论中的星形多边形。如果星形多边形是可以接受的,那确实比我的方法好,但是星形多边形通常不被认为是凸的。这就是我花时间编写和测试这个拒绝星形多边形的函数的原因。另外,感谢您的编辑——它确实改善了我的答案。但是,您确实更改了一个句子的含义,所以我再次对其进行了编辑-希望这次更清楚。
  • 星形多边形不仅是非凸的,而且是自相交的。您的答案可能会扩展测试以正确处理自相交多边形(有这样的解决方案很好),但是如果只考虑非自相交简单多边形,那么混合乘积(@Jason 称为zcrossproduct)方法是更可取。
  • @plasmacel:如果您事先知道多边形不是自相交的,那么 JasonS 的方法很好。我想专注于“凸”的问题,这也是其他人也在关注的问题。我还想要一个对多边形完全不做任何假设的函数——我的例程甚至检查数组中的“点”实际上是包含两个值的结构,即点坐标。
  • @RoryDaulton:我是上述answer 另一个问题的作者,但错过了这里的注释!我重写了那个答案;请重新与您的比较。为了考虑自相交(例如蝴蝶结或星形)多边形,计算边向量的 $x$ 和 $y$ 中符号变化的数量(就像没有符号一样忽略零)就足够了组件;对于凸多边形,每个恰好有两个。 atan2() 很慢。如果需要,我也可以提供 Python 实现进行比较。
【解决方案3】:

以下 Java 函数/方法是 this answer 中描述的算法的实现。

public boolean isConvex()
{
    if (_vertices.size() < 4)
        return true;

    boolean sign = false;
    int n = _vertices.size();

    for(int i = 0; i < n; i++)
    {
        double dx1 = _vertices.get((i + 2) % n).X - _vertices.get((i + 1) % n).X;
        double dy1 = _vertices.get((i + 2) % n).Y - _vertices.get((i + 1) % n).Y;
        double dx2 = _vertices.get(i).X - _vertices.get((i + 1) % n).X;
        double dy2 = _vertices.get(i).Y - _vertices.get((i + 1) % n).Y;
        double zcrossproduct = dx1 * dy2 - dy1 * dx2;

        if (i == 0)
            sign = zcrossproduct > 0;
        else if (sign != (zcrossproduct > 0))
            return false;
    }

    return true;
}

只要顶点是有序的(顺时针或逆时针),并且您没有自相交的边(即它仅适用于simple polygons),该算法就可以保证工作。

【讨论】:

  • 不会“修复”“自相交多边形问题”,使用“zcrossproduct”中保存的值来检查多边形是否执行完美的 360° 扭曲?跨度>
【解决方案4】:

这是一个检查多边形是否的测试。

考虑沿多边形的每组三个点——一个顶点、之前的顶点、之后的顶点。如果每个角度都是 180 度或更小,则您有一个凸多边形。当你计算出每个角度时,还要保持(180 - 角度)的运行总数。对于凸多边形,总共是 360。

这个测试在 O(n) 时间内运行。

另外请注意,在大多数情况下,此计算只需执行一次即可保存 — 大多数情况下,您需要处理一组不会一直变化的多边形。

【讨论】:

  • “考虑沿多边形的每组三个点。[...] 这个测试在 O(n) 时间内运行。” 我认为值得扩展回答。就目前而言,“考虑每组三点”至少需要 n³。
  • @Stef 3 个点跟随多边形的边缘,不是三个顶点的所有组合。
【解决方案5】:

要测试一个多边形是否是凸的,多边形的每个点都应该与每条线齐平或位于每条线的后面。

这是一个示例图片:

【讨论】:

  • 我不知道这意味着什么。一个点在一条线的水平、后面或前面是什么意思?
  • 这应该澄清一点:*.com/questions/1560492/…
  • 这很模糊。这不是算法。您能否在没有模糊链接的情况下展开和解释并简单地编辑答案?
  • 标准基本上相当于将凸多边形定义为半平面或凸包的交点。由于对多边形来说是凸的就等于是它自己的凸包,计算该包允许进行凸性测试,尽管具有O(n log n) 的非最佳复杂度。这也无法区分复杂多边形和非凸简单多边形。
【解决方案6】:

answer by @RoryDaulton 对我来说似乎是最好的,但如果其中一个角度正好是 0 怎么办? 有些人可能希望这样的边缘情况返回 True,在这种情况下,将行中的“

if orientation * angle < 0.0:  # not both pos. or both neg.

这是我突出问题的测试用例:

# A square    
assert is_convex_polygon( ((0,0), (1,0), (1,1), (0,1)) )

# This LOOKS like a square, but it has an extra point on one of the edges.
assert is_convex_polygon( ((0,0), (0.5,0), (1,0), (1,1), (0,1)) )

第二个断言在原始答案中失败。应该是? 对于我的用例,我希望它没有。

【讨论】:

  • 啊,边缘情况。很高兴看到你在照顾他们!算法研究人员倾向于忽略这些(因为这实际上是实现)。这里的一般问题是大多数几何图元是不精确的,因此 '
  • if ndx == 0 .. else 更改为if not np.isclose(angle, 0.): # only check if direction actually changed if orientation is None: orientation = np.sign(angle) elif orientation != np.sign(angle): return False,它也应该适用于您的边缘情况。还要在循环前添加orientation = None
【解决方案7】:

假设顶点是有序的(顺时针或逆时针),此方法适用于简单的多边形(没有自相交的边)

对于顶点数组:

vertices = [(0,0),(1,0),(1,1),(0,1)]

下面的python 实现检查所有叉积的z 组件是否具有相同的符号

def zCrossProduct(a,b,c):
   return (a[0]-b[0])*(b[1]-c[1])-(a[1]-b[1])*(b[0]-c[0])

def isConvex(vertices):
    if len(vertices)<4:
        return True
    signs= [zCrossProduct(a,b,c)>0 for a,b,c in zip(vertices[2:],vertices[1:],vertices)]
    return all(signs) or not any(signs)

【讨论】:

    【解决方案8】:

    我实现了两种算法:@UriGoren 发布的一种算法(略有改进 - 仅整数数学)和@RoryDaulton 的一种,用 Java 编写。我遇到了一些问题,因为我的多边形是封闭的,所以两种算法都认为第二个是凹的,而它是凸的。所以我改变了它以防止这种情况。我的方法还使用了一个基本索引(可以是也可以不是 0)。

    这些是我的测试顶点:

    // concave
    int []x = {0,100,200,200,100,0,0};
    int []y = {50,0,50,200,50,200,50};
    
    // convex
    int []x = {0,100,200,100,0,0};
    int []y = {50,0,50,200,200,50};
    

    现在是算法:

    private boolean isConvex1(int[] x, int[] y, int base, int n) // Rory Daulton
    {
      final double TWO_PI = 2 * Math.PI;
    
      // points is 'strictly convex': points are valid, side lengths non-zero, interior angles are strictly between zero and a straight
      // angle, and the polygon does not intersect itself.
      // NOTES:  1.  Algorithm: the signed changes of the direction angles from one side to the next side must be all positive or
      // all negative, and their sum must equal plus-or-minus one full turn (2 pi radians). Also check for too few,
      // invalid, or repeated points.
      //      2.  No check is explicitly done for zero internal angles(180 degree direction-change angle) as this is covered
      // in other ways, including the `n < 3` check.
    
      // needed for any bad points or direction changes
      // Check for too few points
      if (n <= 3) return true;
      if (x[base] == x[n-1] && y[base] == y[n-1]) // if its a closed polygon, ignore last vertex
         n--;
      // Get starting information
      int old_x = x[n-2], old_y = y[n-2];
      int new_x = x[n-1], new_y = y[n-1];
      double new_direction = Math.atan2(new_y - old_y, new_x - old_x), old_direction;
      double angle_sum = 0.0, orientation=0;
      // Check each point (the side ending there, its angle) and accum. angles for ndx, newpoint in enumerate(polygon):
      for (int i = 0; i < n; i++)
      {
         // Update point coordinates and side directions, check side length
         old_x = new_x; old_y = new_y; old_direction = new_direction;
         int p = base++;
         new_x = x[p]; new_y = y[p];
         new_direction = Math.atan2(new_y - old_y, new_x - old_x);
         if (old_x == new_x && old_y == new_y)
            return false; // repeated consecutive points
         // Calculate & check the normalized direction-change angle
         double angle = new_direction - old_direction;
         if (angle <= -Math.PI)
            angle += TWO_PI;  // make it in half-open interval (-Pi, Pi]
         else if (angle > Math.PI)
            angle -= TWO_PI;
         if (i == 0)  // if first time through loop, initialize orientation
         {
            if (angle == 0.0) return false;
            orientation = angle > 0 ? 1 : -1;
         }
         else  // if other time through loop, check orientation is stable
         if (orientation * angle <= 0)  // not both pos. or both neg.
            return false;
         // Accumulate the direction-change angle
         angle_sum += angle;
         // Check that the total number of full turns is plus-or-minus 1
      }
      return Math.abs(Math.round(angle_sum / TWO_PI)) == 1;
    }
    

    现在来自 Uri Goren

    private boolean isConvex2(int[] x, int[] y, int base, int n)
    {
      if (n < 4)
         return true;
      boolean sign = false;
      if (x[base] == x[n-1] && y[base] == y[n-1]) // if its a closed polygon, ignore last vertex
         n--;
      for(int p=0; p < n; p++)
      {
         int i = base++;
         int i1 = i+1; if (i1 >= n) i1 = base + i1-n;
         int i2 = i+2; if (i2 >= n) i2 = base + i2-n;
         int dx1 = x[i1] - x[i];
         int dy1 = y[i1] - y[i];
         int dx2 = x[i2] - x[i1];
         int dy2 = y[i2] - y[i1];
         int crossproduct = dx1*dy2 - dy1*dx2;
         if (i == base)
            sign = crossproduct > 0;
         else
         if (sign != (crossproduct > 0))
            return false;
      }
      return true;
    }
    

    【讨论】:

      【解决方案9】:

      将 Uri 的代码改编成 matlab。希望这可能会有所帮助。

      请注意,Uri 的算法仅适用于 简单多边形!所以,一定要先测试一下多边形是否简单!

      % M [ x1 x2 x3 ...
      %     y1 y2 y3 ...]
      % test if a polygon is convex
      
      function ret = isConvex(M)
          N = size(M,2);
          if (N<4)
              ret = 1;
              return;
          end
      
          x0 = M(1, 1:end);
          x1 = [x0(2:end), x0(1)];
          x2 = [x0(3:end), x0(1:2)];
          y0 = M(2, 1:end);
          y1 = [y0(2:end), y0(1)];
          y2 = [y0(3:end), y0(1:2)];
          dx1 = x2 - x1;
          dy1 = y2 - y1;
          dx2 = x0 - x1;
          dy2 = y0 - y1;
          zcrossproduct = dx1 .* dy2 - dy1 .* dx2;
      
          % equality allows two consecutive edges to be parallel
          t1 = sum(zcrossproduct >= 0);  
          t2 = sum(zcrossproduct <= 0);  
          ret = t1 == N || t2 == N;
      
      end
      

      【讨论】: