Question

Converting a bitmap to grayscale is pretty easy with AForge:

public static Bitmap ConvertToGrayScale(this Bitmap me)
{
     if (me == null)
         return null;

     // first convert to a grey scale image
     var filterGreyScale = new Grayscale(0.2125, 0.7154, 0.0721);

     me = filterGreyScale.Apply(me);
     return me;
}

But I need something more tricky:

Imagine you want to convert everything to grayscale except for a circle in the middle of the bitmap. In other words: a circle in the middle of the given bitmap should keep its original colours.

Let's assume the radius of the circle is 20px, how should I approach this?

Was it helpful?

Solution

This can be accomplished using MaskedFilter with a mask that defines the circled area you describe. As the documentation states

Mask can be specified as .NET's managed Bitmap, as UnmanagedImage or as byte array. In the case if mask is specified as image, it must be 8 bpp grayscale image. In all case mask size must be the same as size of the image to process.

So the mask image has to be generated based on the source image's width and height.

I haven't compiled the following code but it should get you on your way. If the circle is always in the same spot, you could generate the image mask outside the method so that it doesn't have to be regenerated each time you apply the filter. Actually you could have the whole MaskedFilter generated outside the method that applies it if nothing changes but the source image.

public static Bitmap ConvertToGrayScale(this Bitmap me)
{
     if (me == null)
         return null;

     var radius = 20, x = me.Width / 2, y = me.Height / 2;

     using (Bitmap maskImage = new Bitmap(me.Width, me.Height, PixelFormat.Format8bppIndexed))
     {
        using (Graphics g = Graphics.FromImage(maskImage))
           using (Brush b = new SolidBrush(ColorTranslator.FromHtml("#00000000")))
              g.FillEllipse(b, x, y, radius, radius);

        var maskedFilter = new MaskedFilter(new Grayscale(0.2125, 0.7154, 0.0721), maskImage);

        return maskedFilter.Apply(me);
     }
}

EDIT

The solution for this turned out to be a lot more trickier than I expected. The main problem was that the MaskedFilter doesn't allow the usage of filters that change the images format, which the Grayscale filter does (it changes the source to an 8bpp or 16 bpp image).

The following is the resulting code, which I have tested, with comments added to each part of the ConvertToGrayScale method explaining the logic behind it. The gray-scaled portion of the image has to be converted back to RGB since the Merge filter doesn't support merging two images with different formats.

static class MaskedImage
{
    public static void DrawCircle(byte[,] img, int x, int y, int radius, byte val)
    {
        int west = Math.Max(0, x - radius),
            east = Math.Min(x + radius, img.GetLength(1)),
            north = Math.Max(0, y - radius),
            south = Math.Min(y + radius, img.GetLength(0));

        for (int i = north; i < south; i++)
            for (int j = west; j < east; j++)
            {
                int dx = i - y;
                int dy = j - x;
                if (Math.Sqrt(dx * dx + dy * dy) < radius)
                    img[i, j] = val;
            }
    }

    public static void Initialize(byte[,] arr, byte val)
    {
        for (int i = 0; i < arr.GetLength(0); i++)
            for (int j = 0; j < arr.GetLength(1); j++)
                arr[i, j] = val;
    }

    public static void Invert(byte[,] arr)
    {
        for (int i = 0; i < arr.GetLength(0); i++)
            for (int j = 0; j < arr.GetLength(1); j++)
                arr[i, j] = (byte)~arr[i, j];
    }

    public static Bitmap ConvertToGrayScale(this Bitmap me)
    {
        if (me == null)
            return null;

        int radius = 20, x = me.Width / 2, y = me.Height / 2;
        // Generate a two-dimensional `byte` array that has the same size as the source image, which will be used as the mask.
        byte[,] mask = new byte[me.Height, me.Width];
        // Initialize all its elements to the value 0xFF (255 in decimal).
        Initialize(mask, 0xFF);
        // "Draw" a circle in the `byte` array setting the positions inside the circle with the value 0.
        DrawCircle(mask, x, y, radius, 0);

        var grayFilter = new Grayscale(0.2125, 0.7154, 0.0721);
        var rgbFilter = new GrayscaleToRGB();
        var maskFilter = new ApplyMask(mask);
        // Apply the `Grayscale` filter to everything outside the circle, convert the resulting image back to RGB
        Bitmap img = rgbFilter.Apply(grayFilter.Apply(maskFilter.Apply(me)));
        // Invert the mask
        Invert(mask);
        // Get only the cirle in color from the original image
        Bitmap circleImg = new ApplyMask(mask).Apply(me);
        // Merge both the grayscaled part of the image and the circle in color in a single one.
        return new Merge(img).Apply(circleImg);
    }
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top