合并相邻的多边形

8
我有一对(封闭的)多边形,每个多边形都由一系列点(顶点)定义。这些多边形分别代表着一块土地,被一条小河隔开,因此河流在两个多边形之间形成了一个狭窄的缝隙。
我正在寻找一种算法来识别并消除这个缝隙,将两个多边形连接成一个连通的多边形。
下面的图示是一个例子,其中原始的多边形是绿色和红色的,结果多边形显示为黄色。
迄今为止,我已经能够做到以下几点:
- 对于多边形A中的每条边,找到多边形B中最近的顶点。 - 找到所有在一定距离内的多边形B的顶点,这些顶点与多边形A相邻。
但我不太确定接下来该怎么做。

需要一个更复杂的例子。例如,想象一下将黄色块滑动到红色块上,这样你就有了两个“脚”靠在一个表面上。它们会连接起来吗?还是只有在整个连接处很窄的情况下才会连接?如果它们连接在一起,你会留下一个中间的洞,还是一个大的连接块? - user1676075
@user1676075:我认为需要一些阈值的规定。例如,即使在上面的图中,黄色只是将它们连接起来的一个可能结果。使用更严格的阈值,“河口北部”可能太宽了,因此连接会更加“南”,导致结果多边形中出现凹痕。 - brianmearns
你可以缝合顶点。 - Khaled.K
5个回答

8

为了完整性,我想分享我的解决方案,这是一个基于Retsam的被接受的答案和David Eisenstat在评论中提出的想法的python实现。该方法是将凸包上连接到原多边形上相同顶点的边替换为来自该多边形的中间顶点。

def joinPolygons(polya, polyb):
    """
    Generate and return a single connected polygon which includes the two given
    polygons. The connection between the two polygons is based on the convex hull
    of the composite polygon. All polygons are sequences of two-tuples giving the
    vertices of the polygon as (x, y), in order. That means vertices that are adjacent
    in the sequence are adjacent in the polygon (connected by an edge). The first and
    last vertices in the sequence are also connected by any edge (implicitly closed, do
    not duplicate the first point at the end of the sequence to close it).

    Only simple polygons are supported (no self-intersection).
    """

    #Just to make it easier to identify and access by index.
    polygons = [polya, polyb]

    #Create a single list of points to create the convex hull for (each
    # point is a vertex of one of the polygons).
    #Additionally, each point includes some additional "cargo", indicating which
    # polygon it's from, and it's index into that polygon
    # This assumes the implementation of convexHull simply ignores everything
    # beyond the first two elements of each vertex.
    composite = []
    for i in range(len(polygons)):
        points = polygons[i]
        composite += [(points[j][0], points[j][1], j, i) for j in xrange(len(points))]

    #Get the convex hull of the two polygons together.
    ch = convexHull(composite)

    #Now we're going to walk along the convex hull and find edges that connect two vertices
    # from the same source polygon. We then replace that edge with all the intervening edges
    # from that source polygon.

    #Start with the first vertex in the CH.
    x, y, last_vnum, last_pnum = ch[0]

    #Here is where we will collect the vertices for our resulting polygon, starting with the
    # first vertex on the CH (all the vertices on the CH will end up in the result, plus some
    # additional vertices from the original polygons).
    results = [(x, y)]

    #The vertices of the convex hull will always walk in a particular direction around each
    # polygon (i.e., forwards in the sequence of vertices, or backwards). We will use this
    # to keep track of which way they go.
    directions = [None for poly in polygons]

    #Iterate over all the remaining points in the CH, and then back to the first point to
    # close it.
    for x, y, vnum, pnum in list(ch[1:]) + [ch[0]]:

        #If this vertex came from the same original polygon as the last one, we need to
        # replace the edge between them with all the intervening edges from that polygon.
        if pnum == last_pnum:

            #Number of vertices in the polygon
            vcount = len(polygons[pnum])

            #If an edge of the convex hull connects the first and last vertex of the polygon,
            # then the CH edge must also be an edge of the polygon, because the two vertices are
            # adjacent in the original polygon. Therefore, if the convex
            # hull goes from the first vertex to the last in a single edge, then it's walking
            # backwards around the polygon. Likewise, if it goes from the last to the first in 
            # a single edge, it's walking forwards.
            if directions[pnum] is None:
                if last_vnum < vnum:
                    if last_vnum == 0 and vnum == vcount - 1:
                        direction = -1
                    else:
                        direction = 1
                else:
                    if last_vnum == vcount - 1 and vnum == 0:
                        direction = 1
                    else:
                        direction = -1
                directions[pnum] = direction
            else:
                direction = directions[pnum]

            #Now walk from the previous vertex to the current one on the source
            # polygon, and add all the intevening vertices (as well as the current one
            # from the CH) onto the result.
            v = last_vnum
            while v != vnum:
                v += direction
                if v >= vcount:
                    v = 0
                elif v == -1:
                    v = vcount - 1
                results.append(polygons[pnum][v])

        #This vertex on the CH is from a different polygon originally than the previous
        # vertex, so we just leave them connected.
        else:
            results.append((x, y))

        #Remember this vertex for next time.
        last_vnum = vnum
        last_pnum = pnum

    return results



def convexHull(points, leftMostVert=None):
    """
    Returns a new polygon which is the convex hull of the given polygon.

    :param: leftMostVert    The index into points of the left most vertex in the polygon.
                            If you don't know what it is, pass None and we will figure it
                            out ourselves.
    """
    point_count = len(points)

    #This is implemented using the simple Jarvis march "gift wrapping" algorithm.
    # Generically, to find the next point on the convex hull, we find the point
    # which has the smallest clockwise-angle from the previous edge, around the
    # last point. We start with the left-most point and a virtual vertical edge
    # leading to it.

    #If the left-most vertex wasn't specified, find it ourselves.
    if leftMostVert is None:
        minx = points[0][0]
        leftMostVert = 0
        for i in xrange(1, point_count):
            x = points[i][0]
            if x < minx:
                minx = x
                leftMostVert = i

    #This is where we will build up the vertices we want to include in the hull.
    # They are stored as indices into the sequence `points`.
    sel_verts = [leftMostVert]

    #This is information we need about the "last point" and "last edge" in order to find
    # the next point. We start with the left-most point and a pretend vertical edge.

    #The index into `points` of the last point.
    sidx = leftMostVert

    #The actual coordinates (x,y) of the last point.
    spt = points[sidx]

    #The vector of the previous edge.
    # Vectors are joined tail to tail to measure angle, so it
    # starts at the last point and points towards the previous point.
    last_vect = (0, -1, 0)
    last_mag = 1.0

    #Constant
    twopi = 2.0*math.pi

    #A range object to iterate over the vertex numbers.
    vert_nums = range(point_count)

    #A list of indices of points which have been determined to be colinear with
    # another point and a selected vertex on the CH, and which are not endpoints
    # of the line segment. These points are necessarily not vertices of the convex
    # hull: at best they are internal to one of its edges.
    colinear = []

    #Keep going till we come back around to the first (left-most) point.
    while True:
        #Try all other end points, find the one with the smallest CW angle.
        min_angle = None
        for i in vert_nums:

            #Skip the following points:
            # -The last vertex (sidx)
            # -The second to last vertex (sel_verts[-2]), that would just backtrack along
            #  the edge we just created.
            # -Any points which are determined to be colinear and internal (indices in `colinear`).
            if i == sidx or (len(sel_verts) > 1 and i == sel_verts[-2]) or i in colinear:
                continue

            #The point to test (x,y)
            pt = points[i]

            #vector from current point to test point.
            vect = (pt[0] - spt[0], pt[1] - spt[1], 0)
            mag = math.sqrt(vect[0]*vect[0] + vect[1]*vect[1])

            #Now find clockwise angle between the two vectors. Start by
            # finding the smallest angle between them, using the dot product.
            # Then use cross product and right-hand rule to determine if that
            # angle is clockwise or counter-clockwise, and adjust accordingly.

            #dot product of the two vectors.
            dp = last_vect[0]*vect[0] + last_vect[1]*vect[1]
            cos_theta = dp / (last_mag * mag)

            #Ensure fp erros don't become domain errors.
            if cos_theta > 1.0:
                cos_theta = 1.0
            elif cos_theta < -1.0:
                cos_theta = -1.0

            #Smaller of the two angles between them.
            theta = math.acos(cos_theta)

            #Take cross product of last vector by test vector.
            # Except we know that Z components in both input vectors are 0,
            # So the X and Y components of the resulting vector will be 0. Plus,
            # we only care aboue the Z component of the result.
            cpz = last_vect[0]*vect[1] - last_vect[1]*vect[0]

            #Assume initially that angle between the vectors is clock-wise.
            cwangle = theta
            #If the cross product points up out of the plane (positive Z),
            # then the angle is actually counter-clockwise.
            if cpz > 0:
                cwangle = twopi - theta

            #If this point has a smaller angle than the others we've considered,
            # choose it as the new candidate.
            if min_angle is None or cwangle < min_angle:
                min_angle = cwangle
                next_vert = i
                next_mvect = vect
                next_mag = mag
                next_pt = pt

            #If the angles are the same, then they are colinear with the last vertex. We want
            # to pick the one which is furthest from the vertex, and put all other colinear points
            # into the list so we can skip them in the future (this isn't just an optimization, it
            # appears to be necessary, otherwise we will pick one of the other colinear points as
            # the next vertex, which is incorrect).
            #Note: This is fine even if this turns out to be the next edge of the CH (i.e., we find
            # a point with a smaller angle): any point with is internal-colinear will not be a vertex
            # of the CH.
            elif cwangle == min_angle:
                if mag > next_mag:
                    #This one is further from the last vertex, so keep it as the candidate, and put the
                    # other colinear point in the list.
                    colinear.append(next_vert)
                    min_angle = cwangle
                    next_vert = i
                    next_mvect = vect
                    next_mag = mag
                    next_pt = pt
                else:
                    #This one is closer to the last vertex than the current candidate, so just keep that
                    # as the candidate, and put this in the list.
                    colinear.append(i)

        #We've found the next vertex on the CH.
        # If it's the first vertex again, then we're done.
        if next_vert == leftMostVert:
            break
        else:
            #Otherwise, add it to the list of vertices, and mark it as the
            # last vertex.
            sel_verts.append(next_vert)
            sidx = next_vert
            spt = next_pt
            last_vect = (-next_mvect[0], -next_mvect[1])
            last_mag = next_mag

    #Now we have a list of vertices into points, but we really want a list of points, so
    # create that and return it.
    return tuple(points[i] for i in sel_verts)

6
你可以考虑修改凸包算法。凸包算法将一组点绘制成包含这些点的最小凸形状。你的问题几乎是一个凸包问题,除了顶部的凹形区域。简单地使用凸包算法会给你这个结果,它很接近,但不完全符合你的需求(请注意不同的棕色区域)。

Convex Hull

根据您的需求,凸包可能已经“足够好了”,但如果不行,您仍然可以修改算法以忽略非凸部分,并简单地合并两个多边形。具体而言,this pdf 显示了如何合并两个凸包,这与您所尝试的非常相似。

谢谢你的想法。我曾考虑过使用凸包,但我正在处理的一些形状非常凹,凸包会偏差很大。但也许我可以像你建议的那样,将CH算法作为起点。 - brianmearns
是的,具体来说,我会建议你研究“合并”或“分而治之”算法,因为它专门处理合并现有的外壳。 - Retsam
2
如果您采用凸包并将连接同一多边形顶点的边替换为原始边序列,则会得到黄色区域。我不确定这种方法的所有角落案例是什么。 - David Eisenstat
@sh1ftst0rm 更确切地说,取两个多边形及其两个关键支持线所形成的平面直线图形的无限面。 - David Eisenstat
1
@DavidEisenstat:你替换连接同一多边形顶点的CH边的想法非常好。如果你想把它发表为答案,我很乐意点赞。 - brianmearns
考虑一下阿尔法壳和相关形状。(https://en.wikipedia.org/wiki/Alpha_shape) 即使您现在不需要它们,思考它们也是有趣的,而且将来可能对您有用。(这是一个非常古老的帖子,但是嘿,它目前在计算几何问题的列表中排名很高。) - Rethunk

1
使用Boost.Geometry,您可以使用正距离boost::geometry::strategy::buffer::distance_symmetric<double>{r}调用buffer,该距离足够大以弥合绿色和红色多边形之间的差距,然后再调用buffer并使用boost::geometry::strategy::buffer::distance_symmetric<double>{-r}。如果第二次调用产生了伪影,请将一个非常小的数字添加到r中,例如boost::geometry::strategy::buffer::distance_symmetric<double>{-r+0.001}

如果我们需要自动化这个过程,我们该如何找出r的值? - Jerin Mathew

1
你可以尝试使用形态学运算。具体来说,您可以尝试进行膨胀,然后是腐蚀(也被称为形态学的"闭合")。通过像素n进行扩张 - 其中n大于河流的宽度 - 会合并形状。随后进行的腐蚀操作将撤销对其余图案造成的大部分损害。它可能不完美(它确实会完美地组合两个形状,但会牺牲一些整体形状的柔和效果),但也许通过总体操作的结果,您可以找到纠正它的方法。

通常这些形态学运算是在位图上执行,而不是在多边形上执行。但在多边形的角落上运行简单操作可能有效。


这是一个有趣的想法,但我认为对于我的目的来说它会失真得太厉害。不过我总是乐于了解新事物。 - brianmearns

0

这是一种粗略但可扩展的方法。

  1. 将空间量化为大小为Z-by-Z的网格,并将每个单元格命名为其索引(i,j)。
  2. 对于每个多边形P,对于每个顶点V,将(P,V)与包含它的单元格(i,j)进行匹配。
  3. 对于每个单元格(i,j),考虑已经与之匹配的多边形-顶点对(P_k,V_k),k = 1...K的集合。仅当P_a和P_b不是同一个多边形且V_a和V_b是这些对中最接近的时,才合并多边形P_a和P_b的顶点V_a和V_b。重复合并顶点,直到没有更多的顶点可以合并。合并后的顶点得到源顶点位置的平均值。

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