|
|||||||||||||||||||||
|
|||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
Welcome to this, my first article in C#, and the first in a series on image processing. I figure between Nish and Chris Losinger waiting to bust my chops, I should learn as much as anyone from this article. OverviewThe purpose of the series will be to build a class that allows any C# programmer
access to common, and not so common, image processing functionality. The
reason we are doing it in C# is simply that I want to learn it, but the
functionality we use is available through GDI+ in C++, and indeed the code to
do the same thing using a The AppThe app we will use is a basic Windows Forms application ( it is in fact my
first ). I've included code to load and save images using GDI+, and a
menu to which I will add filters. The filters are all static functions in
a class called Pixel Access, a.k.a. Unsafe code, and other nastinessMy first real disappointment in building this code was to find that the The other down side is that this class is meant to be portable, but anyone using it will need to change their project settings to support compiling of unsafe code. A quirk I noticed from the first beta of GDI+ continues to this day, namely requesting a 24bitRGB image will return a 24bitBGR image. BGR ( that is, pixels are stored as blue, green, red values ) is the way Windows stored things internally, but I'm sure more than a few people will get a surprise when they first use this function and realise they are not getting what they asked for. Invert FilterHere, then is our first, and most simple filter - it simply inverts a bitmap, meaning that we subtract each pixel value from 255. public static bool Invert(Bitmap b)
{
// GDI+ still lies to us - the return format is BGR, NOT RGB.
BitmapData bmData = b.LockBits(new Rectangle(0, 0, b.Width, b.Height),
ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
int stride = bmData.Stride;
System.IntPtr Scan0 = bmData.Scan0;
unsafe
{
byte * p = (byte *)(void *)Scan0;
int nOffset = stride - b.Width*3;
int nWidth = b.Width * 3;
for(int y=0;y < b.Height;++y)
{
for(int x=0; x < nWidth; ++x )
{
p[0] = (byte)(255-p[0]);
++p;
}
p += nOffset;
}
}
b.UnlockBits(bmData);
return true;
}
This example is so simple that it doesn't even matter that the pixels are out of
order. The The key thing when image processing is to do as much outside the loop as
possible. An image of 1024x768 will contain 786432 individual pixels, a
lot of extra overhead if we add a function call, or create a variable inside
the loops. In this case, our That should leave the rest of the code pretty straightforward. We are stepping through each pixel, and reversing it, as you can see here:
Grayscale filterSubsequent examples will show less and less of the code, as you become more familiar with what the boilerplate part of it does. The next, obvious filter is a grayscale filter. You might think that this would involve simply summing the three color values and dividing by three, but this does not take into effect the degree to which our eyes are sensitive to different colors. The correct balance is used in the following code: unsafe
{
byte * p = (byte *)(void *)Scan0;
int nOffset = stride - b.Width*3;
byte red, green, blue;
for(int y=0;y < b.Height;++y)
{
for(int x=0; x < b.Width; ++x )
{
blue = p[0];
green = p[1];
red = p[2];
p[0] = p[1] = p[2] = (byte)(.299 * red
+ .587 * green
+ .114 * blue);
p += 3;
}
p += nOffset;
}
}
As you can see, we are now iterating through the row
A note on the effects of filtersIt's worthwhile observing before we continue that the Invert filter is the only non-destructive filter we will look at. That is to say, the grayscale filter obviously discards information, so that the original bitmap cannot be reconstructed from the data that remains. The same is also true as we move into filters which take parameters. Doing a Brightness filter of 100, and then of -100 will not result in the original image - we will lose contrast. The reason for that is that the values are clamped - the Brightness filter adds a value to each pixel, and if we go over 255 or below 0 the value is adjusted accordingly and so the difference between pixels that have been moved to a boundary is discarded. Brightness filterHaving said that, the actual filter is pretty simple, based on what we already know: for(int y=0;y<b.Height;++y)
{
for (int x = 0; x < nWidth; ++x)
{
nVal = (int) (p[0] + nBrightness);
if (nVal < 0) nVal = 0;
if (nVal > 255) nVal = 255;
p[0] = (byte)nVal;
++p;
}
p += nOffset;
}
The two examples below use the values of 50 and -50 respectively, both on the original image
ContrastThe operation of contrast is certainly the most complex we have attempted.
Instead of just moving all the pixels in the same direction, we must either
increase or decrease the difference between groups of pixels. We accept
values between -100 and 100, but we turn these into a if (nContrast < -100) return false;
if (nContrast > 100) return false;
double pixel = 0, contrast = (100.0+nContrast)/100.0;
contrast *= contrast;
My policy has been to return false when invalid values are passed in, rather than clamp them, because they may be the result of a typo, and therefore clamping may not represent what is wanted, and also so users can find out what values are valid, and thus have a realistic expectation of what result a given value might give. Our loop treats each color in the one iteration, although it's not necessary in this case to do it that way. red = p[2];
pixel = red/255.0;
pixel -= 0.5;
pixel *= contrast;
pixel += 0.5;
pixel *= 255;
if (pixel < 0) pixel = 0;
if (pixel > 255) pixel = 255;
p[2] = (byte) pixel;
We turn the pixel into a value between 0 and 1, and subtract .5. The net result is a negative value for pixels that should be darkened, and positive for values we want to lighten. We multiply this value by our contrast value, then reverse the process. Finally we clamp the result to make sure it is a valid color value. The following images use contrast values of 30 and -30 respectively.
GammaFirst of all, an explanation of this filter. The following explanation of gamma was found on the web: In the early days of television it was discovered that CRT's do not produce a light intensity that is proportional to the input voltage. Instead, the intensity produced by a CRT is proportional to the input voltage raised to the power gamma. The value of gamma varies depending on the CRT, but is usually close to 2.5. The gamma response of a CRT is caused by electrostatic effects in the electron gun. In other words, the blue on my screen might well not be the same as the blue on your screen. A gamma filter attempts to correct for this. It does this by building a gamma ramp, an array of 256 values for red, green and blue based on the gamma value passed in (between .2 and 5). The array is built like this: byte [] redGamma = new byte [256];
byte [] greenGamma = new byte [256];
byte [] blueGamma = new byte [256];
for (int i = 0; i < 256; ++i)
{
redGamma[i] = (byte)Math.Min(255, (int)(( 255.0
* Math.Pow(i/255.0, 1.0/red)) + 0.5));
greenGamma[i] = (byte)Math.Min(255, (int)(( 255.0
* Math.Pow(i/255.0, 1.0/green)) + 0.5));
blueGamma[i] = (byte)Math.Min(255, (int)(( 255.0
* Math.Pow(i/255.0, 1.0/blue)) + 0.5));
}
You'll note at this point in development I found the
Having built this ramp, we step through our image, and set our values to the
values stored at the indices in the array. For example, if a red value is
5, it will be set to
Color FilterOur last filter is a color filter. It is very simple - it just adds or subracts a value to each color. The most useful thing to do with this filter is to set two colors to -255 in order to strip them and see one color component of an image. I imagine by now you'd know exactly what that code will look like, so I'll give you the red, green and blue components of my son to finish with. I hope you found this article informative, the next will cover convolution filters, such as edge detection, smoothing, sharpening, simple embossing, etc. See you then !!!
| ||||||||||||||||||||