为绘图排序多边形的点

时间:2015-02-08 01:06:55

标签: algorithm sorting math polygon

我有一个矩阵(0表示什么都没有,1表示地形),表示我游戏中的关卡。矩阵对应于我的屏幕被分解成的网格,并指示我的地形到达的位置。

我的地形实际上由网格内每个街区角落中的4个点组成。当您有多个连接的块时,我使用合并单元算法来删除重复的点和任何内部点。结果是我最终得到了一个仅列出多边形外边缘的点列表。

为了绘制这个多边形,我需要点以某种顺序(顺时针或逆时针),使得每个点后面跟着它的相邻点。显然,第一点和最后一点必须是邻居。由于这都在网格中,我知道相邻点之间的确切距离。

问题在于我无法提出一种算法,可以让我走路"围绕多边形的边缘,同时按顺序放置点。我相信应该有一种方法来利用我有矩阵表示几何的事实,这意味着只有一种可能的方法来绘制多边形(即使它是凹的)。

我尝试了几种使用贪婪算法的方法,但似乎无法在每种情况下找到我想要进入哪个方向的方法。鉴于任何特定点最多可以有3个邻居(第四个不包括在内,因为它是"起点"点,这意味着我已经对它进行了排序)我需要一种了解移动方式的方法。

更新

我一直在尝试的另一种方法是用X(用Y的决胜局)对点进行排序,这给了我最上面/最左边的边缘。它还保证我在外围开始。但是,我仍然在努力寻找能够保证我不会越过外面的算法。

以下是一个示例矩阵:

0 0 0 0 0 0 0 0 0 0

0 0 0 1 1 1 0 0 0 0

0 0 0 0 0 1 0 0 0 0

0 0 0 0 0 1 1 1 0 0

这对应于此(黑点代表我的观点): Polygon

4 个答案:

答案 0 :(得分:3)

首先请考虑对于一般矩阵,输出可以由多个闭环组成;例如矩阵的边界

multi-boundary example

形成三个不同的循环,其中一个放在另一个循环中。

要提取这些循环,第一步是构建所有“墙”的地图:每当一个单元格的内容与同一行中的下一个单元格不同时,您就会有一个垂直墙;当内容与下一行中的同一个单元格不同时,你会有一个水平墙。

data = [[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
        [ 0, 1, 1, 1, 1, 0, 0, 0, 0, 0 ],
        [ 0, 1, 0, 0, 1, 0, 1, 1, 0, 0 ],
        [ 0, 1, 0, 0, 1, 0, 1, 1, 1, 0 ],
        [ 0, 1, 1, 1, 1, 0, 0, 1, 1, 0 ],
        [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]]

rows = len(data)
cols = len(data[0])

walls = [[2*(data[r][c] != data[r][c+1]) + (data[r][c] != data[r+1][c])
          for c in range(cols-1)]
         for r in range(rows-1)]

在上面的示例中,我使用两个位:0x01标记水平墙,0x02标记垂直墙。对于给定的(r, c)单元格,墙壁是单元格的右壁和底壁。

为简单起见,我还假设有趣的区域没有触及矩阵的极限;这可以通过添加额外的行和零列来解决,或者通过将矩阵访问包装在函数中来解决,该函数对于矩阵外虚拟元素返回0。

要构建边界列表,只需从墙上的任意点开始并移动到墙后,在处理时从地图上移除墙。当你不能再移动时,一个周期已经完成(你可以保证完成周期,因为在这种方式从内部/外部标志矩阵构建的图形中,保证度数在所有顶点都是均匀的。)

使用奇偶填充规则同时填充所有这些循环也可保证重现原始矩阵。

在下面的代码中,我使用rc作为行/列索引,而使用ij来表示边界上的点...小区(r=3, c=2)的示例是架构:

coordinates

其中红色墙对应于位0x02,绿色墙对应位0x01walls矩阵比原始数据矩阵少一行一列,因为假设最后一行或列上没有墙。

result = []
for r in range(rows-1):
    for c in range(cols-1):
        if walls[r][c] & 1:
            i, j = r+1, c
            cycle = [(i, j)]
            while True:
                if i < rows-1 and walls[i][j-1] & 2:
                    ii, jj = i+1, j
                    walls[i][j-1] -= 2
                elif i > 0 and walls[i-1][j-1] & 2:
                    ii, jj = i-1, j
                    walls[i-1][j-1] -= 2
                elif j < cols-1 and walls[i-1][j] & 1:
                    ii, jj = i, j+1
                    walls[i-1][j] -= 1
                elif j > 0 and walls[i-1][j-1] & 1:
                    ii, jj = i, j-1
                    walls[i-1][j-1] -= 1
                else:
                    break
                i, j = ii, jj
                cycle.append((ii, jj))
            result.append(cycle)

基本上,代码从边界上的一个点开始,并检查它是否可以在墙上向上,向下,向左或向右移动。当它不能再移动时,循环已经完成,可以添加到最终结果中。

算法的复杂性是O(行* cols),即它与输入大小成正比,并且它是最佳的(在大O意义上),因为如果不至少读取输入,就无法计算结果。这很容易看到,因为while的主体输入的次数不能超过地图中墙的总数(在每次迭代时都会移除墙)。

修改

可以修改算法,只生成简单的循环(即每个顶点只访问一次的路径)。

result = []
index = [[-1] * cols for x in range(rows)]
for r in range(rows-1):
    for c in range(cols-1):
        if walls[r][c] & 1:
            i, j = r+1, c
            cycle = [(i, j)]
            index[i][j] = 0
            while True:
                if i > 0 and walls[i-1][j-1] & 2:
                    ii, jj = i-1, j
                    walls[i-1][j-1] -= 2
                elif j > 0 and walls[i-1][j-1] & 1:
                    ii, jj = i, j-1
                    walls[i-1][j-1] -= 1
                elif i < rows-1 and walls[i][j-1] & 2:
                    ii, jj = i+1, j
                    walls[i][j-1] -= 2
                elif j < cols-1 and walls[i-1][j] & 1:
                    ii, jj = i, j+1
                    walls[i-1][j] -= 1
                else:
                    break
                i, j = ii, jj
                cycle.append((ii, jj))
                ix = index[i][j]
                if ix >= 0:
                    # closed a loop
                    result.append(cycle[ix:])
                    for i_, j_ in cycle[ix:]:
                        index[i_][j_] = -1
                    cycle = cycle[:ix+1]
                index[i][j] = len(cycle)-1

这是通过在处理中两次满足相同顶点时向输出添加单独的循环来实现的(index表存储给定i,j点当前基于0的索引正在建造周期。)

答案 1 :(得分:0)

我想有不同的方法可以做到这一点,我想当对角线连接的细胞计为不同的轮廓时,情况非常简单:

你只需要保持细胞和角落方向。例如,你是从一些地球细胞的右上角开始的(它假设是上部或右部的细胞,或者两者都不是,如果它是bourder)并且想要顺时针移动。

如果右边的单元格是地球,则将当前单元格更改为它并将角落更改为左上角(这是相同的点)。然后你去下一次迭代。

moving right if we have earth to the right

在其他情况下,如果你从某个地球电池的右上角开始,并想要顺时针方向。如果右边的单元格不是地球,则不会更改当前单元格并将角落更改为右下角(<下一步)

rotate clockwise if there is no earth to the right

所以你也有其他三个角落的对称情况,你可以进入下一次迭代直到返回起点。

behavior for other 6 starting conditions

所以这里是我编写的伪代码,它使用与图片使用相同的索引,并假设沿边界的所有单元格都是空闲的,否则你需要检查索引id是否超出范围。

我还需要额外的阵列,其尺寸与矩阵几乎相同,以标记处理过的轮廓,它需要比矩阵宽1个单元,因为我要标记垂直线,每条垂直线应该有它右边的单元格坐标。请注意,当您需要标记垂直线时,上面只有2个案例中间有8个案例。

    int mark[,] = new int[height,width+1]       
    start_i = i = 0;
    start_j = j = 0;
    direction = start_direction = top_left;
    index = 0;

    //outer cycle through different contours
    while(true)
    {
      ++index;
      //scanning for contours through all the matrix
      //continue from the same place, we stopped last time
      for(/*i = i*/; i < n; i++)
      {
        for(/*j = j*/; j < n; j++)
        {
           //if we found earth
           if(m[i,j] == 1)
           {
               //check if previous cell is nothing
               //check if line between this and previous contour doesn't already added
               if(m[i,j - 1] == 0 && mark[i,j] == 0)
               {
                   direction = bottom_left;
                   break;
               }

               //the same for next cell
               if(m[i,j + 1] == 0 && mark[i,j+1] == 0)
               {
                   direction = top_right;
                   break;
               }
           }
        }
        //break if we found contour
        if(i != start_i || j != start_j)
          break;
      }

      //stop if we didn't find any contour
      if(i == start_i && j == start_j)
      {
        break;
      }

      polygon = new polygon;
      start_i = i;
      start_j = j;
      start_direction = direction;

      //now the main part of algorithm described above
      do
      {
        if(direction == top_left)
        {
           if(n(i-1,j) == 1)
           {
              direction = bottom_left;
              position = (i-1,j)
           }
           else
           {
              direction = top_right;
              polygon.Add(i,j+1);       
           }
        }
        if(direction == top_right;)
        {
           if(n[i,j + 1] == 1)
           {
              direction = top_left;
              position = (i,j + 1)
           }
           else
           {
              direction = bottom_right;
              mark[i, j + 1] = index;//don't forget to mark edges!
              polygon.Add(i+1,j+1);       
           }
        } 
        if(direction == bottom_right;
        {
           if(n[i+1,j] == 1)
           {
              direction = top_right;
              position = (i+1,j)
           }
           else
           {
              direction = bottom_left;
              polygon.Add(i+1,j);       
           }
        } 
        if(direction == bottom_left)
        {
           if(n[i,j - 1] == 1)
           {
              direction = bottom_right;
              position = [i,j - 1]
           }
           else
           {
              direction = top_left;
              mark[i, j] = index;//don't forget to mark edges!
              polygon.Add(i,j);       
           }
        } 
      //and we can stop as we reached the starting state
      }while(i != start_i || j != start_j || direction != start_direction);

      //stop if it was last cell
      if(i == n-1 && j == n- 1)
      {
        break;
      }
    }

此外,您可能需要知道哪个轮廓位于哪个内部,并且您需要一个堆叠以在扫描时保持内部轮廓,因此每次穿过现有轮廓时都需要将其添加到堆栈中如果它已经在堆栈的顶部,则删除。 它将导致代码中的下一个更改:

  ...
  //continue from the same place, we stopped last time
  for(/*i = i*/; i < n; i++)
  {
    for(/*j = j*/; j < n; j++)
    {
       if(mark[i,j] != 0)
       {
           if(stack.top() == mark [i,j])
           {
                stack.pop();
           }
           else
           {
                stack.push(mark [i,j]);
           }
       }
       //if we found earth
       if(m[i,j] == 1)
       {
           ...

答案 2 :(得分:0)

这似乎对我有用:

对于每个填充的方块,检查其中哪个邻居已填满。对于那些不适合的人,将适当的边添加到边列表中。按照您的喜好顺时针或逆时针生成这些边缘。

要构建完整路径,首先从集合中拉出任何边缘并将其添加到路径中。它有一个订单,所以看看第二个顶点。使用与第二个顶点相等的第一个顶点查找集合中的边。从集合中拉出该边缘并将其添加到路径中。继续,直到路径关闭。

重复以生成路径列表。一个简单的多边形应该最终成为一条路径。一个复杂的多边形 - 在这种情况下在中间有一个孔 - 将是几个。

答案 3 :(得分:0)

如果您的矩阵可以包含随机模式,那么答案远比看上去复杂得多。

一方面,它们可能是任意数量的不同多边形,每一个都可能是空心的。

此外,找到一个区域的轮廓(即使没有孔)对于绘制表面几乎没有帮助。您的GPU最终将需要三角形,这意味着您需要将多边形分解为矩形。

找到一组空心方块的最佳分解(即将覆盖它们的最小矩形集)是一个很好的NP完全问题,没有已知的解决方案。

存在一些算法来找到没有孔的这种形状的最佳分解,但它们非常复杂。

贪婪算法更容易实现,通常可以产生可接受的结果。

所以我会对你的矩阵进行贪婪的搜索,收集矩形,直到访问了所有“1”值。将这些矩形转换为坐标应该很容易,因为您确切知道左上角和右下角的位置。

贪婪的扫描看起来像:

while your matrix is not empty
    move to first "1" value. This is the rectangle top left corner
    from this corner, extend the rectangle in x and y to maximize its surface
    store the rectangle in a list and clear all corresponding "1" values