自相交多边形的面积

17

计算一个简单的不规则多边形的面积很容易。但是,考虑左侧所示的自交叉多边形ABCDEF:

                    A self-intersecting polygon shaped like a 'figure 8'

如果我们按照多边形顺序使用上面链接的公式遍历点,我们得到0的面积。(“顺时针”面积抵消了“逆时针”面积。)

然而,如果我们根据中心径向排序点并计算面积,则在上图右侧得到多边形ABEDCF的错误面积。

如何最好地找到自交叉多边形的可见面积?(如果答案需要为每个交点创建幻影点,请提供有关如何最佳找到交点以及如何按正确顺序遍历它们的详细信息。)

当调查解决此问题的边缘情况时,出现了这个问题。

定义面积

我将“面积”定义为使用“非零”或“奇偶”规则填充多边形时可见的像素数量。我将接受任何一种答案,但两种都更好。请注意,我明确不为自重叠定义面积,以将重叠区域计算两次。

输入图像描述


3
你的多边形里是否可能有洞? - Saeed Amiri
也许 0 是正确答案 :) - ypercubeᵀᴹ
2
在自相交多边形的情况下,您需要定义“区域”的含义。您如何定义内部?您上面提供的示例过于简单,不足以满足要求。在更复杂的情况下,什么是“区域”的一部分,什么不是根本不清楚。在您提出有意义的定义之前,寻找答案是没有意义的。 - AnT stands with Russia
@AndreyT 看起来这是一个二维问题,而“area”是用于封闭表面的表达式。 - Semih Ozmen
3
我相信你已经找到了解决方案,但对于未来的读者,这个解决方案可能是一个有趣的起点 - Martin Ender
显示剩余2条评论
3个回答

9
您可以尝试使用Bentley-Ottmann,并使用this page中的伪代码。 Bentley-Ottmann 算法 Bentley-Ottmann 算法的输入是线段集合 OMEGA={Li},其输出将是交点集合 LAMBDA={Ij}。该算法被称为“扫描线算法”,因为它的操作可以被视为另一条线,即“扫描线”SL,在收集过程中经过每个线段 Li。 SL 的每个位置收集的信息基本上是有序列表,其中包含当前被 SL 所交叉的所有线段。维护此信息的数据结构通常也称为“扫描线”。该类结构还在发现交点时检测并输出它们。发现交点的过程是该算法及其效率的核心。
为了实现扫描逻辑,我们首先必须线性地排序OMEGA的线段,以确定SL遇到它们的顺序。也就是说,我们需要按照{x坐标增加,y坐标增加}的顺序对所有线段Li的端点{Ei0,Ei1}i=1,n进行排序,以便在SL开始和停止与OMEGA的每个线段相交时进行检测。传统上,端点首先按x坐标递增排序,然后按y坐标递增排序,但任何线性顺序都可以(有些作者更喜欢先按y坐标递减,然后按x坐标递增)。在传统排序中,扫描线是垂直的,并且随着它遇到每个线段从左到右移动,如下图所示:
在算法的任何时刻,扫描线SL只与一个端点在其左侧(或在其上)且另一个端点在其右侧的线段相交。SL数据结构通过以下方式动态地保持这些线段的列表:(1)当遇到其最左端点时添加一个线段,(2)当遇到其最右端点时删除一个线段。此外,SL使用“以上-以下”关系对线段列表进行排序。因此,要添加或删除线段,必须确定其在列表中的位置,这可以通过对列表中当前线段的最坏情况O(log-n)二分查找来完成。此外,除了添加或删除线段之外,还有另一个事件会改变列表结构;即,每当两个线段相交时,它们在有序列表中的位置必须交换。给定这两个线段,在列表中必须是相邻的,这种交换是一个O(log-n)操作。
为了组织所有这些内容,算法维护了一个有序的“事件队列”EQ,其元素会导致SL段列表发生变化。最初,EQ被设置为所有段端点的扫描排序列表。但是,当找到段之间的交点时,它们也按照相同的扫描顺序添加到EQ中,以用于端点。但必须进行测试,以避免将重复的交点插入到事件队列中。上图中的示例显示了如何发生这种情况。在事件2中,段S1和S2导致计算交点I12并将其放入队列中。然后,在事件3中,段S3介于S1和S2之间并将其分开。接下来,在事件4中,S1和S3在扫描线上交换位置,并且S1再次被带到S2旁边,导致重新计算I12。但是,每个交点只能有一个事件,并且不能将I12放入队列两次。因此,在将交点放入队列时,我们必须找到其在队列中的潜在x排序位置,并检查它是否已经存在。由于任何两个段之间最多只有一个交点,因此使用标识符为段标记交点就足以唯一地标识它。由于所有这些原因,事件队列的最大大小= 2n + k.le.2n + n2,并且可以使用O(log(2n + n2))= O(log-n)二进制搜索执行任何插入或删除操作。
但是,所有这些与高效地找到完整的线段交点有什么关系呢?当线段按顺序添加到SL线段列表中时,它们与其他合格线段的可能相交点被确定。当发现有效交点时,它就会被插入事件队列。此外,在扫描期间处理EQ上的交点事件时,它会导致SL列表的重新排序,并且交点也会添加到输出列表LAMBDA中。最后,当所有事件都被处理完毕时,LAMBDA将包含所有交点的完整有序集。

然而,还有一个关键细节,算法的核心,我们仍然需要描述;即如何计算有效的交点?显然,只有在两个线段同时出现在扫描线上时,它们才能相交。但这本身并不足以使算法高效。重要的观察是,两个相交的线段必须是扫描线上的相邻线段。因此,只有几种受限情况需要计算可能的交点:

当线段添加到SL列表中时,请确定它是否与其上下邻居相交。

当从SL列表中删除线段时,它的上下邻居被合并为新邻居。因此,需要确定它们的可能交点。

在交叉事件中,SL列表中的两个线段会交换位置,并且必须确定它们与新邻居(每个线段有一个新邻居)的交点。这意味着对于任何一个EQ事件(端点或交叉点)的处理,最多需要进行两次交点确定。
还有一个细节需要注意,即将线段从SL结构中添加、查找、交换和删除所需的时间。为此,可以将SL实现为平衡二叉树(例如AVL、2-3或红黑树),它保证这些操作最多需要O(log-n)的时间,因为n是SL列表的最大大小。因此,每个(2n+k)个事件最坏情况下需要O(log-n)的处理时间。将初始排序和事件处理加起来,算法的效率为:O(nlog-n)+O((2n+k)log-n)=O((n+k)log-n)。
伪代码:Bentley-Ottmann算法
将所有这些放在一起,Bentley-Ottmann算法的实现顶级逻辑由以下伪代码给出:
    Initialize event queue EQ = all segment endpoints;
    Sort EQ by increasing x and y;
    Initialize sweep line SL to be empty;
    Initialize output intersection list IL to be empty;

    While (EQ is nonempty) {
        Let E = the next event from EQ;
        If (E is a left endpoint) {
            Let segE = E's segment;
            Add segE to SL;
            Let segA = the segment Above segE in SL;
            Let segB = the segment Below segE in SL;
            If (I = Intersect( segE with segA) exists) 
                Insert I into EQ;
            If (I = Intersect( segE with segB) exists) 
                Insert I into EQ;
        }
        Else If (E is a right endpoint) {
            Let segE = E's segment;
            Let segA = the segment Above segE in SL;
            Let segB = the segment Below segE in SL;
            Delete segE from SL;
            If (I = Intersect( segA with segB) exists) 
                If (I is not in EQ already) 
                    Insert I into EQ;
        }
        Else {  // E is an intersection event
            Add E’s intersect point to the output list IL;
            Let segE1 above segE2 be E's intersecting segments in SL;
            Swap their positions so that segE2 is now above segE1;
            Let segA = the segment above segE2 in SL;
            Let segB = the segment below segE1 in SL;
            If (I = Intersect(segE2 with segA) exists)
                If (I is not in EQ already) 
                    Insert I into EQ;
            If (I = Intersect(segE1 with segB) exists)
                If (I is not in EQ already) 
                    Insert I into EQ;
        }
        remove E from EQ;
    }
    return IL;
}

这个程序输出所有交点的有序列表。

2
看起来非常有帮助,但它并没有完全回答我的问题。具体来说,假设我可以找到上面BC和FE线段的交点X。那么我接下来该怎么做呢?我需要按照ABXEDCXF的顺序行走,但是如何将两个X的出现正确排序到相应的两个位置呢? - Phrogz
5
不,你应该分割多边形。当你看到交点时,你应该创建一个子多边形,并继续进行,好像原来的多边形不再有那个子多边形一样。扫描完所有区域后,你将得到一个子多边形列表,这些子多边形加在一起形成你的原始多边形。因此,你的原始面积将是这些子多边形面积的总和。你可能还想看看这本有关计算几何学的宝贵书籍。 - Semih Ozmen
2
以上评论是正确的。正确的方法是按照Ozmen先生上面描述的方式分割多边形。Javascript Clipper http://sourceforge.net/projects/jsclipper/ 就是这样做的。在不久的将来,它还将引入递归自适应细分贝塞尔曲线,将其转换为多边形,以便可以计算曲线形状,并且需要最少的计算时间。 - nicholaswmin
1
答案应该是自给自足的,因为超链接可能在未来失效。 - mbomb007
虽然这个链接可能回答了问题,但最好在此处包含答案的基本部分并提供参考链接。仅有链接的答案如果链接页面发生更改可能会变得无效。 - SOFe

3

这是我凭记忆翻译的,我认为您的多边形没有洞,如果有洞,那么会更加复杂,您需要先从您的多边形中去除洞:

  1. 首先找到您的多边形的凸包,为此,您需要找到多边形顶点的凸包。并计算凸包面积。

  2. 之后,找到您的多边形的所有交点。

  3. 您应该从凸包中减去不属于原始多边形的额外多边形,将它们命名为badpolybadpolys总是至少有一条边与凸包相连,但此边不属于您的原始多边形,将其命名为badborder。 通过迭代凸包,您可以找到所有badborders,但要找到badpoly的其他边界,给定badborder的下一个连接边相对于badborder的角度最小的是badpoly的其中一个边界,您可以继续这样查找所有badpoly的边界,然后计算其面积,同样可以通过重复此方法计算所有badpolys的面积。


看起来这也可以工作,但由于所有计数器和面积计算,计算量当然会增加。我会记住这个方法用于类似情况,谢谢,思路不错。 - Semih Ozmen

-2

Bentley-Ottmann算法在这种情况下并不好。

因为它只适用于需要计算线段交点的情况。

哈哈,我通过将自相交多边形转换为多个简单多边形来解决了计算自相交多边形的问题。

这是我的代码。

https://github.com/zslzz/intersection_polygon

class SdPolygon(object):

  def __init__(self, points=None):
    points = self.parafloat(points)
    self.points = points
    self.current_points = []
    self.sd_polygons = []
    self.gene_polygon()
    from shapely.ops import cascaded_union
    self.sd_polygon = cascaded_union(self.sd_polygons)

  def parafloat(self, points):
    """
    为保证准确,将所有的浮点数转化为整数
    :return:
    """
    para_point = [(int(x), int(y)) for x, y in points]
    return para_point

  def gene_polygon(self):
    for point in self.points:
        self.add_point_to_current(point)  # 依次将点加入数组
    p0 = Polygon(self.current_points)
    self.sd_polygons.append(p0)

  def add_point_to_current(self, point):
    """
    将该点加入到current_points 中,倒序遍历current_points中的点,如果能围成多边形,则将所围成的点弹出
    :param point:
    :return:
    """
    if len(self.current_points) < 2:
        self.current_points.append(point)
        return
    cross_point_dict = {}  # 记录线段与其他点的相交点,{0:P1,6:P2}
    l0 = Line(Point(point[0], point[1]), Point(self.current_points[-1][0], self.current_points[-1][1]))
    for i in range(0, len(self.current_points) - 1):
        line = Line(Point(self.current_points[i][0], self.current_points[i][1]),
                    Point(self.current_points[i + 1][0], self.current_points[i + 1][1]))
        cross_point = self.get_cross_point(l0, line)  # 获取相交点
        if self.is_in_two_segment(cross_point, l0, line):  # 如果相交点在两个线段上
            cross_point_dict.update({i: cross_point})
    flag_dict = {}  # 保存剪下点的信息
    cross_points_list = sorted(cross_point_dict.items(), key=lambda item: item[0], reverse=True)  # [(3,P),(1,P)]
    for cross_point_info in cross_points_list:
        cross_i, cross_point = cross_point_info[0], cross_point_info[1]
        if flag_dict:  # 对应需要剪下多个多边形的情形,
            points = self.current_points[cross_i + 1:flag_dict['index'] + 1]
            points.append((flag_dict['point'].x, flag_dict['point'].y))
            points.append((cross_point.x, cross_point.y))
            p = Polygon(points)
            self.sd_polygons.append(p)
        else:
            points = self.current_points[cross_i + 1:]
            points.append((cross_point.x, cross_point.y))
            p = Polygon(points)
            self.sd_polygons.append(p)  # 将生成的polygon保存
        flag_dict.update(index=cross_i, point=cross_point)
    if flag_dict:
        point_list = self.current_points[:flag_dict['index'] + 1]  # 还未围成多边形的数组
        point_list.append((flag_dict['point'].x, flag_dict['point'].y))  # 加上交点
        self.current_points = point_list
    self.current_points.append(point)

  def is_in_segment(self, point, point1, point2):
    """
    交点是否在线段上
    :param point:(x,y)
    :param point1:[(x1,y1),(x2,y2)]
    :param point2:
    :return:
    """
    if point1.x > point2.x:
        minx = point2.x
        maxx = point1.x
    else:
        minx = point1.x
        maxx = point2.x
    if point1.y > point2.y:
        miny = point2.y
        maxy = point1.y
    else:
        miny = point1.y
        maxy = point2.y
    if minx <= point.x <= maxx and miny <= point.y <= maxy:
        return True
    return False

  def is_in_two_segment(self, point, l1, l2):
    """
    点 是否在两段 线段中间
    :param point:
    :param l1:
    :param l2:
    :return:
    """

    def is_same_point(p1, p2):
        """
        判断点是否相同
        :param p1:
        :param p2:
        :return:
        """
        if abs(p1.x - p2.x) < 0.1 and abs(p1.y - p2.y) < 0.1:
            return True
        return False

    if self.is_in_segment(point, l1.p1, l1.p2) and self.is_in_segment(point, l2.p1, l2.p2):
        if (is_same_point(point, l1.p1) or is_same_point(point, l1.p2)) and (
                    is_same_point(point, l2.p1) or is_same_point(point, l2.p2)):
            # 判断是否在两条线段的端点上
            return False
        return True
    return False

  def get_line_para(self, line):
    """
    规整line
    :param line:
    :return:
    """
    line.a = line.p1.y - line.p2.y
    line.b = line.p2.x - line.p1.x
    line.c = line.p1.x * line.p2.y - line.p2.x * line.p1.y

  def get_cross_point(self, l1, l2):
    """
    得到交点
    :param l1: 直线Line
    :param l2:
    :return: 交点坐标Point
    """
    self.get_line_para(l1)
    self.get_line_para(l2)
    d = l1.a * l2.b - l2.a * l1.b
    p = Point()
    if d == 0:
        return p
    p.x = ((l1.b * l2.c - l2.b * l1.c) // d)
    p.y = ((l1.c * l2.a - l2.c * l1.a) // d)
    return p

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