Converting Color-Keyed Bitmaps to Custom Regions

Using Unsafe Code in C# to converting color-keyed bitmaps to custom regions.

.

Environment: Microsoft .NET Framework 1.0, Microsoft Visual Studio .NET, Windows Forms

Introduction

There are two ways in the .NET Framework Windows Forms classes to make a nonrectangular window: Use the Form.TransparencyKey (also exposed in the Win32 API through the (Get|Set)LayeredWindowAttributes functions) property or use regions. The first one is by far easier to work with, but it is not supported on any of the Windows platforms earlier than Win2K. It is also only supported on Forms -- if you want nonrectangular shapes on controls not derived from Form, you will have to resort to regions anyway.

What Is a Region?

A region is a combination of geometrically defined shapes, such as ellipses, rectangles, or polygons. It is mainly used by the GDI to clip drawing operations against windows. No painting operations will be allowed on a window outside its clipping region. Hit-testing (mouse clicks, and so forth) is also performed against the window's region. For most windows, the clipping region is rectangular, but it does not have to be. In Windows Forms, the region for an arbitrary control can be set through the Region property. Effectively setting the Region property defines the shape of the window.

Creating complex window shapes by using regions is a tedious operation. You have to break the shape down into geometrical elements, and then add each separate element to the region. Creating anything but simple shapes this way is far from intuitive.

What Is a Transparency Key?

A transparency key is another approach to defining the transparent portions of a window. A transparency key is a color you assign to a form to represent the areas on it that should be transparent. If you assign Color.Yellow as the TransparencyKey to a form, and then draw a big yellow circle, you will end up with a big hole in the form through which you can see the windows below. Any mouse activity in that hole will be transferred to the underlying windows as well.

As you probably understand, this is a far more natural and intuitive way to create custom-shaped windows. You can just create a bitmap in any paint package; then fill the areas you want transparent with some color you're not using elsewhere. Then you can just assign the bitmap to the form's BackgroundImage property, and the color you chose to the TransparencyKey property. Voila! -- your own custom-shaped window.

Unfortunately, as I mentioned above, this functionality is not supported on Windows 9X, Me, or NT 4.0. This of course severely limits the usability of this approach, since these platforms are by far the most prevalent out there.

Having Our Cake and Eating It, Too

So, how can we design our form using the second way and still have it be available on all .NET-supported platforms? We simply have to convert the bitmap containing the color key into a region. Unfortunately, because there are no classes in the framework that do this for us, we have to do it ourselves.

The Algorithm

The algorithm for doing this conversion is pretty well known. Basically, we scan the bitmap line by line and look for consecutive non-transparent stretches. Each stretch we find is added to the region as a rectangle with height 1. When we have scanned the whole bitmap, the resulting region will represent the bitmap minus the transparent areas. If we now assign the bitmap to the BackgroundImage property of the form, and the region to the Region property, we will get the same result as we would have by setting the TransparencyKey property.

Unsafe Code

This algorithm obviously involves going through every individual pixel of the provided bitmap. Unfortunately, the GetPixel method of the Bitmap class is awfully slow. The other way to gain access to the bitmap's pixels is through the LockBits method. This method returns a BitmapData object containing a pointer to the top left pixel of the bitmap.

Yes, I did say pointer. While you usually program using safe references instead of raw pointers, C# lets you use C-style pointers in special sections of code marked as unsafe. The fact that these sections have to be explicitly labeled, and that you have to add the /unsafe switch to the compiler to make them compile, goes a long way in showing that this is not something MS intends you to use on a regular basis. However, there are circumstances under which their use can be justified. I believe this is one of those circumstances.

The Code

The conversion is performed by a single static method, Convert, which returns a Region object. (Despite being a self-professed OO freak, I was unable to find any justification for making this an instance method.) This method takes three arguments, as follows:

public unsafe static Region Convert( Bitmap bitmap, 
                                     Color transparencyKey,
                                     TransparencyMode mode )

The first two arguments should be fairly obvious, but the third one deserves some further explanation. TransparencyMode is an enum defined like this:

public enum TransparencyMode
{
    ColorKeyTransparent,
    ColorKeyOpaque
}

Basically, if you use ColorKeyTransparent, the code will exclude all the pixels that have the same color as the color key from the resulting region, making areas with that color transparent. With ColorKeyOpaque, the opposite will happen; all areas NOT containing that color will be seen as transparent. This can be pretty useful in some circumstances.

We acquire the pointer to the bitmap surface with the following code:

BitmapData bitmapData = bitmap.LockBits(bounds,
                                        ImageLockMode.ReadOnly,
                                        PixelFormat.Format32bppArgb);
uint* pixelPtr = (uint*)bitmapData.Scan0.ToPointer();

The pixel format we requested, Format32bppArgb, will map nicely onto an unsigned int. We convert the color key into a uint as well, so we can compare them efficiently:

uint key = (uint)((transparencyKey.A << 24) | 
                  (transparencyKey.R << 16) | 
                  (transparencyKey.G << 8) |
                  (transparencyKey.B << 0));

We cannot add rectangles to a region directly, so we take an indirect approach and create a GraphicsPath object instead. Now the main body of the loop looks like this:

for ( int y = 0; y < yMax; y++ )
{
    //store the pointer so we can offset the stride directly
    //from it later to get to the next line
    byte* basePos = (byte*)pixelPtr;

    for ( int x = 0; x < xMax; x++, pixelPtr++  )
    {
        //is this transparent? if yes, just go on with the loop
        if ( modeFlag ^ ( *pixelPtr == key ) )
            continue;

        //store where the scan starts
        int x0 = x;

        //not transparent - scan until we find the next 
        //transparent byte
        while( x < xMax && !(modeFlag ^ (*pixelPtr == key)))
        {
            ++x;
            pixelPtr++;
        }

        //add the rectangle we have found to the path
        path.AddRectangle( new Rectangle( x0, y, x-x0, 1 ) );
    }
    //jump to the next line
    pixelPtr = (uint*)(basePos + bitmapData.Stride);
}

Basically it scans along each line until it finds a non-transparent pixel, using the continue statement to short circuit the inner loop. When one is found, it uses another loop to scan ahead to the next transparent pixel or the right end of the bitmap, whichever comes first. This scan is then added as a rectangle of height 1 to the GraphicsPath object. As you can see, the BitmapData object has a property Stride, which gives the number of bytes per line.

This process is repeated for each line in the bitmap. When we have gone through them all, we use the GraphicsPath object to create the region:

//now create the region from all the rectangles
Region region = new Region( path );

From here on, it's just a matter of some cleanup before we can return our freshly created Region object.

Sample

You can download the source for the whole BitmapRegion class, along with a sample project using it, through the link below.

Downloads

Download project - 159 Kb



Comments

  • plz little help if possible ??

    Posted by Mr.Ayman on 07/27/2004 12:38pm

    i hope you have some time to read my question & answer it. when i tried to use Bitmap.LockBits(.. , ImageLockMode.WriteOnly , .. ) with different access it gives me system argument exception with an invalid parameter used. i wanted to have another access mode so i can change contents of the image , if this method can't work this way , why ?? and how we can change contents of the image but ofcourse without using Bitmap.SetPixel() i am waten fort your reply and thanks in advance for your help

    Reply
  • Make it Save

    Posted by Legacy on 12/19/2003 12:00am

    Originally posted by: Benjamin Lutz

    with this Change you can turn off the AllowUnsaveCode Switch:
    
    

    public static Region Convert( Bitmap bitmap, Color transparencyKey, TransparencyMode mode )
    {
    bool modeFlag = ( mode == TransparencyMode.ColorKeyOpaque );
    GraphicsUnit unit = GraphicsUnit.Pixel;
    RectangleF boundsF = bitmap.GetBounds( ref unit );
    Rectangle bounds = new Rectangle( (int)boundsF.Left, (int)boundsF.Top, (int)boundsF.Width, (int)boundsF.Height );
    uint key = (uint)((transparencyKey.A << 24) | (transparencyKey.R << 16) | (transparencyKey.G << 8) | (transparencyKey.B << 0));
    BitmapData bitmapData = bitmap.LockBits( bounds, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb );
    uint pixelPtr = (uint)bitmapData.Scan0.ToInt32();
    uint pixelPtrValue = (uint)Marshal.ReadInt32(new IntPtr(bitmapData.Scan0.ToInt32()));
    int yMax = (int)boundsF.Height;
    int xMax = (int)boundsF.Width;
    GraphicsPath path = new GraphicsPath();
    for ( int y = 0; y < yMax; y++ )
    {
    for ( int x = 0; x < xMax; x++, pixelPtr+=4)
    {
    pixelPtrValue = (uint)Marshal.ReadInt32(new IntPtr(pixelPtr));
    if ( modeFlag ^ ( pixelPtrValue == key ) )
    continue;
    int x0 = x;
    while( x < xMax && !( modeFlag ^ ( pixelPtrValue == key ) ) )
    {
    ++x;
    pixelPtr+=4;
    pixelPtrValue = (uint)Marshal.ReadInt32(new IntPtr(pixelPtr));
    }
    path.AddRectangle( new Rectangle( x0, y, x-x0, 1 ) );
    }
    }
    Region region = new Region( path );
    path.Dispose();
    bitmap.UnlockBits( bitmapData );
    return region;
    }


    Reply
  • Performance hit for yr algorithm -- see http://codeguru.earthweb.com/dialog/AnyFormDialog.html

    Posted by Legacy on 10/09/2003 12:00am

    Originally posted by: Zhefu Zhang

    Due to yr region merge code will be execute whenever u meet a non-backcolor pixel, and region-merge code is CPU comsuming, the performance hit on large image will be noticable. You can either check the artile I put as the title, or u want a quick glance go to my article --- http://www.codeguru.com/dialog/RenderingRegion.html

    Regards,

    Reply
  • Little Help?

    Posted by Legacy on 06/13/2003 12:00am

    Originally posted by: Michael Fox

    I have been looking all over for something like this.
    
    

    I am running Windows XP Professional with VS.NET

    I am having a slight problem. I am learning to create my own user controls and the control is the right shape, however the image is displaced down and to the right.

    I have a solution with your BitmapToRegion C# project and a vb control project. Here is the code that paints the control. Can you tell me what I am doing wrong?


    Protected Overrides Sub OnPaint(ByVal e As System.Windows.Forms.PaintEventArgs)
    e.Graphics.Clear(Me.BackColor)
    mImageFile = "C:\image1.bmp"
    mImage = New System.Drawing.Bitmap(ImageFile)
    mImageAlphaColor = mImage.GetPixel(1, 1)
    Me.Width = mImage.Width
    Me.Height = mImage.Height
    Me.Region = BitmapToRegion.BitmapToRegion.Convert(mImage, mImageAlphaColor, BitmapToRegion.TransparencyMode.ColorKeyTransparent)
    e.Graphics.DrawImage(mImage, 0, 0)
    End Sub


    btw, if I run your complete project, it works fine...

    Thanks a million,
    Michael Fox

    Reply
  • Stride is in bytes(?)

    Posted by Legacy on 11/27/2002 12:00am

    Originally posted by: Tony Reynolds

    Hi -

    The code supplied is just what I was looking for to get to the pixel data, but I found it crashed at first. After some research I found Stride is defined in *bytes*, so to advance a line in this case you have to do:- basePos + stride/4.
    I'm a newbie to GDI+ so maybe I've missed something?

    Regards
    Tony Reynolds

    Reply
  • Sorry, but this is much easier

    Posted by Legacy on 11/13/2002 12:00am

    Originally posted by: Erlend Robaye

    You can have the same effect with safe code, just make a bitmap as before, having one color as the transparent color and do the following :
    
    

    protected override void OnLoad ( System.EventArgs e )
    {
    // Set the transparent color used in the bitmap
    this.TransparencyKey = Color.Red;
    this.BackgroundImage = new Bitmap (
    @"path\FileName.bmp");
    }

    Yep, it is as easy as that !

    Brgds,

    Erlend Robaye

    Reply
  • I WANT TO COMPARE TWO IMAGE FILE

    Posted by Legacy on 05/30/2002 12:00am

    Originally posted by: saira

    I want to make a programe who compare two image file
    On both image file there are some check-boxes, some are fill and some are not.

    The programe recognized and compare that the check-box of one file is to another that both are same that is fill or not.

    size of check-box are same


    Reply
  • I WANT TO COMPARE TWO IMAGE FILE

    Posted by Legacy on 05/30/2002 12:00am

    Originally posted by: saira

    I want to make a programe who compare two image file
    On both image file there are some check-boxes, some are fill and some are not.

    The programe recognized and compare that the check-box of one file is to another that both are same that is fill or not.

    size of check-box are same


    Reply
Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • As mobile devices have pushed their way into the enterprise, they have brought cloud apps along with them. This app explosion means account passwords are multiplying, which exposes corporate data and leads to help desk calls from frustrated users. This paper will discover how IT can improve user productivity, gain visibility and control over SaaS and mobile apps, and stop password sprawl. Download this white paper to learn: How you can leverage your existing AD to manage app access. Key capabilities to …

  • The Red Hat® Standard Operating Environment SOE helps you define, deploy, and maintain Red Hat Enterprise Linux® and third-party applications as an SOE. The SOE is fully aligned with your requirements as an effective and managed process, and fully integrated with your IT environment and processes. Benefits of an SOE: SOE is a specification for a tested, standard selection of computer hardware, software, and their configuration for use on computers within an organization. The modular nature of the Red …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds