Understanding Bitmaps
<< Back to the EasyImage Toolkit page
How-To - Understanding Bitmaps
What you will learn
This Howt-To describes how bitmaps are constructed as a data structure. With this knowledge, you can then understand how to manipulate them at a pixel level.
The EasyImages package works with Images and Bitmaps. While EasyImages gives you the means to do some simple manipulation of these bitmaps (as does C#), at some point you may want to actually access and manipulate bitmaps yourself at the pixel level. Before you do this, you need to understand how bitmaps are stored as bytes.
Note: This article is another good intro to bitmaps.
Background.
The Bitmap class provides two functions: SetPixel() and GetPixel() which lets you access particular pixels in the bitmap. While ok for poking around a few pixels, these methods are slow if you want to do some serious pixel manipulation, e.g., to examine and/or alter all pixels in an image.
Instead, you can access the bitmap data structure directly as a set of bytes that you just walk through. This will let you very quickly examining the bitmap byte by byte (and also pixel by pixel, and row/column by row/column), perhaps altering pixels as you go through them. To do this, you need to know how bitmaps are laid out as a byte structure.
The Bitmap data structure.
A few things that you need to know, along with some terminology.
- RGB. Each color is comprixed of a mix of three colors: R (red), G (green) and B (blue).
- Bits per pixel (bpp). If you have a bitmap that is (say) 24 bits per pixel, this mean that each RGB color is represented by 8 bits or 1 byte each. 8 bits means that each RGB value can be between 0-255. Also, bytes are actually ordered in B G R order. For example if we have three bytes 11111111 00000000 00000000 (i.e, B=255, G=0, R=0), then that pixel is blue.
- Image Stride. Each row in a bitmap always contains multiples of 4 bytes. If your bitmap row size is set smaller than that, then bytes are added automatically to each bitmap row to make it a multiple of 4. This length is called the Stride. For example, lets say you have a bitmap that is 15 x 15 pixels. 15 pixels / row at 3 bytes each = 45 bytes. This means that each bitmap row is actually stored in memory as (4 * 12) = 48 bytes - the last three bytes are just unused padding to make it a multiple of 4.
Code
So, to see how it works, lets look at some code. This example will walk through a bitmap, and examine it pixel by pixel. We can get the actual RGB values, but what we will do is wash out the image, i.e., if a pixel has a value above 127, we will reduce its color to 127. Here is the whole thing, and then we will describe what happens line by line.
Warning and Compiling. This code uses pointer operations. While this is the norm in langauges like C, it is definitely not for C#, which does not normally allow direct memory manipulation unless its critical sections are declared as unsafe. Even so, you have to compile the project in UNSAFE mode. To do this, you will have to select the Visual Studio 2005 Build tab in the Project Properties, and check the 'Allow Unsafe Code' checkbox. More on this shortly.
...
using System.Drawing.Imaging;
...
private void ManipulateBits (Bitmap bm)
{
System.Drawing.Imaging.BitmapData data =
bm.LockBits(new Rectangle(0, 0, bm.Width, bm.Height),
ImageLockMode.ReadWrite,
PixelFormat.Format24bppRgb);
unsafe
{
byte* imgPtr = (byte*)(data.Scan0);
for (int row = 0; row < data.Height; row ++)
{
for (int column = 0; column < data.Width; column ++)
{
byte blue = (byte) (*imgPtr);
if (blue > 127) *imgPtr = 127;
imgPtr++;
byte green= (byte) (*imgPtr);
if (green > 127) *imgPtr = 127;
imgPtr++;
byte red = (byte) (*imgPtr);
if (red > 127) *imgPtr = 127;
imgPtr++;
}
imgPtr += data.Stride - data.Width * 3;
}
}
bm.UnlockBits(data);
}
Ok, now lets go through this line by line.
1). If you want to manipulate bitmaps easily, you should include the above package.
2). We need a source bitmap. We just pass it into the method. Note that this method will alter the bitmap, so if you want to keep the original you should pass in a copy (e.g., use the Bitmap.Clone() method).
3). The BitmapData specifies the attributes of the Bitmap:
- its size,
- pixel format,
- starting address of the pixel data in memory,
- stride, i.e., length of each scan line or row in memory.
Its arguments as used below are a rectangle with the same width and height as your bitmap, a lock mode (which in this case allowsy you to both read and write each value), and the pixel format (in this case 24bits per pixel in RGB).
bm.LockBits(new Rectangle(0, 0, bm.Width, bm.Height),
ImageLockMode.ReadWrite,
PixelFormat.Format24bppRgb);
4). We are now about to do pointer operations. As previously mentioned, we can only do this in C# when we declare the the code containing these opeartons as unsafe. Note that we also to compile the project in UNSAFE mode! To do this, you will have to select the Visual Studio 2005 Build tab in the Project Properties, and check the 'Allow Unsafe Code' checkbox. Also a warning... if you get this code wrong, you can do horrible things, i.e., writing memory you are not supposed to! Be careful when writing unsafe code.
{
5). Before we start going through our bytes, we need to know where the bytes begin. We get theis through the data.Scan0 method, which returnes a pointer to the first byte in our bitmap. Note that imgPtr is a pointer to each byte, while *imgPtr is the contents of each byte.
6). Now we will walk through each row and each column of the bitmap. The data.Height is the height of our bitmap in pixels, while the data.Width is the number of pixels (not bytes!) in our bitmap.
{
for (int column = 0; column < data.Width; column ++)
{
7). We are using the PixelFormat.Format24bppRgb format. Thus there are 3 bytes for each Pixel, but surprisingly, these are in BGR (Blue, Green, Red) order rather than RGB order! Remember that imgPtr is a pointer to each byte, and *imgPtr is the contents of each byte. For clarity, we create a new byte with the color name and set it to the byte's contents. To produce the washout effect, if a byte's color value is greater than 127 (it could be as high as 255), we limit it to 127. Note that we could have also written this without using the byte blue declaration, e.g., as if (*imgPter > 127) *imgPtr = 127; Note too that this code is repeated in the Columns for loop, so it will iterate across every pixel (3 bytes each) in the row.
if (blue > 127) *imgPtr = 127;
imgPtr++;
byte green= (byte) (*imgPtr);
if (green > 127) *imgPtr = 127;
imgPtr++;
byte red = (byte) (*imgPtr);
if (red > 127) *imgPtr = 127;
imgPtr++;
8) At this point, we have reached the end of the row, so we have to get to the beginning of the next row. Remember that there may be some empty bytes after our pixels at the end of the row (see Stride above), so we have to account for this. What we do is add the data.Stride (i.e. all of the bytes in a row) which gets us to the same position in the next row, and then subtract the data.Width * 3 (the number of pixels * 3 bytes each). This brings us back to the beginning of the new row.
imgPtr += data.Stride - data.Width * 3;
}
9) We have now completed the last row in our bitmap, so all that is left to do is unlock the bitmap from system memory.
bm.UnlockBits(data);
}