Introduction
Here are some Visual Basic .NET classes that implement image filters for changing RGB images to color and grayscale images. A small demo project is added to show the filters in use.
Background
I was playing with a grayscale filter described in the article "How to convert a color image to grayscale" and getting the idea to make a filter that would make images look like some old brown and white pictures. When solving the problem, I found out it is easy to convert an image to any color scale.
In the article mentioned, we can find two methods for converting a color image to a grayscale image, favoring the method using a color matrix being "an effective and quick method of performing the monochrome manipulation". This is true if we take methods as written, but with some effort, the other method proves to be much faster.
Improving the grayscale filter
The method mentioned in the above article looks like this:
Public Function ConvertToGrayscale(ByVal source As Bitmap) as Bitmap
Dim bm as new Bitmap(source.Width,source.Height)
Dim x
Dim y
For y=0 To bm.Height
For x=0 To bm.Width
Dim c as Color = source.GetPixel(x,y)
Dim luma as Integer = CInt(c.R*0.3 + c.G*0.59 + c.B*0.11)
bm.SetPixel(x,y,Color.FromArgb(luma,luma,luma))
Next
Next
Return bm
End Function
In the main loop, we have three function calls that make the execution time longer than necessary. We can get rid of getting and setting the pixel if we copy the bitmap data to an array and then manipulate the pixel bytes directly. Calling the function Color.FromArgb(luma,luma,luma)
is not necessary, since it always returns R=G=B=luma. The final method for conversion looks like this:
Public Function ExecuteRgb8( _
ByVal img As System.Drawing.Image) As System.Drawing.Image
Dim result As Bitmap = New Bitmap(img)
Dim bmpData As BitmapData = result.LockBits( _
New Rectangle(0, 0, result.Width, result.Height), _
ImageLockMode.ReadWrite, img.PixelFormat)
Dim pixelBytes As Integer = _
System.Drawing.Image.GetPixelFormatSize(img.PixelFormat) \ 8
Dim ptr As IntPtr = bmpData.Scan0
Dim size As Integer = bmpData.Stride * result.Height
Dim pixels(size - 1) As Byte
Dim index As Integer
Dim Y As Integer
Dim mulR As Double = _factorRed / 100
Dim mulG As Double = _factorGreen / 100
Dim mulB As Double = _factorBlue / 100
System.Runtime.InteropServices.Marshal.Copy(ptr, pixels, 0, size)
For row As Integer = 0 To result.Height - 1
For col As Integer = 0 To result.Width - 1
index = (row * bmpData.Stride) + (col * pixelBytes)
Y = CInt(System.Math.Round( _
mulR * pixels(index + 2) + _
mulG * pixels(index + 1) + _
mulB * pixels(index + 0)))
If (Y > 255) Then Y = 255
pixels(index + 2) = CByte(Y)
pixels(index + 1) = CByte(Y)
pixels(index + 0) = CByte(Y)
Next
Next
System.Runtime.InteropServices.Marshal.Copy(pixels, 0, ptr, size)
result.UnlockBits(bmpData)
Return result
End Function
The method uses separate factors for color components defining the influence of the color component on a shade of gray.
The function has more program steps and looks more complicated, but the execution time is 15 to 20 percent shorter than using the method with the color matrix.
The conversion function is wrapped in a suitable class allowing changes of color factors for conversion. The filter basics are covered in my previous article: Hue Saturation Lightness Filter. Two standard sets of color factors can be set with the Brightness
property.
The class implements a method with the color matrix as well; you can control which method will be used for conversion with the UseColorMatrix
property.
Extending the filter to any color scale
You can find old pictures that are not black and white but brown and white. I wanted to change the grayscale filter to do such a conversion. When finished, I found out that the conversion can be done to any color scale so filter is not named brown-scale but color-scale.
There are two small changes done to the grayscale filter to perform the color scale conversion. First, we prepare the color tables for each color component outside the main loop:
Dim paletteR(255) As Byte
Dim paletteG(255) As Byte
Dim paletteB(255) As Byte
Dim c As Integer
For i As Integer = 0 To 255
c = CInt(Math.Round((CDbl(_endColor.R) - CDbl(_startColor.R)) * _
CDbl(i) / 255.0 + _startColor.R, 0))
If c < 0 Then c = 0
If c > 255 Then c = 255
paletteR(i) = CByte(c)
c = CInt(Math.Round((CDbl(_endColor.G) - CDbl(_startColor.G)) * _
CDbl(i) / 255.0 + _startColor.G, 0))
If c < 0 Then c = 0
If c > 255 Then c = 255
paletteG(i) = CByte(c)
c = CInt(Math.Round((CDbl(_endColor.B) - CDbl(_startColor.B)) * _
CDbl(i) / 255.0 + _startColor.B, 0))
If c < 0 Then c = 0
If c > 255 Then c = 255
paletteB(i) = CByte(c)
Next
The color table calculation was at first simpler, but when playing with different start and end colors, I had to switch to doubles and integers to avoid overflows.
In the main loop, we search the values in tables.
pixels(index + 2) = paletteR(Y)
pixels(index + 1) = paletteG(Y)
pixels(index + 0) = paletteB(Y)
Using the code
Using the filters is simple. Set the properties and call the execution function.
I have provided a small demo project where you can play with both the filters, change the color factors, colors, and finally, save the filtered images.
Two tabs are used, one for RGB to grayscale, and an other for RGB to color scale conversion. On both tabs, sliders are used to change thresholds for color components if we select custom thresholds. On the "Color" tab, we can use color selection dialogs to select the start and end colors.
The method for RGB to color scale conversion is as fast as that for RGB to grayscale conversion since the color table is prepared only once outside the main loop. Anyway, a small trick is used to make the program faster. The image is scaled before display and all conversions are done on the scaled image. When the user is satisfied with the filtered image and saves it, the conversion is done on the original image before saving.
Points of interest
What is found as a program example is not always the best solution. It is always worth experimenting, checking, and improving.
When you learn something, it is up to your imagination how to use this knowledge.
Programmers converting code to C will probably get even faster execution if using "dirty" programming without copying the bitmap data to an array.
History