洪水填充算法

问题描述 投票:0回答:7

又到了周末,这意味着我可以开始玩我的爱好项目

我已经厌倦了手动创建测试关卡,所以我想我应该从引擎开发中休息一下,并在关卡编辑器上工作:

关卡编辑器http://gfilter.net/junk/Editor.JPG

我想在编辑器中实现洪水填充算法,它的工作方式就像在绘画程序中一样。有人知道什么技术对我有用吗?

关卡只是一个二维数组,所以它实际上可以被认为与位图相同。

谢谢!

c# algorithm flood-fill
7个回答
8
投票

维基百科的文章非常好。只要您的网格很小,几乎任何东西都可以工作。

今年秋天早些时候,我对 10 兆像素的扫描图像进行了一些洪水填充。 (问题是从复印机上扫描的书页中删除黑色边缘。)在这种情况下,只有两种颜色,所以我基本上将这个问题视为无向图中的搜索,每个像素都与其相邻像素相连四个罗盘方向。我维护了一个单独的位图来跟踪哪些像素被访问过

主要发现是

  • 不要尝试递归深度优先搜索。你真的想要一个明确的数据结构。

  • 辅助队列使用的空间比堆栈少得多。大约减少了四十倍的空间。换句话说,比起深度优先搜索,更喜欢广度优先搜索。

再次强调,这些发现仅适用于具有多个百万像素的网格。在像您问题中所示的漂亮的小网格上,任何简单的算法都应该有效。


6
投票

我们必须为学校进行编程:

1: stuff the start pixel into a queue, note its color. note it as added.
2: begin picking a pixel off the queue. If it's similar to the start pixel:
   2: put all its neighbours into the queue
      for each added pixel, note it's added. if already noted for a pixel, don't 
      add it anymore.
   3: color it with the destination color.
3: nonempty => jump back to 2
4: empty => we are finished

根据我们是进行 8 邻居还是 4 邻居,我们检查所有 8 个邻居像素,或者仅检查某个像素的左/右或上方/下方的像素。这是代码(使用 ImageJ。我删除了一些不相关的代码)。我希望这是有道理的,它是Java。有问题就问吧:

public class Uebung1_2 implements PlugInFilter, MouseListener {
    private ImageProcessor ip;
    boolean[] state;
    int[] pixels;
    Queue<Integer> nextPixels;
    int threshould;

    /**
     * adds one pixel to the next-pixel queue only if it's not
     * already added.
     */
    void addNextPixel(int p) {
        if(!state[p]) {
            nextPixels.add(p);
            state[p] = true;
        }
    }

    boolean pixelsSimilar(int color1, int color2) {
        int dr = Math.abs(((color1 >> 16) & 0xff) -
                          ((color2 >> 16) & 0xff));
        int dg = Math.abs(((color1 >>  8) & 0xff) -
                          ((color2 >>  8) & 0xff));
        int db = Math.abs(((color1 >>  0) & 0xff) -
                          ((color2 >>  0) & 0xff));
        return ((double)(dr + dg + db) / 3.0) <= threshould;
    }

    /**
     * actually does the hard work :)
     * @param x the x position from which to start filling
     * @param y the y position from which to start filling
     */
    private void doFill(int x, int y, boolean connect8) {
        // first, add the start pixel
        int width = ip.getWidth(),
            height = ip.getHeight();
        /* for 8bit, we just gonna take the median of rgb */
        Color colorC = ij.gui.Toolbar.getForegroundColor();
        int color = colorC.getRGB();
        int firstPixel = ip.get(x, y);

        // go on with the mainloop
        addNextPixel(y * width + x);
        while(!nextPixels.isEmpty()) {
            int nextPixel = nextPixels.remove();
            int pixel = pixels[nextPixel];
            if(pixelsSimilar(pixel, firstPixel)) {
                // yay it matches. put the neighbours.
                int xN = nextPixel % width,
                    yN = nextPixel / width;
                /* the three pixels above */
                if(yN - 1 >= 0) {
                    if(connect8) {
                        if(xN + 1 < width) { 
                            addNextPixel(nextPixel - width + 1);
                        }
                        if(xN - 1 >= 0) {
                            addNextPixel(nextPixel - width - 1);
                        }
                    }
                    addNextPixel(nextPixel - width);
                }

                /* pixels left and right from the current one */
                if(xN > 0) {
                    addNextPixel(nextPixel - 1);
                }
                if(xN + 1 < width) {
                    addNextPixel(nextPixel + 1);
                }

                /* three pixels below */
                if(yN + 1 < height) {
                    if(connect8) {
                        if(xN + 1 < width) { 
                            addNextPixel(nextPixel + width + 1);
                        }
                        if(xN - 1 >= 0) {
                            addNextPixel(nextPixel + width - 1);
                        }
                    }
                    addNextPixel(nextPixel + width);
                }

                /* color it finally */
                pixels[nextPixel] = color;
            }
        }
    }

    @Override
    public void run(ImageProcessor ip) {
        ij.WindowManager.getCurrentImage().getCanvas().addMouseListener(this);
        this.ip = ip;
        this.pixels = (int[])ip.getPixels();
        this.state = new boolean[ip.getPixelCount()];
        this.nextPixels = new LinkedList<Integer>();
    }

    @Override
    public int setup(String arg0, ImagePlus arg1) {
        return DOES_RGB;
    }

    @Override
    public void mouseClicked(MouseEvent e) {
        ij.WindowManager.getCurrentWindow().getCanvas().removeMouseListener(this);
        ij.gui.GenericDialog g = new GenericDialog("Please enter parameters");
        g.addChoice("connection", new String[]{"4-connect", "8-connect"}, "8-connect");
        g.addNumericField("Threshould (0..255)", 0.0, 3);
        g.showDialog();

        boolean connect8 = g.getNextChoice().equals("8-connect");
        threshould = (int) g.getNextNumber();
        doFill(e.getX(), e.getY(), connect8);
        ij.WindowManager.getCurrentImage().draw();
    }
}


5
投票

维基百科在其 Flood fill 文章中提供了一些不同洪水填充技术的伪代码示例。您选择哪种技术取决于应用。

请记住,洪水填充可以轻松地进行线程化(类似于快速排序)。


4
投票

功能简单,无需检查色差

使用:

var img = Image.FromFile("test.png");
img = img.FloodFill(new Point(0, 0), Color.Red);
img.Save("testcomplete.png", ImageFormat.Png);

主要功能:

    public static Image FloodFill(this Image img, Point pt, Color color)
    {
        Stack<Point> pixels = new Stack<Point>();
        var targetColor = ((Bitmap)img).GetPixel(pt.X, pt.Y);
        pixels.Push(pt);

        while (pixels.Count > 0)
        {
            Point a = pixels.Pop();
            if (a.X < img.Width && a.X > -1 && a.Y < img.Height && a.Y > -1)
            {
                if (((Bitmap)img).GetPixel(a.X, a.Y) == targetColor)
                {
                    ((Bitmap)img).SetPixel(a.X, a.Y, color);
                    pixels.Push(new Point(a.X - 1, a.Y));
                    pixels.Push(new Point(a.X + 1, a.Y));
                    pixels.Push(new Point(a.X, a.Y - 1));
                    pixels.Push(new Point(a.X, a.Y + 1));
                }
            }
        }
        return img;
    }

2
投票

平心而论,这应该很简单。既然你已经有了基本的图块结构,那么算法就会相当简单:

Select Tile To Fill:    
Fill Till    
Check neighbouring Tiles - If Empty Then Fill    
Repeat, for all filled tiles.

免责声明:以上是非常基本的描述。网上有很多参考资料,比我能解释得更好。


2
投票

这里是如何在 C# 程序中使用 GDI+ 例程的示例。

https://web.archive.org/web/20130515080911/https://www.pinvoke.net/default.aspx/gdi32.extfloodfill

using System.Runtime.InteropServices;
//insert by Zswang(wjhu111#21cn.com) at 2007-05-22
[DllImport("gdi32.dll")]
public static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj);
[DllImport("gdi32.dll")]
public static extern IntPtr CreateSolidBrush(int crColor);
[DllImport("gdi32.dll")]
public static extern bool ExtFloodFill(IntPtr hdc, int nXStart, int nYStart, 
    int crColor, uint fuFillType);
[DllImport("gdi32.dll")]
public static extern bool DeleteObject(IntPtr hObject);
[DllImport("gdi32.dll")]
public static extern int GetPixel(IntPtr hdc, int x, int y);
public static uint FLOODFILLBORDER = 0;
public static uint FLOODFILLSURFACE = 1;

private void button1_Click(object sender, EventArgs e)
{
    Graphics vGraphics = Graphics.FromHwnd(Handle);
    vGraphics.DrawRectangle(Pens.Blue, new Rectangle(0, 0, 300, 300));
    vGraphics.DrawRectangle(Pens.Blue, new Rectangle(50, 70, 300, 300));
    IntPtr vDC = vGraphics.GetHdc();
    IntPtr vBrush = CreateSolidBrush(ColorTranslator.ToWin32(Color.Red));
    IntPtr vPreviouseBrush = SelectObject(vDC, vBrush);
    ExtFloodFill(vDC, 10, 10, GetPixel(vDC, 10, 10), FLOODFILLSURFACE);
    SelectObject(vDC, vPreviouseBrush);
    DeleteObject(vBrush);
    vGraphics.ReleaseHdc(vDC);
}

如果您在 OnPaint 事件处理程序中调用此函数,则可以使用

Graphics vGraphics = Graphics.FromHwnd(Handle);
,而不是使用
e.Graphics
。对我来说效果很好。

有时最好不要重新发明算法并使用现有例程,尽管在移植到 Mono 时可能会遇到一些问题。

© www.soinside.com 2019 - 2024. All rights reserved.