Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: histogram equalization #644

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
7380a63
Merge pull request #1 from SixLabors/master
brianpopow Jun 12, 2018
d41ece1
added first attempt of histogram equalization
brianpopow Jun 30, 2018
687db06
added HistogramEqualizationTest
brianpopow Jun 30, 2018
5b1b8b1
changed the cdf to be the cumulative histogram
brianpopow Jul 1, 2018
7890186
Merge remote-tracking branch 'upstream/master' into feature/histogram…
brianpopow Jul 1, 2018
8117d82
added support for 16 bit greyscale
brianpopow Jul 1, 2018
1d5a8f7
SixLabors.ImageSharp.Processing.Contrast -> SixLabors.ImageSharp.Proc…
brianpopow Jul 2, 2018
f4d9786
using memoryAllocator
brianpopow Jul 2, 2018
7ecfa0c
removed unnecessary allocation of the lut, using cdf instead
brianpopow Jul 2, 2018
e6ba776
moved test to Normalization folder
brianpopow Jul 2, 2018
a5b180a
fixed rounding issue in calculating the luminance
brianpopow Jul 2, 2018
dd698dd
using GetPixelSpan instead of GetPixelRowSpan
brianpopow Jul 3, 2018
f78d9b7
allocating cdf and histogram buffer with a using statement
brianpopow Jul 3, 2018
ac69aa7
using Vector4 to calculate the luminance and set the pixel value
brianpopow Jul 3, 2018
3e08e30
Merge branch 'master' into feature/histogramEqualization
brianpopow Jul 3, 2018
5708144
luminance levels is now a parameter of the constructor, defaults to 6…
brianpopow Jul 4, 2018
b102652
Merge branch 'master' into feature/histogramEqualization
brianpopow Jul 4, 2018
c36b0df
Merge branch 'master' into feature/histogramEqualization
JimBobSquarePants Jul 9, 2018
4fcefc5
moved extension to the processing namespace and the processor accordi…
brianpopow Jul 15, 2018
8ad5b34
Merge branch 'master' into feature/histogramEqualization
brianpopow Jul 15, 2018
0da5e68
Cleanup and remove double cast.
JimBobSquarePants Jul 16, 2018
2108bf1
Merge remote-tracking branch 'upstream/master' into feature/histogram…
JimBobSquarePants Jul 16, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
}
}
}
}