haebel
haebel

Reputation: 35

c# how to make flood fill work over color gradients?

I am not very experienced in c# yet and have to write a flood fill algorithm that also works when there is a slight change in color (like a shadow in a picture). I found a stack-based 4-way algorithm which I modified to also change pixels that have a slightly different value in the RGB-spectrum than the one before (the whole "RGB-Test" part) instead of only an area with a single color:

private void FloodFill2(Bitmap bmp, Point pt, Color targetColor, Color replacementColor)
    {
        Stack<Point> pixels = new Stack<Point>();
        pixels.Push(pt);

        while (pixels.Count > 0)
        {
            Point a = pixels.Pop();
            if (a.X < bmp.Width && a.X > 0 &&
                    a.Y < bmp.Height && a.Y > 0)//make sure we stay within bounds
            {
                // RGB-Test Start
                green = false;
                red = false;
                blue = false;

                if (bmp.GetPixel(a.X, a.Y).G > targetColor.G)
                {
                    if (targetColor.G - bmp.GetPixel(a.X, a.Y).G > (-20))
                    {
                        green = true;
                    }
                }
                else
                {
                    if (bmp.GetPixel(a.X, a.Y).G - targetColor.G > (-20))
                    {
                        green = true;
                    }
                }

                if (bmp.GetPixel(a.X, a.Y).R > targetColor.R)
                {
                    if (targetColor.R - bmp.GetPixel(a.X, a.Y).R > (-20))
                    {
                        red = true;
                    }
                }
                else
                {
                    if (bmp.GetPixel(a.X, a.Y).R - targetColor.R > (-20))
                    {
                        red = true;
                    }
                }

                if (bmp.GetPixel(a.X, a.Y).B > targetColor.B)
                {
                    if (targetColor.B - bmp.GetPixel(a.X, a.Y).B > (-20))
                    {
                        blue = true;
                    }
                }
                else
                {
                    if (bmp.GetPixel(a.X, a.Y).B - targetColor.B > (-20))
                    {
                        blue = true;
                    }
                }
                // RGB-Test End

                if (red == true && blue == true && green == true)
                { 
                    bmp.SetPixel(a.X, a.Y, replacementColor);
                    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));
                }
            }
        }
        //refresh our main picture box
        pictureBox1.Image = bmp;
        pictureBox1.SizeMode = PictureBoxSizeMode.Zoom;
        return;
    }    

The problem is now that it will stop if the gradient in the image is getting too strong, which then looks like this: https://i.sstatic.net/15jhd.png

As a solution I thought of changing the "targetColor" to the new color of the pixel that is currently being changed, so that it can "travel" over the gradient and only stop if there suddenly is too big of a difference in color.

But here comes the problem of my little knowledge with stacks and c# in general, because with a first attempt of modifying this part of the code like this

if (red == true && blue == true && green == true)
                { 
                    newColor = bmp.GetPixel(a.X, a.Y); // added this
                    bmp.SetPixel(a.X, a.Y, replacementColor);
                    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));
                    targetColor = newColor; // and this
                }

I get results that look like that: https://i.sstatic.net/U52mF.png

This is weird because it does exactly what it should but just not everywhere it is supposed to and only in the form of some stripes across the picture.

I thank you for every solution and other ideas on how to make this work properly.

Upvotes: 1

Views: 484

Answers (2)

Mark
Mark

Reputation: 740

If, like me, the accepted answer is not working for you: try this modified code below: it is very similar to @Adam maleks's answer except the tolerance is sent to the method (as opposed to hard coding it) and the Stack has been split into two objects as to prevent a type error.

    private Bitmap floodfill(Bitmap input, Point pt, Color target, Color replacementColor, int r_tol, int g_tol, int b_tol)
    {
        Stack<Point> pixels = new Stack<Point>();
        Stack<Color> colour = new Stack<Color>();

        pixels.Push(pt);
        colour.Push(target);

        while (pixels.Count > 0)
        {

            var current_pixels = pixels.Pop();
            var current_colour = colour.Pop();
            var a = new Point(current_pixels.X, current_pixels.Y);
            Color targetColor = current_colour;

            if (a.X < input.Width && a.X > 0 && a.Y < input.Height && a.Y > 0)
            {
                var green = Math.Abs(targetColor.G - input.GetPixel(a.X, a.Y).G) < r_tol;
                var red = Math.Abs(targetColor.R - input.GetPixel(a.X, a.Y).R) < g_tol;
                var blue = Math.Abs(targetColor.B - input.GetPixel(a.X, a.Y).B) < b_tol;

                if (red == true && blue == true && green == true)
                {
                    var old_pixels = input.GetPixel(a.X, a.Y);
                    input.SetPixel(a.X, a.Y, replacementColor);
                    pixels.Push(new Point(a.X - 1, a.Y));
                    colour.Push(old_pixels);
                    pixels.Push(new Point(a.X + 1, a.Y));
                    colour.Push(old_pixels);
                    pixels.Push(new Point(a.X, a.Y - 1));
                    colour.Push(old_pixels);
                    pixels.Push(new Point(a.X, a.Y + 1));
                    colour.Push(old_pixels);
                }
            }
        }

        return input;
    }

Upvotes: 0

Adam Małek
Adam Małek

Reputation: 311

My approach to this problem was to also store in stack information about color that was checked when point was added to stack so e.g. we checked pixel (10,15) with colour (255,10,1), and during that we added to stack pixel (10,16) with information about previous color of (10,15), and during pixel check we compare its colour to previous one. Changes I made:

1) include information about color of prev pixel in stack: for that I used c# construct named touple:

Stack<(Point point, Color target)> pixels = new Stack<(Point, Color)>();
pixels.Push((pt, target));

2) while working with stack we get pair pixel/targetColour

    var curr = pixels.Pop();
    var a = curr.point;
    Color targetColor = curr.target;

3) While adding points to stack we also include old pixel color:

var old = bmp.GetPixel(a.X, a.Y);
bmp.SetPixel(a.X, a.Y, replacementColor);
pixels.Push((new Point(a.X - 1, a.Y), old));
pixels.Push((new Point(a.X + 1, a.Y), old));
pixels.Push((new Point(a.X, a.Y - 1), old));
pixels.Push((new Point(a.X, a.Y + 1), old));

Code after some refactoring:

void FloodFill2(Bitmap bmp, Point pt, Color target, Color replacementColor)
{
    Stack<(Point point, Color target)> pixels = new Stack<(Point, Color)>();
    pixels.Push((pt, target));

    while (pixels.Count > 0)
    {
        var curr = pixels.Pop();
        var a = curr.point;
        Color targetColor = curr.target;

        if (a.X < bmp.Width && a.X > 0 &&
                a.Y < bmp.Height && a.Y > 0)//make sure we stay within bounds
        {
            var tolerance = 10;
            var green = Math.Abs(targetColor.G - bmp.GetPixel(a.X, a.Y).G) < tolerance;
            var red = Math.Abs(targetColor.R - bmp.GetPixel(a.X, a.Y).R) < tolerance;
            var blue = Math.Abs(targetColor.B - bmp.GetPixel(a.X, a.Y).B) < tolerance;

            if (red == true && blue == true && green == true)
            {
                var old = bmp.GetPixel(a.X, a.Y);
                bmp.SetPixel(a.X, a.Y, replacementColor);
                pixels.Push((new Point(a.X - 1, a.Y), old));
                pixels.Push((new Point(a.X + 1, a.Y), old));
                pixels.Push((new Point(a.X, a.Y - 1), old));
                pixels.Push((new Point(a.X, a.Y + 1), old));
            }
        }
    }
    //refresh our main picture box
    pictureBox1.Image = bmp;
    pictureBox1.SizeMode = PictureBoxSizeMode.Zoom;
    return;
}

Final effect:

Final effect

Upvotes: 2

Related Questions