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