Browse Source

Implement auto level processor

pull/2235/head
Ynse Hoornenborg 3 years ago
parent
commit
976747490c
  1. 37
      src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor.cs
  2. 136
      src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor{TPixel}.cs
  3. 43
      src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs
  4. 53
      src/ImageSharp/Processing/Processors/Normalization/GrayscaleLevelsRowOperation{TPixel}.cs
  5. 5
      src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationMethod.cs
  6. 3
      src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs
  7. 18
      tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs
  8. 3
      tests/Images/External/ReferenceOutput/HistogramEqualizationTests/AutoLevel_CompareToReferenceOutput_Rgba32_forest_bridge.png

37
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;
/// <summary>
/// Applies a luminance histogram equilization to the image.
/// </summary>
public class AutoLevelProcessor : HistogramEqualizationProcessor
{
/// <summary>
/// Initializes a new instance of the <see cref="AutoLevelProcessor"/> 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.
/// </summary>
/// <param name="luminanceLevels">The number of different luminance levels. Typical values are 256 for 8-bit grayscale images
/// or 65536 for 16-bit grayscale images.</param>
/// <param name="clipHistogram">Indicating whether to clip the histogram bins at a specific value.</param>
/// <param name="clipLimit">The histogram clip limit. Histogram bins which exceed this limit, will be capped at this value.</param>
public AutoLevelProcessor(
int luminanceLevels,
bool clipHistogram,
int clipLimit)
: base(luminanceLevels, clipHistogram, clipLimit)
{
}
/// <inheritdoc />
public override IImageProcessor<TPixel> CreatePixelSpecificProcessor<TPixel>(Configuration configuration, Image<TPixel> source, Rectangle sourceRectangle)
=> new AutoLevelProcessor<TPixel>(
configuration,
this.LuminanceLevels,
this.ClipHistogram,
this.ClipLimit,
source,
sourceRectangle);
}

136
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;
/// <summary>
/// Applies a luminance histogram equalization to the image.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal class AutoLevelProcessor<TPixel> : HistogramEqualizationProcessor<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
/// <summary>
/// Initializes a new instance of the <see cref="AutoLevelProcessor{TPixel}"/> class.
/// </summary>
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
/// <param name="luminanceLevels">
/// The number of different luminance levels. Typical values are 256 for 8-bit grayscale images
/// or 65536 for 16-bit grayscale images.
/// </param>
/// <param name="clipHistogram">Indicating whether to clip the histogram bins at a specific value.</param>
/// <param name="clipLimit">The histogram clip limit. Histogram bins which exceed this limit, will be capped at this value.</param>
/// <param name="source">The source <see cref="Image{TPixel}"/> for the current processor instance.</param>
/// <param name="sourceRectangle">The source area to process for the current processor instance.</param>
public AutoLevelProcessor(
Configuration configuration,
int luminanceLevels,
bool clipHistogram,
int clipLimit,
Image<TPixel> source,
Rectangle sourceRectangle)
: base(configuration, luminanceLevels, clipHistogram, clipLimit, source, sourceRectangle)
{
}
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source)
{
MemoryAllocator memoryAllocator = this.Configuration.MemoryAllocator;
int numberOfPixels = source.Width * source.Height;
var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds());
using IMemoryOwner<int> histogramBuffer = memoryAllocator.Allocate<int>(this.LuminanceLevels, AllocationOptions.Clean);
// Build the histogram of the grayscale levels.
var grayscaleOperation = new GrayscaleLevelsRowOperation<TPixel>(interest, histogramBuffer, source.PixelBuffer, this.LuminanceLevels);
ParallelRowIterator.IterateRows(
this.Configuration,
interest,
in grayscaleOperation);
Span<int> histogram = histogramBuffer.GetSpan();
if (this.ClipHistogramEnabled)
{
this.ClipHistogram(histogram, this.ClipLimit);
}
using IMemoryOwner<int> cdfBuffer = memoryAllocator.Allocate<int>(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);
}
/// <summary>
/// A <see langword="struct"/> implementing the cdf application levels logic for <see cref="GlobalHistogramEqualizationProcessor{TPixel}"/>.
/// </summary>
private readonly struct CdfApplicationRowOperation : IRowOperation
{
private readonly Rectangle bounds;
private readonly IMemoryOwner<int> cdfBuffer;
private readonly Buffer2D<TPixel> source;
private readonly int luminanceLevels;
private readonly float numberOfPixelsMinusCdfMin;
[MethodImpl(InliningOptions.ShortMethod)]
public CdfApplicationRowOperation(
Rectangle bounds,
IMemoryOwner<int> cdfBuffer,
Buffer2D<TPixel> source,
int luminanceLevels,
float numberOfPixelsMinusCdfMin)
{
this.bounds = bounds;
this.cdfBuffer = cdfBuffer;
this.source = source;
this.luminanceLevels = luminanceLevels;
this.numberOfPixelsMinusCdfMin = numberOfPixelsMinusCdfMin;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(int y)
{
ref int cdfBase = ref MemoryMarshal.GetReference(this.cdfBuffer.GetSpan());
var sourceAccess = new PixelAccessor<TPixel>(this.source);
Span<TPixel> 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));
}
}
}
}

43
src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs

@ -51,7 +51,7 @@ internal class GlobalHistogramEqualizationProcessor<TPixel> : HistogramEqualizat
using IMemoryOwner<int> histogramBuffer = memoryAllocator.Allocate<int>(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<TPixel>(interest, histogramBuffer, source.PixelBuffer, this.LuminanceLevels);
ParallelRowIterator.IterateRows(
this.Configuration,
interest,
@ -81,47 +81,6 @@ internal class GlobalHistogramEqualizationProcessor<TPixel> : HistogramEqualizat
in cdfOperation);
}
/// <summary>
/// A <see langword="struct"/> implementing the grayscale levels logic for <see cref="GlobalHistogramEqualizationProcessor{TPixel}"/>.
/// </summary>
private readonly struct GrayscaleLevelsRowOperation : IRowOperation
{
private readonly Rectangle bounds;
private readonly IMemoryOwner<int> histogramBuffer;
private readonly Buffer2D<TPixel> source;
private readonly int luminanceLevels;
[MethodImpl(InliningOptions.ShortMethod)]
public GrayscaleLevelsRowOperation(
Rectangle bounds,
IMemoryOwner<int> histogramBuffer,
Buffer2D<TPixel> source,
int luminanceLevels)
{
this.bounds = bounds;
this.histogramBuffer = histogramBuffer;
this.source = source;
this.luminanceLevels = luminanceLevels;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(int y)
{
ref int histogramBase = ref MemoryMarshal.GetReference(this.histogramBuffer.GetSpan());
Span<TPixel> 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));
}
}
}
/// <summary>
/// A <see langword="struct"/> implementing the cdf application levels logic for <see cref="GlobalHistogramEqualizationProcessor{TPixel}"/>.
/// </summary>

53
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;
/// <summary>
/// A <see langword="struct"/> implementing the grayscale levels logic as <see cref="IRowOperation"/>.
/// </summary>
internal readonly struct GrayscaleLevelsRowOperation<TPixel> : IRowOperation
where TPixel : unmanaged, IPixel<TPixel>
{
private readonly Rectangle bounds;
private readonly IMemoryOwner<int> histogramBuffer;
private readonly Buffer2D<TPixel> source;
private readonly int luminanceLevels;
[MethodImpl(InliningOptions.ShortMethod)]
public GrayscaleLevelsRowOperation(
Rectangle bounds,
IMemoryOwner<int> histogramBuffer,
Buffer2D<TPixel> source,
int luminanceLevels)
{
this.bounds = bounds;
this.histogramBuffer = histogramBuffer;
this.source = source;
this.luminanceLevels = luminanceLevels;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(int y)
{
ref int histogramBase = ref MemoryMarshal.GetReference(this.histogramBuffer.GetSpan());
Span<TPixel> 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));
}
}
}

5
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.
/// </summary>
AdaptiveSlidingWindow,
/// <summary>
/// Global histogram equalization, but applied to each color channel separately.
/// </summary>
AutoLevel
}

3
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),
};
}

18
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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using (Image<TPixel> 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");
}
}
/// <summary>
/// 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.

3
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
Loading…
Cancel
Save