【问题标题】:Different Methods of Performing FloodFill执行洪水填充的不同方法
【发布时间】:2011-10-18 09:11:42
【问题描述】:

好的,我有几种不同的方法来执行 FloodFill。所有这些都会导致问题。我将列出 3 种方法并解释每种方法会发生什么。如果有人能给我一些建议,那就太好了。我看过一些类似的帖子,但没有一篇是针对 C#、java 或 VB.net(我知道的唯一语言)的。

为此,我有一个名为 PixelData 的类,它将颜色存储在 CellColor 成员变量中。我有一个大小为 50x50 的 PixelData 对象数组,称为“像素”。我还有一个名为 CANVAS_SIZE 的常量,在这种情况下为 50。以下是我尝试使用的三种方法。

这是递归的。它非常容易发生堆栈溢出。在此方法完成后,我尝试设置一个启用 CanFill 成员的计时器。这仍然不能防止溢出:

private void FloodFill(Point node, Color targetColor, Color replaceColor)
{
  //perform bounds checking X
  if ((node.X >= CANVAS_SIZE) || (node.X < 0))
    return; //outside of bounds

  //perform bounds checking Y
  if ((node.Y >= CANVAS_SIZE) || (node.Y < 0))
    return; //ouside of bounds

  //check to see if the node is the target color
  if (pixels[node.X, node.Y].CellColor != targetColor)
    return; //return and do nothing
  else
  {
    pixels[node.X, node.Y].CellColor = replaceColor;

    //recurse
    //try to fill one step to the right
    FloodFill(new Point(node.X + 1, node.Y), targetColor, replaceColor);
    //try to fill one step to the left
    FloodFill(new Point(node.X - 1, node.Y), targetColor, replaceColor);
    //try to fill one step to the north
    FloodFill(new Point(node.X, node.Y - 1), targetColor, replaceColor);
    //try to fill one step to the south
    FloodFill(new Point(node.X, node.Y + 1), targetColor, replaceColor);

    //exit method
    return;
  }
}

接下来我有一个使用基于队列的填充的方法。此方法在运行时会导致 OutOfMemory 异常,并且在填充整个画布时非常慢。如果只是填充画布的一小部分,它会有些效果:

private void QueueFloodFill(Point node, Color targetColor, Color replaceColor)
{
  Queue<Point> points = new Queue<Point>();
  if (pixels[node.X, node.Y].CellColor != targetColor)
    return;

  points.Enqueue(node);

  while (points.Count > 0)
  {
    Point n = points.Dequeue();
    if (pixels[n.X, n.Y].CellColor == targetColor)
      pixels[n.X, n.Y].CellColor = replaceColor;

    if (n.X != 0)
    {
      if (pixels[n.X - 1, n.Y].CellColor == targetColor)
        points.Enqueue(new Point(n.X - 1, n.Y));
    }

    if (n.X != CANVAS_SIZE - 1)
    {
      if (pixels[n.X + 1, n.Y].CellColor == targetColor)
        points.Enqueue(new Point(n.X + 1, n.Y));
    }

    if (n.Y != 0)
    {
      if (pixels[n.X, n.Y - 1].CellColor == targetColor)
        points.Enqueue(new Point(n.X, n.Y - 1));
    }

    if (n.Y != CANVAS_SIZE - 1)
    {
      if (pixels[n.X, n.Y + 1].CellColor == targetColor)
        points.Enqueue(new Point(n.X, n.Y + 1));
    }
  }
  DrawCanvas();
  return;
}

我尝试过的最后一种方法也使用基于队列的填充。这种方法比以前基于队列的洪水填充要快得多,但最终也会在运行时导致 OutOfMemory 异常。同样,我尝试设置一个 FillDelay 计时器,以防止用户快速单击,但这仍然不能阻止异常的发生。这个问题的另一个错误是它很难正确填充小区域。在我让它不崩溃之前,我认为修复它没有任何意义。

private void RevisedQueueFloodFill(Point node, Color targetColor, Color replaceColor)
{
  Queue<Point> q = new Queue<Point>();
  if (pixels[node.X, node.Y].CellColor != targetColor)
    return;

  q.Enqueue(node);
  while (q.Count > 0)
  {
    Point n = q.Dequeue();
    if (pixels[n.X, n.Y].CellColor == targetColor)
    {
      Point e = n;
      Point w = n;
      while ((w.X != 0) && (pixels[w.X, w.Y].CellColor == targetColor))
      {
        pixels[w.X, w.Y].CellColor = replaceColor;
        w = new Point(w.X - 1, w.Y);
      }

      while ((e.X != CANVAS_SIZE - 1) && (pixels[e.X, e.Y].CellColor == targetColor))
      {
        pixels[e.X, e.Y].CellColor = replaceColor;
        e = new Point(e.X + 1, e.Y);
      }

      for (int i = w.X; i <= e.X; i++)
      {
        Point x = new Point(i, e.Y);
        if (e.Y + 1 != CANVAS_SIZE - 1)
        {
          if (pixels[x.X, x.Y + 1].CellColor == targetColor)
            q.Enqueue(new Point(x.X, x.Y + 1));
        }
        if (e.Y - 1 != -1)
        {
          if (pixels[x.X, x.Y - 1].CellColor == targetColor)
            q.Enqueue(new Point(x.X, x.Y - 1));
        }
      }
    }
  }
}

感谢大家的帮助!所有这些方法都是基于维基百科上的伪代码。

编辑:

我选择了 RevisedQueueFloodFill 并按照建议进行了修改,以便在循环中不声明任何变量。仍然会生成 OutOfMemory。即使有一个填充延迟计时器。

private void RevisedQueueFloodFill(Point node, Color targetColor, Color replaceColor)
{
  Queue<Point> q = new Queue<Point>();

  if (pixels[node.X, node.Y].CellColor != targetColor)
    return;

  q.Enqueue(node);

  Point n, e, w, x;
  while (q.Count > 0)
  {
    n = q.Dequeue();
    if (pixels[n.X, n.Y].CellColor == targetColor)
    {
      e = n;
      w = n;
      while ((w.X != 0) && (pixels[w.X, w.Y].CellColor == targetColor))
      {
        pixels[w.X, w.Y].CellColor = replaceColor;
        w = new Point(w.X - 1, w.Y);
      }

      while ((e.X != CANVAS_SIZE - 1) && (pixels[e.X, e.Y].CellColor == targetColor))
      {
        pixels[e.X, e.Y].CellColor = replaceColor;
        e = new Point(e.X + 1, e.Y);
      }

      for (int i = w.X; i <= e.X; i++)
      {
        x = new Point(i, e.Y);
        if (e.Y + 1 != CANVAS_SIZE - 1)
        {
          if (pixels[x.X, x.Y + 1].CellColor == targetColor)
            q.Enqueue(new Point(x.X, x.Y + 1));
        }
        if (e.Y - 1 != -1)
        {
          if (pixels[x.X, x.Y - 1].CellColor == targetColor)
            q.Enqueue(new Point(x.X, x.Y - 1));
        }
      }
    }
  }
}

【问题讨论】:

  • 听起来你在这里做的不仅仅是洪水填充; 50x50 显然只有 2500 像素,即使在最高颜色深度下,也只有 20k。任何洪水填充算法都不会自行耗尽内存。
  • 你用完内存的网格有多大?您已经描述了 Floodfill 中的几乎所有内容。如果需要大量内存,您可能需要完全选择不同的数据结构来表示您的网格。
  • 另外,一些有用的阅读:codeproject.com/KB/GDI-plus/floodfillincsharp.aspx
  • @Flynn1179 O_O 你用的是什么位深度?我想要那个显示器。
  • @Flynn1179 那是 18,446,744,073,709,551,616 种颜色。

标签: c# recursion queue stack-overflow flood-fill


【解决方案1】:

这里的基本算法的问题是您将多次访问排队到一个点并进行广度优先搜索。这意味着您在每次传递期间创建同一点的多个副本。这会以指数方式累积,因为每个点都可以传播(排队更多点),即使它不是目标颜色(已经被替换)。

在将它们入队的同时设置颜色(而不是在出队时),这样您就不会最终将它们添加到队列中两次。

【讨论】:

    【解决方案2】:

    我不知道这是否可行,但我自己的怀疑是,由于所有“新”运算符,可能由于算法的密集性,垃圾收集器,使用的内存比必要的多得多没有机会参与进来?

    我已经重写了算法,以便所有 Point 变量都可以被重用,但我目前还没有办法对此进行测试。

    我还擅自更改了前几行代码,但这是因为我的一个小毛病,您发现的大多数填充算法都需要用户指定目标颜色,在事实上,您可以简单地从参数中给出的点自动获取目标颜色。

    无论如何,尝试使用它,否则只是一笑置之:

    private void RevisedQueueFloodFill(Point node, Color replaceColor)
    {
        Color targetColor = pixels[node.X, node.Y].CellColor;
        if (targetColor == replaceColor) return;
    
        Queue<Point> q = new Queue<Point>();
        q.Enqueue(node);
    
        Point n, t, u;
    
        while (q.Count > 0)
        {
            n = q.Dequeue();
            if (pixels[n.X, n.Y].CellColor == targetColor)
            {
    
                t = n;
                while ((t.X > 0) && (pixels[t.X, t.Y].CellColor == targetColor))
                {
                    pixels[t.X, t.Y].CellColor = replaceColor;
                    t.X--;
                }
                int XMin = t.X + 1;
    
    
                t = n;
                t.X++;
                while ((t.X < CANVAS_SIZE - 1) &&
                       (pixels[t.X, t.Y].CellColor == targetColor))
                {
                    pixels[t.X, t.Y].CellColor = replaceColor;
                    t.X++;
                }
                int XMax = t.X - 1;
    
                t = n;
                t.Y++;
    
                u = n;
                u.Y--;
    
                for (int i = XMin; i <= XMax; i++)
                {
                    t.X = i;
                    u.X = i;
    
                    if ((t.Y < CANVAS_SIZE - 1) &&
                        (pixels[t.X, t.Y].CellColor == targetColor)) q.Enqueue(t);
    
                    if ((u.Y >= 0) &&
                        (pixels[u.X, u.Y].CellColor == targetColor)) q.Enqueue(u);
                }
            }
        }
    }
    

    【讨论】:

    • 'new' 在 .NET 中并不可怕。它的内存模型专门设计用于容纳大量的小型、短暂的对象。此外,由于 Point 是一种值类型,它甚至不需要在此处分配新内存(至少在初始化局部变量时)。您需要警惕的是 Enqueue;这里的罪魁祸首是算法,而不是语言或运行时。
    【解决方案3】:

    在第三种方法中,您应该检查当前点以西和以东的像素。而不是检查pixels[w.X, w.Y],你应该检查pixels[w.X - 1, w.Y],而不是pixels[e.X, e.Y],你应该检查pixels[e.X + 1, e.Y]。这是我对您的第三种方法的看法:

    private void RevisedQueueFloodFill(Point node, Color targetColor, Color replaceColor)
    {
        if (pixels[node.X, node.Y].CellColor != targetColor) return;
    
        Queue<Point> Q = new Queue<Point>();
        Q.Enqueue(node);
    
        while (Q.Count != 0)
        {
            Point n = Q.Dequeue();
            if (pixels[n.X, n.Y].CellColor == targetColor)
            {
                int y = n.Y;
                int w = n.X;
                int e = n.X;
                while (w > 0 && pixels[w - 1, y].CellColor == targetColor) w--;
                while (e < CANVAS_SIZE - 1 && pixels[e + 1, y].CellColor == targetColor) e++;
    
                for (int x = w; x <= e; x++)
                {
                    pixels[x, y].CellColor = replaceColor;
                    if (y > 0 && pixels[x, y - 1].CellColor == targetColor)
                    {
                        Q.Enqueue(new Point(x, y - 1));
                    }
                    if (y < CANVAS_SIZE - 1 && pixels[x, y + 1].CellColor == targetColor)
                    {
                        Q.Enqueue(new Point(x, y + 1));
                    }
                }
            }
        }
    }
    

    【讨论】:

      【解决方案4】:

      好几件事:

      1. C# 的递归限制(由堆栈大小决定)深度为几千。 这意味着您不能在不导致堆栈溢出的情况下向下递归进行 DEEPER。一旦方法返回,它的指针就会从堆栈中弹出。您的问题与 OutOfMemoryException 不同。堆栈保存指针而不是实际内存,因此并不意味着保存数千个指针。

      2. 垃圾收集是导致内存不足异常的原因。您需要停止在循环内声明变量。垃圾收集器将这些视为“仍在范围内”,并且在循环完成所有迭代之前不会释放内存空间。但是如果你使用相同的内存地址,它每次都会覆盖它,几乎不会使用任何内存。

      要明确:

      for (int i = w.X; i <= e.X; i++)
      {
          Point x = new Point(i, e.Y);
      }
      

      应该是这样的:

      Point x;
      
      for(int i = w.X; i<= e.X; i++)
      {
         x = new Point(i, e.Y);
      }
      

      这将像您希望的那样重用内存地址。

      希望有帮助!

      【讨论】:

      • 查看我上面的编辑。我还能做些什么来阻止 OutOfMemory 异常?
      • 您对内存使用情况的评论不正确。 Point 是一种值类型,在任何一种情况下都只会使用一个本地插槽。此外,即使它是一个引用类型,在每次循环迭代中分配的引用仍然会变得不可访问并且可用于收集。真正的问题是队列可以通过正确的输入增长到巨大的规模。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-01-07
      • 2014-02-16
      • 1970-01-01
      相关资源
      最近更新 更多