When we are creating a binary image we are defining objects of interest. Defining these objects is very specific to the task at hand. In some cases a simple threshold will work while others require a more complex approach such as color segmentation, edge detection, or something even more sinister.

Thresholding

The most common way to create a binary image from a grayscale image is to pick an intensity threshold (also known as gray-level segmentation). Everything below the threshold becomes a pixel of interest (black). Everything else is not (white). Let’s explore a few methods for picking that threshold.

The Image

Interestingly, this picture is a white piece of poster board (not blue) with some dark puzzle pieces on it. If you look closely you can see the famous HD7 pink tint. The image on the right is simply converted to grayscale using the average of the RGB values.

Mean Threshold

Almost every time an image processing problem arises and a simple average is a possible answer my gut tells me to go for it. In this case, my gut is going to lead me astray, but first some code:

WriteableBitmap ToAverageBinary(WriteableBitmap grayscale)

{

WriteableBitmap binary =

newWriteableBitmap(grayscale.PixelWidth,

grayscale.PixelHeight);

int[] histogramData = newint[256];

int maxCount = 0;

//first we will determine the histogram

//for the grayscale image

for (int pixelIndex = 0;

pixelIndex < grayscale.Pixels.Length;

pixelIndex++)

{

byte intensity = (byte)grayscale.Pixels[pixelIndex];

//simply add another to the count

//for that intensity

histogramData[intensity]++;

if (histogramData[intensity] > maxCount)

{

maxCount = histogramData[intensity];

}

}

//now we need to figure out the average intensity

long average = 0;

for (int intensity = 0; intensity < 256; intensity++)

{

average += intensity * histogramData[intensity];

}

//this is our threshold

average /= grayscale.Pixels.Length;

for (int pixelIndex = 0;

pixelIndex < grayscale.Pixels.Length;

pixelIndex++)

{

byte intensity = (byte)grayscale.Pixels[pixelIndex];

//now we’re going to set the pixels

//greater than or equal to the average

//to white and everything else to black

if (intensity >= average)

{

intensity = 255;

unchecked { binary.Pixels[pixelIndex] = (int)0xFFFFFFFF; }

}

else

{

intensity = 0;

unchecked { binary.Pixels[pixelIndex] = (int)0xFF000000; }

}

}

//note that this is the ORIGINAL histogram

//not the histogram for the binary image

PlotHistogram(histogramData, maxCount);

//this is a line to show where the

//average is relative to the rest of

//the histogram

Line averageLine = newLine();

averageLine.X1 = HistogramPlot.Width * average / 255;

averageLine.X2 = HistogramPlot.Width * average / 255;

averageLine.Y1 = 0;

averageLine.Y2 = HistogramPlot.Height;

averageLine.Stroke = newSolidColorBrush(Colors.Magenta);

averageLine.StrokeThickness = 2;

averageLine.StrokeDashArray = newDoubleCollection() { 5, 2.5 };

HistogramPlot.Children.Add(averageLine);

return binary;

}

While the puzzle pieces came through loud and clear so did a lot of garbage. Notice how the average line (magenta) splits the peak? This is going to lead to a large number of false black pixels. What we want is an automated technique for shifting the threshold left.

Two Peak

If you look at the original grayscale image you’ll notice that the puzzle pieces seem to be close to the same intensity while the background is another. This is represented in the histogram by the large peak (the background) and an almost imperceptible peak toward the dark end of the spectrum (the puzzle pieces).

Finding the large peak is trivial. It’s just the highest occurring intensity. The small peak on the other hand is a little trickier. It’s not the second most frequent intensity – that’s right next to the largest peak. A little trick is to give a higher weight to intensities that are far from the highest peak. To do this we multiply the intensity count at each point by the square of its distance to the peak. That’s a mouthful, but it’s pretty easy. Here’s the code:

int secondPeak = 0;

long secondPeakCount = 0;

for (int intensity = 0; intensity < 256; intensity++)

{

long adjustedPeakCount =

(long)(histogramData[intensity]*Math.Pow(intensity-maxPeak, 2));

if (adjustedPeakCount > secondPeakCount)

{

secondPeak = intensity;

secondPeakCount = adjustedPeakCount;

}

}

So we calculate an adjusted count for each intensity. By multiplying it by the square of the distance we give higher counts to those further from the first peak. Amazingly, this works even in this case where the second peak is so small.

Wow! Those results are so much better. Notice I marked the two peaks with green and the new threshold is in magenta.

Summary

Don’t use the mean. You might get lucky in a few cases, but in general you’ll end up in a messy situation. That said, the two-peak solution will work well in cases where your objects are of a consistent color and that color is separate from the background. I created this photo because I knew it would have good results. I highly recommend you try these techniques out on your own photos so you can get a feel for when they will work and when they won’t.

We will most likely revisit different thresholding techniques for different situations when they arise, but for now we have a binary image so we’re going to see what we can do with it.