Skip to content

Commit

Permalink
Merge pull request #644 from brianpopow/feature/histogramEqualization
Browse files Browse the repository at this point in the history
Feature: histogram equalization
  • Loading branch information
JimBobSquarePants authored Jul 17, 2018
2 parents d53547a + 2108bf1 commit c1bc017
Show file tree
Hide file tree
Showing 3 changed files with 230 additions and 0 deletions.
36 changes: 36 additions & 0 deletions src/ImageSharp/Processing/HistogramEqualizationExtension.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.

using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Normalization;

namespace SixLabors.ImageSharp.Processing
{
/// <summary>
/// Adds extension that allow the adjustment of the contrast of an image via its histogram.
/// </summary>
public static class HistogramEqualizationExtension
{
/// <summary>
/// Equalizes the histogram of an image to increases the global contrast using 65536 luminance levels.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image this method extends.</param>
/// <returns>The <see cref="Image{TPixel}"/>.</returns>
public static IImageProcessingContext<TPixel> HistogramEqualization<TPixel>(this IImageProcessingContext<TPixel> source)
where TPixel : struct, IPixel<TPixel>
=> HistogramEqualization(source, 65536);

/// <summary>
/// Equalizes the histogram of an image to increases the global contrast.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image this method extends.</param>
/// <param name="luminanceLevels">The number of different luminance levels. Typical values are 256 for 8-bit grayscale images
/// or 65536 for 16-bit grayscale images.</param>
/// <returns>The <see cref="Image{TPixel}"/>.</returns>
public static IImageProcessingContext<TPixel> HistogramEqualization<TPixel>(this IImageProcessingContext<TPixel> source, int luminanceLevels)
where TPixel : struct, IPixel<TPixel>
=> source.ApplyProcessor(new HistogramEqualizationProcessor<TPixel>(luminanceLevels));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.

using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory;
using SixLabors.Primitives;

namespace SixLabors.ImageSharp.Processing.Processors.Normalization
{
/// <summary>
/// Applies a global histogram equalization to the image.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal class HistogramEqualizationProcessor<TPixel> : ImageProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
{
/// <summary>
/// Initializes a new instance of the <see cref="HistogramEqualizationProcessor{TPixel}"/> class.
/// </summary>
/// <param name="luminanceLevels">The number of different luminance levels. Typical values are 256 for 8-bit grayscale images
/// or 65536 for 16-bit grayscale images.</param>
public HistogramEqualizationProcessor(int luminanceLevels)
{
Guard.MustBeGreaterThan(luminanceLevels, 0, nameof(luminanceLevels));

this.LuminanceLevels = luminanceLevels;
}

/// <summary>
/// Gets the number of luminance levels.
/// </summary>
public int LuminanceLevels { get; }

/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration)
{
MemoryAllocator memoryAllocator = configuration.MemoryAllocator;
int numberOfPixels = source.Width * source.Height;
Span<TPixel> pixels = source.GetPixelSpan();

// Build the histogram of the grayscale levels.
using (IBuffer<int> histogramBuffer = memoryAllocator.AllocateClean<int>(this.LuminanceLevels))
using (IBuffer<int> cdfBuffer = memoryAllocator.AllocateClean<int>(this.LuminanceLevels))
{
Span<int> histogram = histogramBuffer.GetSpan();
for (int i = 0; i < pixels.Length; i++)
{
TPixel sourcePixel = pixels[i];
int luminance = this.GetLuminance(sourcePixel, this.LuminanceLevels);
histogram[luminance]++;
}

// Calculate the cumulative distribution function, which will map each input pixel to a new value.
Span<int> cdf = cdfBuffer.GetSpan();
int cdfMin = this.CalculateCdf(cdf, histogram);

// Apply the cdf to each pixel of the image
float numberOfPixelsMinusCdfMin = numberOfPixels - cdfMin;
for (int i = 0; i < pixels.Length; i++)
{
TPixel sourcePixel = pixels[i];

int luminance = this.GetLuminance(sourcePixel, this.LuminanceLevels);
float luminanceEqualized = cdf[luminance] / numberOfPixelsMinusCdfMin;

pixels[i].PackFromVector4(new Vector4(luminanceEqualized));
}
}
}

/// <summary>
/// Calculates the cumulative distribution function.
/// </summary>
/// <param name="cdf">The array holding the cdf.</param>
/// <param name="histogram">The histogram of the input image.</param>
/// <returns>The first none zero value of the cdf.</returns>
private int CalculateCdf(Span<int> cdf, Span<int> histogram)
{
// Calculate the cumulative histogram
int histSum = 0;
for (int i = 0; i < histogram.Length; i++)
{
histSum += histogram[i];
cdf[i] = histSum;
}

// Get the first none zero value of the cumulative histogram
int cdfMin = 0;
for (int i = 0; i < histogram.Length; i++)
{
if (cdf[i] != 0)
{
cdfMin = cdf[i];
break;
}
}

// Creating the lookup table: subtracting cdf min, so we do not need to do that inside the for loop
for (int i = 0; i < histogram.Length; i++)
{
cdf[i] = Math.Max(0, cdf[i] - cdfMin);
}

return cdfMin;
}

/// <summary>
/// Convert the pixel values to grayscale using ITU-R Recommendation BT.709.
/// </summary>
/// <param name="sourcePixel">The pixel to get the luminance from</param>
/// <param name="luminanceLevels">The number of luminance levels (256 for 8 bit, 65536 for 16 bit grayscale images)</param>
[MethodImpl(InliningOptions.ShortMethod)]
private int GetLuminance(TPixel sourcePixel, int luminanceLevels)
{
// Convert to grayscale using ITU-R Recommendation BT.709
var vector = sourcePixel.ToVector4();
int luminance = Convert.ToInt32(((.2126F * vector.X) + (.7152F * vector.Y) + (.0722F * vector.Y)) * (luminanceLevels - 1));

return luminance;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.

using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using Xunit;

namespace SixLabors.ImageSharp.Tests.Processing.Normalization
{
public class HistogramEqualizationTests
{
[Theory]
[InlineData(256)]
[InlineData(65536)]
public void HistogramEqualizationTest(int luminanceLevels)
{
// Arrange
byte[] pixels = new byte[]
{
52, 55, 61, 59, 70, 61, 76, 61,
62, 59, 55, 104, 94, 85, 59, 71,
63, 65, 66, 113, 144, 104, 63, 72,
64, 70, 70, 126, 154, 109, 71, 69,
67, 73, 68, 106, 122, 88, 68, 68,
68, 79, 60, 79, 77, 66, 58, 75,
69, 85, 64, 58, 55, 61, 65, 83,
70, 87, 69, 68, 65, 73, 78, 90
};

var image = new Image<Rgba32>(8, 8);
for (int y = 0; y < 8; y++)
{
for (int x = 0; x < 8; x++)
{
byte luminance = pixels[y * 8 + x];
image[x, y] = new Rgba32(luminance, luminance, luminance);
}
}

byte[] expected = new byte[]
{
0, 12, 53, 32, 146, 53, 174, 53,
57, 32, 12, 227, 219, 202, 32, 154,
65, 85, 93, 239, 251, 227, 65, 158,
73, 146, 146, 247, 255, 235, 154, 130,
97, 166, 117, 231, 243, 210, 117, 117,
117, 190, 36, 190, 178, 93, 20, 170,
130, 202, 73, 20, 12, 53, 85, 194,
146, 206, 130, 117, 85, 166, 182, 215
};

// Act
image.Mutate(x => x.HistogramEqualization(luminanceLevels));

// Assert
for (int y = 0; y < 8; y++)
{
for (int x = 0; x < 8; x++)
{
Rgba32 actual = image[x, y];
Assert.Equal(expected[y * 8 + x], actual.R);
Assert.Equal(expected[y * 8 + x], actual.G);
Assert.Equal(expected[y * 8 + x], actual.B);
}
}
}
}
}

0 comments on commit c1bc017

Please sign in to comment.