From d664870dc33e4acb19a326ce098d036edbb9d43b Mon Sep 17 00:00:00 2001 From: popow Date: Thu, 2 Aug 2018 20:06:08 +0200 Subject: [PATCH] added abstract base class for histogram equalization, added option to enable / disable clipping --- .../HistogramEqualizationExtension.cs | 36 ++++++-- .../AdaptiveHistEqualizationProcessor.cs | 63 ++++---------- .../GlobalHistogramEqualizationProcessor.cs | 74 ++++++++++++++++ .../HistogramEqualizationMethod.cs | 21 +++++ .../HistogramEqualizationOptions.cs | 38 ++++++++ .../HistogramEqualizationProcessor.cs | 87 +++++++++---------- .../HistogramEqualizationTests.cs | 6 +- 7 files changed, 228 insertions(+), 97 deletions(-) create mode 100644 src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs create mode 100644 src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationMethod.cs create mode 100644 src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs diff --git a/src/ImageSharp/Processing/HistogramEqualizationExtension.cs b/src/ImageSharp/Processing/HistogramEqualizationExtension.cs index 8dabfcc05..3a279cc5f 100644 --- a/src/ImageSharp/Processing/HistogramEqualizationExtension.cs +++ b/src/ImageSharp/Processing/HistogramEqualizationExtension.cs @@ -12,25 +12,47 @@ namespace SixLabors.ImageSharp.Processing public static class HistogramEqualizationExtension { /// - /// 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. /// /// The pixel format. /// The image this method extends. /// The . public static IImageProcessingContext HistogramEqualization(this IImageProcessingContext source) where TPixel : struct, IPixel - => HistogramEqualization(source, 65536); + => HistogramEqualization(source, new HistogramEqualizationOptions()); /// - /// Equalizes the histogram of an image to increases the global contrast. + /// Equalizes the histogram of an image to increases the contrast. /// /// The pixel format. /// The image this method extends. - /// The number of different luminance levels. Typical values are 256 for 8-bit grayscale images - /// or 65536 for 16-bit grayscale images. + /// The histogram equalization options to use. /// The . - public static IImageProcessingContext HistogramEqualization(this IImageProcessingContext source, int luminanceLevels) + public static IImageProcessingContext HistogramEqualization(this IImageProcessingContext source, HistogramEqualizationOptions options) where TPixel : struct, IPixel - => source.ApplyProcessor(new HistogramEqualizationProcessor(luminanceLevels)); + => source.ApplyProcessor(GetProcessor(options)); + + private static HistogramEqualizationProcessor GetProcessor(HistogramEqualizationOptions options) + where TPixel : struct, IPixel + { + HistogramEqualizationProcessor processor; + + switch (options.Method) + { + case HistogramEqualizationMethod.Global: + processor = new GlobalHistogramEqualizationProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimit); + break; + + case HistogramEqualizationMethod.Adaptive: + processor = new AdaptiveHistEqualizationProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimit, options.GridSize); + break; + + default: + processor = new GlobalHistogramEqualizationProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimit); + break; + } + + return processor; + } } } diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs index 3d2ef02d5..218d2680f 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistEqualizationProcessor.cs @@ -22,18 +22,17 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization /// /// Initializes a new instance of the class. /// - /// The grid size of the adaptive histogram equalization. - /// The histogram clip limit. Histogram bins which exceed this limit, will be capped at this value. /// The number of different luminance levels. Typical values are 256 for 8-bit grayscale images /// or 65536 for 16-bit grayscale images. - public AdaptiveHistEqualizationProcessor(int gridSize, int clipLimit, int luminanceLevels) - : base(luminanceLevels) + /// Indicating whether to clip the histogram bins at a specific value. + /// The histogram clip limit. Histogram bins which exceed this limit, will be capped at this value. + /// The grid size of the adaptive histogram equalization. + 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; } /// @@ -41,11 +40,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization /// public int GridSize { get; } - /// - /// Gets the histogram clip limit. Histogram bins which exceed this limit, will be capped at this value. - /// - public int ClipLimit { get; } - /// protected override void OnFrameApply(ImageFrame 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 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); } - /// - /// 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. - /// - /// The histogram to apply the clipping. - /// The histogram clip limit. Histogram bins which exceed this limit, will be capped at this value. - private void ClipHistogram(Span 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; - } - } - /// /// Adds a row of grey values to the histogram. /// diff --git a/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor.cs new file mode 100644 index 000000000..423840097 --- /dev/null +++ b/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 +{ + /// + /// Applies a global histogram equalization to the image. + /// + /// The pixel format. + internal class GlobalHistogramEqualizationProcessor : HistogramEqualizationProcessor + where TPixel : struct, IPixel + { + /// + /// Initializes a new instance of the class. + /// + /// The number of different luminance levels. Typical values are 256 for 8-bit grayscale images + /// or 65536 for 16-bit grayscale images. + /// Indicating whether to clip the histogram bins at a specific value. + /// The histogram clip limit. Histogram bins which exceed this limit, will be capped at this value. + public GlobalHistogramEqualizationProcessor(int luminanceLevels, bool clipHistogram, int clipLimit) + : base(luminanceLevels, clipHistogram, clipLimit) + { + } + + /// + protected override void OnFrameApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) + { + MemoryAllocator memoryAllocator = configuration.MemoryAllocator; + int numberOfPixels = source.Width * source.Height; + Span pixels = source.GetPixelSpan(); + + // Build the histogram of the grayscale levels. + using (IBuffer histogramBuffer = memoryAllocator.AllocateClean(this.LuminanceLevels)) + using (IBuffer cdfBuffer = memoryAllocator.AllocateClean(this.LuminanceLevels)) + { + Span 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 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)); + } + } + } + } +} diff --git a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationMethod.cs b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationMethod.cs new file mode 100644 index 000000000..cae745c9b --- /dev/null +++ b/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 +{ + /// + /// Enumerates the different types of defined histogram equalization methods. + /// + public enum HistogramEqualizationMethod : int + { + /// + /// A global histogram equalization. + /// + Global, + + /// + /// Adaptive histogram equalization. + /// + Adaptive + } +} diff --git a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs new file mode 100644 index 000000000..20aa113ac --- /dev/null +++ b/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 +{ + /// + /// Data container providing the different options for the histogram equalization. + /// + public class HistogramEqualizationOptions + { + /// + /// Gets or sets the histogram equalization method to use. Defaults to global histogram equalization. + /// + public HistogramEqualizationMethod Method { get; set; } = HistogramEqualizationMethod.Global; + + /// + /// 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. + /// + public int LuminanceLevels { get; set; } = 256; + + /// + /// Gets or sets a value indicating whether to clip the histogram bins at a specific value. Defaults to true. + /// + public bool ClipHistogram { get; set; } = true; + + /// + /// Gets or sets the histogram clip limit. Histogram bins which exceed this limit, will be capped at this value. + /// Defaults to 80. + /// + public int ClipLimit { get; set; } = 80; + + /// + /// Gets or sets the size of the grid for the adaptive histogram equalization. Defaults to 32. + /// + public int GridSize { get; set; } = 32; + } +} diff --git a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs index ab4f8332b..298de1388 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs +++ b/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 { /// - /// Applies a global histogram equalization to the image. + /// Defines a processor that normalizes the histogram of an image. /// /// The pixel format. - internal class HistogramEqualizationProcessor : ImageProcessor + internal abstract class HistogramEqualizationProcessor : ImageProcessor where TPixel : struct, IPixel { /// @@ -23,11 +18,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization /// /// The number of different luminance levels. Typical values are 256 for 8-bit grayscale images /// or 65536 for 16-bit grayscale images. - public HistogramEqualizationProcessor(int luminanceLevels) + /// Indicates, if histogram bins should be clipped. + /// The histogram clip limit. Histogram bins which exceed this limit, will be capped at this value. + 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; } /// @@ -35,42 +35,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization /// public int LuminanceLevels { get; } - /// - protected override void OnFrameApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) - { - MemoryAllocator memoryAllocator = configuration.MemoryAllocator; - int numberOfPixels = source.Width * source.Height; - Span pixels = source.GetPixelSpan(); - - // Build the histogram of the grayscale levels. - using (IBuffer histogramBuffer = memoryAllocator.AllocateClean(this.LuminanceLevels)) - using (IBuffer cdfBuffer = memoryAllocator.AllocateClean(this.LuminanceLevels)) - { - Span 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 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; + /// + /// Gets a value indicating whether to clip the histogram bins at a specific value. + /// + public bool ClipHistogramEnabled { get; } - pixels[i].PackFromVector4(new Vector4(luminanceEqualized)); - } - } - } + /// + /// Gets the histogram clip limit. Histogram bins which exceed this limit, will be capped at this value. + /// + public int ClipLimit { get; } /// /// Calculates the cumulative distribution function. @@ -108,12 +81,38 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization return cdfMin; } + /// + /// 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. + /// + /// The histogram to apply the clipping. + /// The histogram clip limit. Histogram bins which exceed this limit, will be capped at this value. + protected void ClipHistogram(Span 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; + } + } + /// /// Convert the pixel values to grayscale using ITU-R Recommendation BT.709. /// /// The pixel to get the luminance from /// The number of luminance levels (256 for 8 bit, 65536 for 16 bit grayscale images) - [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 diff --git a/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs b/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs index 1595ed32c..86d343e42 100644 --- a/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs +++ b/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++)