Browse Source

added abstract base class for histogram equalization, added option to enable / disable clipping

pull/673/head
popow 8 years ago
parent
commit
d664870dc3
  1. 36
      src/ImageSharp/Processing/HistogramEqualizationExtension.cs
  2. 63
      src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs
  3. 74
      src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs
  4. 21
      src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationMethod.cs
  5. 38
      src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs
  6. 87
      src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs
  7. 6
      tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs

36
src/ImageSharp/Processing/HistogramEqualizationExtension.cs

@ -12,25 +12,47 @@ namespace SixLabors.ImageSharp.Processing
public static class HistogramEqualizationExtension
{
/// <summary>
/// Equalizes the histogram of an image to increases the global contrast using 65536 luminance levels.
/// Equalizes the histogram of an image to increases the contrast.
/// </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);
=> HistogramEqualization(source, new HistogramEqualizationOptions());
/// <summary>
/// Equalizes the histogram of an image to increases the global contrast.
/// Equalizes the histogram of an image to increases the 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>
/// <param name="options">The histogram equalization options to use.</param>
/// <returns>The <see cref="Image{TPixel}"/>.</returns>
public static IImageProcessingContext<TPixel> HistogramEqualization<TPixel>(this IImageProcessingContext<TPixel> source, int luminanceLevels)
public static IImageProcessingContext<TPixel> HistogramEqualization<TPixel>(this IImageProcessingContext<TPixel> source, HistogramEqualizationOptions options)
where TPixel : struct, IPixel<TPixel>
=> source.ApplyProcessor(new HistogramEqualizationProcessor<TPixel>(luminanceLevels));
=> source.ApplyProcessor(GetProcessor<TPixel>(options));
private static HistogramEqualizationProcessor<TPixel> GetProcessor<TPixel>(HistogramEqualizationOptions options)
where TPixel : struct, IPixel<TPixel>
{
HistogramEqualizationProcessor<TPixel> processor;
switch (options.Method)
{
case HistogramEqualizationMethod.Global:
processor = new GlobalHistogramEqualizationProcessor<TPixel>(options.LuminanceLevels, options.ClipHistogram, options.ClipLimit);
break;
case HistogramEqualizationMethod.Adaptive:
processor = new AdaptiveHistEqualizationProcessor<TPixel>(options.LuminanceLevels, options.ClipHistogram, options.ClipLimit, options.GridSize);
break;
default:
processor = new GlobalHistogramEqualizationProcessor<TPixel>(options.LuminanceLevels, options.ClipHistogram, options.ClipLimit);
break;
}
return processor;
}
}
}

63
src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs

@ -22,18 +22,17 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
/// <summary>
/// Initializes a new instance of the <see cref="AdaptiveHistEqualizationProcessor{TPixel}"/> class.
/// </summary>
/// <param name="gridSize">The grid size of the adaptive histogram equalization.</param>
/// <param name="clipLimit">The histogram clip limit. Histogram bins which exceed this limit, will be capped at this value.</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>
public AdaptiveHistEqualizationProcessor(int gridSize, int clipLimit, int luminanceLevels)
: base(luminanceLevels)
/// <param name="clipHistogram">Indicating whether to clip the histogram bins at a specific value.</param>
/// <param name="clipLimit">The histogram clip limit. Histogram bins which exceed this limit, will be capped at this value.</param>
/// <param name="gridSize">The grid size of the adaptive histogram equalization.</param>
public AdaptiveHistEqualizationProcessor(int luminanceLevels, bool clipHistogram, int clipLimit, int gridSize)
: base(luminanceLevels, clipHistogram, clipLimit)
{
Guard.MustBeGreaterThan(gridSize, 8, nameof(gridSize));
Guard.MustBeGreaterThan(clipLimit, 1, nameof(clipLimit));
this.GridSize = gridSize;
this.ClipLimit = clipLimit;
}
/// <summary>
@ -41,11 +40,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
/// </summary>
public int GridSize { get; }
/// <summary>
/// Gets the histogram clip limit. Histogram bins which exceed this limit, will be capped at this value.
/// </summary>
public int ClipLimit { get; }
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration)
{
@ -82,23 +76,28 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
for (int y = 0; y < source.Height; y++)
{
// Clipping the histogram, but doing it on a copy to keep the original un-clipped values for the next iteration
histogram.CopyTo(histogramCopy);
this.ClipHistogram(histogramCopy, this.ClipLimit);
if (this.ClipHistogramEnabled)
{
// Clipping the histogram, but doing it on a copy to keep the original un-clipped values for the next iteration.
histogram.CopyTo(histogramCopy);
this.ClipHistogram(histogramCopy, this.ClipLimit);
}
// Calculate the cumulative distribution function, which will map each input pixel in the current grid to a new value.
cdf.Clear();
int cdfMin = this.CalculateCdf(cdf, histogramCopy);
double numberOfPixelsMinusCdfMin = (double)(pixelsInGrid - cdfMin);
int cdfMin = this.ClipHistogramEnabled ? this.CalculateCdf(cdf, histogramCopy) : this.CalculateCdf(cdf, histogram);
float numberOfPixelsMinusCdfMin = pixelsInGrid - cdfMin;
// Map the current pixel to the new equalized value
int luminance = this.GetLuminance(source[x, y], this.LuminanceLevels);
double luminanceEqualized = cdf[luminance] / numberOfPixelsMinusCdfMin;
float luminanceEqualized = cdf[luminance] / numberOfPixelsMinusCdfMin;
targetPixels[x, y].PackFromVector4(new Vector4((float)luminanceEqualized));
// Remove top most row from the histogram, mirroring rows which exceeds the borders
// Remove top most row from the histogram, mirroring rows which exceeds the borders.
Span<TPixel> rowSpan = this.GetPixelRow(source, x - halfGridSize, y - halfGridSize, this.GridSize);
this.RemovePixelsFromHistogram(rowSpan, histogram, this.LuminanceLevels);
// Add new bottom row to the histogram, mirroring rows which exceeds the borders
// Add new bottom row to the histogram, mirroring rows which exceeds the borders.
rowSpan = this.GetPixelRow(source, x - halfGridSize, y + halfGridSize, this.GridSize);
this.AddPixelsTooHistogram(rowSpan, histogram, this.LuminanceLevels);
}
@ -162,32 +161,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
return source.GetPixelRowSpan(y).Slice(start: x, length: gridSize);
}
/// <summary>
/// AHE tends to over amplify the contrast in near-constant regions of the image, since the histogram in such regions is highly concentrated.
/// Clipping the histogram is meant to reduce this effect, by cutting of histogram bin's which exceed a certain amount and redistribute
/// the values over the clip limit to all other bins equally.
/// </summary>
/// <param name="histogram">The histogram to apply the clipping.</param>
/// <param name="clipLimit">The histogram clip limit. Histogram bins which exceed this limit, will be capped at this value.</param>
private void ClipHistogram(Span<int> histogram, int clipLimit)
{
int sumOverClip = 0;
for (int i = 0; i < histogram.Length; i++)
{
if (histogram[i] > clipLimit)
{
sumOverClip += histogram[i] - clipLimit;
histogram[i] = clipLimit;
}
}
int addToEachBin = (int)Math.Floor(sumOverClip / (double)this.LuminanceLevels);
for (int i = 0; i < histogram.Length; i++)
{
histogram[i] += addToEachBin;
}
}
/// <summary>
/// Adds a row of grey values to the histogram.
/// </summary>

74
src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs

@ -0,0 +1,74 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Numerics;
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 GlobalHistogramEqualizationProcessor<TPixel> : HistogramEqualizationProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
{
/// <summary>
/// Initializes a new instance of the <see cref="GlobalHistogramEqualizationProcessor{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>
/// <param name="clipHistogram">Indicating whether to clip the histogram bins at a specific value.</param>
/// <param name="clipLimit">The histogram clip limit. Histogram bins which exceed this limit, will be capped at this value.</param>
public GlobalHistogramEqualizationProcessor(int luminanceLevels, bool clipHistogram, int clipLimit)
: base(luminanceLevels, clipHistogram, clipLimit)
{
}
/// <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]++;
}
if (this.ClipHistogramEnabled)
{
this.ClipHistogram(histogram, this.ClipLimit);
}
// 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));
}
}
}
}
}

21
src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationMethod.cs

@ -0,0 +1,21 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Processing.Processors.Normalization
{
/// <summary>
/// Enumerates the different types of defined histogram equalization methods.
/// </summary>
public enum HistogramEqualizationMethod : int
{
/// <summary>
/// A global histogram equalization.
/// </summary>
Global,
/// <summary>
/// Adaptive histogram equalization.
/// </summary>
Adaptive
}
}

38
src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs

@ -0,0 +1,38 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Processing.Processors.Normalization
{
/// <summary>
/// Data container providing the different options for the histogram equalization.
/// </summary>
public class HistogramEqualizationOptions
{
/// <summary>
/// Gets or sets the histogram equalization method to use. Defaults to global histogram equalization.
/// </summary>
public HistogramEqualizationMethod Method { get; set; } = HistogramEqualizationMethod.Global;
/// <summary>
/// Gets or sets the number of different luminance levels. Typical values are 256 for 8-bit grayscale images
/// or 65536 for 16-bit grayscale images. Defaults to 256.
/// </summary>
public int LuminanceLevels { get; set; } = 256;
/// <summary>
/// Gets or sets a value indicating whether to clip the histogram bins at a specific value. Defaults to true.
/// </summary>
public bool ClipHistogram { get; set; } = true;
/// <summary>
/// Gets or sets the histogram clip limit. Histogram bins which exceed this limit, will be capped at this value.
/// Defaults to 80.
/// </summary>
public int ClipLimit { get; set; } = 80;
/// <summary>
/// Gets or sets the size of the grid for the adaptive histogram equalization. Defaults to 32.
/// </summary>
public int GridSize { get; set; } = 32;
}
}

87
src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs

@ -2,20 +2,15 @@
// 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.
/// Defines a processor that normalizes the histogram of an image.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal class HistogramEqualizationProcessor<TPixel> : ImageProcessor<TPixel>
internal abstract class HistogramEqualizationProcessor<TPixel> : ImageProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
{
/// <summary>
@ -23,11 +18,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
/// </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)
/// <param name="clipHistogram">Indicates, if histogram bins should be clipped.</param>
/// <param name="clipLimit">The histogram clip limit. Histogram bins which exceed this limit, will be capped at this value.</param>
protected HistogramEqualizationProcessor(int luminanceLevels, bool clipHistogram, int clipLimit)
{
Guard.MustBeGreaterThan(luminanceLevels, 0, nameof(luminanceLevels));
Guard.MustBeGreaterThan(clipLimit, 1, nameof(clipLimit));
this.LuminanceLevels = luminanceLevels;
this.ClipHistogramEnabled = clipHistogram;
this.ClipLimit = clipLimit;
}
/// <summary>
@ -35,42 +35,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
/// </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;
/// <summary>
/// Gets a value indicating whether to clip the histogram bins at a specific value.
/// </summary>
public bool ClipHistogramEnabled { get; }
pixels[i].PackFromVector4(new Vector4(luminanceEqualized));
}
}
}
/// <summary>
/// Gets the histogram clip limit. Histogram bins which exceed this limit, will be capped at this value.
/// </summary>
public int ClipLimit { get; }
/// <summary>
/// Calculates the cumulative distribution function.
@ -108,12 +81,38 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
return cdfMin;
}
/// <summary>
/// AHE tends to over amplify the contrast in near-constant regions of the image, since the histogram in such regions is highly concentrated.
/// Clipping the histogram is meant to reduce this effect, by cutting of histogram bin's which exceed a certain amount and redistribute
/// the values over the clip limit to all other bins equally.
/// </summary>
/// <param name="histogram">The histogram to apply the clipping.</param>
/// <param name="clipLimit">The histogram clip limit. Histogram bins which exceed this limit, will be capped at this value.</param>
protected void ClipHistogram(Span<int> histogram, int clipLimit)
{
int sumOverClip = 0;
for (int i = 0; i < histogram.Length; i++)
{
if (histogram[i] > clipLimit)
{
sumOverClip += histogram[i] - clipLimit;
histogram[i] = clipLimit;
}
}
int addToEachBin = (int)Math.Floor(sumOverClip / (double)this.LuminanceLevels);
for (int i = 0; i < histogram.Length; i++)
{
histogram[i] += addToEachBin;
}
}
/// <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)]
[System.Runtime.CompilerServices.MethodImpl(InliningOptions.ShortMethod)]
protected int GetLuminance(TPixel sourcePixel, int luminanceLevels)
{
// Convert to grayscale using ITU-R Recommendation BT.709

6
tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs

@ -3,6 +3,7 @@
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Normalization;
using Xunit;
namespace SixLabors.ImageSharp.Tests.Processing.Normalization
@ -50,7 +51,10 @@ namespace SixLabors.ImageSharp.Tests.Processing.Normalization
};
// Act
image.Mutate(x => x.HistogramEqualization(luminanceLevels));
image.Mutate(x => x.HistogramEqualization(new HistogramEqualizationOptions()
{
LuminanceLevels = luminanceLevels
}));
// Assert
for (int y = 0; y < 8; y++)

Loading…
Cancel
Save