Applying a mask to a bitmap and generating a composite image at runtime
Asked Answered
T

2

8

Background

I am trying to create a 2D tile engine from scratch using c# 4.0 and WPF. I am not attempting to build the next greatest 2D game on the planet, rather I am trying to learn how to manipulate images and graphics using .NET.

Goal

I am trying to apply a mask to a bitmap. My bitmap is in colour and my mask is a monochrome bitmap. Where ever the colour white appears, I am trying to replace the colour in the original bitmap at the corresponding location with a transparent colour.

My goal is to be able to store a collection of images in memory which have been masked at runtime so that I can build a composite of them in layers - i.e. a background first, an item on the map on top of it, and finally a player avatar on top of that on demand.

What I have looked at so far...

I have been at this for quite a while now, hence my posting on SO - the following details what I have looked at so far:

I have looked at this post Alpha masking in c# System.Drawing? which shows how to use an unsafe method to manipulate an image using pointer arithmetic.

Also, this post Create Image Mask which shows how to use SetPixel / GetPixel to swap colours.

Various MSDN pages and other ad-hoc blogs encompassing the topic.

What I have tried...

I have tried the unsafe pointer arithmetic method: Something like this (Please be aware this has been butchered, put together again, and repeated because I have been tinkering trying to understand why this wasn't doing what I wanted it to do):

private static Bitmap DoApplyMask(Bitmap input, Bitmap mask)
    {
        Bitmap output = new Bitmap(input.Width, input.Height, PixelFormat.Format32bppArgb);
        output.MakeTransparent();
        var rect = new Rectangle(0, 0, input.Width, input.Height);
        var bitsMask = mask.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
        var bitsInput = input.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
        var bitsOutput = output.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
        unsafe
        {
            for (int y = 0; y < input.Height; y++)
            {
                byte* ptrMask = (byte*)bitsMask.Scan0 + y * bitsMask.Stride;
                byte* ptrInput = (byte*)bitsInput.Scan0 + y * bitsInput.Stride;
                byte* ptrOutput = (byte*)bitsOutput.Scan0 + y * bitsOutput.Stride;
                for (int x = 0; x < input.Width; x++)
                {
                    //I think this is right - if the blue channel is 0 than all of them are which is white?
                    if (ptrMask[4*x] == 0)
                    {
                        ptrOutput[4*x] = ptrInput[4*x]; // blue
                        ptrOutput[4*x + 1] = ptrInput[4*x + 1]; // green
                        ptrOutput[4*x + 2] = ptrInput[4*x + 2]; // red
                    }
                    else
                    ptrOutput[4 * x + 3] =255;        // alpha
                }
            }
        }
        mask.UnlockBits(bitsMask);
        input.UnlockBits(bitsInput);
        output.UnlockBits(bitsOutput);


        return output;
    }

Also tried a variation on the other method which looked a bit like this (sorry I appear to have binned the code along the way - this is part real code/ part pseudo code - just to get across the approach I was trying...)

{
Bitmap input...
Bitmap mask...

Bitmap output = new Bitmap(input.Width, input.Height, PixelFormat.Format32bppArgb);
output.MakeTransparent();
for (int x = 0; x < output.Width; x++)
{
    for (int y = 0; y < output.Height; y++)
        {
            Color pixel = mask.GetPixel(x, y);
            //Again - not sure - my thought is if any channel has colour then it isn't white - so make it transparent.
            if (pixel.B > 0)
                output.SetPixel(x, y, Color.Transparent);
            else
        {
            pixel = input.GetPixel(x,y);
            output.SetPixel(x,y, Color.FromArgb(pixel.A, pixel.R, pixel.G,pixel.B));
        }
    }
}

My actual problem...

So the methods above yield either the colour image on a black background or a colour image on a white background (as far as I can tell!) When I try to composite the images using code like this:

public Bitmap MakeCompositeBitmap(params string[] names)
    {
        Bitmap output = new Bitmap(32, 32, PixelFormat.Format32bppArgb);
        output.MakeTransparent();

        foreach (string name in names)
        {
    //GetImage() returns an image which I have previously masked and cached.
            Bitmap layer = GetImage(name);

                for (int x = 0; x < output.Width; x++)
                {
                    for (int y = 0; y < output.Height; y++)
                    {
                        Color pixel = layer.GetPixel(x, y);
                        if (pixel.A > 0)
                            output.SetPixel(x, y, Color.Transparent);
                        else
                            output.SetPixel(x,y, Color.FromArgb(pixel.A, pixel.R, pixel.G,pixel.B));
                    }
                }


        }

        return output;
    }

I am calling this code with the names of various layers I want composited at runtime, the background first, then the foreground to give me an image I can show on my UI. I am expecting the cached image to have a transparency set by the mask, and I am expecting this transparency to be ignored when rendering my image (and I have given up on pointer arithmetic at the moment, I just want to get it working before optimizing it!). What I end up with is the foreground image only.

So - I must re-iterate again, I am totally new at this, and know that there is probably going to be a faster way of doing this - but it seems like such a simple problem and I just want to get to the bottom of it!

I am using WPF, c# 4.0, VS2013, displaying the images in an Image element which is hosted in a grid, which is hosted by a listviewItem, hosted in a ListView, which is hosted in a grid, and finally hosted by a window (if that is of any relevance at all...)

So to summarize my approach.. I am Getting an image and a corresponding mask. The image is colour, the mask is monochrome. I am applying my mask to my image and caching it. I am building a composite image from multiple images in the cache; drawing each one in turn (apart from the transparent bits) onto a new bitmap

And now I am pulling my hair out, because I have been at this for a while, I'm only seeing the foreground image! I can see a lot of information on the topic but don't have the necessary experience to apply that information to create a solution to my problem.

Edit: My solution (thanks to thumbmunkeys for pointing out the errors in my logic:

Fixed DoApplyMask method

private static Bitmap DoApplyMask(Bitmap input, Bitmap mask)
    {
        Bitmap output = new Bitmap(input.Width, input.Height, PixelFormat.Format32bppArgb);
        output.MakeTransparent();
        var rect = new Rectangle(0, 0, input.Width, input.Height);

        var bitsMask = mask.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
        var bitsInput = input.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
        var bitsOutput = output.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
        unsafe
        {
            for (int y = 0; y < input.Height; y++)
            {
                byte* ptrMask = (byte*)bitsMask.Scan0 + y * bitsMask.Stride;
                byte* ptrInput = (byte*)bitsInput.Scan0 + y * bitsInput.Stride;
                byte* ptrOutput = (byte*)bitsOutput.Scan0 + y * bitsOutput.Stride;
                for (int x = 0; x < input.Width; x++)
                {
                    //I think this is right - if the blue channel is 0 than all of them are (monochrome mask) which makes the mask black
                    if (ptrMask[4 * x] == 0)
                    {
                        ptrOutput[4 * x] = ptrInput[4 * x]; // blue
                        ptrOutput[4 * x + 1] = ptrInput[4 * x + 1]; // green
                        ptrOutput[4 * x + 2] = ptrInput[4 * x + 2]; // red

                        //Ensure opaque
                        ptrOutput[4*x + 3] = 255;
                    }
                    else
                    {
                        ptrOutput[4 * x] = 0; // blue
                        ptrOutput[4 * x + 1] = 0; // green
                        ptrOutput[4 * x + 2] = 0; // red

                        //Ensure Transparent
                        ptrOutput[4 * x + 3] = 0; // alpha
                    }
                }
            }

        }
        mask.UnlockBits(bitsMask);
        input.UnlockBits(bitsInput);
        output.UnlockBits(bitsOutput);

        return output;
    }

Fixed MakeCompositeBitmap() method

 public Bitmap MakeCompositeBitmap(params string[] names)
    {
        Bitmap output = new Bitmap(32, 32, PixelFormat.Format32bppArgb);
        output.MakeTransparent();

        foreach (string name in names)
        {
            Bitmap layer = GetImage(name);

            for (int x = 0; x < output.Width; x++)
            {
                for (int y = 0; y < output.Height; y++)
                {
                    Color pixel = layer.GetPixel(x, y);
                    if (pixel.A > 0) // 0 means transparent, > 0 means opaque
                        output.SetPixel(x, y, Color.FromArgb(pixel.A, pixel.R, pixel.G, pixel.B));
                }
            }
        }

        return output;
    }
Trothplight answered 1/2, 2014 at 15:39 Comment(0)
B
3

The first method DoApplyMask has the following problem:

  • if (ptrMask[4*x] == 0) means the mask is black, in your case opaque, so you have to set alpha to 0xFF (its the opposite way around in your code)

For your blending code you have to do the following things:

  • start off with the background layer and render it to output
  • for every layer above the background do the following:
    • read pixel from output
    • read pixel from layer
    • calculate pixel = AlphaLayer * PixelLayer + (1 - AlphaLayer) * PixelOutput
    • Write pixel to output

The reason you always end up with uppermost layer is that you overwrite whatever is in the output with the topmost layer color, or a transparent color. Instead of writing the transparent color you should blend output and layer color based on alpha value of the layer:

 for (int y = 0; y < output.Height; y++)
 {
      Color pixel = layer.GetPixel(x, y);
      Color oPixel = output.GetPixel(x, y);          

      byte a = pixel.A;
      byte ai = 0xFF - pixel.A;

      output.SetPixel(x,y, 
              Color.FromArgb(0xFF, 
                   (pixel.R *a + oPixel.R * ai)/0xFF,  
                   (pixel.G *a + oPixel.G * ai)/0xFF,  
                   (pixel.B *a + oPixel.B * ai)/0xFF);
 }
Blackshear answered 1/2, 2014 at 17:38 Comment(3)
Thanks for taking the time to answer, I will have another go later this evening and let you know how I get on.Trothplight
I've edited my question with a solution after spending a bit of time trying to apply what you suggested in your answer. Many thanks; so relieved to have this working. I might have a go at reworking the MakeCompositeBitmap() method at some point - but tbh, beeing a complete n00b, I'm having a bit of the hard time with implementing the math as you suggested (I assume you mean that I should be doing this using LockBits too rather than Get/SetPixel?) If you fancy expanding on how that calculate pixel formula works then I'm all ears. Anyway, thanks again!Trothplight
great that it worked, I extended my answer, not sure if it compiles, but you get the ideaBlackshear
R
0

Like thumbmunkeys said, the code for Black or White to mask is reversed. Should be:

                if (ptrMask[4 * x] == 0)
                {
                   ptrOutput[4 * x] = 0; // blue
                    ptrOutput[4 * x + 1] = 0; // green
                    ptrOutput[4 * x + 2] = 0; // red

                    //Ensure Transparent
                    ptrOutput[4 * x + 3] = 0; // alpha
                }
                else
                {
                    ptrOutput[4 * x] = ptrInput[4 * x]; // blue
                    ptrOutput[4 * x + 1] = ptrInput[4 * x + 1]; // green
                    ptrOutput[4 * x + 2] = ptrInput[4 * x + 2]; // red

                    //Ensure opaque
                    ptrOutput[4*x + 3] = 255;

                }
Romanticize answered 8/7, 2014 at 6:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.