Introduction
While I was developping an XNA game, I found that XNA sprites use alpha blending to define the transparent parts in the sprite bitmap. Since I had no tool to edit the alpha layer of a bitmap, I wrote this small utility in C#.
Background
Most of the image editors (e.g., Microsoft Paint) allow you to edit bitmaps and consequently choose a color for each pixel. This color is defined by three values: Red, Green, Blue.
Examples:
- Red=255, Green=0, Blue=0 is a pure red pixel,
- Red=0, Green=0, Blue=0 is a black pixel,
and so on.
For applications that use alpha blending, a fourth value is added to each pixel. The alpha value defines the transparency:
- Alpha=255 is opaque
- Alpha=127 is partially transparent
- Alpha=0 is fully transparent
A bitmap file (BMP or PNG) may include an alpha layer, but most of the time it does not.
Using the application
The application window shows three bitmaps:
- The top left image is the original bitmap as loaded from the disk
- The top right image is the mask that will be used to define the alpha layer (important: this bitmap must have the same size as the original bitmap)
- The bottom image is the result of applying the mask on the original
The mask dark parts are transparent and the light parts are opaque. If your mask was drawn the other way, use the "invert mask" checkbox to perform an inversion of the mask image.
If you don't have a separate mask file, use the checkbox "Use Loaded Image as Mask", and the program will automatically create a mask by making a gray level bitmap from the loaded image. You can always edit this automatic mask by saving it (press the "Save mask" button), editing it with your favorite bitmap editor, unchecking the checkbox "Use Loaded Image as Mask", and loading back your edited mask.
Most of the time, I don't use partial transparence. So, I uncheck the "Allow partial opacity" check box. This forces the mask to become a two color (black and white) image instead of a grey scale image. The threshold that defines the limit between black and white is adjustable.
How it works
Writing into the bitmap alpha layer
The programs first copies the original image into the object bitmap maskedImage
and then performs a copy pixel per pixel of mask (the object bitmap maskImage)
into the alpha layer of maskedImage
.
To access the pixels, we need a managed byte vector of both the bitmap pixels.
BitmapData bmpData1 = maskedImage.LockBits(new Rectangle(0, 0, maskedImage.Width,
maskedImage.Height),
System.Drawing.Imaging.ImageLockMode.ReadWrite,
maskedImage.PixelFormat);
byte[] maskedImageRGBAData = new byte[bmpData1.Stride * bmpData1.Height];
System.Runtime.InteropServices.Marshal.Copy(bmpData1.Scan0,
maskedImageRGBAData,
0,
maskedImageRGBAData.Length);
This creates the byte vector bmpData1
that contains the pixel data for the bitmap maskedImage
.
BitmapData bmpData2 = maskImage.LockBits(new Rectangle(0, 0, maskImage.Width,
maskImage.Height),
System.Drawing.Imaging.ImageLockMode.ReadOnly,
maskImage.PixelFormat);
byte[] maskImageRGBAData = new byte[bmpData2.Stride * bmpData2.Height];
System.Runtime.InteropServices.Marshal.Copy(bmpData2.Scan0,
maskImageRGBAData,
0,
maskImageRGBAData.Length);
This creates the byte vector bmpData2
that contains the pixel data for the bitmap maskImage
.
Important note: the internal pixels storage used by the .NET Bitmap
objects when the pixel format is PixelFormat.Format32bppArgb
has the following unusual sequence:
byte 0: Blue value of pixel 1
Byte 1: Green value of pixel 1
Byte 2: Red value of pixel 1
Byte 3: Alpha value of pixel 1
byte 4: Blue value of pixel 2
Byte 5: Green value of pixel 2
Byte 6: Red value of pixel 2
Byte 7: Alpha value of pixel 2
...
Once we have the two vectors, we simply copy the value of the blue component from the mask vector to the alpha component of the masked bitmap vector:
for (int i = 0; i + 2 < maskedImageRGBAData.Length; i += 4)
{
maskedImageRGBAData[i + 3] = maskImageRGBAData[i];
}
Then, we copy back the pixel information to the masked image and we won't forget to unlock the unmanaged internal part of the bitmap objects.
System.Runtime.InteropServices.Marshal.Copy(maskedImageRGBAData, 0,
bmpData1.Scan0, maskedImageRGBAData.Length);
this.maskedImage.UnlockBits(bmpData1);
this.maskImage.UnlockBits(bmpData2);
Converting any bitmap to the 32 bits per pixel RGBA format
Since the bitmap loaded can have any pixel format and since the program expects a PixelFormat.Format32bppArgb
format, we have to convert the bitmap format. This is done by creating a new bitmap of the same size and drawing the original bitmap into it.
Bitmap returnedImage = new Bitmap(tmpImage.Width,
tmpImage.Height,
PixelFormat.Format32bppArgb);
Rectangle rect = new Rectangle(0, 0, tmpImage.Width, tmpImage.Height)
Graphics g = Graphics.FromImage(returnedImage);
g.DrawImage(tmpImage, rect, 0, 0, tmpImage.Width, tmpImage.Height,GraphicsUnit.Pixel);
g.Dispose();
Note: This function has a positive side effect: for optimization reasons, when you create a bitmap instance from a file, the .NET framework keeps the file open as long as the bitmap is used. This mean you get an exception when you try to write back to the bitmap file. Since the function above creates a totally new bitmap independent of the original bitmap loaded from the file, the .NET framework closes the file and it becomes possible to overwrite it.
Extracting a mask from a bitmap
The mask is a black and white or grayscale image made from a loaded image. This is achieved by directly manipulating the pixel information. This is done as explained above in the section "Writing into the bitmap alpha layer", except that here we set the red, green, blue values to the greyValue
obtained by the following code:
for (int i = 0; i + 2 < maskImageRGBData.Length; i += 4)
{
greyLevel = (byte)(0.3 * maskImageRGBData[i + 2] + 0.59 *
maskImageRGBData[i + 1] + 0.11 * maskImageRGBData[i]);
if (opaque)
{
greyLevel = (greyLevel < OpacityThreshold) ? byte.MinValue : byte.MaxValue;
}
if (invertedMask)
{
greyLevel = (byte)(255 - (int)greyLevel);
}
maskImageRGBData[i] = greyLevel;
maskImageRGBData[i + 1] = greyLevel;
maskImageRGBData[i + 2] = greyLevel;
}
Note: This loop is executed tons of times. It must execute as fast as possible. That's why we use the variables opaque
and invertedMask
instead of losing precious microseconds in calling the accessors this.checkBoxAllowPartialOpacity.Checked
and this.checkBoxInvertMask.Checked
on each iteration.
Forcing the alpha layer to zero
When the opened bitmap files already have an alpha layer defined, we need to reset it to full opacity. We could have done it as explained above by changing each byte of the alpha layer to 255, but the .NET framework offers a nicer solution: the class ImageAttributes
allows to perform the following operation on each pixel:
- Red = M00*Red + M10*Green + M20*Blue + M30*Alpha + M40 *255
- Green = M01*Red + M11*Green + M21*Blue + M30*Alpha + M41 *255
- Blue = M02*Red + M12*Green + M22*Blue + M30*Alpha + M42 *255
- Alpha = M03*Red + M13*Green + M23*Blue + M30*Alpha + M43 *255
This means that the following matrix will keep the colors unchanged and force the alpha layer values to 255.
float[][] colorMatrixElements = {
new float[] {1,0,0,0,0},
new float[] {0,1,0,0,0},
new float[] {0,0,1,0,0},
new float[] {0,0,0,0,0},
new float[] {0,0,0,1,1}};
To use this matrix, we associate it to a ColorMatrix
and then we associate this matrix to an ImageAttribute
:
ColorMatrix colorMatrix = new ColorMatrix(colorMatrixElements);
ImageAttributes imageAttributes = new ImageAttributes();
imageAttributes.SetColorMatrix(colorMatrix, ColorMatrixFlag.Default,
ColorAdjustType.Bitmap);
And then, use the imageAttribute
to specify the way the image must be drawn:
g.DrawImage(tmpImage, rect, 0, 0, tmpImage.Width, tmpImage.Height,
GraphicsUnit.Pixel, imageAttributes);