Article purpose
In this article we discover creating basic image filters implemented by directly manipulating the ARGB colour values associated with an image’s pixels. The different types of filters discussed are: Grayscale, Transparency, Image Negative and Sepia tone. All filters are implemented as extension methodstargeting the Image class, as well as the Bitmap class as the result of inheritance and upcasting.
ARGB Overview
ARGB is an abbreviation for the term: “Alpha, Red, Green and Blue”. ARGBrefers to four colour components represented by each pixel that forms part of an image. In C# 32 bit ARGB images are a fairly common occurrence, each pixel being of a fixed size, namely 32 bits or 4 bytes, which also equates to a standard integer. Each colour component consists of 8 bits or 1 byte, equating to a range of possible values starting at 0 inclusive and a maximum value of 255 inclusive. It can thus be logically deduced that each of the four ARGBcomponents can be expressed as a value ranging from 0 to 255 inclusive.
A pixel’s alpha component represents a level of transparency, 255 being no transparency and 0 being completely transparent. The combination of Red,Green and Blue values together represent a single colour, the colour associated with an individual pixel.
We can apply filtering on an image by manipulating the individual Alpha, Red,Green and Blue components of each pixel.
Extracting the ARGB components of each pixel in an image
In C# an Image’s ARGB components are actually stored in the format Blue,Green, Red, Alpha. Before attempting to extract each pixel’s individual components we need to ensure that our source image is in fact formatted as a 32Bit ARGB image. The source code snippet listed below converts source images into 32Bit ARGB formatted images:
private static Bitmap GetArgbCopy(Image sourceImage) { Bitmap bmpNew = new Bitmap(sourceImage.Width, sourceImage.Height, PixelFormat.Format32bppArgb); using(Graphics graphics = Graphics.FromImage(bmpNew)) { graphics.DrawImage(sourceImage, new Rectangle (0, 0, bmpNew.Width, bmpNew.Height), new Rectangle (0, 0, bmpNew.Width, bmpNew.Height), GraphicsUnit.Pixel); graphics.Flush(); } return bmpNew; }
The GetArgbCopy method creates a blank memory Bitmap having the same size dimensions as the source image. The newly created Bitmap is explicitly specified to conform to a 32Bit ARGB format. By making use of a Graphicsobject of which the context is bound to the new Bitmap instance the source code draws the original image to the new Bitmap.
The Transparency Filter
The transparency filter is intended to create a copy of an image, increase the copy’s level of transparency and return the modified copy to the calling code. Listed below is source code which defines the CopyWithTransparencyextension method.
public static Bitmap CopyWithTransparency(this Image sourceImage, byte alphaComponent = 100) { Bitmap bmpNew = GetArgbCopy(sourceImage); BitmapData bmpData = bmpNew.LockBits(new Rectangle(0, 0, sourceImage.Width, sourceImage.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); IntPtr ptr = bmpData.Scan0; byte[] byteBuffer = new byte[bmpData.Stride * bmpNew.Height]; Marshal.Copy(ptr, byteBuffer, 0, byteBuffer.Length); for (int k = 3; k < byteBuffer.Length; k += 4) { byteBuffer[k] = alphaComponent; } Marshal.Copy(byteBuffer, 0, ptr, byteBuffer.Length); bmpNew.UnlockBits(bmpData); bmpData = null; byteBuffer = null; return bmpNew; }
As discussed earlier, the CopyWithTransparency method creates a 32BitARGB formatted copy of the specified source image by invoking the methodGetArgbCopy. In order to extract the image pixel data we implement theBitmapData class and the LockBits method defined by the Bitmap class. Next we create an IntPtr which references the very first pixel. BitmapData.Scan0points to the memory address of the first pixel. Next the source code instantiates a byte array which will used to represent pixel components.
The Marshal.Copy method copies bytes from memory, starting at the address of the first pixel continuing up until the last pixel. The alpha component can be specified by the calling code, or if not specified defaults to a value of 100 due to being a optional method parameter.
How much transparency results from an alpha component of 100? If the maximum value is set at 255 representing no transparency 100 expressed as a percentage of 255 equates to roughly 39.2%. Defining an alpha value of 100 will thus result in an image being roughly 60% transparent.
Notice how the for loop only affects every fourth array element, beginning at index 3. Each element in the byte array represents a pixel colour component, either Alpha, Red, Green or Blue. Remember as mentioned earlier, the components are in fact stored in the format Blue, Green, Red, Alpha. Every four array elements together represents one pixel. We only want to change the value of Alpha components, thus we start at the first pixel’s Alphacomponent at index 3 and then continue to iterate through the byte array, incrementing the index by four with each loop.
After having updated every pixel’s Alpha component the modified byte array is copied back into the actual image object and returned to the calling code.
Transparency Filter
The Grayscale Filter
The grayscale filter operates in a fashion similar to the transparency filter discussed in the previous section. The following details the source code implementation of the CopyAsGrayScale extension method.
public static Bitmap CopyAsGrayscale(this Image sourceImage) { Bitmap bmpNew = GetArgbCopy(sourceImage); BitmapData bmpData = bmpNew.LockBits(new Rectangle(0, 0, sourceImage.Width, sourceImage.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); IntPtr ptr = bmpData.Scan0; byte[] byteBuffer = new byte[bmpData.Stride * bmpNew.Height]; Marshal.Copy(ptr, byteBuffer, 0, byteBuffer.Length); float rgb = 0; for (int k = 0; k < byteBuffer.Length; k += 4) { rgb = byteBuffer[k] * 0.11f; rgb += byteBuffer[k+1] * 0.59f; rgb += byteBuffer[k+2] * 0.3f; byteBuffer[k] = (byte)rgb; byteBuffer[k + 1] = byteBuffer[k]; byteBuffer[k + 2] = byteBuffer[k]; byteBuffer[k + 3] = 255; } Marshal.Copy(byteBuffer, 0, ptr, byteBuffer.Length); bmpNew.UnlockBits(bmpData); bmpData = null; byteBuffer = null; return bmpNew; }
Notice how this filter starts iterating the pixel components at index 0. The source code assigns a weight to each colour component. The grayscale filter is achieved by adding together 11% Blue, 59% Green and 30% Red, then assigning the total value to each colour component. Transparency is set to 255, effectively disabling any level of transparency.
Grayscale Filter
The Sepia Tone Filter
The sepia tone filter is implemented in the extension methodCopyAsSepiaTone. Notice how this method follows the same convention as the previously discussed filters. The source code listing is detailed below.
public static Bitmap CopyAsSepiaTone(this Image sourceImage) { Bitmap bmpNew = GetArgbCopy(sourceImage); BitmapData bmpData = bmpNew.LockBits(new Rectangle(0, 0, sourceImage.Width, sourceImage.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); IntPtr ptr = bmpData.Scan0; byte[] byteBuffer = new byte[bmpData.Stride * bmpNew.Height]; Marshal.Copy(ptr, byteBuffer, 0, byteBuffer.Length); byte maxValue = 255; float r = 0; float g = 0; float b = 0; for (int k = 0; k < byteBuffer.Length; k += 4) { r = byteBuffer[k] * 0.189f + byteBuffer[k + 1] * 0.769f + byteBuffer[k + 2] * 0.393f; g = byteBuffer[k] * 0.168f + byteBuffer[k + 1] * 0.686f + byteBuffer[k + 2] * 0.349f; b = byteBuffer[k] * 0.131f + byteBuffer[k + 1] * 0.534f + byteBuffer[k + 2] * 0.272f; byteBuffer[k+2] = (r > maxValue ? maxValue : (byte)r); byteBuffer[k + 1] = (g > maxValue ? maxValue : (byte)g); byteBuffer[k] = (b > maxValue ? maxValue : (byte)b); } Marshal.Copy(byteBuffer, 0, ptr, byteBuffer.Length); bmpNew.UnlockBits(bmpData); bmpData = null; byteBuffer = null; return bmpNew; }
The formula used to calculate a sepia tone differs significantly from thegrayscale filter discussed previously. The formula can be simplified as follows:
- Red Component: Sum total of: 18.9% blue, 76.9% green, 39.3% red
- Green Component: Sum total of: 16.8% blue, 68.6% green, 34.9% red
- Blue Component: Sum total of: 13.1% blue, 53.4% green, 27.2% red
If any of the totalled values exceeds the value of 255 that value is then defined as 255.
Sepia Tone Filter
The Negative Image Filter
Non digital film based cameras produce what is referred to as negatives, which have to be developed into printed photographs using various chemical processes. (I shudder at the thought of labelling 35mm film cameras as “old school” or vintage. I’m old enough to remember a time when film cameras were the norm yet young enough to never have used a rotary phone.)
We can implement an image filter that resembles film negatives by literally inverting every pixel’s colour components. It is fairly simple to invert colour data by implementing the bitwise compliment operator ~, the result being each bit will be reversed. Note: Only colour components are inverted, the Alphacomponent remains unchanged. Listed below is the source code implementation of the CopyAsNegative extension method.
public static Bitmap CopyAsNegative(this Image sourceImage) { Bitmap bmpNew = GetArgbCopy(sourceImage); BitmapData bmpData = bmpNew.LockBits(new Rectangle(0, 0, sourceImage.Width, sourceImage.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); IntPtr ptr = bmpData.Scan0; byte[] byteBuffer = new byte[bmpData.Stride * bmpNew.Height]; Marshal.Copy(ptr, byteBuffer, 0, byteBuffer.Length); byte[] pixelBuffer = null; int pixel = 0; for (int k = 0; k < byteBuffer.Length; k += 4) { pixel = ~BitConverter.ToInt32(byteBuffer, k); pixelBuffer = BitConverter.GetBytes(pixel); byteBuffer[k] = pixelBuffer[0]; byteBuffer[k + 1] = pixelBuffer[1]; byteBuffer[k + 2] = pixelBuffer[2]; } Marshal.Copy(byteBuffer, 0, ptr, byteBuffer.Length); bmpNew.UnlockBits(bmpData); bmpData = null; byteBuffer = null; return bmpNew; }
The negative filter formula extracts the four ARGB components storing the result as an integer value which represents a pixel. All of the pixel’s bits are reversed using the bitwise compliment operator. The resulting integer value is converted back into the four pixel components and assigned to replace the pixel’s original values, all except the alpha component.
Negative Filter
The implementation
The image filters described in this article are all implemented by means of aWindows Forms application. Image filtering is applied by selecting the corresponding radio button. The source image loaded from the file system serves as input to the various image filter methods, the filtered image copy returned will be displayed next to the original source image.
private void OnCheckChangedEventHandler(object sender, EventArgs e) { if (picSource.BackgroundImage != null) { if (rdGrayscale.Checked == true) { picOutput.BackgroundImage = picSource.BackgroundImage.CopyAsGrayscale(); } else if (rdTransparency.Checked == true) { picOutput.BackgroundImage = picSource.BackgroundImage.CopyWithTransparency(); } else if (rdNegative.Checked == true) { picOutput.BackgroundImage = picSource.BackgroundImage.CopyAsNegative(); } else if (rdSepia.Checked == true) { picOutput.BackgroundImage = picSource.BackgroundImage.CopyAsSepiaTone(); } } }
No comments:
Post a Comment