Other Posts in Image Editing

  1. Perlin Noise
  2. Fault Formation
  3. Cellular Textures
  4. Resizing an Image in C#
  5. Box Blur and Gaussian Blur... Sort of...
  6. Thermal Erosion
  7. Using Mid Point Displacement to Create Cracks
  8. Fluvial Erosion
  9. Creating Marble Like Textures Procedurally
  10. Procedural Textures and Dilation
  11. Converting Image to Black and White in C#
  12. Getting an HTML Based Color Palette from an Image in C#
  13. Adding Noise/Jitter to an Image in C#
  14. Creating Pixelated Images in C#
  15. Edge detection in C#
  16. Using Sin to Get What You Want... In C#...
  17. Noise Reduction of an Image in C# using Median Filters
  18. Image Dilation in C#
  19. Sepia Tone in C#
  20. Kuwahara Filter in C#
  21. Matrix Convolution Filters in C#
  22. Symmetric Nearest Neighbor in C#
  23. Bump Map Creation Using C#
  24. Normal Map Creation Using C#
  25. Creating Negative Images using C#
  26. Red, Blue, and Green Filters in C#
  27. Converting an Image to ASCII Art in C#
  28. Adjusting Brightness of an Image in C#
  29. Adding Noise to an Image in C#
  30. Adjusting the Gamma of an Image Using C#
  31. Adjusting Contrast of an Image in C#
  32. Drawing a Box With Rounded Corners in C#
  33. Anding Two Images Together Using C#
  34. Motion Detection in C#
  35. Creating Thermometer Chart in C#
  36. Colorizing a Black and White Image in C#
  37. Extracting an Icon From a File
  38. Setting the Pixel Format and Image Format of an Image in .Net
  39. Using Unsafe Code for Faster Image Manipulation
  40. Sobel Edge Detection and Laplace Edge Detection in C#

Using Unsafe Code for Faster Image Manipulation

8/20/2010

Over the past couple months, I've had a lot of traffic (well a lot for me anyway) going toward my various image editing posts. While the general concepts are there, I get a decent number of people complaining that they're a bit slow. They are slow when the image gets to be rather large. The reason for this is because I intentionally use two functions that are incredibly slow, GetPixel and SetPixel... These functions have to figure out the format that the image is actually displayed in, and give us/set the individual pixel in the image. To say the least that's a slow process (safe but slow). So why would I use them if they're so slow? Because they're easy to understand. Most of my code was designed such that someone who didn't know much about images or maybe even that much about C# could see what was going on and get something together for their needs. The alternative gets much more complicated, much faster...

That alternative is by using unsafe code. I can honestly say that there are only a couple times that I've ever seen someone use unsafe code (usually when doing image manipulation). So if you're a C# developer, there is a good chance that you've never (or at least rarely) seen the keyword used. So what the heck is the unsafe keyword? The unsafe keyword is used to define a section of code that uses pointers.  You can define it on a function, encapsulate a small bit of code with it, etc. You're just saying this bit of code uses pointers. If you've done C++ programming, you've dealt with pointers enough that this should be rather simple. For everyone else, well I'm not going to explain pointers that much other than to say that they point to things in memory.

OK, well maybe a little bit more is needed. In our case, think of our image as an array of bytes in memory. When we create a pointer and point it to our image, it does not hold our image. The pointer is simply an int (actually it depends on the language/architecture as to what the type is) that says we can find this image at location X in memory. On top of this we can change where the pointer points. For instance if we add 1 to it, we get the next byte in memory (1 pixel in on our image, assuming 1 pixel=1 byte). It's referred to as pointer arithmetic. That's all you need to know for our purposes, but If you want to know more Google is your friend.

Getting back to the unsafe keyword, the advantages of using pointers and unsafe code is it gets rid of a lot of bounds checks, etc. that slow down your code. The downside is that it can introduce potential security flaws if it's not written correctly... Not to mention they tend to increase the number of bugs that come about (especially with people who have limited experience with them). That being said, we're going to use unsafe code to speed up our image manipulation.

The two functions that were slowing us down were GetPixel and SetPixel. So let's create our own. So how do we do this? Well with images there are a couple steps that you need to do to get at the data so that we can set/get a pixel. The first step (after loading the image) is to lock it. I've written a function to do this step for me:

   1: internal static BitmapData LockImage(Bitmap Image)
   2: {
   3:         return Image.LockBits(new Rectangle(0, 0, Image.Width, Image.Height),
   4:             ImageLockMode.ReadWrite, Image.PixelFormat);
   5: }

With this function we're telling the image to lock all of it (since the rectangle is the entire image) for both reading and writing. The last item is the pixel format of the image. In our case we're using the image's own pixel format, because we don't exactly know what the format is suppose to be. That being said, in later code we're going to limit ourselves to 24 and 32 bit images (expanding this for 48 or 64 bit is actually fairly simple but not something that I needed). In return the function returns a BitmapData class. This class contains our actual bits of data, but we still aren't quite ready to edit it yet. The next step is we need to figure out the actual size of each pixel (luckily for us, they tell us):

   1: internal static int GetPixelSize(BitmapData Data)
   2:  
   3: {
   4:  
   5:     if (Data.PixelFormat == PixelFormat.Format24bppRgb)
   6:  
   7:         return 3;
   8:  
   9:     else if (Data.PixelFormat == PixelFormat.Format32bppArgb 
  10:  
  11:         || Data.PixelFormat == PixelFormat.Format32bppPArgb 
  12:  
  13:         || Data.PixelFormat == PixelFormat.Format32bppRgb)
  14:  
  15:         return 4;
  16:  
  17:     return 0;
  18:  
  19: }

As I said earlier, we're only really going to handle 24 and 32 bit images. Once we have our data and know our pixel size, we can actually get/set our needed pixel:

   1: internal static unsafe Color GetPixel(BitmapData Data, int x, int y,int PixelSizeInBytes)
   2:  
   3: {
   4:  
   5:     try
   6:  
   7:     {
   8:  
   9:         byte* DataPointer = (byte*)Data.Scan0;
  10:  
  11:         DataPointer = DataPointer + (y * Data.Stride) + (x * PixelSizeInBytes);
  12:  
  13:         if (PixelSizeInBytes == 3)
  14:  
  15:         {
  16:  
  17:             return Color.FromArgb(DataPointer[2], DataPointer[1], DataPointer[0]);
  18:  
  19:         }
  20:  
  21:         return Color.FromArgb(DataPointer[3], DataPointer[2], DataPointer[1], DataPointer[0]);
  22:  
  23:     }
  24:  
  25:     catch { throw; }
  26:  
  27: }
  28:  
  29:  
  30:  
  31: internal static unsafe void SetPixel(BitmapData Data, int x, int y,Color PixelColor,int PixelSizeInBytes)
  32:  
  33: {
  34:  
  35:     try
  36:  
  37:     {
  38:  
  39:         byte* DataPointer = (byte*)Data.Scan0;
  40:  
  41:         DataPointer = DataPointer + (y * Data.Stride) + (x * PixelSizeInBytes);
  42:  
  43:         if (PixelSizeInBytes == 3)
  44:  
  45:         {
  46:  
  47:             DataPointer[2] = PixelColor.R;
  48:  
  49:             DataPointer[1] = PixelColor.G;
  50:  
  51:             DataPointer[0] = PixelColor.B;
  52:  
  53:             return;
  54:  
  55:         }
  56:  
  57:         DataPointer[3] = PixelColor.A;
  58:  
  59:         DataPointer[2] = PixelColor.R;
  60:  
  61:         DataPointer[1] = PixelColor.G;
  62:  
  63:         DataPointer[0] = PixelColor.B;
  64:  
  65:     }
  66:  
  67:     catch { throw; }
  68:  
  69: }

That code gets/sets our individual pixels respectively. In it you'll notice that we finally use the unsafe keyword. Up until now we didn't use any pointers, but here we're using them to point to the actual data. The BitmapData class contains an IntPtr called Scan0. This is the actual data of our image... Well the pointer to our actual data. The Stride property tells us the actual width of the image. You see, if we have an image that is 207 pixels wide, it might not actually be 207 pixels wide. Images, to make them faster to manipulate and load, tend to have actual widths that are multiple of 4s (although with 64 bit processors, I wonder if it would be a multiple of 8...). So in reality the image would request enough memory such that the width was 208. The extra pixel would just be sitting out there, not doing anything. And in normal managed code, it wouldn't matter, but in unsafe pointer land we need to know about it. So using some pointer arithmetic (y*ActualWidthInBytes+x*PixelSizeInBytes), we can get the actual location of the pixel that we want. From here, you'll notice that we're accessing it in reverse order (BGR instead of RGB). I don't know why this is done in memory, but I do remember that bitmaps are actually stored in reverse order. The bottom right pixel is the first one that you come to, stored in BGR order, and you end with the upper left (no idea why this is the case though). Anyway, we get our R, G, B, and potentially A values and create a Color object and return it.

The last thing that we have to do, once we're done manipulating our image, is unlock the Bitmap object. Once again I've built a function to help with that:

   1: internal static void UnlockImage(Bitmap Image,BitmapData ImageData)
   2: {
   3:     Image.UnlockBits(ImageData);
   4: }

That's all there is to it really. Once unlocked, we're done. We can simply replace our functions from before with these and we end up with a much faster system. For instance we can do a median filter like this:

   1: public static Bitmap MedianFilter(Bitmap OriginalImage, int Size)
   2:  
   3: {
   4:  
   5:     try
   6:  
   7:     {
   8:  
   9:         System.Drawing.Bitmap NewBitmap = new System.Drawing.Bitmap(OriginalImage.Width, OriginalImage.Height);
  10:  
  11:         BitmapData NewData = Image.LockImage(NewBitmap);
  12:  
  13:         BitmapData OldData = Image.LockImage(OriginalImage);
  14:  
  15:         int NewPixelSize = Image.GetPixelSize(NewData);
  16:  
  17:         int OldPixelSize = Image.GetPixelSize(OldData);
  18:  
  19:         Random.Random TempRandom = new Random.Random();
  20:  
  21:         int ApetureMin = -(Size / 2);
  22:  
  23:         int ApetureMax = (Size / 2);
  24:  
  25:         for (int x = 0; x < NewBitmap.Width; ++x)
  26:  
  27:         {
  28:  
  29:             for (int y = 0; y < NewBitmap.Height; ++y)
  30:  
  31:             {
  32:  
  33:                 List<int> RValues = new List<int>();
  34:  
  35:                 List<int> GValues = new List<int>();
  36:  
  37:                 List<int> BValues = new List<int>();
  38:  
  39:                 for (int x2 = ApetureMin; x2 < ApetureMax; ++x2)
  40:  
  41:                 {
  42:  
  43:                     int TempX = x + x2;
  44:  
  45:                     if (TempX >= 0 && TempX < NewBitmap.Width)
  46:  
  47:                     {
  48:  
  49:                         for (int y2 = ApetureMin; y2 < ApetureMax; ++y2)
  50:  
  51:                         {
  52:  
  53:                             int TempY = y + y2;
  54:  
  55:                             if (TempY >= 0 && TempY < NewBitmap.Height)
  56:  
  57:                             {
  58:  
  59:                                 Color TempColor = Image.GetPixel(OldData, TempX, TempY, OldPixelSize);
  60:  
  61:                                 RValues.Add(TempColor.R);
  62:  
  63:                                 GValues.Add(TempColor.G);
  64:  
  65:                                 BValues.Add(TempColor.B);
  66:  
  67:                             }
  68:  
  69:                         }
  70:  
  71:                     }
  72:  
  73:                 }
  74:  
  75:                 Color MedianPixel = Color.FromArgb(Math.MathHelper.Median<int>(RValues),
  76:  
  77:                     Math.MathHelper.Median<int>(GValues),
  78:  
  79:                     Math.MathHelper.Median<int>(BValues));
  80:  
  81:                 Image.SetPixel(NewData, x, y, MedianPixel, NewPixelSize);
  82:  
  83:             }
  84:  
  85:         }
  86:  
  87:         Image.UnlockImage(NewBitmap, NewData);
  88:  
  89:         Image.UnlockImage(OriginalImage, OldData);
  90:  
  91:         return NewBitmap;
  92:  
  93:     }
  94:  
  95:     catch { throw; }
  96:  
  97: }

The Math.MathHelper.Median function simply sorts the list and gets the middle item. But it's not that different from the code before (although a bit cleaned up). The speed difference though is rather amazing. For a small 274x350 image we go from 8 seconds to about 2 seconds. And that's with a simple update. If you wanted, you could increase this further by breaking out the GetPixel and SetPixel functions and doing the pointer arithmetic in the function itself (doing 1 add and 1 assignment instead of 2 multiplications, 2 adds, and an assignment and Color object creation makes things run a bit faster). But still, for a simple update that's a big speed increase.

So try it out. Copy it into your IDE and compile it (you may want to set up a function call, load up an image, save it, etc. so it's at least a somewhat interesting test), I'll wait... Oh, it failed didn't it? The reason for this is because you must compile unsafe code using /unsafe (just go into the properties of the project, build tab, and check the Allow unsafe code box). OK, now try compiling it and you should see a nice, much faster, median filter applied to your image. So try it out, leave feedback, and happy coding.



Comments

Greg
November 05, 2011 10:30 PM
From http://support.microsoft.com/kb/q81498/ :"Red, green, and blue bytes are in reverse order (red swaps position with blue) from the Windows convention. This is another leftover from Presentation Manager compatibility. "PM it appears basically turned into OS/2, and in it coordinate (0,0) was at the bottom right, causing these arrays to be in reverse order form what you'd expect in Windows.