How to Create an Image Histogram Using C# and WPF

April 27, 2012

In this post I'll show you how to calculate luminance and RGB histograms in C# and how to display them using nothing more than a standard WPF UIElement – a Polygon to be more precise.

There are 4 steps in the process:

  1. Calculate the histogram values;
  2. Smooth the histogram (optional);
  3. Convert the histogram values into a format that can be bound to a Polygon element;
  4. Display the histogram.

To calculate the histogram values we'll rely on the imaging processing library of the AForge.NET framework. After referencing AForge.dll, AForge.Imaging.dll and AForge.Math.dll, the histogram calculation code is as simple as this:

System.Drawing.Bitmap bmp = new System.Drawing.Bitmap(imageFilePath);
// Luminance
ImageStatisticsHSL hslStatistics = new ImageStatisticsHSL(bmp);
int[] luminanceValues = hslStatistics.Luminance.Values;
// RGB
ImageStatistics rgbStatistics = new ImageStatistics(bmp);
int[] redValues = rgbStatistics.Red.Values;
int[] greenValues = rgbStatistics.Green.Values;
int[] blueValues = rgbStatistics.Blue.Values;

Histogram calculation results are stored in single-dimension integer arrays which will later be used as basis for the histogram drawing.

Histogram smoothing, put in simple terms, consists on the removal of spikes without compromising the overall meaning of the histogram. This is better illustrated with a concrete example:

Smoothed histogram example

There are several histogram smoothing approaches, but for illustration purposes I'll use a simple algorithm adapted from a Stack Overflow answer. Here's the C# code:

private int[] SmoothHistogram(int[] originalValues)
{
    int[] smoothedValues = new int[originalValues.Length];

    double[] mask = new double[] { 0.25, 0.5, 0.25 };

    for (int bin = 1; bin < originalValues.Length - 1; bin++)
    {
        double smoothedValue = 0;
        for (int i = 0; i < mask.Length; i++)
        {
            smoothedValue += originalValues[bin - 1 + i] * mask[i];
        }
        smoothedValues[bin] = (int)smoothedValue;
    }

    return smoothedValues;
}

NOTE: With the latest version of the AForge framework (2.2.4 at the time of this writing) the difference between the smoothed and non-smoothed histograms is not as noticeable as in the example shown above. The smoothing algorithm is included for use with older versions of the library or with different libraries that don't provide smoothing capabilities.

Up to this moment, whether smoothing was performed or not, the calculated histogram values are stored in a single-dimension integer array that cannot be bound directly to a Polygon element. Looking at the MSDN documentation for the Polygon class we see that its Points property accepts a PointCollection instance containing the vertex points of the polygon to be drawn. The following code snippet shows how to create a PointCollection from an integer array:

int max = values.Max();

PointCollection points = new PointCollection();
// first point (lower-left corner)
points.Add(new Point(0, max));
// middle points
for (int i = 0; i < values.Length; i++)
{
    points.Add(new Point(i, max - values[i]));
}
// last point (lower-right corner)
points.Add(new Point(values.Length - 1, max));

The code above is very straightforward and there is only one detail that may not obvious at first sight: since the (0,0) coordinate of any WPF UIElement is located at its upper left corner we need to "invert" the histogram values to make it appear upwards. I'm doing this by obtaining the maximum histogram value and then subtracting each value from it (line 9). An alternative would be to use the original values and then apply a ScaleTransform to the Polygon in XAML:

<Polygon (...)>
    <Polygon.RenderTransform>
        <ScaleTransform ScaleY="-1" />
    </Polygon.RenderTransform>
</Polygon>

Assuming no transformation is required, the code above can be simplified and displaying the histogram becomes as simple as this:

<Polygon Points="{Binding LuminanceHistogramPoints}"
         Stretch="Fill" Fill="Black" Opacity="0.8" />

The Stretch property is set to Fill in order to make the histogram adjust its size to the available space.

To illustrate the full process I've implemented a sample Visual Studio solution that can be downloaded from here. After compiling and running, the main window will look like this:

Histogram sample application in .NET/C#

The sample application grabs the image from the URL typed in the text box, downloads it to a temporary location and then displays it together with the luminance and RGB histograms. You can resize the window to see how the histograms adapt to size changes.