Tuesday, 24 March 2009

Speeding up GetPixel with Unsafe Code

Although GDI+ provides a method, GetPixel, for getting the colour of a particular pixel at a particular set of co-ordinates, you'll quickly discover how slow it is when you come to examine every pixel in even an average sized bitmap.

This is due to
GetPixel locking, then unlocking the bitmap into system memory every time GetPixel is called. The process of locking and unlocking the bitmap itself is what takes the time.

However, with the aid of a little unsafe code, you can easily speed this up. For those unfamiliar with the unsafe keyword, in C#, putting the keyword unsafe before a method generally means it uses pointers. By default, the Visual Studio IDE will not let you compile unsafe code. You'll need to either use the /unsafe compiler switch if compiling from the command line, or tick the "Allow unsafe code" checkbox in the project properties if you're using the Visual Studio IDE:




So why the need for unsafe code?

Instead of using
GetPixel to get the colour of each pixel, we'll lock the bitmap into system memory, which will then give us direct access to the bitmap data via the BitmapData class and the use of a byte pointer. Only when we're finished with the data will we unlock the data, thus saving us potentially thousands of locking and unlocking operations.

To illustrate the speed difference I've written a small program which makes use of two functions. One returns a list of Colors obtained by using the existing .NET
GetPixel method. The second returns a list of Colors via unsafe code.

The speed of the two functions is also measured and displayed crudely.


private void TestGetPixelMethods()
{
Bitmap B = (Bitmap)Bitmap.FromFile(@"C:\MyImage.jpg");

// Execute and Time Managed Code
DateTime StartTime = DateTime.Now;
GetAllPixelsSafe(B);
DateTime FinishTime = DateTime.Now;
TimeSpan Duration = FinishTime - StartTime;
Debug.WriteLine(string.Format("GetAllPixelsSafe : {0} ", Duration));

// Execute and Time unsafe code
StartTime = DateTime.Now;
GetAllPixelsUnsafe(B);
FinishTime = DateTime.Now;
Duration = FinishTime - StartTime;
Debug.WriteLine(string.Format("GetAllPixelsUnsafe: {0} ", Duration));
Debug.WriteLine(string.Empty);
}

/// <summary>
/// Returns a list of Colors representing each pixel in the passed image
/// </summary>
private List<Color> GetAllPixelsSafe(Bitmap b)
{
List<Color> ColorList = new List<Color>();

for (int x = 0; x < b.Width; x++)
{
for (int y = 0; y < b.Height; y++)
{
ColorList.Add(b.GetPixel(x, y));
}
}

return ColorList;
}

/// <summary>
/// Returns a list of Colors representing each pixel in the passed image
/// Uses unsafe code
/// </summary>
private unsafe List<Color> GetAllPixelsUnsafe(Bitmap b)
{
Rectangle Rect = new Rectangle(0, 0, b.Width, b.Height);
BitmapData TempBitmapData =
b.LockBits(Rect, ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);

byte* PixelPointer = null; // Pointer to our pixel data
int ColorPlanes = 3; // 3 Color Planes in a 24bpp image
List<Color> ColorList = new List<Color>();

for (int x = 0; x < b.Width; x++)
{
for (int y = 0; y < b.Height; y++)
{
PixelPointer = (byte*)TempBitmapData.Scan0 + y * TempBitmapData.Stride + ColorPlanes * x;
ColorList.Add(Color.FromArgb(PixelPointer[2], PixelPointer[1], PixelPointer[0]));
}
}

b.UnlockBits(TempBitmapData);

return ColorList;
}

When
TestGetPixelMethods is called, the two pixel reading methods are called in turn, and the length of time elapsed for each is output to the debug console.

As this is just an example, note that
GetAllPixelsUnsafe assumes that a 24bpp image is passed. If a 32bpp image were to be passed, PixelFormat.Format32bppArgb would be used and the integer ColorPlanes would need to be set to 4, as a 32bpp image also contains an alpha channel.

Here is the output to my debug console:



Although the measurements are crude, notice that in most cases the method that uses the .NET GetPixel function is taking 0.266s. The method that is using unsafe code is taking 0.0625s. That's about 4.25 times faster, and well worth taking the trouble to implement with image manipulation code that works at the per-pixel level.

Of course, a similar function could be written to replace
SetPixel, which, armed with the above knowledge, you should now be able to implement yourself!

kick it on DotNetKicks.com

Labels:

Bookmark and Share

0 Comments:

Post a Comment



<< Home