查找2D网格中的所有循环/封闭形状

时间:2017-07-07 21:07:06

标签: c# algorithm grid 2d

我有一个"无限" 2D网格,我想检测关闭/完整"结构" - 任何形状的区域,四周都是封闭的。但是,我需要确定每个闭路 - 包括更大的形状,如果有的话。

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

例如,给出以下两个"完成"结构:

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,但他们都基于已经知道哪些节点连接到其他节点。我已经玩弄了识别每个人的逻辑,并且#34; line"我可以继续从那里开始,但感觉多余。

6 个答案:

答案 0 :(得分:4)

我会这样做:

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
    

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

  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. 选择1附近的所有3

    这是你的电路。如果您在填写#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. 使用3 填充2

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

     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
  7. 循环#2

  8. 将所有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
    

答案 1 :(得分:2)

根据问题的图论理论,您可以将地图的每个0解释为一个节点,相邻的0与边连接。听起来你想要做的就是计算这个图的连通分量(也许可以通过1个值连接它们,找到相同结构的'相邻房间')

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

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

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

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

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)

我有一个类似的问题,试图在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'。

实施

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

答案 5 :(得分:0)

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

在发布任何答案之前花了几天我试图找到一个解决方案并且相信我找到了一个能够满足我需求的解决方案。

因为我总是有一个起点,所以我从那一点走过边缘,每次路径“分支”时都会分叉访问点列表 - 允许我找到多个循环。

给定一个单元格中包含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. 使用邻居点
    3. 调用步骤1
  3. 克隆允许每个“分支”成为一个没有混合点的独特循环。

    我没有进行任何性能分析,但考虑到我抛出的示例,它的效果非常好。

    可以给我两个周期的副本。例如,如果我从NW角开始,东部和南部的单元都有有效的路径可供遵循。它们都被视为新路径并被跟踪,但它们只是同一周期的镜像。现在,我只修剪这些周期 - 只要你忽略它们的顺序,它们就有完全相同的点。

    还有一些过滤涉及 - 比如问题#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(包括一些不相关的图形工具)