diff --git a/src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor.cs
new file mode 100644
index 0000000000..43609367ed
--- /dev/null
+++ b/src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor.cs
@@ -0,0 +1,46 @@
+// 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.
+ /// Whether to apply a synchronized luminance value to each color channel.
+ public AutoLevelProcessor(
+ int luminanceLevels,
+ bool clipHistogram,
+ int clipLimit,
+ bool syncChannels)
+ : base(luminanceLevels, clipHistogram, clipLimit)
+ {
+ this.SyncChannels = syncChannels;
+ }
+
+ ///
+ /// Gets a value indicating 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(
+ configuration,
+ 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
new file mode 100644
index 0000000000..856fba3dcb
--- /dev/null
+++ b/src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor{TPixel}.cs
@@ -0,0 +1,204 @@
+// 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.
+ /// 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 a value indicating whether to apply a synchronized luminance value to each color channel.
+ ///
+ private bool SyncChannels { get; }
+
+ ///
+ 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;
+
+ 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 logic for separate color channels.
+ ///
+ private readonly struct SeperateChannelsRowOperation : 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 SeperateChannelsRowOperation(
+ 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 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() * levelsMinusOne;
+
+ 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..e104734820 100644
--- a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationMethod.cs
+++ b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationMethod.cs
@@ -22,4 +22,10 @@ public enum HistogramEqualizationMethod : int
/// Adaptive histogram equalization using sliding window. Slower then the tile interpolation mode, but can yield to better results.
///
AdaptiveSlidingWindow,
+
+ ///
+ /// Adjusts the brightness levels of a particular image by scaling the
+ /// minimum and maximum values to the full brightness range.
+ ///
+ AutoLevel
}
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 f90a810790..8a9056b1f3 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, 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 09ba486a6f..60e33835af 100644
--- a/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs
+++ b/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs
@@ -134,6 +134,44 @@ public void Adaptive_TileInterpolation_10Tiles_WithClipping(TestImagePro
}
}
+ [Theory]
+ [WithFile(TestImages.Jpeg.Baseline.ForestBridgeDifferentComponentsQuality, PixelTypes.Rgba32)]
+ public void AutoLevel_SeparateChannels_CompareToReferenceOutput(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using (Image image = provider.GetImage())
+ {
+ var options = new HistogramEqualizationOptions
+ {
+ 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);
+ 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/ImageSharp.Tests/Processing/Normalization/MagickCompareTests.cs b/tests/ImageSharp.Tests/Processing/Normalization/MagickCompareTests.cs
new file mode 100644
index 0000000000..5fb0a4e934
--- /dev/null
+++ b/tests/ImageSharp.Tests/Processing/Normalization/MagickCompareTests.cs
@@ -0,0 +1,81 @@
+// 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
+ {
+ 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);
+ 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;
+ }
+}
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
new file mode 100644
index 0000000000..de79ec729c
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/HistogramEqualizationTests/AutoLevel_SeparateChannels_CompareToReferenceOutput_Rgba32_forest_bridge.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:aada4a2ccf45de24f2a591a18d9bc0260ceb3829e104fee6982061013ed87282
+size 14107709
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