Gant Software Systems

A C# Bulk Image GrayScale Converter

I frequently evaluate potential user interface layouts to determine which of a set is the most appropriate for the intended application. Usually I break this process down into three parts, all of which have to pass for the design to be considered acceptable. Frequently parts from multiple prototypes may be combined that are the best of the set in particular categories. These parts are as follows:

  1. Color scheme
  2. Layout of user interface elements
  3. Images used in the user interface
    I find that treating these three items separately allows me to pick reasonably good user interface designs for applications that I’m working on. Full disclosure, though, I do not own a Mac, a black turtleneck, or square glasses, so my opinions on design are suspect at best. Nevertheless, this approach has enabled me a reasonable degree of ability to avoid putting something truly horrendous out for end-user consumption. Typically, I evaluate the mockups of the user interface to see how the colors interact and to evaluate the images used in the prototype to make sure that they clearly indicate what that particular element does. The layout, however, has been an interesting problem for me. Long experience has taught me that I simply lack the ability to properly evaluate a layout when it is in color – I tend to be distracted by color. That is, I tend to do better looking at the images in grayscale.

This obviously presents a problem, since most graphic designers don’t send grayscale proofs of their images along for the ride. Fortunately, it’s a fairly short piece of work to make an application that can convert an arbitrary number of images into grayscale (while preserving the originals) so that I can actually see what I’m doing. In fact, here’s the entire program.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
using System;
using System.Drawing.Imaging;
using System.IO;
using System.Drawing;

namespace ImageGrayscale
{
class Program
{
private static string InputPath = @"d:\input";
private static string outputPath = @"d:\output";
static void Main(string[] args)
{

var diSource = new DirectoryInfo(InputPath);
var colorMatrix = new ColorMatrix(new float[][]{ new float[]{0.3f,0.3f,0.3f,0,0},
new float[]{0.59f,0.59f,0.59f,0,0},
new float[]{0.11f,0.11f,0.11f,0,0},
new float[]{0,0,0,1,0,0},
new float[]{0,0,0,0,1,0},
new float[]{0,0,0,0,0,1}});

foreach (var file in diSource.GetFiles())
{
using (var bmpSrc = new Bitmap(file.FullName))
using(var bmpDst = new Bitmap(bmpSrc.Width, bmpSrc.Height))
using(var graphics = Graphics.FromImage(bmpDst))
{
var imageAttributes = new ImageAttributes();
imageAttributes.SetColorMatrix(colorMatrix);

graphics.DrawImage(bmpSrc, new Rectangle(0,0,bmpSrc.Width, bmpSrc.Height),0,0,bmpDst.Width, bmpDst.Height,GraphicsUnit.Pixel, imageAttributes);
var fileOutPath = Path.Combine(outputPath, file.Name);
Console.WriteLine("Making grayscale copy of {0} to {1}", file.FullName, fileOutPath);
bmpDst.Save(fileOutPath);
}
}
}
}
}

As usual, we’ll break this into pieces. Also, as usual, this is a little console application. All you have to do to point it at a different path is change the static input and output path variables declared in the program. You will need to add a reference to System.Data.Drawing in order to be able to build this application, as there are several classes in there that we need. First, we’ll declare the input and output paths. Were this a production application, we’d allow these to be passed in on the command-line, but I’m just declaring them for simplicity.

1
2
private static string InputPath = @"d:\_input";
private static string outputPath = @"d:\_output";

Next, we’ll need the Main method, which is where the application logic actually runs. We’ll also go ahead and get directory information for the input path, since we’re going to be looping over the set of files.

1
2
3
4
static void Main(string[] args)
{

var diSource = new DirectoryInfo(InputPath);
}

After that, we need to create a color matrix to transform the pixels in our input image into grayscale pixels. We are assuming four color channels (red, green, blue, and alpha, which is the opacity level), so our matrix will be as follows:

1
2
3
4
5
6
var colorMatrix = new ColorMatrix(new float[][]{   new float[]{0.3f,0.3f,0.3f,0,0},
new float[]{0.59f,0.59f,0.59f,0,0},
new float[]{0.11f,0.11f,0.11f,0,0},
new float[]{0,0,0,1,0,0},
new float[]{0,0,0,0,1,0},
new float[]{0,0,0,0,0,1}});

Without getting too far into the specifics (and publicly beclowning myself, because my linear algebra is admittedly a bit rusty), the above specifies a way to transform the red, green, and blue channels of an pixel into its grayscale form based on the luminosity. The formula is 0.3R + 0.59G + 0.11*B, reflected in the first three lines of the matrix above (you’ll note we’re doing nothing with the alpha channel – that’s because if the alpha isn’t 1.0, whatever is behind it influences the luminosity of the pixel. In essence, we’re assuming a lack of transparent regions in the image, because it hurts the brain to do otherwise. I’m not 100% sure on what the purpose of the latter three lines is, but I will point out that the right three columns form a 3X3 identity matrix. Something tickles in the back of my mind about the importance of that (and the fact that left three columns are completely clear). Regardless, this matrix does work and transforms colors to grayscale if used properly.

With that done, now we need to loop through the set of files. For each one, we have some variables that we need to create, that will survive for the length of this particular iteration of the loop. This is where life gets tricky. Due to some vagaries of the way the C# garbage collector (the thing that deallocates memory that is no longer in use) deals with unmanaged resources (including the GDI handles and memory bitmaps, which are used under the hood in the Graphics and Bitmap types, respectively), we need to explicitly manage the lifetime of these objects so that they don’t hang around. There are far fewer GDI handles available in a windows system than the number of them that could theoretically reside in memory. Towards this end, we’ll use the C# using statement to define explicit boundaries for the lifetimes of these objects so that we don’t leak memory, fall over, and die. The objects would eventually be released under memory pressure, but being unable to allocate new ones can make a pretty big mess. For each file, we’ll start by loading the file into a memory bitmap so that we can work with it. We’ll also define a destination bitmap of the same size to draw the source bitmap onto. Finally, we’ll initialize a Graphics object, which wraps a lot of the lower-level GDI functionality required to draw to a bitmap in memory (and I promise you, it’s way less of a pain than the old way).

1
2
3
4
5
6
foreach (var file in diSource.GetFiles())
{
using (var bmpSrc = new Bitmap(file.FullName))
using(var bmpDst = new Bitmap(bmpSrc.Width, bmpSrc.Height))
using(var graphics = Graphics.FromImage(bmpDst))
{

With this done, now we need to set up our image attributes. In this case, we’re only setting a color matrix, but there a lot of other options on the ImageAttributes object that can be leveraged.

1
2
var imageAttributes = new ImageAttributes();
imageAttributes.SetColorMatrix(colorMatrix);

Now we need to draw our old image to the new bitmap, using the new color matrix (which will force the pixels into grayscale behind the scenes). You can also iterate over all the pixels in the source image, calculate the new pixel value that you should use based on the luminosity formula, and individually draw that pixel the new image, but this approach has incredibly bad performance and is not even recommended for hacky programs such as this one, even though it is a bit easier to follow. Our approach does require us to tell the graphics object to draw starting at the upper left-hand corner of the image and to fit the image into exactly the same size space as it was in before. It’s silly that we have to do this, but that’s life.

1
graphics.DrawImage(bmpSrc, new Rectangle(0,0,bmpSrc.Width, bmpSrc.Height),0,0,bmpDst.Width, bmpDst.Height,GraphicsUnit.Pixel, imageAttributes);

Finally, we figure out what the output path should be and save the file to it, which is actually fairly straightforward. We simply take the filename (without the directory) and append it to the output directory path. We use the System.IO.Path object for this because it handles trailing slashes and any number of other edge cases for us.

1
2
3
var fileOutPath = Path.Combine(outputPath, file.Name);
Console.WriteLine("Making grayscale copy of {0} to {1}", file.FullName, fileOutPath);
bmpDst.Save(fileOutPath);

Until next time….