This is part 2 of my heatmap tutorial for WPF using C#. I’m going to explain how to color the mask that we created in the first part of the tutorial. To do this, we will need to create a gradient and then replace the colors one by one. Surprisingly, (to me at least) this goes really quickly. I mean, who would think iterating through a for loop on a byte[] with a length of 8.7 million would be quick?
Previously I wrote Heatmaps in WPF using C#: Part 1 The Mask. That part illustrates creating a black and white mask - the level of whiteness indicates heat while the darker a pixel is indicates coolness. Now we need to convert that to something a little more colorful.
Update: There is also a 3rd part: Heatmaps in WPF using C#: Part 3 Mapping
The first step is to create the gradient. There are probably a variety of ways to do this but I chose to use a Color[] with a length of 256. My black and white mask was created using a maximum of 256 colors so once the gradient is built, we can correlate the colors pixel by pixel and replace them.
The way it will work is like this: The mask will have RGB colors ranging from 255,255,255 (white) to 0,0,0 (black). My gradient will therefore have the “hottest” color in the 255 position of my array and the “coldest” color in th 0 position. You can try importing a graphic and creating a color palette with that - I had difficulty getting it just right so I went ahead and just created it programmatically. This gradient goes from White to Yellow to Orange to Red and finally to Purple.
private void CreateGradientPalette()
{
Color[] colors = new Color[256];
//Create white to yellow in positions 0-63
for (int i = 0; i < 64; i++)
{
colors[i] = Color.FromRgb(255, 255, (byte)(255 - (i * 4)));
}
//create yellow to orange in positions 64-127
for (int i = 0; i < 64; i++)
{
colors[i + 64] = Color.FromRgb(255, (byte)(255 - (i * 2)), 0);
}
//create orange to red in positions 128-191
for (int i = 0; i < 64; i++)
{
colors[i + 128] = Color.FromRgb(255, (byte)(128 - (i * 2)), 0);
}
//create red to purple in positions 192-255
for (int i = 0; i < 64; i++)
{
colors[i + 192] = Color.FromArgb(
(byte)(i == 63 ? 0 : 255 - i * 4),
(byte)(255 - (i * 2)), 0, (byte)(i * 2));
}
gradientPalette = new BitmapPalette(colors.ToList());
}
You’ll notice that I’m also assigning the alpha value - leaving it at full strength until it fades out at the end. You can modify this code to create any kind of gradient you’d like if you take the time to learn RGB values of the colors you are trying to blend between.
The process to recolor the image in WPF is something I really struggled with. I tried a variety of techniques but nothing really worked. This method for replacing colors seems a little brutish - I would think there is a more elegant way to go about it, but this is the only way I’ve found so far that actually works.
OK, so now that we have created our new color gradient palette, let’s use it. We are going to have to create a byte array that will store the pixel data for our entire image and then loop through it and modify that pixel data one by one. Actually it’s done 4×1 - The byte array will contain 4 positions for each pixel - one for each of it’s red, green, and blue values, and one for the alpha value. It’s all reversed so in the array it will go Blue, Green, Red, Alpha, Blue, Green, Red, Alpha, etc, etc.
private void Colorize(RenderTargetBitmap renderer)
{
int buffer = 8700000;
byte[] pixels = new byte[buffer];
int stride = 8000;
renderer.CopyPixels(pixels, stride, 0);
Color c;
for (int i = 0; i < buffer; i += 4)
{
c = gradientPalette.Colors[255 - pixels[i]];
pixels[i] = c.B;
pixels[i + 1] = c.G;
pixels[i + 2] = c.R;
if (c.A >= 128)
{
pixels[i + 3] = 255;
}
else
{
pixels[i + 3] = (byte)(c.A * 2);
}
}
source = BitmapSource.Create(size.X, size.Y,
72, 72, PixelFormats.Pbgra32, null, pixels, stride);
}
Let’s explain some parts of this - first off, I have no idea what stride is. Some quick research suggests that it has to do with how the data in this large array is stored in memory (see: Stride of an Array). All I know is that Visual Studio will throw an error if you have it set too low and tell you it has to be of at least a certain size, so I just adjust it up to that size when that happens.
The same goes for the actual byte array size (buffer in my code). you would think that it would be length x width x 4 but that doesn’t seem to always be the case. It will tell you if its too small and what the minimum value must be.
You’ll notice that I pass in a RenderTargetBitmap - this is the the same RenderTargetBitmap that I created in the last part of this tutorial - the black and white mask image. Once the pixels are copied in to the byte array we then iterate through the entire thing in chunks of 4. Since I’m starting at 0 and grabbing every fourth element, pixels[i] will always be the Blue value of that pixel. Since it’s a grayscale image, Blue, Green and Red will always be equal so this is somewhat inconsequential. Because of how I constructed my color gradient I need the inverse value, hence the 255 - pixel[i]. This means, when the value is white (255) I’ll grad the color in the 0 position of the gradientPalette’s color array.
The other slightly strange part is where I handle transparency. You can do it anyway you’d like - in this example I don’t start applying transparency until I get to the darkest half of the colors and then it’s a rather steady progression to full transparent. One thing to note is that in this example, even thought black is set to full transparent, in my working example it still comes through and tints everything slightly purple. I’m not sure if it’s an error in my code or something else but it doesn’t quite work the way I want it to just yet.
At the end we take the pixel array and convert it back into a BitmapSource object.
You can now take this source object and assign it to an image on your form or create a new image using this source and add it to your StackPanel or whatever it is you use on your form. If everything went well you should have a nice colored image.
If you like this blog please take a second and subscribe to my rss feed
Tags: c#, Code, gradient, heatmaps, pixel, recolor, RGB, visual studio, wpf
Comments: One comment
All the fields that are marked with REQ must be filled
ryan
July 22nd, 2008 at 3:19 pm
UPDATE: To fix the issue of not going fully transparent, change the PixelFormat in the bitmapSource.Create method call from PixelFormat.Pbgra32 to PixelFormat.Bgra32. This will give you a full transparency without a slight haze over all the “transparent” spots.
Leave a reply