diff --git a/src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor.cs
index b33e46ce3..2721efac6 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 class AutoLevelProcessor : HistogramEqualizationProcessor
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 bdb2a500b..28a799a61 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 @@ internal class AutoLevelProcessor : HistogramEqualizationProcessor
+ /// 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 @@ internal class AutoLevelProcessor : HistogramEqualizationProcessor cdfBuffer,
Buffer2D source,
diff --git a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs
index 634378842..1736a067a 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 d493d1734..8a9056b1f 100644
--- a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs
+++ b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs
@@ -61,7 +61,7 @@ public abstract class HistogramEqualizationProcessor : IImageProcessor
=> 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 9ef69f76e..60e33835a 100644
--- a/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs
+++ b/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs
@@ -136,7 +136,7 @@ public class HistogramEqualizationTests
[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 class HistogramEqualizationTests
{
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 000000000..ff5b35a5f
--- /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