Title: Use the Bitmap24 class to manipulate image pixels very quickly in C#
The Bitmap class's GetPixel and SetPixel methods let you easily manipulate image pixels. They're easy to use, but they're also relatively slow.
This program inverts the pixels in an image. When you click the No Lock Bits button, the program uses the following code to loop through the pixels using GetPixel and SetPixel to invert the image.
// Invert the image without Lockbits.
private void btnNoLockBits_Click(object sender, EventArgs e)
{
Cursor = Cursors.WaitCursor;
Stopwatch watch = new Stopwatch();
watch.Start();
Bitmap bm = new Bitmap(picHidden.Image);
for (int Y = 0; Y < bm.Height; Y++)
{
for (int X = 0; X < bm.Width; X++)
{
Color clr = bm.GetPixel(X, Y);
clr = Color.FromArgb(
255 - clr.R,
255 - clr.G,
255 - clr.B);
bm.SetPixel(X, Y, clr);
}
}
picVisible.Image = bm;
watch.Stop();
Cursor = Cursors.Default;
lblElapsed.Text = watch.Elapsed.TotalSeconds.ToString("0.000000") + " seconds";
}
This code creates a Bitmap object that holds a copy of the image displayed in the picHidden PictureBox. It then creates and starts a Stopwatch.
It then loops through the image's pixels and uses the Bitmap object's GetPixel method to get the pixel's value. It inverts each of the pixel's color components and uses SetPixel to save the new value in the Bitmap.
Finally the code displays the result and the elapsed time.
When you click the Lock Bits button, the program uses the following code to invert the image.
// Invert the image using Lockbits.
private void btnLockBits_Click(object sender, EventArgs e)
{
const byte BYTE_255 = 255;
Bitmap bm = new Bitmap(picHidden.Image);
Cursor = Cursors.WaitCursor;
Stopwatch watch = new Stopwatch();
watch.Start();
// Make a Bitmap24 object.
Bitmap24 bm24 = new Bitmap24(bm);
// Lock the bitmap.
bm24.LockBitmap();
// Invert the pixels.
for (int i = 0; i < bm.Height * bm24.RowSizeBytes; i++)
{
// bm24.ImageBytes[i] = Convert.ToByte(BYTE_255 - bm24.ImageBytes[i]);
bm24.ImageBytes[i] = (byte)(BYTE_255 - bm24.ImageBytes[i]);
}
// Unlock the bitmap.
bm24.UnlockBitmap();
picVisible.Image = bm;
DateTime stop_time = DateTime.Now;
Cursor = Cursors.Default;
watch.Stop();
lblElapsed.Text = watch.Elapsed.TotalSeconds.ToString("0.000000") + " seconds";
}
The key pieces of code are in the middle shown in blue. The program creates a Bitmap24 object (described shortly) and calls its LockBitmap method to lock the image in memory so it won't move while the program manipulates it.
The code then loops through the image's pixels using the object's ImageBytes array to access the bytes in the image and inverts them. This array contains the red, green, and blue color components of the image's pixel values. The bytes are all strung out in the array in a single long list. For example, the first three bytes give the blue, green, and red color components for the image's first pixel.
After it has inverted all of the pixels, the code finishes by calling the Bitmap24 object's UnlockBitmap method to unlock the bitmap. The program then displays the resulting bitmap.
The following code shows the Bitmap24 class. This class represents a bitmap with 24-bit pixels that use one byte for each pixel's red, green, and blue color component.
public class Bitmap24
{
// Provide public access to the picture's byte data.
public byte[] ImageBytes;
public int RowSizeBytes;
public const int PixelDataSize = 24;
// A reference to the Bitmap.
private Bitmap m_Bitmap;
// Save a reference to the bitmap.
public Bitmap24(Bitmap bm)
{
m_Bitmap = bm;
}
// Bitmap data.
private BitmapData m_BitmapData;
// Lock the bitmap's data.
public void LockBitmap()
{
// Lock the bitmap data.
Rectangle bounds = new Rectangle(
0, 0, m_Bitmap.Width, m_Bitmap.Height);
m_BitmapData = m_Bitmap.LockBits(bounds,
ImageLockMode.ReadWrite,
PixelFormat.Format24bppRgb);
RowSizeBytes = m_BitmapData.Stride;
// Allocate room for the data.
int total_size = m_BitmapData.Stride * m_BitmapData.Height;
ImageBytes = new byte[total_size];
// Copy the data into the ImageBytes array.
Marshal.Copy(m_BitmapData.Scan0, ImageBytes, 0, total_size);
}
// Copy the data back into the Bitmap
// and release resources.
public void UnlockBitmap()
{
// Copy the data back into the bitmap.
int total_size = m_BitmapData.Stride * m_BitmapData.Height;
Marshal.Copy(ImageBytes, 0, m_BitmapData.Scan0, total_size);
// Unlock the bitmap.
m_Bitmap.UnlockBits(m_BitmapData);
// Release resources.
ImageBytes = null;
m_BitmapData = null;
}
}
The class's ImageBytes array provides direct access to the bitmap's pixel data. RowSizeBytes tells you how many bytes are in a row. Note that this may include some padding bytes to align the data in memory, so some of the bytes at the end of each row may not actually be part of a pixel's data.
The LockBitmap method calls the bitmap's LockBits method to lock the object's data. This prevents the data from moving while the program is manipulating it and is important before a program starts modifying the bitmap data. The program allocates an array big enough to hold the pixel data and then calls Marshal.Copy to copy the data from the bitmap into this array.
Subroutine UnlockBitmap calls Marshal.Copy to copy the data from the pixel array back into the Bitmap object. It then calls the object's UnlockBits method.
Note: This program takes about 3.5 times as long to invert its image using GetPixel and SetPixel as it does using LockBits. I was surprised to find, however, that a comparable Visual Basic program was about 20 times faster than the C# version using LockBits! Although C# is not generally faster than Visual Basic, it was surprising that C# was this much slower in this example.
As far as I can tell, the reason for the difference is in the loop that inverts the byte values, specifically this statement:
bm24.ImageBytes[i] = (byte)(BYTE_255 - bm24.ImageBytes[i]);
In C#, operations that involve byte values return an integer so this code needs to use (byte) to cast the result back into a byte. In Visual Basic, subtracting a byte value from another byte value results in a new byte value, so no conversion is needed. I think the big difference in time is caused by C# promoting the byte values to integers and then the code converting the result back into a byte.
If you discover a reasonable solution to this problem (without using unsafe methods to manipulate the byte array--that's another issue), please let me know.
Download the example to experiment with it and to see additional details.
|