【问题标题】:Finding all cycles/enclosed shapes in a 2D grid在 2D 网格中查找所有循环/封闭形状
【发布时间】:2017-07-07 21:07:06
【问题描述】:

我有一个“无限的”二维网格,我想检测封闭/完整的“结构” - 任何形状的区域,这些区域被包围在所有侧面。但是,我需要识别每个单独的闭合电路 - 包括较大的形状(如果有)。

在研究这一点时,我发现了循环检测算法,但我没有看到一种干净/有效的方法来将较大的电路与较小的电路分开。

例如给定以下两个“完整”结构:

0 1 1 1 0
0 1 0 1 0
0 1 1 1 0
0 0 0 0 0
0 1 1 1 1 1
0 1 0 1 0 1
0 1 1 1 1 1

第一个是由 8 个“墙”包围的单个单元格。循环检测使得检测这一点变得微不足道。

第二个示例由示例一的两个副本组成,但它们共享一面墙。我关心三个独立的电路——左边的房间、右边的房间和整体结构。

循环算法的多次传递可能有效,但我必须确保我没有回溯已经找到的形状。

我还查看了洪水填充算法,但它似乎假设您已经知道有界区域内的一个点。对于无限的 2D 网格,如果它 不是 在有效结构中,我需要一个大小限制来强制它放弃。

是否有我遗漏的解决方案或我的想法遗漏了什么?

我只会在添加边界值时执行此“检查”。使用上面的示例,如果我更改任何 0 -> 1,则 可能 创建了一个新循环,我将运行逻辑。我不关心识别单独的结构,并且总是有一个原点坐标。

我一直在研究解决方案posted here,但它们都是基于已经知道哪些节点连接到其他节点。我已经玩弄了识别每条“线”的逻辑,我可以从那里继续前进,但感觉是多余的。

【问题讨论】:

  • 怎么无限?我的意思是,如果右边有一百万个地方有一个 1 的平方,你怎么知道?您只是存储 1 的位置吗?
  • 网格真的是无限的吗?
  • 无限,我的意思是这些“结构”可以是任何形状、合理的大小,以及“无限”2D 网格上的任何位置——在本例中是程序生成的 2D 瓦片地图。
  • 看看这篇优秀的论文:Fu Chang、Chun-Jen Chen 和 Chi-Jen Lu 的“使用轮廓跟踪技术的线性时间组件标记算法”。我想您可以通过围绕初始扫描的起始像素螺旋来使该方法适应无限网格的情况。
  • 这个问题无法回答,因为缺少这么多细节。看我两天前的评论。您甚至没有定义结构、电路等的含义。如果您计算每个独特的电路,数量将会激增。

标签: c# algorithm grid 2d


【解决方案1】:

我会这样做:

0 0 0 0 0 0 0
0 1 1 1 1 1 0
0 1 0 1 0 1 0
0 1 1 1 1 1 0
0 0 0 0 0 0 0
  1. 2填充背景

    要确定您是否在后台,只需投射光线并计算随后的 zeores。一旦找到射线长度大于电路尺寸限制的位置,您就得到了起点。

    [0]0-0-0-0-0-0
     0 1 1 1 1 1 0
     0 1 0 1 0 1 0
     0 1 1 1 1 1 0
     0 0 0 0 0 0 0
    
     2 2 2 2 2 2 2
     2 1 1 1 1 1 2
     2 1 0 1 0 1 2
     2 1 1 1 1 1 2
     2 2 2 2 2 2 2
    

    不要为此使用未绑定的递归洪水填充!!!,因为对于 "infinite" 区域,您将堆栈溢出。您可以限制递归级别,如果达到而不是递归,则将点添加到某个队列以便稍后进行进一步处理。这通常会加快速度并限制堆栈的使用...

  2. 先找到0

     2 2 2 2 2 2 2
     2 1 1 1 1 1 2
     2 1[0]1 0 1 2
     2 1 1 1 1 1 2
     2 2 2 2 2 2 2
    
  3. 3填充它

     2 2 2 2 2 2 2
     2 1 1 1 1 1 2
     2 1 3 1 0 1 2
     2 1 1 1 1 1 2
     2 2 2 2 2 2 2
    
  4. 选择3附近的所有1

    这是你的电路。如果您在填充 #3 时记得 bbox,那么您只需要扫描每边一个单元格放大的区域...选定的单元格就是您的电路。

     2 2 2 2 2 2 2
     2 * * * 1 1 2
     2 * 3 * 0 1 2
     2 * * * 1 1 2
     2 2 2 2 2 2 2
    
  5. 2填充3

    这将避免使用已处理的电路

     2 2 2 2 2 2 2
     2 1 1 1 1 1 2
     2 1 2 1 0 1 2
     2 1 1 1 1 1 2
     2 2 2 2 2 2 2
    
  6. 在找到任何 0 时循环 #2

  7. 将所有2改回0

     0 0 0 0 0 0 0
     0 1 1 1 1 1 0
     0 1 0 1 0 1 0
     0 1 1 1 1 1 0
     0 0 0 0 0 0 0
    

【讨论】:

  • OP 声明还需要找到包含两个孔的对象:“第二个示例由示例一的两个副本组成,但它们共享一堵墙。我关心三个独立的电路- 左边的房间,右边的房间,整体结构。”你能用这种方法找到外部边界吗?
  • @LeonidVasilyev 外部边界很容易,只需在子弹#2 之前选择所有1 相邻2,这将选择所有外部电路。但是有必要避免后面的重复电路......
  • 这是一种有趣的方法,但我认为它不适合我。对我来说关键是我将永远有一个起点,我知道它已经在赛道的某个地方。鉴于此,跟随路径比洪水填充更有效,部分原因是洪水填充总是会比跟随路径接触更多的单元格,特别是因为您建议进行多次填充。从列出的步骤来看,它似乎比当前的解决方案更复杂、成本更高。
  • @helion3 唯一昂贵的填充是背景,如果您更改编码,它具有与孔不同的代码,那么您不需要它。通过跟踪边缘,您可能会错过内部电路,除非所有边缘情况都得到正确处理。另一方面,您可以使用新边缘的洪水填充来获取其 bbox 以大大加快扫描过程。因此,这可以迭代地完成,其复杂性仅取决于新增电路的面积。如果您只是更改电路,则必须首先从找到的电路列表中删除旧电路。
【解决方案2】:

这是一个轮廓查找问题。

Satoshi Suzuki 和 Keiichi Abe 在他们 1985 年题为“通过边界跟踪对数字化二进制图像的拓扑结构分析”的论文中描述了一种可能的算法。这并非易事。但是你可以使用OpenCV,它的cv2.findContours()函数实现了这个算法。

如果您选择使用 OpenCV,解决方案很简单。您在其层次结构旁边提取轮廓。具有至少一个子(孔)的轮廓及其子轮廓是您正在寻找的对象。使用名为 OpenCvSharp 的托管 OpenCV 包装器的示例:

byte[,] a = new byte[7, 6]
{
    { 0, 1, 1, 1, 0, 0 },
    { 0, 1, 0, 1, 0, 0 },
    { 0, 1, 1, 1, 0, 0 },
    { 0, 0, 0, 0, 0, 0 },
    { 0, 1, 1, 1, 1, 1 },
    { 0, 1, 0, 1, 0, 1 },
    { 0, 1, 1, 1, 1, 1 }
};
// Clone the matrix if you want to keep original array unmodified.
using (var mat = new MatOfByte(a.GetLength(0), a.GetLength(1), a))
{
    // Turn 1 pixel values into 255.
    Cv2.Threshold(mat, mat, thresh: 0, maxval: 255, type: ThresholdTypes.Binary);
    // Note that in OpenCV Point.X is a matrix column index and Point.Y is a row index.
    Point[][] contours;
    HierarchyIndex[] hierarchy;
    Cv2.FindContours(mat, out contours, out hierarchy, RetrievalModes.CComp, ContourApproximationModes.ApproxNone);
    for (var i = 0; i < contours.Length; ++i)
    {
        var hasHole = hierarchy[i].Child > -1;
        if (hasHole)
        {
            var externalContour = contours[i];
            // Process external contour.
            var holeIndex = hierarchy[i].Child;
            do
            {
                var hole = contours[holeIndex];
                // Process hole.
                holeIndex = hierarchy[holeIndex].Next;
            }
            while (holeIndex > -1);
        }
    }
}

【讨论】:

    【解决方案3】:

    您可以尝试点列表并验证链接的点。

    class PointList : List<Point>
    {
        /// <summary>
        /// Adds the point to the list and checks for perimeters
        /// </summary>
        /// <param name="point"></param>
        /// <returns>Returns true if it created at least one structure</returns>
        public bool AddAndVerify(Point point)
        {
            this.Add(point);
    
            bool result = LookForPerimeter(point, point, point);
            Console.WriteLine(result);
            return result;
        }
    
        private bool LookForPerimeter(Point point, Point last, Point original)
        {
            foreach (Point linked in this.Where(p => 
                (p.X == point.X -1 && p.Y == point.Y)
                || (p.X == point.X + 1 && p.Y == point.Y)
                || (p.X == point.X && p.Y == point.Y - 1)
                || (p.X == point.X && p.Y == point.Y + 1)
            ))
            {
                if (!linked.Equals(last))
                {
                    if (linked == original) return true;
    
                    bool subResult = LookForPerimeter(linked, point, original);
                    if (subResult) return true;
                }
            }
    
            return false;
        }
    }
    

    此代码旨在作为起点,它可能存在错误并且不考虑内部没有 0 的周长

    使用示例:

    class Program
    {
        static void Main(string[] args)
        {
            PointList list = new PointList();
    
            list.AddAndVerify(new Point() { X = 0, Y = 0 }); //returns false
            list.AddAndVerify(new Point() { X = 0, Y = 1 }); //returns false
            list.AddAndVerify(new Point() { X = 0, Y = 2 }); //returns false
            list.AddAndVerify(new Point() { X = 1, Y = 2 }); //returns false
            list.AddAndVerify(new Point() { X = 2, Y = 2 }); //returns false
            list.AddAndVerify(new Point() { X = 2, Y = 1 }); //returns false
            list.AddAndVerify(new Point() { X = 2, Y = 0 }); //returns false
            list.AddAndVerify(new Point() { X = 1, Y = 0 }); //returns True
        }
    }
    

    【讨论】:

      【解决方案4】:

      从问题的图论观点来看,您可以将地图的每个 0 解释为一个节点,相邻的 0 与一条边相连。在我看来,您想要做的是计算该图的连通分量(也许它们的连通性为 1,以找到相同结构的“相邻房间”)

      如果您只想计算一次此信息,使用union-find data structure 的简单方法就足够了,您可以在每个边上应用一次union

      如果您想动态编辑地图,基于图模型的最佳方法可能是支持splitde-union 操作的动态数据结构,例如参见herehere

      【讨论】:

        【解决方案5】:

        我在尝试查找 2D 街道地图图形(以 SVG 文件形式提供)中的所有圆圈时遇到了类似的问题。正如你所说,我也找不到算法。

        我找到了以下解决方案。

        假设

        网格布局: 网格中的每个“1”都处于以下状态之一(或该状态的同态):

        1.   0      2.   0      3.   0      4.   0      5.   0      6.   1
           0 1 0       1 1 0       1 1 0       1 1 1       1 1 1       1 1 1
             0           0           1           0           1           1
        

        但只有示例 3 到 6 对连接墙有意义,因为在连接墙中,每个“1”在其附近至少有两个“1”。

        • 示例 3 表示一个角。这个角落最多可以容纳一个结构。
        • 示例 4 表示直线。它可以是零个、一个或两个结构的墙。
        • 示例 5 表示 t 型墙。它可以是零、一、二或三结构的墙。
        • 示例 6 表示交叉墙。它可以是零、一、二、三或四结构的角。

        算法

        想法

        假设上述情况,该算法的工作原理是找到一个“1”并进行深度优先搜索,以标记所有连接的“1”。仅当深度优先搜索到达起始位置或已标记位置时,才标记遍历的 '1'。

        实现

        我将在接下来的几天发布一个实现。

        【讨论】:

          【解决方案6】:

          用解释和一些代码重新发布我的解决方案。

          在发布任何答案之前花了几天时间我试图找到一个解决方案,并相信我找到了一个非常适合我需要的解决方案。

          因为我总是有一个起点,所以我从那个点开始走边缘,并在每次路径“分支”时分叉访问点列表 - 让我可以找到多个循环。

          给定一个 1 或 0 单元格中的 2D 网格:

          0 1 1 1 1 1
          0 1 0 1 0 1
          0 1 1 1 1 1
          

          从一个我已经知道是 1 的单元格开始,我开始搜索:

          1. 对于当前有效点:
            1. 将其添加到“已访问”列表中
            2. 寻找任何有效的邻居(除了我访问的最后一个点,以避免无限循环)
          2. 对于每个有效邻居:
            1. 克隆点列表,这是我们到这个新点的“轨迹”
            2. 用邻居点调用步骤 1

          克隆让每个“分支”成为一个独特的循环,没有混合点。

          我没有运行任何性能分析,但考虑到我提供的示例,它运行良好。

          可以给我一个循环的两个副本。例如,如果我从西北角开始,则东边和南边的单元格都有有效的路径可循。它们都被视为新路径并被遵循,但它们只是同一周期的镜像。现在,我只是剪掉这样的循环——只要你忽略它们的顺序,它们的点完全相同。

          还涉及到一些过滤 - 例如问题 #1 和修剪点,如果终点匹配不是我们开始的访问点。我认为这几乎是不可避免的,也没什么大不了的,但如果有一种干净的方法可以避免这种情况,我会的。在我找到它之前,我无法知道是什么“开始”了一个新的循环,所以你知道,线性时间流再次出现。

          public class CycleDetection {
              // Cache found cycles
              List<Cycle> cycles = new List<Cycle>();
          
              // Provide public readonly access to our cycle list
              public ReadOnlyCollection<Cycle> Cycles {
                  get { return new ReadOnlyCollection<Cycle>(cycles); }
              }
          
              // Steps/slopes that determine how we iterate grid points
              public Point[] Steps = new Point[] {
                  new Point(1, 0),
                  new Point(0, 1),
                  new Point(-1, 0),
                  new Point(0, -1)
              };
          
              // Cache our starting position
              Point origin;
          
              // Cache the validation function
              Func<Point, bool> validator;
          
              public CycleDetection(Point origin, Func<Point, bool> validator) {
                  this.origin = origin;
                  this.validator = validator;
          
                  this.Scan();
              }
          
              // Activate a new scan.
              public void Scan() {
                  cycles.Clear();
          
                  if (validator(origin)) {
                      Scan(new List<Point>(), origin);
                  }
              }
          
              // Add a cycle to our final list.
              // This ensures the cycle doesn't already exist (compares points, ignoring order).
              void AddCycle(Cycle cycle) {
                  // Cycles have reached some existing point in the trail, but not necessarily
                  // the exact starting point. To filter out "strands" we find the index of
                  // the actual starting point and skip points that came before it
                  var index = cycle.Points.IndexOf(cycle.Points[cycle.Points.Count - 1]);
          
                  // Make a new object with only the points forming the exact cycle
                  // If the end point is the actual starting point, this has no effect.
                  cycle = new Cycle(cycle.Points.Skip(index).ToList());
          
                  // Add unless duplicate
                  if (!cycles.Contains(cycle)) {
                      cycles.Add(cycle);
                  }
              }
          
              // Scan a new point and follow any valid new trails.
              void Scan(List<Point> trail, Point start) {
                  // Cycle completed?
                  if (trail.Contains(start)) {
                      // Add this position as the end point
                      trail.Add(start);
          
                      // Add the finished cycle
                      AddCycle(new Cycle(trail));
          
                      return;
                  }
          
                  trail.Add(start);
          
                  // Look for neighbors
                  foreach (var step in Steps) {
                      var neighbor = start + step;
          
                      // Make sure the neighbor isn't the last point we were on... that'd be an infinite loop
                      if (trail.Count >= 2 && neighbor.Equals(trail[trail.Count - 2])) {
                          continue;
                      }
          
                      // If neighbor is new and matches
                      if (validator(neighbor)) {
                          // Continue the trail with the neighbor
                          Scan(new List<Point>(trail), neighbor);
                      }
                  }
              }
          }
          

          我在这里发布了完整的源代码:https://github.com/OutpostOmni/OmniGraph(还包括一些不相关的图形工具)

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2013-11-29
            • 2021-11-26
            • 2013-10-22
            • 1970-01-01
            • 1970-01-01
            • 2010-10-07
            • 1970-01-01
            • 2020-07-03
            相关资源
            最近更新 更多