BLOG.CSHARPHELPER.COM: Apply filters to images to perform edge detection, smoothing, embossing, and more in C#
Apply filters to images to perform edge detection, smoothing, embossing, and more in C#
(This is a fairly large example so you may want to spend some time walking through the code.)
In one kind of image filter, you have an array of values called the filter's kernel. For each pixel in the image, you center the array over that pixel. You then multiply the value of each pixel under the kernel by the corresponding kernel value. You add them up, divide by a "weight" value, and optionally add an offset to make the result look nicer. (For example, embossing filters tend to make the result very dark so you can add 127 to move the result to a mostly neutral value.) The result gives you the new value for the center pixel.
To handle color, treat the red, green, and blue color components separately.
This example demonstrates several filters. The program is based on the example Manipulate 32-bit image pixels using a class with simple pixel get and set methods in C#. That example shows how to build a Bitmap32 class to manipulate bitmaps quickly.
This example adds a few things to that class. First, it adds a Filter class to represent a filter. The following code shows the basic class. The class also provides two method that are useful for building certain kinds of kernels. They're fairly simple so you can read the code to see how they work.
// A public class to represent filters. public class Filter { public float[,] Kernel; public float Weight, Offset; }
The biggest addition to the Bitmap32 class is the ApplyFilter method shown in the following code. This code loops through the image's pixels applying a filter to each. The only tricky part is figuring out the bounds to loop over to apply the filter correctly.
// Apply a filter to the image. public Bitmap32 ApplyFilter(Filter filter, bool lock_result) { // Make a copy of this Bitmap32. Bitmap32 result = this.Clone();
// Lock both bitmaps. bool was_locked = this.IsLocked; this.LockBitmap(); result.LockBitmap();
// Apply the filter. int xoffset = -(int)(filter.Kernel.GetUpperBound(1) / 2); int yoffset = -(int)(filter.Kernel.GetUpperBound(0) / 2); int xmin = -xoffset; int xmax = Bitmap.Width - filter.Kernel.GetUpperBound(1); int ymin = -yoffset; int ymax = Bitmap.Height - filter.Kernel.GetUpperBound(0); int row_max = filter.Kernel.GetUpperBound(0); int col_max = filter.Kernel.GetUpperBound(1);
for (int x = xmin; x <= xmax; x++) { for (int y = ymin; y <= ymax; y++) { // Skip the pixel if any under the kernel // is completely transparent. bool skip_pixel = false;
// Apply the filter to pixel (x, y). float red = 0, green = 0, blue = 0; for (int row = 0; row <= row_max; row++) { for (int col = 0; col <= col_max; col++) { int ix = x + col + xoffset; int iy = y + row + yoffset; byte new_red, new_green, new_blue, new_alpha; this.GetPixel(ix, iy, out new_red, out new_green, out new_blue, out new_alpha);
// See if we should skip this pixel. if (new_alpha == 0) { skip_pixel = true; break; }
red += new_red * filter.Kernel[row, col]; green += new_green * filter.Kernel[row, col]; blue += new_blue * filter.Kernel[row, col]; } if (skip_pixel) break; }
if (!skip_pixel) { // Divide by the weight, add the offset, and // make sure the result is between 0 and 255. red = filter.Offset + red / filter.Weight; if (red < 0) red = 0; if (red > 255) red = 255;
green = filter.Offset + green / filter.Weight; if (green < 0) green = 0; if (green > 255) green = 255;
blue = filter.Offset + blue / filter.Weight; if (blue < 0) blue = 0; if (blue > 255) blue = 255;
// Set the new pixel's value. result.SetPixel(x, y, (byte)red, (byte)green, (byte)blue, this.GetAlpha(x, y)); } } }
// Unlock the bitmaps. if (!lock_result) result.UnlockBitmap(); if (!was_locked) this.UnlockBitmap();
// Return the result. return result; }
The new version of Bitmap32 also defines several pre-built filters. For example, the following code returns a simple embossing filter.
// A standard embossing filter. public static Filter EmbossingFilter { get { return new Filter() { Weight = 1, Offset = 127, Kernel = new float[,] { {-1, 0, 0}, {0, 0, 0}, {0, 0, 1}, } }; } }
When you apply this filter to an area of uniform color, the -1 and 1 entries in the kernel cancel each other out (since the pixels they are under have about the same value) so you get a result close to 0. You then divide by 1 (the weight) and add an offset to move the result toward a neutral gray value.
In places where there is a change in color, the -1 and 1 don't cancel and you get a value that's either a bit lighter or darker than the neutral value in the middle. The result gives an embossed appearance. (Note that some of the filters, including the embossing filters, often look better if you convert the image to grayscale before applying the filter.)
The main program's ApplyFilter method shown in the following code applies a filter. It makes a Bitmap32 object to represent the current image. It then calls its ApplyFilter method to apply the filter and it displays the result.
In addition to support for these kinds of filters, the new Bitmap32 class provides new Average, Grayscale, ClearRed, ClearGreen, ClearBlue, and Invert methods.
See the code for additional details. I know this is a big program and a lot to absorb but I don't really want to make this blog entry too terribly huge.
Download the program and experiment with it. It's pretty fun! If time permits, I'll add more to this program later. For example, I may add some non-linear image processing tools that let you warp an image in interesting ways.
Comments