From 976747490c43cce3a03ef7fa20084072e10c00be Mon Sep 17 00:00:00 2001 From: Ynse Hoornenborg Date: Sat, 17 Sep 2022 13:03:17 +0200 Subject: [PATCH 1/8] Implement auto level processor --- .../Normalization/AutoLevelProcessor.cs | 37 +++++ .../AutoLevelProcessor{TPixel}.cs | 136 ++++++++++++++++++ ...lHistogramEqualizationProcessor{TPixel}.cs | 43 +----- .../GrayscaleLevelsRowOperation{TPixel}.cs | 53 +++++++ .../HistogramEqualizationMethod.cs | 5 + .../HistogramEqualizationProcessor.cs | 3 + .../HistogramEqualizationTests.cs | 18 +++ ...ToReferenceOutput_Rgba32_forest_bridge.png | 3 + 8 files changed, 256 insertions(+), 42 deletions(-) create mode 100644 src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor.cs create mode 100644 src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor{TPixel}.cs create mode 100644 src/ImageSharp/Processing/Processors/Normalization/GrayscaleLevelsRowOperation{TPixel}.cs create mode 100644 tests/Images/External/ReferenceOutput/HistogramEqualizationTests/AutoLevel_CompareToReferenceOutput_Rgba32_forest_bridge.png diff --git a/src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor.cs new file mode 100644 index 0000000000..b33e46ce37 --- /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 0000000000..bdb2a500b5 --- /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 59c37373ea..d506777be0 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs @@ -51,7 +51,7 @@ protected override void OnFrameApply(ImageFrame source) 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 @@ ref MemoryMarshal.GetReference(histogram), 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 0000000000..f4fcd15782 --- /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 c8fb361398..e5cfd0dc78 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 f90a810790..d493d1734b 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs @@ -60,6 +60,9 @@ public abstract IImageProcessor CreatePixelSpecificProcessor(Con 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 09ba486a6f..9ef69f76e4 100644 --- a/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs +++ b/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs @@ -134,6 +134,24 @@ public void Adaptive_TileInterpolation_10Tiles_WithClipping(TestImagePro } } + [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 0000000000..123cd582c9 --- /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 From 34891da46b32fa87bb4183112c6cfa2fda2015c8 Mon Sep 17 00:00:00 2001 From: Ynse Hoornenborg Date: Sat, 17 Sep 2022 14:18:34 +0200 Subject: [PATCH 2/8] Add SyncChannels option --- .../Normalization/AutoLevelProcessor.cs | 11 ++- .../AutoLevelProcessor{TPixel}.cs | 86 +++++++++++++++++-- .../HistogramEqualizationOptions.cs | 7 ++ .../HistogramEqualizationProcessor.cs | 2 +- .../HistogramEqualizationTests.cs | 22 ++++- ...oReferenceOutput_Rgba32_forest_bridge.png} | 0 ...ToReferenceOutput_Rgba32_forest_bridge.png | 3 + 7 files changed, 119 insertions(+), 12 deletions(-) rename tests/Images/External/ReferenceOutput/HistogramEqualizationTests/{AutoLevel_CompareToReferenceOutput_Rgba32_forest_bridge.png => AutoLevel_SeparateChannels_CompareToReferenceOutput_Rgba32_forest_bridge.png} (100%) create mode 100644 tests/Images/External/ReferenceOutput/HistogramEqualizationTests/AutoLevel_SynchronizedChannels_CompareToReferenceOutput_Rgba32_forest_bridge.png diff --git a/src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor.cs index b33e46ce37..2721efac6d 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor.cs @@ -17,14 +17,22 @@ public class AutoLevelProcessor : HistogramEqualizationProcessor /// 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. + /// Whether to apply a synchronized luminance value to each color channel. public AutoLevelProcessor( int luminanceLevels, bool clipHistogram, - int clipLimit) + int clipLimit, + bool syncChannels) : base(luminanceLevels, clipHistogram, clipLimit) { + this.SyncChannels = syncChannels; } + /// + /// Gets whether to apply a synchronized luminance value to each color channel. + /// + public bool SyncChannels { get; } + /// public override IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) => new AutoLevelProcessor( @@ -32,6 +40,7 @@ public override IImageProcessor CreatePixelSpecificProcessor(Con this.LuminanceLevels, this.ClipHistogram, this.ClipLimit, + this.SyncChannels, source, sourceRectangle); } diff --git a/src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor{TPixel}.cs index bdb2a500b5..28a799a61e 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor{TPixel}.cs @@ -30,17 +30,25 @@ internal class AutoLevelProcessor : HistogramEqualizationProcessorThe 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. + /// Whether to apply a synchronized luminance value to each color channel. public AutoLevelProcessor( Configuration configuration, int luminanceLevels, bool clipHistogram, int clipLimit, + bool syncChannels, Image source, Rectangle sourceRectangle) : base(configuration, luminanceLevels, clipHistogram, clipLimit, source, sourceRectangle) { + this.SyncChannels = syncChannels; } + /// + /// Gets whether to apply a synchronized luminance value to each color channel. + /// + private bool SyncChannels { get; } + /// protected override void OnFrameApply(ImageFrame source) { @@ -73,18 +81,78 @@ ref MemoryMarshal.GetReference(histogram), 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); + if (this.SyncChannels) + { + var cdfOperation = new SynchronizedChannelsRowOperation(interest, cdfBuffer, source.PixelBuffer, this.LuminanceLevels, numberOfPixelsMinusCdfMin); + ParallelRowIterator.IterateRows( + this.Configuration, + interest, + in cdfOperation); + } + else + { + var cdfOperation = new SeperateChannelsRowOperation(interest, cdfBuffer, source.PixelBuffer, this.LuminanceLevels, numberOfPixelsMinusCdfMin); + ParallelRowIterator.IterateRows( + this.Configuration, + interest, + in cdfOperation); + } + } + + /// + /// A implementing the cdf logic for synchronized color channels. + /// + private readonly struct SynchronizedChannelsRowOperation : 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 SynchronizedChannelsRowOperation( + 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(); + int luminance = ColorNumerics.GetBT709Luminance(ref vector, levels); + float scaledLuminance = Unsafe.Add(ref cdfBase, luminance) / noOfPixelsMinusCdfMin; + float scalingFactor = scaledLuminance * levels / luminance; + Vector4 scaledVector = new Vector4(scalingFactor * vector.X, scalingFactor * vector.Y, scalingFactor * vector.Z, vector.W); + pixel.FromVector4(scaledVector); + } + } } /// - /// A implementing the cdf application levels logic for . + /// A implementing the cdf logic for separate color channels. /// - private readonly struct CdfApplicationRowOperation : IRowOperation + private readonly struct SeperateChannelsRowOperation : IRowOperation { private readonly Rectangle bounds; private readonly IMemoryOwner cdfBuffer; @@ -93,7 +161,7 @@ ref MemoryMarshal.GetReference(histogram), private readonly float numberOfPixelsMinusCdfMin; [MethodImpl(InliningOptions.ShortMethod)] - public CdfApplicationRowOperation( + public SeperateChannelsRowOperation( Rectangle bounds, IMemoryOwner cdfBuffer, Buffer2D source, diff --git a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs index 6343788425..1736a067ae 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs @@ -42,4 +42,11 @@ public class HistogramEqualizationOptions /// Defaults to 8. /// public int NumberOfTiles { get; set; } = 8; + + /// + /// Gets or sets a value indicating whether to synchronize the scaling factor over all color channels. + /// This parameter is only applicable to AutoLevel and is ignored for all others. + /// Defaults to true. + /// + public bool SyncChannels { get; set; } = true; } diff --git a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs index d493d1734b..8a9056b1f3 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs @@ -61,7 +61,7 @@ public abstract IImageProcessor CreatePixelSpecificProcessor(Con => new AdaptiveHistogramEqualizationSlidingWindowProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimit, options.NumberOfTiles), HistogramEqualizationMethod.AutoLevel - => new AutoLevelProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimit), + => new AutoLevelProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimit, options.SyncChannels), _ => 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 9ef69f76e4..60e33835af 100644 --- a/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs +++ b/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs @@ -136,7 +136,7 @@ public void Adaptive_TileInterpolation_10Tiles_WithClipping(TestImagePro [Theory] [WithFile(TestImages.Jpeg.Baseline.ForestBridgeDifferentComponentsQuality, PixelTypes.Rgba32)] - public void AutoLevel_CompareToReferenceOutput(TestImageProvider provider) + public void AutoLevel_SeparateChannels_CompareToReferenceOutput(TestImageProvider provider) where TPixel : unmanaged, IPixel { using (Image image = provider.GetImage()) @@ -145,6 +145,26 @@ public void AutoLevel_CompareToReferenceOutput(TestImageProvider { Method = HistogramEqualizationMethod.AutoLevel, LuminanceLevels = 256, + SyncChannels = false + }; + image.Mutate(x => x.HistogramEqualization(options)); + image.DebugSave(provider); + image.CompareToReferenceOutput(ValidatorComparer, provider, extension: "png"); + } + } + + [Theory] + [WithFile(TestImages.Jpeg.Baseline.ForestBridgeDifferentComponentsQuality, PixelTypes.Rgba32)] + public void AutoLevel_SynchronizedChannels_CompareToReferenceOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = provider.GetImage()) + { + var options = new HistogramEqualizationOptions + { + Method = HistogramEqualizationMethod.AutoLevel, + LuminanceLevels = 256, + SyncChannels = true }; image.Mutate(x => x.HistogramEqualization(options)); image.DebugSave(provider); diff --git a/tests/Images/External/ReferenceOutput/HistogramEqualizationTests/AutoLevel_CompareToReferenceOutput_Rgba32_forest_bridge.png b/tests/Images/External/ReferenceOutput/HistogramEqualizationTests/AutoLevel_SeparateChannels_CompareToReferenceOutput_Rgba32_forest_bridge.png similarity index 100% rename from tests/Images/External/ReferenceOutput/HistogramEqualizationTests/AutoLevel_CompareToReferenceOutput_Rgba32_forest_bridge.png rename to tests/Images/External/ReferenceOutput/HistogramEqualizationTests/AutoLevel_SeparateChannels_CompareToReferenceOutput_Rgba32_forest_bridge.png diff --git a/tests/Images/External/ReferenceOutput/HistogramEqualizationTests/AutoLevel_SynchronizedChannels_CompareToReferenceOutput_Rgba32_forest_bridge.png b/tests/Images/External/ReferenceOutput/HistogramEqualizationTests/AutoLevel_SynchronizedChannels_CompareToReferenceOutput_Rgba32_forest_bridge.png new file mode 100644 index 0000000000..ff5b35a5f7 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/HistogramEqualizationTests/AutoLevel_SynchronizedChannels_CompareToReferenceOutput_Rgba32_forest_bridge.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dca9b5b890d3a79b0002b7093d254d484ada4207e5010d1f0c6248d4dd6e22db +size 13909894 From c086afd273537ee8018635e875b989c3efd30daa Mon Sep 17 00:00:00 2001 From: Ynse Hoornenborg Date: Sat, 17 Sep 2022 14:43:02 +0200 Subject: [PATCH 3/8] Fix SA1623 warnings --- .../Processing/Processors/Normalization/AutoLevelProcessor.cs | 2 +- .../Processors/Normalization/AutoLevelProcessor{TPixel}.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor.cs index 2721efac6d..43609367ed 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor.cs @@ -29,7 +29,7 @@ public AutoLevelProcessor( } /// - /// Gets whether to apply a synchronized luminance value to each color channel. + /// Gets a value indicating whether to apply a synchronized luminance value to each color channel. /// public bool SyncChannels { get; } diff --git a/src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor{TPixel}.cs index 28a799a61e..fb3de130a9 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor{TPixel}.cs @@ -45,7 +45,7 @@ public AutoLevelProcessor( } /// - /// Gets whether to apply a synchronized luminance value to each color channel. + /// Gets a value indicating whether to apply a synchronized luminance value to each color channel. /// private bool SyncChannels { get; } From 44477a9c25c4c26b713488dda54da90f8849f663 Mon Sep 17 00:00:00 2001 From: Ynse Hoornenborg Date: Fri, 23 Sep 2022 12:32:34 +0200 Subject: [PATCH 4/8] Test to compare behavior to ImageMagick --- .../Normalization/MagickCompareTests.cs | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 tests/ImageSharp.Tests/Processing/Normalization/MagickCompareTests.cs diff --git a/tests/ImageSharp.Tests/Processing/Normalization/MagickCompareTests.cs b/tests/ImageSharp.Tests/Processing/Normalization/MagickCompareTests.cs new file mode 100644 index 0000000000..6b01624906 --- /dev/null +++ b/tests/ImageSharp.Tests/Processing/Normalization/MagickCompareTests.cs @@ -0,0 +1,78 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors.Normalization; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; + +using ImageMagick; + +namespace SixLabors.ImageSharp.Tests.Processing.Normalization; + +// ReSharper disable InconsistentNaming +[Trait("Category", "Processors")] +public class MagickCompareTests +{ + [Theory] + [WithFile(TestImages.Jpeg.Baseline.ForestBridgeDifferentComponentsQuality, PixelTypes.Rgba32)] + public void AutoLevel_CompareToMagick(TestImageProvider provider) + where TPixel : unmanaged, ImageSharp.PixelFormats.IPixel + { + using Stream stream = LoadAsStream(provider); + var magickImage = new MagickImage(stream); + + // Apply Auto Level using the Grey (BT.709) channel. + magickImage.AutoLevel(Channels.Gray); + Image imageFromMagick = ConvertImageFromMagick(magickImage); + + using (Image image = provider.GetImage()) + { + var options = new HistogramEqualizationOptions + { + Method = HistogramEqualizationMethod.AutoLevel, + LuminanceLevels = 256, + SyncChannels = true + }; + image.Mutate(x => x.HistogramEqualization(options)); + image.DebugSave(provider); + ExactImageComparer.Instance.CompareImages(imageFromMagick, image); + } + } + + private Stream LoadAsStream(TestImageProvider provider) + where TPixel : unmanaged, ImageSharp.PixelFormats.IPixel + { + string path = TestImageProvider.GetFilePathOrNull(provider); + if (path == null) + { + throw new InvalidOperationException("CompareToMagick() works only with file providers!"); + } + + var testFile = TestFile.Create(path); + return new FileStream(testFile.FullPath, FileMode.Open); + } + + private Image ConvertImageFromMagick(MagickImage magickImage) + where TPixel : unmanaged, ImageSharp.PixelFormats.IPixel + { + Configuration configuration = Configuration.Default.Clone(); + configuration.PreferContiguousImageBuffers = true; + var result = new Image(configuration, magickImage.Width, magickImage.Height); + + Assert.True(result.DangerousTryGetSinglePixelMemory(out Memory resultPixels)); + + using (IUnsafePixelCollection pixels = magickImage.GetPixelsUnsafe()) + { + byte[] data = pixels.ToByteArray(PixelMapping.RGBA); + + PixelOperations.Instance.FromRgba32Bytes( + configuration, + data, + resultPixels.Span, + resultPixels.Length); + } + + return result; + } +} From 41fbfb4e5f2c47cea13de57aced62e7bb78cbf04 Mon Sep 17 00:00:00 2001 From: Ynse Hoornenborg Date: Sat, 24 Sep 2022 11:01:31 +0200 Subject: [PATCH 5/8] Rephrase comment to align with ImageMagick --- .../Processors/Normalization/HistogramEqualizationMethod.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationMethod.cs b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationMethod.cs index e5cfd0dc78..e31ca62795 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationMethod.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationMethod.cs @@ -24,7 +24,8 @@ public enum HistogramEqualizationMethod : int AdaptiveSlidingWindow, /// - /// Global histogram equalization, but applied to each color channel separately. + /// Adjusts the brightness levels of a particular image by scaling to the + /// minimum and maximum values to the full brightness range. /// AutoLevel } From 86da190455c06c0915cea75a6c3a7d8e8d61e7cd Mon Sep 17 00:00:00 2001 From: Ynse Hoornenborg Date: Sat, 24 Sep 2022 11:33:12 +0200 Subject: [PATCH 6/8] Typo --- .../Processors/Normalization/HistogramEqualizationMethod.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationMethod.cs b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationMethod.cs index e31ca62795..e104734820 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationMethod.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationMethod.cs @@ -24,7 +24,7 @@ public enum HistogramEqualizationMethod : int AdaptiveSlidingWindow, /// - /// Adjusts the brightness levels of a particular image by scaling to the + /// Adjusts the brightness levels of a particular image by scaling the /// minimum and maximum values to the full brightness range. /// AutoLevel From 80def2642e63a5af4b317279bbe8bcb5d98710af Mon Sep 17 00:00:00 2001 From: Ynse Hoornenborg Date: Sat, 24 Sep 2022 11:59:45 +0200 Subject: [PATCH 7/8] Prevent file reading issues in windows --- .../Processing/Normalization/MagickCompareTests.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/ImageSharp.Tests/Processing/Normalization/MagickCompareTests.cs b/tests/ImageSharp.Tests/Processing/Normalization/MagickCompareTests.cs index 6b01624906..5fb0a4e934 100644 --- a/tests/ImageSharp.Tests/Processing/Normalization/MagickCompareTests.cs +++ b/tests/ImageSharp.Tests/Processing/Normalization/MagickCompareTests.cs @@ -19,12 +19,15 @@ public class MagickCompareTests public void AutoLevel_CompareToMagick(TestImageProvider provider) where TPixel : unmanaged, ImageSharp.PixelFormats.IPixel { - using Stream stream = LoadAsStream(provider); - var magickImage = new MagickImage(stream); + Image imageFromMagick; + using (Stream stream = LoadAsStream(provider)) + { + var magickImage = new MagickImage(stream); - // Apply Auto Level using the Grey (BT.709) channel. - magickImage.AutoLevel(Channels.Gray); - Image imageFromMagick = ConvertImageFromMagick(magickImage); + // Apply Auto Level using the Grey (BT.709) channel. + magickImage.AutoLevel(Channels.Gray); + imageFromMagick = ConvertImageFromMagick(magickImage); + } using (Image image = provider.GetImage()) { From 3912bda111537fded67be805b12fa01b313808b3 Mon Sep 17 00:00:00 2001 From: Ynse Hoornenborg Date: Sun, 25 Sep 2022 11:47:19 +0200 Subject: [PATCH 8/8] Fix overflow artifacts --- .../Processors/Normalization/AutoLevelProcessor{TPixel}.cs | 4 ++-- ...Channels_CompareToReferenceOutput_Rgba32_forest_bridge.png | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor{TPixel}.cs index fb3de130a9..856fba3dcb 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor{TPixel}.cs @@ -182,14 +182,14 @@ 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; + int levelsMinusOne = this.luminanceLevels - 1; 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; + var vector = pixel.ToVector4() * levelsMinusOne; uint originalX = (uint)MathF.Round(vector.X); float scaledX = Unsafe.Add(ref cdfBase, originalX) / noOfPixelsMinusCdfMin; diff --git a/tests/Images/External/ReferenceOutput/HistogramEqualizationTests/AutoLevel_SeparateChannels_CompareToReferenceOutput_Rgba32_forest_bridge.png b/tests/Images/External/ReferenceOutput/HistogramEqualizationTests/AutoLevel_SeparateChannels_CompareToReferenceOutput_Rgba32_forest_bridge.png index 123cd582c9..de79ec729c 100644 --- a/tests/Images/External/ReferenceOutput/HistogramEqualizationTests/AutoLevel_SeparateChannels_CompareToReferenceOutput_Rgba32_forest_bridge.png +++ b/tests/Images/External/ReferenceOutput/HistogramEqualizationTests/AutoLevel_SeparateChannels_CompareToReferenceOutput_Rgba32_forest_bridge.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:25041d2dafe6c01cfec0ae3c2ec15046accd44c02b737a4cfa464ad5f61d01af +oid sha256:aada4a2ccf45de24f2a591a18d9bc0260ceb3829e104fee6982061013ed87282 size 14107709