diff --git a/src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor.cs new file mode 100644 index 000000000..b33e46ce3 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor.cs @@ -0,0 +1,37 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Processing.Processors.Normalization; + +/// +/// Applies a luminance histogram equilization to the image. +/// +public class AutoLevelProcessor : HistogramEqualizationProcessor +{ + /// + /// Initializes a new instance of the class. + /// It uses the exact minimum and maximum values found in the luminance channel, as the BlackPoint and WhitePoint to linearly stretch the colors + /// (and histogram) of the image. + /// + /// 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 AutoLevelProcessor( + int luminanceLevels, + bool clipHistogram, + int clipLimit) + : base(luminanceLevels, clipHistogram, clipLimit) + { + } + + /// + public override IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) + => new AutoLevelProcessor( + configuration, + this.LuminanceLevels, + this.ClipHistogram, + this.ClipLimit, + source, + sourceRectangle); +} diff --git a/src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor{TPixel}.cs new file mode 100644 index 000000000..bdb2a500b --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor{TPixel}.cs @@ -0,0 +1,136 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Normalization; + +/// +/// Applies a luminance histogram equalization to the image. +/// +/// The pixel format. +internal class AutoLevelProcessor : HistogramEqualizationProcessor + where TPixel : unmanaged, IPixel +{ + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// + /// 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. + /// The source for the current processor instance. + /// The source area to process for the current processor instance. + public AutoLevelProcessor( + Configuration configuration, + int luminanceLevels, + bool clipHistogram, + int clipLimit, + Image source, + Rectangle sourceRectangle) + : base(configuration, luminanceLevels, clipHistogram, clipLimit, source, sourceRectangle) + { + } + + /// + protected override void OnFrameApply(ImageFrame source) + { + MemoryAllocator memoryAllocator = this.Configuration.MemoryAllocator; + int numberOfPixels = source.Width * source.Height; + var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); + + using IMemoryOwner histogramBuffer = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean); + + // Build the histogram of the grayscale levels. + var grayscaleOperation = new GrayscaleLevelsRowOperation(interest, histogramBuffer, source.PixelBuffer, this.LuminanceLevels); + ParallelRowIterator.IterateRows( + this.Configuration, + interest, + in grayscaleOperation); + + Span histogram = histogramBuffer.GetSpan(); + if (this.ClipHistogramEnabled) + { + this.ClipHistogram(histogram, this.ClipLimit); + } + + using IMemoryOwner cdfBuffer = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean); + + // Calculate the cumulative distribution function, which will map each input pixel to a new value. + int cdfMin = CalculateCdf( + ref MemoryMarshal.GetReference(cdfBuffer.GetSpan()), + ref MemoryMarshal.GetReference(histogram), + histogram.Length - 1); + + float numberOfPixelsMinusCdfMin = numberOfPixels - cdfMin; + + // Apply the cdf to each pixel of the image + var cdfOperation = new CdfApplicationRowOperation(interest, cdfBuffer, source.PixelBuffer, this.LuminanceLevels, numberOfPixelsMinusCdfMin); + ParallelRowIterator.IterateRows( + this.Configuration, + interest, + in cdfOperation); + } + + /// + /// A implementing the cdf application levels logic for . + /// + private readonly struct CdfApplicationRowOperation : IRowOperation + { + private readonly Rectangle bounds; + private readonly IMemoryOwner cdfBuffer; + private readonly Buffer2D source; + private readonly int luminanceLevels; + private readonly float numberOfPixelsMinusCdfMin; + + [MethodImpl(InliningOptions.ShortMethod)] + public CdfApplicationRowOperation( + Rectangle bounds, + IMemoryOwner cdfBuffer, + Buffer2D source, + int luminanceLevels, + float numberOfPixelsMinusCdfMin) + { + this.bounds = bounds; + this.cdfBuffer = cdfBuffer; + this.source = source; + this.luminanceLevels = luminanceLevels; + this.numberOfPixelsMinusCdfMin = numberOfPixelsMinusCdfMin; + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(int y) + { + ref int cdfBase = ref MemoryMarshal.GetReference(this.cdfBuffer.GetSpan()); + var sourceAccess = new PixelAccessor(this.source); + Span pixelRow = sourceAccess.GetRowSpan(y); + int levels = this.luminanceLevels; + float noOfPixelsMinusCdfMin = this.numberOfPixelsMinusCdfMin; + + for (int x = 0; x < this.bounds.Width; x++) + { + // TODO: We should bulk convert here. + ref TPixel pixel = ref pixelRow[x]; + var vector = pixel.ToVector4() * levels; + + uint originalX = (uint)MathF.Round(vector.X); + float scaledX = Unsafe.Add(ref cdfBase, originalX) / noOfPixelsMinusCdfMin; + uint originalY = (uint)MathF.Round(vector.Y); + float scaledY = Unsafe.Add(ref cdfBase, originalY) / noOfPixelsMinusCdfMin; + uint originalZ = (uint)MathF.Round(vector.Z); + float scaledZ = Unsafe.Add(ref cdfBase, originalZ) / noOfPixelsMinusCdfMin; + pixel.FromVector4(new Vector4(scaledX, scaledY, scaledZ, vector.W)); + } + } + } +} diff --git a/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs index 59c37373e..d506777be 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs @@ -51,7 +51,7 @@ internal class GlobalHistogramEqualizationProcessor : HistogramEqualizat using IMemoryOwner histogramBuffer = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean); // Build the histogram of the grayscale levels. - var grayscaleOperation = new GrayscaleLevelsRowOperation(interest, histogramBuffer, source.PixelBuffer, this.LuminanceLevels); + var grayscaleOperation = new GrayscaleLevelsRowOperation(interest, histogramBuffer, source.PixelBuffer, this.LuminanceLevels); ParallelRowIterator.IterateRows( this.Configuration, interest, @@ -81,47 +81,6 @@ internal class GlobalHistogramEqualizationProcessor : HistogramEqualizat in cdfOperation); } - /// - /// A implementing the grayscale levels logic for . - /// - private readonly struct GrayscaleLevelsRowOperation : IRowOperation - { - private readonly Rectangle bounds; - private readonly IMemoryOwner histogramBuffer; - private readonly Buffer2D source; - private readonly int luminanceLevels; - - [MethodImpl(InliningOptions.ShortMethod)] - public GrayscaleLevelsRowOperation( - Rectangle bounds, - IMemoryOwner histogramBuffer, - Buffer2D source, - int luminanceLevels) - { - this.bounds = bounds; - this.histogramBuffer = histogramBuffer; - this.source = source; - this.luminanceLevels = luminanceLevels; - } - - /// - [MethodImpl(InliningOptions.ShortMethod)] - public void Invoke(int y) - { - ref int histogramBase = ref MemoryMarshal.GetReference(this.histogramBuffer.GetSpan()); - Span pixelRow = this.source.DangerousGetRowSpan(y); - int levels = this.luminanceLevels; - - for (int x = 0; x < this.bounds.Width; x++) - { - // TODO: We should bulk convert here. - var vector = pixelRow[x].ToVector4(); - int luminance = ColorNumerics.GetBT709Luminance(ref vector, levels); - Interlocked.Increment(ref Unsafe.Add(ref histogramBase, luminance)); - } - } - } - /// /// A implementing the cdf application levels logic for . /// diff --git a/src/ImageSharp/Processing/Processors/Normalization/GrayscaleLevelsRowOperation{TPixel}.cs b/src/ImageSharp/Processing/Processors/Normalization/GrayscaleLevelsRowOperation{TPixel}.cs new file mode 100644 index 000000000..f4fcd1578 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Normalization/GrayscaleLevelsRowOperation{TPixel}.cs @@ -0,0 +1,53 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors.Normalization; + +/// +/// A implementing the grayscale levels logic as . +/// +internal readonly struct GrayscaleLevelsRowOperation : IRowOperation + where TPixel : unmanaged, IPixel +{ + private readonly Rectangle bounds; + private readonly IMemoryOwner histogramBuffer; + private readonly Buffer2D source; + private readonly int luminanceLevels; + + [MethodImpl(InliningOptions.ShortMethod)] + public GrayscaleLevelsRowOperation( + Rectangle bounds, + IMemoryOwner histogramBuffer, + Buffer2D source, + int luminanceLevels) + { + this.bounds = bounds; + this.histogramBuffer = histogramBuffer; + this.source = source; + this.luminanceLevels = luminanceLevels; + } + + /// + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(int y) + { + ref int histogramBase = ref MemoryMarshal.GetReference(this.histogramBuffer.GetSpan()); + Span pixelRow = this.source.DangerousGetRowSpan(y); + int levels = this.luminanceLevels; + + for (int x = 0; x < this.bounds.Width; x++) + { + // TODO: We should bulk convert here. + var vector = pixelRow[x].ToVector4(); + int luminance = ColorNumerics.GetBT709Luminance(ref vector, levels); + Interlocked.Increment(ref Unsafe.Add(ref histogramBase, luminance)); + } + } +} diff --git a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationMethod.cs b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationMethod.cs index c8fb36139..e5cfd0dc7 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationMethod.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationMethod.cs @@ -22,4 +22,9 @@ public enum HistogramEqualizationMethod : int /// Adaptive histogram equalization using sliding window. Slower then the tile interpolation mode, but can yield to better results. /// AdaptiveSlidingWindow, + + /// + /// Global histogram equalization, but applied to each color channel separately. + /// + AutoLevel } diff --git a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs index f90a81079..d493d1734 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs @@ -60,6 +60,9 @@ public abstract class HistogramEqualizationProcessor : IImageProcessor HistogramEqualizationMethod.AdaptiveSlidingWindow => new AdaptiveHistogramEqualizationSlidingWindowProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimit, options.NumberOfTiles), + HistogramEqualizationMethod.AutoLevel + => new AutoLevelProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimit), + _ => new GlobalHistogramEqualizationProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimit), }; } diff --git a/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs b/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs index 09ba486a6..9ef69f76e 100644 --- a/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs +++ b/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs @@ -134,6 +134,24 @@ public class HistogramEqualizationTests } } + [Theory] + [WithFile(TestImages.Jpeg.Baseline.ForestBridgeDifferentComponentsQuality, PixelTypes.Rgba32)] + public void AutoLevel_CompareToReferenceOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = provider.GetImage()) + { + var options = new HistogramEqualizationOptions + { + Method = HistogramEqualizationMethod.AutoLevel, + LuminanceLevels = 256, + }; + image.Mutate(x => x.HistogramEqualization(options)); + image.DebugSave(provider); + image.CompareToReferenceOutput(ValidatorComparer, provider, extension: "png"); + } + } + /// /// This is regression test for a bug with the calculation of the y-start positions, /// where it could happen that one too much start position was calculated in some cases. diff --git a/tests/Images/External/ReferenceOutput/HistogramEqualizationTests/AutoLevel_CompareToReferenceOutput_Rgba32_forest_bridge.png b/tests/Images/External/ReferenceOutput/HistogramEqualizationTests/AutoLevel_CompareToReferenceOutput_Rgba32_forest_bridge.png new file mode 100644 index 000000000..123cd582c --- /dev/null +++ b/tests/Images/External/ReferenceOutput/HistogramEqualizationTests/AutoLevel_CompareToReferenceOutput_Rgba32_forest_bridge.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25041d2dafe6c01cfec0ae3c2ec15046accd44c02b737a4cfa464ad5f61d01af +size 14107709