mirror of https://github.com/SixLabors/ImageSharp
committed by
GitHub
11 changed files with 445 additions and 42 deletions
@ -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); |
|||
} |
|||
@ -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)); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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)); |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:aada4a2ccf45de24f2a591a18d9bc0260ceb3829e104fee6982061013ed87282 |
|||
size 14107709 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:dca9b5b890d3a79b0002b7093d254d484ada4207e5010d1f0c6248d4dd6e22db |
|||
size 13909894 |
|||
Loading…
Reference in new issue