C Tutorial 3 - Thresholding and Histogram
C Tutorial 3 - Thresholding and Histogram
Terence Morley
02 October 2012
http://www.tmorley.net
http://sites.google.com/site/morleysoftware
1 Introduction
This document adds the intensity transformation Thresholding to the IpTestBed program
created in Tutorial 2. Intensity transformations simply change the grey level of each pixel
according some algorithm. The ability to create an image histogram is also added.
The code described was written using Microsoft C] Express 2010 which can be downloaded
free of charge from Microsoft.
This document was created using LATEX on MiKTeX. MiKTeX can be downloaded
from http://miktex.org/.
This function modifies the image in-place, i.e. the original is no longer available to us.
In more complicated processing we may need the original image to use again later. So we
will change this function to return a new image. The code is shown below.
1
3 SAVING YOUR IMAGES
For this, we have made a new constructor that just takes the width and height and
allocates the memory for it. This can be seen in the accompanying project or in the full
listing at the end of the tutorial.
In the project we previously called the function like this:
So in this case we are effectively still doing the processing in-place but we now have
the flexibility to keep the original if we need to.
Add a button to the main window and name it btnSave. Set the Content property to
‘Save...’ and double-click on the button to be taken into the code window for the event
function. Paste the contents of the following function into your click event.
Terence Morley 2
3 SAVING YOUR IMAGES
if (imgRight.Source is BitmapSource)
{
try
{
BitmapSource image = (BitmapSource)imgRight.Source;
This function calls the getSaveImageFile function below. Paste this function into your
MainWindow class:
return filename;
}
Terence Morley 3
4 THRESHOLDING
4 Thresholding
So now that we have got code to open, save and display greyscale images we can go about
adding more image processing functions to the IpTestBed program.
This section will explain Thresholding. An image and a thresholded version is shown
below.
As we have described, pixels in a greyscale image are represented by one of the 256
grey-levels 0 to 255, where 0 is black and 255 is white. The technique of thresholding
compares each pixel value to a particular threshold value and if the pixel value is less than
the threshold valve, the pixel is set 0. If the pixel value is greater than or equal to the
threshold value, the pixel is set to 255. In the above picture, the chosen threshold value
was 128 which is mid-grey.
Thresholding can be described mathematically as follows:
0 if r(x, y) < T ,
s(x, y) =
255 Otherwise.
This means that for any pixel s(x, y) in a resulting image, the value is set to 0 if
the corresponding pixel in the source image is less than the threshold value, T , and 255
otherwise. As an example for a threshold value of T = 100, if a pixel has a value of 54 it
is set to black and if it has a value of 177 it is set to white.
The value of T must be chosen to achieve the desired result. A method of choosing a
suitable value is described later in this tutorial.
1. Paste the following function into the GreyImage class in the IpTestBed program.
Terence Morley 4
4.1 Thresholding Code 4 THRESHOLDING
2. Add a button to the window, name it btnThreshold and set the Content property
to ‘Threshold...’.
3. With the threshold button selected in the window, go to the Properties window and
click on ‘Events’ at the top. Find the Click event and set it to ‘btnProcess Click’.
This, you will remember, is the general click event function that we are using for
multiple image processing functions in the MainWindow class.
4. Go into the code editor window for the MainWindow and locate the btnProcess Click
function. Modify it so it matches that shown below.
Terence Morley 5
4.1 Thresholding Code 4 THRESHOLDING
{
// Create negative of the image
testImage = testImage.Negative();
}
if (sender == btnThreshold)
{
// Create thresholded image
testImage = testImage.Threshold(128);
}
if (sender == btnThreshold)
{
// Create thresholded image
testImage = testImage.Threshold(128);
}
So, the event function opens an image as it did with the Negative function then
it performs the threshold and displays the original and processed images in the
window. The Threshold function in the GreyImage class is called with a value of
128 as described in the discussion above. You can change this value to see different
effects or you can work out how to write C] code to ask the user for a value.
You may have noticed that the btnProcess Click function is slightly different to
that given in the previous tutorial. The change was to remove the bits of code
that display the images in the image controls on the window. This code was
moved into a displayImage function. The code for this can be seen in the project
code supplied with this tutorial. The function accepts a GreyImage object and a
Display enumeration which has been declared to contain the constants Left and
Right. For example, displayImage can be called with Display.Right to display an
image in the right-hand image control.
5. If you now select ‘Start without debugging’ from the Debug menu, if no mistakes
have been made, you should be able to click on the ‘Threshold...’ button, select an
image and see the results in the window as shown below.
Terence Morley 6
5 IMAGE HISTOGRAM
5 Image Histogram
While you were experimenting with different threshold values with the Threshold function,
you may have found that you didn’t know exactly what value to use to extract a particular
feature of your sample image. What you needed was some way of seeing what grey-levels
are present in your image. This is where the Image Histogram comes in.
The image histogram is a bar-chart which has on the x-axis the values 0 to 255 for
the grey-levels in the image. The chart has a vertical bar at each grey-level whose height
represents the number of pixels in the image with that grey-level. Below is shown an image
and its histogram.
In the image histogram shown above the ticks on the x-axis are for greyscale values
0, 64, 128, 192 and 255. The grey-level that has the largest count of pixels is about 10
(the spike near the left). This is scaled to fit the maximum height of the histogram. The
counts for the other grey-levels are then shown as a proportion of this maximum count.
As can be seen from this histogram, the image mostly contains dark pixels - this is
because most of the histogram is towards the left side of the graph. You can also see a
peak near the centre of the histogram. This is made by the mid-grey pixels making up
the lilies themselves.
In the threshold example given above, the thresholding was done at a grey-value of
128. In the histogram, you can see that the mid-grey peak finishes slightly after 128. We
Terence Morley 7
5.1 Histogram Code 5 IMAGE HISTOGRAM
can do the thresholding again at a value of 192 for example to get rid of some of mid-greys
that were sent to white. These thresholded images can be compared below.
We will now modify the IpTestBed program to allow it to generate the histogram of an
image. The histogram will be created as a greyscale image. This is simply because we have
already written the code to display and save images so we may as well use that instead
of writing more code to draw a histogram using vector graphics (if you want to, you can
rewrite the code to create a vector graphics histogram - you could then write axis labels
on the histogram).
1. Paste the following function into the GreyImage class in the IpTestBed program.
Terence Morley 8
5.1 Histogram Code 5 IMAGE HISTOGRAM
will throw an exception. It then goes through the image one pixel at a time and
increments the count in the array for the grey-level for that particular pixel. Finally
it finds the maximum count in the array and divides all the counts by the maximum.
This then leaves the largest count with a value of 1 and all other counts as a fraction
of 1.
The y-coordinates in an image go down the screen but when drawing a graph it is
convenient for the origin to be at the bottom-left. The convertY function performs
this conversion. There is no need to do this for the x-coordinates because they go
from left to right anyway.
In C] , an array is automatically cleared to zeroes. The array of bytes used for the
images are set to 0 which gives a black image but we want the background of the
histogram to be white. The ClearImageToValue function is used to set all pixels in
the image to 255 (white).
imgHistogram.ClearImageToValue(255);
calculateHistogram(hist);
Terence Morley 9
5.1 Histogram Code 5 IMAGE HISTOGRAM
i = imgHistogram.ImageWidth *
convertY(y, imgHistogram.ImageHeight) + x;
imgHistogram.pixels[i] = 0;
}
// Draw x-axis ticks
for (tick = 0; tick <= 4; tick++)
{
x = margin + xTickPositions[tick];
for (y = margin / 2; y < margin; y++)
{
i = imgHistogram.ImageWidth *
convertY(y, imgHistogram.ImageHeight) + x;
imgHistogram.pixels[i] = 0;
}
i = imgHistogram.ImageWidth *
convertY(y, imgHistogram.ImageHeight) + x;
imgHistogram.pixels[i] = 0;
}
// Draw y-axis line
x = margin / 2;
for (y = margin; y < margin + fullBarHeight; y++)
{
i = imgHistogram.ImageWidth *
convertY(y, imgHistogram.ImageHeight) + x;
imgHistogram.pixels[i] = 0;
}
// Draw y-axis ticks
for (tick = 0; tick <= 4; tick++)
{
y = margin + (int)(fullBarHeight * yTickPositions[tick]);
for (x = 0; x < margin / 2; x++)
{
i = imgHistogram.ImageWidth *
convertY(y, imgHistogram.ImageHeight) + x;
imgHistogram.pixels[i] = 0;
}
i = imgHistogram.ImageWidth *
convertY(y, imgHistogram.ImageHeight) + x;
imgHistogram.pixels[i] = 0;
}
// Draw histogram
for (level = 0; level < 256; level++)
{
int barHeight = (int)(hist[level] * fullBarHeight);
x = margin + level;
for (y = 0; y < barHeight; y++)
{
i = imgHistogram.ImageWidth *
convertY(y + margin, imgHistogram.ImageHeight) + x;
imgHistogram.pixels[i] = 0;
}
}
return imgHistogram;
}
This function, calls the calculateHistogram function to fill an array with the histogram
Terence Morley 10
6 PROGRAM LISTINGS
values. There are then loops that draw the x and y axes and the tick marks. They
do this by setting particular pixels to 0 (black). Finally there is a loop that draws
each histogram bar from the histogram array.
4. Add a button to the window using the window editor. Name it btnHistogram, set
the Content property to ‘Histogram...’ and set the Click event to btnProcess Click
so that it calls our general processing event function when clicked.
5. Add the following fragment of code to the btnProcess Click function in the MainWindow
class below the fragment that was added for Thresholding in the previous section.
(This can be seen in the listings at the end of this tutorial or in the supplied C]
project.
if (sender == btnHistogram)
{
// Generate Histogram
testImage = testImage.Histogram();
}
6. Run the program and you should be able to view a histogram as shown below.
6 Program Listings
6.1 GreyImage.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
// For bitmap handling:
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace IpTestBed
{
class GreyImage
{
Terence Morley 11
6.1 GreyImage.cs 6 PROGRAM LISTINGS
///////////////////////////////////////////////
// Constructor
public GreyImage(int Width, int Height)
{
width = Width;
height = Height;
pixels = new byte[width * height];
}
///////////////////////////////////////////////
// Constructor
public GreyImage(int Width, int Height, byte[] PixelArray)
{
width = Width;
height = Height;
pixels = new byte[width * height];
pixels = PixelArray;
}
///////////////////////////////////////////////
// Constructor
public GreyImage(BitmapSource bitmap)
{
// Convert the image to greyscale format
if (bitmap.Format != PixelFormats.Gray8)
{
bitmap = new FormatConvertedBitmap(bitmap, PixelFormats.Gray8, null, 0);
}
// Copy pixels
bitmap.CopyPixels(pixels, bitmap.PixelWidth, 0);
}
///////////////////////////////////////////////
// Width property
public int ImageWidth
{
get { return this.width; }
}
///////////////////////////////////////////////
// Height property
public int ImageHeight
{
get { return this.height; }
}
///////////////////////////////////////////////
// PixelData property
public byte[] PixelData
Terence Morley 12
6.1 GreyImage.cs 6 PROGRAM LISTINGS
{
get { return this.pixels.Clone() as byte[]; }
}
///////////////////////////////////////////////
//
public void Create(int Width, int Height)
{
width = Width;
height = Height;
pixels = new byte[width * height];
}
///////////////////////////////////////////////
//
public GreyImage Negative()
{
// Create new image for the result
GreyImage imgResult = new GreyImage(this.ImageWidth, this.ImageHeight);
///////////////////////////////////////////////
//
public GreyImage Threshold(byte ThresholdLevel)
{
// Create new image for the result
GreyImage imgResult = new GreyImage(this.ImageWidth, this.ImageHeight);
///////////////////////////////////////////////
Terence Morley 13
6.1 GreyImage.cs 6 PROGRAM LISTINGS
//
public void calculateHistogram(double[] hist)
{
// Loop through all pixels in the image and count grey-levels
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
int i = width * y + x;
hist[pixels[i]]++;
}
}
///////////////////////////////////////////////
//
private int convertY(int y, int imgHeight)
{
return imgHeight - y - 1;
}
///////////////////////////////////////////////
//
public void ClearImageToValue(byte value)
{
long size = width * height;
for (int i = 0; i < size; i++)
{
pixels[i] = value;
}
}
///////////////////////////////////////////////
//
public GreyImage Histogram()
{
double[] hist = new double[256];
const int margin = 10;
const int fullBarHeight = 200;
// Create new image for the result
GreyImage imgHistogram = new GreyImage(256 + 2 * margin, 200 + 2 * margin);
int i, x, y, level, tick;
int[] xTickPositions = { 0, 64, 128, 192, 255 };
double[] yTickPositions = { 0.0, 0.25, 0.50, 0.75, 1.0 };
imgHistogram.ClearImageToValue(255);
calculateHistogram(hist);
Terence Morley 14
6.1 GreyImage.cs 6 PROGRAM LISTINGS
i = imgHistogram.ImageWidth *
convertY(y, imgHistogram.ImageHeight) + x;
imgHistogram.pixels[i] = 0;
}
// Draw y-axis line
x = margin / 2;
for (y = margin; y < margin + fullBarHeight; y++)
{
i = imgHistogram.ImageWidth *
convertY(y, imgHistogram.ImageHeight) + x;
imgHistogram.pixels[i] = 0;
}
// Draw y-axis ticks
for (tick = 0; tick <= 4; tick++)
{
y = margin + (int)(fullBarHeight * yTickPositions[tick]);
for (x = 0; x < margin / 2; x++)
{
i = imgHistogram.ImageWidth *
convertY(y, imgHistogram.ImageHeight) + x;
imgHistogram.pixels[i] = 0;
}
i = imgHistogram.ImageWidth *
convertY(y, imgHistogram.ImageHeight) + x;
imgHistogram.pixels[i] = 0;
}
// Draw histogram
for (level = 0; level < 256; level++)
{
int barHeight = (int)(hist[level] * fullBarHeight);
x = margin + level;
for (y = 0; y < barHeight; y++)
{
i = imgHistogram.ImageWidth *
convertY(y + margin, imgHistogram.ImageHeight) + x;
imgHistogram.pixels[i] = 0;
}
}
Terence Morley 15
6.2 MainWindow.xaml.cs 6 PROGRAM LISTINGS
return imgHistogram;
}
} // end class
} // end namespace
6.2 MainWindow.xaml.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
//
using System.IO;
namespace IpTestBed
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private Image imgLeft;
private Image imgRight;
private enum Display { Left, Right };
public MainWindow()
{
InitializeComponent();
// Put the image controls in the scroll viewers to scroll larger images
scrollerLeft.Content = imgLeft;
imgLeft.Stretch = Stretch.None;
scrollerRight.Content = imgRight;
imgRight.Stretch = Stretch.None;
}
///////////////////////////////////////////////
// Display a GreyImage in one of the image controls
private void displayImage(GreyImage img, Display pos)
{
WriteableBitmap bitmapOut;
Terence Morley 16
6.2 MainWindow.xaml.cs 6 PROGRAM LISTINGS
if (pos == Display.Left)
{
imgLeft.Source = bitmapOut;
}
else
{
imgRight.Source = bitmapOut;
}
}
///////////////////////////////////////////////
//
private string getOpenImageFilename()
{
string filename = null;
return filename;
}
///////////////////////////////////////////////
//
private string getSaveImageFilename()
{
string filename = null;
Terence Morley 17
6.2 MainWindow.xaml.cs 6 PROGRAM LISTINGS
if (result == true)
{
filename = dlg.FileName;
}
return filename;
}
///////////////////////////////////////////////
// Function called by different processing buttons
private void btnProcess_Click(object sender, RoutedEventArgs e)
{
try
{
BitmapSource bitmapIn;
string imageFilename;
Terence Morley 18
7 DOCUMENT HISTORY
if (imgRight.Source is BitmapSource)
{
try
{
BitmapSource image = (BitmapSource)imgRight.Source;
}
}
7 Document History
22 December 2011 First issue.
02 October 2012 Added hyperlinks and bookmarks.
Terence Morley 19