Title: Easily manipulate pixels in WPF and C#
After building the example Save bitmap files in WPF and C#, I couldn't get it out of my head. Doing the math to set the correct bytes in a one-dimensional pixel array isn't all that hard, but it is annoying and I don't see why Microsoft had to make it so unfriendly. I thought there had to be a better way to manipulate pixels, so I decided to make it a bit friendlier.
This example defines a BitmapPixelMaker class to represent a one-dimensional array that will be used to make a WriteableBitmap object. It only supports the Bgra32 format, but you could write similar classes to handle other formats (or maybe even modify the class to handle them all).
I'll describe the class in pieces below. The following code shows how the class starts.
// A class to represent WriteableBitmap pixels in Bgra32 format.
public class BitmapPixelMaker
{
// The bitmap's size.
private int Width, Height;
// The pixel array.
private byte[] Pixels;
// The number of bytes per row.
private int Stride;
// Constructor. Width and height required.
public BitmapPixelMaker(int width, int height)
{
// Save the width and height.
Width = width;
Height = height;
// Create the pixel array.
Pixels = new byte[width * height * 4];
// Calculate the stride.
Stride = width * 4;
}
...
The class stores the bitmap's width and height in private fields. The Pixels array will hold the bitmap's pixel data.
Stride is the number of bytes in a row of pixel data. For the Bgra32 format, it's just 4 bytes per pixel times the bitmap's width.
The class's constructor takes width and height as parameters. It allocates enough bytes for all of the pixels and calculates the stride for later use.
Most of the other methods manipulate pixels and are quite straightforward. They just do a little math to figure out where a byte needs to be in the Pixels array and then they get or set that byte. The following code shows how the class gets a pixel's red, green, and blue values.
// Get a pixel's value.
public void GetPixel(int x, int y, out byte red,
out byte green, out byte blue, out byte alpha)
{
int index = y * Stride + x * 4;
blue = Pixels[index++];
green = Pixels[index++];
red = Pixels[index++];
alpha = Pixels[index];
}
The code starts by calculating the index of the first byte for this pixel. The index includes y * Stride to skip bytes used by earlier rows in the pixel data. It adds x * 4 to skip the 4 bytes for each of the pixels to the left of the target pixel in its row.
Next, the code simply copies the target pixel's byte data into its red, green, blue, and alpha return parameters. The only thing to note here is that the Bgra32 format stores a pixel's color components in the order: blue, green, red, alpha.
If you only need to get one color component for a pixel, the GetPixel method is a bit heavy-handed, so the class also includes methods to get the red, green, blue, and alpha components separately. For example, the following code shows the GetRed method.
public byte GetRed(int x, int y)
{
return Pixels[y * Stride + x * 4 + 2];
}
The class defines corresponding methods to manipulate pixels by setting byte values. The SetPixel method is similar to GetPixel except it sets the byte values. The class also provides methods to set the red, green, blue, and alpha components separately. For example, the following code sets the green component.
public void SetGreen(int x, int y, byte green)
{
Pixels[y * Stride + x * 4 + 1] = green;
}
The following SetColor method sets every pixel's bytes to represent the same color
// Set all pixels to a specific color.
public void SetColor(byte red, byte green, byte blue,
byte alpha)
{
int num_bytes = Width * Height * 4;
int index = 0;
while (index < num_bytes)
{
Pixels[index++] = blue;
Pixels[index++] = green;
Pixels[index++] = red;
Pixels[index++] = alpha;
}
}
// Set all pixels to a specific opaque color.
public void SetColor(byte red, byte green, byte blue)
{
SetColor(red, green, blue, 255);
}
The first version of the method simply loops through the pixel data and sets each bytes' color data. The second version calls the first to set the pixels to the same opaque color.
The following code shows the end of the class. The MakeBitmap method converts the pixel data into a WriteableBitmap object.
// Use the pixel data to create a WriteableBitmap.
public WriteableBitmap MakeBitmap(double dpiX, double dpiY)
{
// Create the WriteableBitmap.
WriteableBitmap wbitmap = new WriteableBitmap(
Width, Height, dpiX, dpiY,
PixelFormats.Bgra32, null);
// Load the pixel data.
Int32Rect rect = new Int32Rect(0, 0, Width, Height);
wbitmap.WritePixels(rect, Pixels, Stride, 0);
// Return the bitmap.
return wbitmap;
}
}
This method creates a new WriteableBitmap object of the correct size and dots per inch vertically and horizontally. It uses the Bgra32 format.
Next, it creates an Int32Rect to represent the part of the bitmap that should be written and uses the bitmap's WritePixels method to write the pixel data into the bitmap. Finally the method returns the result.
The following shows part of the code that the program uses to test the BitmapPixelMaker.
private void Window_Loaded(object sender, RoutedEventArgs e)
{
const int width = 240;
const int height = 240;
// Make the BitmapPixelMaker.
BitmapPixelMaker bm_maker = new BitmapPixelMaker(width, height);
// Clear to black.
bm_maker.SetColor(0, 0, 0);
... Use BitmapPixelMaker methods to set pixel values ...
// Convert the pixel data into a WriteableBitmap.
WriteableBitmap wbitmap = bm_maker.MakeBitmap(96, 96);
// Create an Image to display the bitmap.
Image image = new Image();
image.Stretch = Stretch.None;
image.Margin = new Thickness(0);
grdMain.Children.Add(image);
// Set the Image source.
image.Source = wbitmap;
}
The code creates a BitmapPixelMaker object and calls its SetColor method to set all of the bitmap's pixels to black. It then uses BitmapPixelMaker methods to set pixel colors. Download the example to see how it works.
After the program has set the pixels' values, it calls the BitmapPixelMaker object's MakeBitmap method to create the WriteableBitmap.
The program finishes by creating an Image and displaying the WriteableBitmap in it.
The process is still a bit cumbersome, but at least the BitmapPixelMaker class makes working with the pixel data a lot easier.
Download the example to experiment with it and to see additional details.
|