Browse Source

Merge branch 'main' into sp/auto-matrix-linear-decomposition-2

pull/2224/head
James Jackson-South 3 years ago
committed by GitHub
parent
commit
897e4726ca
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 46
      src/ImageSharp/Processing/Processors/Normalization/AutoLevelProcessor.cs
  2. 204
      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. 6
      src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationMethod.cs
  6. 7
      src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs
  7. 3
      src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs
  8. 38
      tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs
  9. 81
      tests/ImageSharp.Tests/Processing/Normalization/MagickCompareTests.cs
  10. 3
      tests/Images/External/ReferenceOutput/HistogramEqualizationTests/AutoLevel_SeparateChannels_CompareToReferenceOutput_Rgba32_forest_bridge.png
  11. 3
      tests/Images/External/ReferenceOutput/HistogramEqualizationTests/AutoLevel_SynchronizedChannels_CompareToReferenceOutput_Rgba32_forest_bridge.png

46
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;
/// <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>
/// <param name="syncChannels">Whether to apply a synchronized luminance value to each color channel.</param>
public AutoLevelProcessor(
int luminanceLevels,
bool clipHistogram,
int clipLimit,
bool syncChannels)
: base(luminanceLevels, clipHistogram, clipLimit)
{
this.SyncChannels = syncChannels;
}
/// <summary>
/// Gets a value indicating whether to apply a synchronized luminance value to each color channel.
/// </summary>
public bool SyncChannels { get; }
/// <inheritdoc />
public override IImageProcessor<TPixel> CreatePixelSpecificProcessor<TPixel>(Configuration configuration, Image<TPixel> source, Rectangle sourceRectangle)
=> new AutoLevelProcessor<TPixel>(
configuration,
this.LuminanceLevels,
this.ClipHistogram,
this.ClipLimit,
this.SyncChannels,
source,
sourceRectangle);
}

204
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;
/// <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>
/// <param name="syncChannels">Whether to apply a synchronized luminance value to each color channel.</param>
public AutoLevelProcessor(
Configuration configuration,
int luminanceLevels,
bool clipHistogram,
int clipLimit,
bool syncChannels,
Image<TPixel> source,
Rectangle sourceRectangle)
: base(configuration, luminanceLevels, clipHistogram, clipLimit, source, sourceRectangle)
{
this.SyncChannels = syncChannels;
}
/// <summary>
/// Gets a value indicating whether to apply a synchronized luminance value to each color channel.
/// </summary>
private bool SyncChannels { get; }
/// <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;
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);
}
}
/// <summary>
/// A <see langword="struct"/> implementing the cdf logic for synchronized color channels.
/// </summary>
private readonly struct SynchronizedChannelsRowOperation : 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 SynchronizedChannelsRowOperation(
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();
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);
}
}
}
/// <summary>
/// A <see langword="struct"/> implementing the cdf logic for separate color channels.
/// </summary>
private readonly struct SeperateChannelsRowOperation : 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 SeperateChannelsRowOperation(
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 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));
}
}
}
}

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

6
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.
/// </summary>
AdaptiveSlidingWindow,
/// <summary>
/// Adjusts the brightness levels of a particular image by scaling the
/// minimum and maximum values to the full brightness range.
/// </summary>
AutoLevel
}

7
src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationOptions.cs

@ -42,4 +42,11 @@ public class HistogramEqualizationOptions
/// Defaults to 8.
/// </summary>
public int NumberOfTiles { get; set; } = 8;
/// <summary>
/// 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.
/// </summary>
public bool SyncChannels { get; set; } = true;
}

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, options.SyncChannels),
_ => new GlobalHistogramEqualizationProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimit),
};
}

38
tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs

@ -134,6 +134,44 @@ public class HistogramEqualizationTests
}
}
[Theory]
[WithFile(TestImages.Jpeg.Baseline.ForestBridgeDifferentComponentsQuality, PixelTypes.Rgba32)]
public void AutoLevel_SeparateChannels_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,
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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using (Image<TPixel> 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");
}
}
/// <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.

81
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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, ImageSharp.PixelFormats.IPixel<TPixel>
{
Image<TPixel> 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<TPixel>(magickImage);
}
using (Image<TPixel> 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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, ImageSharp.PixelFormats.IPixel<TPixel>
{
string path = TestImageProvider<TPixel>.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<TPixel> ConvertImageFromMagick<TPixel>(MagickImage magickImage)
where TPixel : unmanaged, ImageSharp.PixelFormats.IPixel<TPixel>
{
Configuration configuration = Configuration.Default.Clone();
configuration.PreferContiguousImageBuffers = true;
var result = new Image<TPixel>(configuration, magickImage.Width, magickImage.Height);
Assert.True(result.DangerousTryGetSinglePixelMemory(out Memory<TPixel> resultPixels));
using (IUnsafePixelCollection<ushort> pixels = magickImage.GetPixelsUnsafe())
{
byte[] data = pixels.ToByteArray(PixelMapping.RGBA);
PixelOperations<TPixel>.Instance.FromRgba32Bytes(
configuration,
data,
resultPixels.Span,
resultPixels.Length);
}
return result;
}
}

3
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

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