Browse Source

Merge branch 'master' into bp/tiff4bitaligned

pull/1646/head
Brian Popow 5 years ago
committed by GitHub
parent
commit
f36332faf0
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 9
      src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor.cs
  2. 9
      src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor{TPixel}.cs
  3. 44
      src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs
  4. 1
      src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor{TPixel}.cs
  5. 76
      tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
  6. 42
      tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs
  7. 11
      tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs
  8. 39
      tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs
  9. 145
      tests/ImageSharp.Tests/TestUtilities/PausedStream.cs
  10. 19
      tests/ImageSharp.Tests/TestUtilities/SingleStreamFileSystem.cs

9
src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor.cs

@ -22,10 +22,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
bool clipHistogram,
int clipLimit,
int numberOfTiles)
: base(luminanceLevels, clipHistogram, clipLimit)
{
this.NumberOfTiles = numberOfTiles;
}
: base(luminanceLevels, clipHistogram, clipLimit) => this.NumberOfTiles = numberOfTiles;
/// <summary>
/// Gets the number of tiles the image is split into (horizontal and vertically) for the adaptive histogram equalization.
@ -34,8 +31,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
/// <inheritdoc />
public override IImageProcessor<TPixel> CreatePixelSpecificProcessor<TPixel>(Configuration configuration, Image<TPixel> source, Rectangle sourceRectangle)
{
return new AdaptiveHistogramEqualizationProcessor<TPixel>(
=> new AdaptiveHistogramEqualizationProcessor<TPixel>(
configuration,
this.LuminanceLevels,
this.ClipHistogram,
@ -43,6 +39,5 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
this.NumberOfTiles,
source,
sourceRectangle);
}
}
}

9
src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor{TPixel}.cs

@ -459,10 +459,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
private readonly Configuration configuration;
private readonly MemoryAllocator memoryAllocator;
// Used for storing the minimum value for each CDF entry.
/// <summary>
/// Used for storing the minimum value for each CDF entry.
/// </summary>
private readonly Buffer2D<int> cdfMinBuffer2D;
// Used for storing the LUT for each CDF entry.
/// <summary>
/// Used for storing the LUT for each CDF entry.
/// </summary>
private readonly Buffer2D<int> cdfLutBuffer2D;
private readonly int pixelsInTile;
private readonly int sourceWidth;
@ -596,6 +600,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
int y = this.tileYStartPositions[index].y;
int endY = Math.Min(y + this.tileHeight, this.sourceHeight);
Span<int> cdfMinSpan = this.cdfMinBuffer2D.GetRowSpan(cdfY);
cdfMinSpan.Clear();
using IMemoryOwner<int> histogramBuffer = this.allocator.Allocate<int>(this.luminanceLevels);
Span<int> histogram = histogramBuffer.GetSpan();

44
src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs

@ -49,44 +49,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
/// </summary>
/// <param name="options">The <see cref="HistogramEqualizationOptions"/>.</param>
/// <returns>The <see cref="HistogramEqualizationProcessor"/>.</returns>
public static HistogramEqualizationProcessor FromOptions(HistogramEqualizationOptions options)
public static HistogramEqualizationProcessor FromOptions(HistogramEqualizationOptions options) => options.Method switch
{
HistogramEqualizationProcessor processor;
HistogramEqualizationMethod.Global
=> new GlobalHistogramEqualizationProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimit),
switch (options.Method)
{
case HistogramEqualizationMethod.Global:
processor = new GlobalHistogramEqualizationProcessor(
options.LuminanceLevels,
options.ClipHistogram,
options.ClipLimit);
break;
HistogramEqualizationMethod.AdaptiveTileInterpolation
=> new AdaptiveHistogramEqualizationProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimit, options.NumberOfTiles),
case HistogramEqualizationMethod.AdaptiveTileInterpolation:
processor = new AdaptiveHistogramEqualizationProcessor(
options.LuminanceLevels,
options.ClipHistogram,
options.ClipLimit,
options.NumberOfTiles);
break;
HistogramEqualizationMethod.AdaptiveSlidingWindow
=> new AdaptiveHistogramEqualizationSlidingWindowProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimit, options.NumberOfTiles),
case HistogramEqualizationMethod.AdaptiveSlidingWindow:
processor = new AdaptiveHistogramEqualizationSlidingWindowProcessor(
options.LuminanceLevels,
options.ClipHistogram,
options.ClipLimit,
options.NumberOfTiles);
break;
default:
processor = new GlobalHistogramEqualizationProcessor(
options.LuminanceLevels,
options.ClipHistogram,
options.ClipLimit);
break;
}
return processor;
}
_ => new GlobalHistogramEqualizationProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimit),
};
}
}

1
src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor{TPixel}.cs

@ -142,6 +142,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
[MethodImpl(InliningOptions.ShortMethod)]
public static int GetLuminance(TPixel sourcePixel, int luminanceLevels)
{
// TODO: We need a bulk per span equivalent.
var vector = sourcePixel.ToVector4();
return ColorNumerics.GetBT709Luminance(ref vector, luminanceLevels);
}

76
tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs

@ -12,6 +12,7 @@ using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.Formats.Jpg.Utils;
using SixLabors.ImageSharp.Tests.TestUtilities;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using Xunit;
@ -127,60 +128,53 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
}
[Theory]
[InlineData(TestImages.Jpeg.Baseline.Jpeg420Small, 0)]
[InlineData(TestImages.Jpeg.Issues.ExifGetString750Transform, 1)]
[InlineData(TestImages.Jpeg.Issues.ExifGetString750Transform, 15)]
[InlineData(TestImages.Jpeg.Issues.ExifGetString750Transform, 30)]
[InlineData(TestImages.Jpeg.Issues.BadRstProgressive518, 1)]
[InlineData(TestImages.Jpeg.Issues.BadRstProgressive518, 15)]
[InlineData(TestImages.Jpeg.Issues.BadRstProgressive518, 30)]
public async Task Decode_IsCancellable(string fileName, int cancellationDelayMs)
[InlineData(0)]
[InlineData(0.5)]
[InlineData(0.9)]
public async Task Decode_IsCancellable(int percentageOfStreamReadToCancel)
{
// Decoding these huge files took 300ms on i7-8650U in 2020. 30ms should be safe for cancellation delay.
string hugeFile = Path.Combine(
TestEnvironment.InputImagesDirectoryFullPath,
fileName);
const int numberOfRuns = 5;
for (int i = 0; i < numberOfRuns; i++)
var cts = new CancellationTokenSource();
var file = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImages.Jpeg.Baseline.Jpeg420Small);
using var pausedStream = new PausedStream(file);
pausedStream.OnWaiting(s =>
{
var cts = new CancellationTokenSource();
if (cancellationDelayMs == 0)
if (s.Position >= s.Length * percentageOfStreamReadToCancel)
{
cts.Cancel();
pausedStream.Release();
}
else
{
cts.CancelAfter(cancellationDelayMs);
}
try
{
using Image image = await Image.LoadAsync(hugeFile, cts.Token);
}
catch (TaskCanceledException)
{
// Successfully observed a cancellation
return;
// allows this/next wait to unblock
pausedStream.Next();
}
}
});
throw new Exception($"No cancellation happened out of {numberOfRuns} runs!");
var config = Configuration.CreateDefaultInstance();
config.FileSystem = new SingleStreamFileSystem(pausedStream);
await Assert.ThrowsAsync<TaskCanceledException>(async () =>
{
using Image image = await Image.LoadAsync(config, "someFakeFile", cts.Token);
});
}
[Theory(Skip = "Identify is too fast, doesn't work reliably.")]
[InlineData(TestImages.Jpeg.Baseline.Exif)]
[InlineData(TestImages.Jpeg.Progressive.Bad.ExifUndefType)]
public async Task Identify_IsCancellable(string fileName)
[Fact]
public async Task Identify_IsCancellable()
{
string file = Path.Combine(
TestEnvironment.InputImagesDirectoryFullPath,
fileName);
var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromTicks(1));
await Assert.ThrowsAsync<TaskCanceledException>(() => Image.IdentifyAsync(file, cts.Token));
var file = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImages.Jpeg.Baseline.Jpeg420Small);
using var pausedStream = new PausedStream(file);
pausedStream.OnWaiting(s =>
{
cts.Cancel();
pausedStream.Release();
});
var config = Configuration.CreateDefaultInstance();
config.FileSystem = new SingleStreamFileSystem(pausedStream);
await Assert.ThrowsAsync<TaskCanceledException>(async () => await Image.IdentifyAsync(config, "someFakeFile", cts.Token));
}
// DEBUG ONLY!

42
tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs

@ -13,6 +13,7 @@ using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Tests.TestUtilities;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using Xunit;
@ -310,28 +311,33 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
}
[Theory]
[InlineData(JpegSubsample.Ratio420, 0)]
[InlineData(JpegSubsample.Ratio420, 3)]
[InlineData(JpegSubsample.Ratio420, 10)]
[InlineData(JpegSubsample.Ratio444, 0)]
[InlineData(JpegSubsample.Ratio444, 3)]
[InlineData(JpegSubsample.Ratio444, 10)]
public async Task Encode_IsCancellable(JpegSubsample subsample, int cancellationDelayMs)
[InlineData(JpegSubsample.Ratio420)]
[InlineData(JpegSubsample.Ratio444)]
public async Task Encode_IsCancellable(JpegSubsample subsample)
{
using var image = new Image<Rgba32>(5000, 5000);
using var stream = new MemoryStream();
var cts = new CancellationTokenSource();
if (cancellationDelayMs == 0)
{
cts.Cancel();
}
else
using var pausedStream = new PausedStream(new MemoryStream());
pausedStream.OnWaiting(s =>
{
cts.CancelAfter(cancellationDelayMs);
}
// after some writing
if (s.Position >= 500)
{
cts.Cancel();
pausedStream.Release();
}
else
{
// allows this/next wait to unblock
pausedStream.Next();
}
});
var encoder = new JpegEncoder() { Subsample = subsample };
await Assert.ThrowsAsync<TaskCanceledException>(() => image.SaveAsync(stream, encoder, cts.Token));
using var image = new Image<Rgba32>(5000, 5000);
await Assert.ThrowsAsync<TaskCanceledException>(async () =>
{
var encoder = new JpegEncoder() { Subsample = subsample };
await image.SaveAsync(pausedStream, encoder, cts.Token);
});
}
}
}

11
tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs

@ -140,10 +140,15 @@ namespace SixLabors.ImageSharp.Tests
using var stream = new MemoryStream();
var asyncStream = new AsyncStreamWrapper(stream, () => false);
var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromTicks(1));
await Assert.ThrowsAnyAsync<TaskCanceledException>(() =>
image.SaveAsync(asyncStream, encoder, cts.Token));
var pausedStream = new PausedStream(asyncStream);
pausedStream.OnWaiting(s =>
{
cts.Cancel();
pausedStream.Release();
});
await Assert.ThrowsAsync<TaskCanceledException>(async () => await image.SaveAsync(pausedStream, encoder, cts.Token));
}
}
}

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

@ -141,6 +141,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Normalization
/// See: https://github.com/SixLabors/ImageSharp/pull/984
/// </summary>
/// <typeparam name="TPixel">The pixel type of the image.</typeparam>
/// <param name="provider">The test image provider.</param>
[Theory]
[WithTestPatternImages(110, 110, PixelTypes.Rgb24)]
[WithTestPatternImages(170, 170, PixelTypes.Rgb24)]
@ -162,5 +163,43 @@ namespace SixLabors.ImageSharp.Tests.Processing.Normalization
image.CompareToReferenceOutput(ValidatorComparer, provider);
}
}
[Theory]
[WithTestPatternImages(5120, 9234, PixelTypes.L16)]
public unsafe void Issue1640<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
if (!TestEnvironment.Is64BitProcess)
{
return;
}
// https://github.com/SixLabors/ImageSharp/discussions/1640
// Test using isolated memory to ensure clean buffers for reference
provider.Configuration = Configuration.CreateDefaultInstance();
var options = new HistogramEqualizationOptions
{
Method = HistogramEqualizationMethod.AdaptiveTileInterpolation,
LuminanceLevels = 4096,
ClipHistogram = false,
ClipLimit = 350,
NumberOfTiles = 8
};
using Image<TPixel> image = provider.GetImage();
using Image<TPixel> referenceResult = image.Clone(ctx =>
{
ctx.HistogramEqualization(options);
ctx.Resize(image.Width / 4, image.Height / 4, KnownResamplers.Bicubic);
});
using Image<TPixel> processed = image.Clone(ctx =>
{
ctx.HistogramEqualization(options);
ctx.Resize(image.Width / 4, image.Height / 4, KnownResamplers.Bicubic);
});
ValidatorComparer.VerifySimilarity(referenceResult, processed);
}
}
}

145
tests/ImageSharp.Tests/TestUtilities/PausedStream.cs

@ -0,0 +1,145 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace SixLabors.ImageSharp.Tests.TestUtilities
{
public class PausedStream : Stream
{
private readonly SemaphoreSlim semaphore = new SemaphoreSlim(0);
private readonly CancellationTokenSource cancelationTokenSource = new CancellationTokenSource();
private readonly Stream innerStream;
private Action<Stream> onWaitingCallback;
public void OnWaiting(Action<Stream> onWaitingCallback) => this.onWaitingCallback = onWaitingCallback;
public void OnWaiting(Action onWaitingCallback) => this.OnWaiting(_ => onWaitingCallback());
public void Release()
{
this.semaphore.Release();
this.cancelationTokenSource.Cancel();
}
public void Next() => this.semaphore.Release();
private void Wait()
{
if (this.cancelationTokenSource.IsCancellationRequested)
{
return;
}
this.onWaitingCallback?.Invoke(this.innerStream);
try
{
this.semaphore.Wait(this.cancelationTokenSource.Token);
}
catch (OperationCanceledException)
{
// ignore this as its just used to unlock any waits in progress
}
}
private async Task Await(Func<Task> action)
{
await Task.Yield();
this.Wait();
await action();
}
private async Task<T> Await<T>(Func<Task<T>> action)
{
await Task.Yield();
this.Wait();
return await action();
}
private T Await<T>(Func<T> action)
{
this.Wait();
return action();
}
private void Await(Action action)
{
this.Wait();
action();
}
public PausedStream(byte[] data)
: this(new MemoryStream(data))
{
}
public PausedStream(string filePath)
: this(File.OpenRead(filePath))
{
}
public PausedStream(Stream innerStream) => this.innerStream = innerStream;
public override bool CanTimeout => this.innerStream.CanTimeout;
public override void Close() => this.Await(() => this.innerStream.Close());
public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) => this.Await(() => this.innerStream.CopyToAsync(destination, bufferSize, cancellationToken));
public override bool CanRead => this.innerStream.CanRead;
public override bool CanSeek => this.innerStream.CanSeek;
public override bool CanWrite => this.innerStream.CanWrite;
public override long Length => this.Await(() => this.innerStream.Length);
public override long Position { get => this.Await(() => this.innerStream.Position); set => this.Await(() => this.innerStream.Position = value); }
public override void Flush() => this.Await(() => this.innerStream.Flush());
public override int Read(byte[] buffer, int offset, int count) => this.Await(() => this.innerStream.Read(buffer, offset, count));
public override long Seek(long offset, SeekOrigin origin) => this.Await(() => this.innerStream.Seek(offset, origin));
public override void SetLength(long value) => this.Await(() => this.innerStream.SetLength(value));
public override void Write(byte[] buffer, int offset, int count) => this.Await(() => this.innerStream.Write(buffer, offset, count));
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => this.Await(() => this.innerStream.ReadAsync(buffer, offset, count, cancellationToken));
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => this.Await(() => this.innerStream.WriteAsync(buffer, offset, count, cancellationToken));
public override void WriteByte(byte value) => this.Await(() => this.innerStream.WriteByte(value));
public override int ReadByte() => this.Await(() => this.innerStream.ReadByte());
protected override void Dispose(bool disposing) => this.innerStream.Dispose();
#if NETCOREAPP
public override void CopyTo(Stream destination, int bufferSize) => this.Await(() => this.innerStream.CopyTo(destination, bufferSize));
public override int Read(Span<byte> buffer)
{
this.Wait();
return this.innerStream.Read(buffer);
}
public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default) => this.Await(() => this.innerStream.ReadAsync(buffer, cancellationToken));
public override void Write(ReadOnlySpan<byte> buffer)
{
this.Wait();
this.innerStream.Write(buffer);
}
public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default) => this.Await(() => this.innerStream.WriteAsync(buffer, cancellationToken));
#endif
}
}

19
tests/ImageSharp.Tests/TestUtilities/SingleStreamFileSystem.cs

@ -0,0 +1,19 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System.IO;
using SixLabors.ImageSharp.IO;
namespace SixLabors.ImageSharp.Tests.TestUtilities
{
internal class SingleStreamFileSystem : IFileSystem
{
private readonly Stream stream;
public SingleStreamFileSystem(Stream stream) => this.stream = stream;
Stream IFileSystem.Create(string path) => this.stream;
Stream IFileSystem.OpenRead(string path) => this.stream;
}
}
Loading…
Cancel
Save