diff --git a/src/ImageSharp/Processing/HistogramEqualizationExtension.cs b/src/ImageSharp/Processing/HistogramEqualizationExtension.cs new file mode 100644 index 0000000000..8dabfcc05c --- /dev/null +++ b/src/ImageSharp/Processing/HistogramEqualizationExtension.cs @@ -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 +{ + /// + /// Adds extension that allow the adjustment of the contrast of an image via its histogram. + /// + public static class HistogramEqualizationExtension + { + /// + /// Equalizes the histogram of an image to increases the global contrast using 65536 luminance levels. + /// + /// The pixel format. + /// The image this method extends. + /// The . + public static IImageProcessingContext HistogramEqualization(this IImageProcessingContext source) + where TPixel : struct, IPixel + => HistogramEqualization(source, 65536); + + /// + /// Equalizes the histogram of an image to increases the global 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 . + public static IImageProcessingContext HistogramEqualization(this IImageProcessingContext source, int luminanceLevels) + where TPixel : struct, IPixel + => source.ApplyProcessor(new HistogramEqualizationProcessor(luminanceLevels)); + } +} diff --git a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs new file mode 100644 index 0000000000..ba56e392f4 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs @@ -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 +{ + /// + /// Applies a global histogram equalization to the image. + /// + /// The pixel format. + internal class HistogramEqualizationProcessor : ImageProcessor + 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. + public HistogramEqualizationProcessor(int luminanceLevels) + { + Guard.MustBeGreaterThan(luminanceLevels, 0, nameof(luminanceLevels)); + + this.LuminanceLevels = luminanceLevels; + } + + /// + /// Gets the number of luminance levels. + /// + 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; + + pixels[i].PackFromVector4(new Vector4(luminanceEqualized)); + } + } + } + + /// + /// Calculates the cumulative distribution function. + /// + /// The array holding the cdf. + /// The histogram of the input image. + /// The first none zero value of the cdf. + private int CalculateCdf(Span cdf, Span 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; + } + + /// + /// 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)] + 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; + } + } +} diff --git a/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs b/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs new file mode 100644 index 0000000000..1595ed32cc --- /dev/null +++ b/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs @@ -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(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); + } + } + } + } +}