自相交多边形的面积

时间:2012-04-06 21:27:20

标签: algorithm svg 2d polygon

计算简单不规则多边形is trivial的面积。但是,请考虑左下方显示的自相交多边形ABCDEF:

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

如果我们使用上面的链接式公式遍历多边形顺序中的点,我们得到的区域为0.('顺时针'区域取消'逆时针'区域。)

但是,如果我们sort the points radially around a center并计算面积,我们会在右上方得到多边形ABEDCF的错误区域。

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

在调查我的this question解决方案的边缘情况时出现了这个问题。

定义区域

我将'area'定义为使用“非零”或“偶数”规则填充多边形时可见的像素数量。我会接受其中任何一个的答案,尽管两者都会更好。请注意,我明确地 not 定义了自重叠的区域,以便对重叠区域进行两次计数。

enter image description here

3 个答案:

答案 0 :(得分:7)

您可以使用Bentley–Ottmann

中的以下伪代码尝试this page

Bentley-Ottmann算法

Bentley-Ottmann算法的输入是线段Li的集合OMEGA = {Li},其输出将是交集点的集合LAMBDA = {Ij}。该算法被称为"扫描线算法"因为它的操作可以被视为具有另一条线,一条扫描线" SL,扫过OMEGA收集品并收集信息,因为它通过各个段Li。为SL的每个位置收集的信息基本上是OMEGA中当前与SL相交的所有段的有序列表。维护此信息的数据结构通常也称为"扫描线"。此类结构还会在发现交叉点时检测并输出交叉点。它发现交叉点的过程是算法的核心及其效率。

为了实现扫描逻辑,我们必须首先对OMEGA的线段进行线性排序,以确定SL遇到它们的顺序。也就是说,我们需要对所有段Li的端点{Ei0,Ei1} i = 1,n进行排序,以便我们可以检测SL何时开始和停止与OMEGA的每个段相交。传统上,端点是通过先增加x然后增加y坐标值来排序的,但任何线性顺序都会这样做(某些作者更喜欢首先减小y然后增加x)。使用传统的排序方式,扫描线是垂直的,并在遇到每个线段时从左向右移动,如图所示:

PIC-扫掠迹线

在算法中的任何一点,扫描线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或红黑树),这保证了这些操作从n开始最多需要O(log-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;
}

此例程输出所有交叉点的完整有序列表。

答案 1 :(得分:3)

这是我的想法,我假设你的多边形上没有洞,洞会更复杂,你应该首先从你的多边形中删除洞:

  1. 首先找到多边形的凸包,为此你需要找到多边形顶点的凸包。并计算凸包区域。

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

  3. 您应该从凸包中减去不属于原始多边形的额外多边形,以找到您的多边形区域,将它们命名为 badpoly badpolys 总是在凸包上至少有一个边框,这样这个边框不属于原始多边形,将它们命名为 badborder ,通过迭代凸包,你可以找到所有 badborders ,但是为了找到 badpoly 的其他边框,下一个连接边框到给定的 badborder ,它相对于 badborder 是 badpoly 的边框之一,您可以继续此查找 badpoly 的所有边框,然后对其区域进行计算,同样通过重复此方式可以计算区域所有 badpolys

答案 2 :(得分:-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