mirror of https://github.com/SixLabors/ImageSharp
committed by
GitHub
79 changed files with 2014 additions and 1178 deletions
@ -1,181 +0,0 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Buffers; |
|||
using System.Numerics; |
|||
using System.Threading; |
|||
using SixLabors.ImageSharp.Advanced; |
|||
using SixLabors.ImageSharp.Memory; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using JpegColorConverter = SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters.JpegColorConverter; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder |
|||
{ |
|||
/// <summary>
|
|||
/// Encapsulates the execution od post-processing algorithms to be applied on a <see cref="IRawJpegData"/> to produce a valid <see cref="Image{TPixel}"/>: <br/>
|
|||
/// (1) Dequantization <br/>
|
|||
/// (2) IDCT <br/>
|
|||
/// (3) Color conversion form one of the <see cref="JpegColorSpace"/>-s into a <see cref="Vector4"/> buffer of RGBA values <br/>
|
|||
/// (4) Packing <see cref="Image{TPixel}"/> pixels from the <see cref="Vector4"/> buffer. <br/>
|
|||
/// These operations are executed in <see cref="NumberOfPostProcessorSteps"/> steps.
|
|||
/// <see cref="PixelRowsPerStep"/> image rows are converted in one step,
|
|||
/// which means that size of the allocated memory is limited (does not depend on <see cref="ImageFrame.Height"/>).
|
|||
/// </summary>
|
|||
internal class JpegImagePostProcessor : IDisposable |
|||
{ |
|||
private readonly Configuration configuration; |
|||
|
|||
/// <summary>
|
|||
/// The number of block rows to be processed in one Step.
|
|||
/// </summary>
|
|||
public const int BlockRowsPerStep = 4; |
|||
|
|||
/// <summary>
|
|||
/// The number of image pixel rows to be processed in one step.
|
|||
/// </summary>
|
|||
public const int PixelRowsPerStep = 4 * 8; |
|||
|
|||
/// <summary>
|
|||
/// Temporal buffer to store a row of colors.
|
|||
/// </summary>
|
|||
private readonly IMemoryOwner<Vector4> rgbaBuffer; |
|||
|
|||
/// <summary>
|
|||
/// The <see cref="JpegColorConverter"/> corresponding to the current <see cref="JpegColorSpace"/> determined by <see cref="IRawJpegData.ColorSpace"/>.
|
|||
/// </summary>
|
|||
private readonly JpegColorConverter colorConverter; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="JpegImagePostProcessor"/> class.
|
|||
/// </summary>
|
|||
/// <param name="configuration">The <see cref="Configuration"/> to configure internal operations.</param>
|
|||
/// <param name="rawJpeg">The <see cref="IRawJpegData"/> representing the uncompressed spectral Jpeg data</param>
|
|||
public JpegImagePostProcessor(Configuration configuration, IRawJpegData rawJpeg) |
|||
{ |
|||
this.configuration = configuration; |
|||
this.RawJpeg = rawJpeg; |
|||
IJpegComponent c0 = rawJpeg.Components[0]; |
|||
this.NumberOfPostProcessorSteps = c0.SizeInBlocks.Height / BlockRowsPerStep; |
|||
this.PostProcessorBufferSize = new Size(c0.SizeInBlocks.Width * 8, PixelRowsPerStep); |
|||
|
|||
MemoryAllocator memoryAllocator = configuration.MemoryAllocator; |
|||
|
|||
this.ComponentProcessors = new JpegComponentPostProcessor[rawJpeg.Components.Length]; |
|||
for (int i = 0; i < rawJpeg.Components.Length; i++) |
|||
{ |
|||
this.ComponentProcessors[i] = new JpegComponentPostProcessor(memoryAllocator, this, rawJpeg.Components[i]); |
|||
} |
|||
|
|||
this.rgbaBuffer = memoryAllocator.Allocate<Vector4>(rawJpeg.ImageSizeInPixels.Width); |
|||
this.colorConverter = JpegColorConverter.GetConverter(rawJpeg.ColorSpace, rawJpeg.Precision); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the <see cref="JpegComponentPostProcessor"/> instances.
|
|||
/// </summary>
|
|||
public JpegComponentPostProcessor[] ComponentProcessors { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the <see cref="IRawJpegData"/> to be processed.
|
|||
/// </summary>
|
|||
public IRawJpegData RawJpeg { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the total number of post processor steps deduced from the height of the image and <see cref="PixelRowsPerStep"/>.
|
|||
/// </summary>
|
|||
public int NumberOfPostProcessorSteps { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the size of the temporary buffers we need to allocate into <see cref="JpegComponentPostProcessor.ColorBuffer"/>.
|
|||
/// </summary>
|
|||
public Size PostProcessorBufferSize { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the value of the counter that grows by each step by <see cref="PixelRowsPerStep"/>.
|
|||
/// </summary>
|
|||
public int PixelRowCounter { get; private set; } |
|||
|
|||
/// <inheritdoc />
|
|||
public void Dispose() |
|||
{ |
|||
foreach (JpegComponentPostProcessor cpp in this.ComponentProcessors) |
|||
{ |
|||
cpp.Dispose(); |
|||
} |
|||
|
|||
this.rgbaBuffer.Dispose(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Process all pixels into 'destination'. The image dimensions should match <see cref="RawJpeg"/>.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The pixel type</typeparam>
|
|||
/// <param name="destination">The destination image</param>
|
|||
/// <param name="cancellationToken">The token to request cancellation.</param>
|
|||
public void PostProcess<TPixel>(ImageFrame<TPixel> destination, CancellationToken cancellationToken) |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
{ |
|||
this.PixelRowCounter = 0; |
|||
|
|||
if (this.RawJpeg.ImageSizeInPixels != destination.Size()) |
|||
{ |
|||
throw new ArgumentException("Input image is not of the size of the processed one!"); |
|||
} |
|||
|
|||
while (this.PixelRowCounter < this.RawJpeg.ImageSizeInPixels.Height) |
|||
{ |
|||
cancellationToken.ThrowIfCancellationRequested(); |
|||
this.DoPostProcessorStep(destination); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Execute one step processing <see cref="PixelRowsPerStep"/> pixel rows into 'destination'.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The pixel type</typeparam>
|
|||
/// <param name="destination">The destination image.</param>
|
|||
public void DoPostProcessorStep<TPixel>(ImageFrame<TPixel> destination) |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
{ |
|||
foreach (JpegComponentPostProcessor cpp in this.ComponentProcessors) |
|||
{ |
|||
cpp.CopyBlocksToColorBuffer(); |
|||
} |
|||
|
|||
this.ConvertColorsInto(destination); |
|||
|
|||
this.PixelRowCounter += PixelRowsPerStep; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Convert and copy <see cref="PixelRowsPerStep"/> row of colors into 'destination' starting at row <see cref="PixelRowCounter"/>.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The pixel type</typeparam>
|
|||
/// <param name="destination">The destination image</param>
|
|||
private void ConvertColorsInto<TPixel>(ImageFrame<TPixel> destination) |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
{ |
|||
int maxY = Math.Min(destination.Height, this.PixelRowCounter + PixelRowsPerStep); |
|||
|
|||
var buffers = new Buffer2D<float>[this.ComponentProcessors.Length]; |
|||
for (int i = 0; i < this.ComponentProcessors.Length; i++) |
|||
{ |
|||
buffers[i] = this.ComponentProcessors[i].ColorBuffer; |
|||
} |
|||
|
|||
for (int yy = this.PixelRowCounter; yy < maxY; yy++) |
|||
{ |
|||
int y = yy - this.PixelRowCounter; |
|||
|
|||
var values = new JpegColorConverter.ComponentValues(buffers, y); |
|||
this.colorConverter.ConvertToRgba(values, this.rgbaBuffer.GetSpan()); |
|||
|
|||
Span<TPixel> destRow = destination.GetPixelRowSpan(yy); |
|||
|
|||
// TODO: Investigate if slicing is actually necessary
|
|||
PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, this.rgbaBuffer.GetSpan().Slice(0, destRow.Length), destRow); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder |
|||
{ |
|||
/// <summary>
|
|||
/// Converter used to convert jpeg spectral data.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// This is tightly coupled with <see cref="HuffmanScanDecoder"/> and <see cref="JpegDecoderCore"/>.
|
|||
/// </remarks>
|
|||
internal abstract class SpectralConverter |
|||
{ |
|||
/// <summary>
|
|||
/// Injects jpeg image decoding metadata.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// This is guaranteed to be called only once at SOF marker by <see cref="HuffmanScanDecoder"/>.
|
|||
/// </remarks>
|
|||
/// <param name="frame"><see cref="JpegFrame"/> instance containing decoder-specific parameters.</param>
|
|||
/// <param name="jpegData"><see cref="IRawJpegData"/> instance containing decoder-specific parameters.</param>
|
|||
public abstract void InjectFrameData(JpegFrame frame, IRawJpegData jpegData); |
|||
|
|||
/// <summary>
|
|||
/// Called once per spectral stride for each component in <see cref="HuffmanScanDecoder"/>.
|
|||
/// This is called only for baseline interleaved jpegs.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Spectral 'stride' doesn't particularly mean 'single stride'.
|
|||
/// Actual stride height depends on the subsampling factor of the given component.
|
|||
/// </remarks>
|
|||
public abstract void ConvertStrideBaseline(); |
|||
} |
|||
} |
|||
@ -0,0 +1,146 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Buffers; |
|||
using System.Numerics; |
|||
using System.Threading; |
|||
using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters; |
|||
using SixLabors.ImageSharp.Memory; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder |
|||
{ |
|||
internal sealed class SpectralConverter<TPixel> : SpectralConverter, IDisposable |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
{ |
|||
private readonly Configuration configuration; |
|||
|
|||
private CancellationToken cancellationToken; |
|||
|
|||
private JpegComponentPostProcessor[] componentProcessors; |
|||
|
|||
private JpegColorConverter colorConverter; |
|||
|
|||
private IMemoryOwner<Vector4> rgbaBuffer; |
|||
|
|||
private Buffer2D<TPixel> pixelBuffer; |
|||
|
|||
private int blockRowsPerStep; |
|||
|
|||
private int pixelRowsPerStep; |
|||
|
|||
private int pixelRowCounter; |
|||
|
|||
public SpectralConverter(Configuration configuration, CancellationToken cancellationToken) |
|||
{ |
|||
this.configuration = configuration; |
|||
this.cancellationToken = cancellationToken; |
|||
} |
|||
|
|||
private bool Converted => this.pixelRowCounter >= this.pixelBuffer.Height; |
|||
|
|||
public Buffer2D<TPixel> PixelBuffer |
|||
{ |
|||
get |
|||
{ |
|||
if (!this.Converted) |
|||
{ |
|||
int steps = (int)Math.Ceiling(this.pixelBuffer.Height / (float)this.pixelRowsPerStep); |
|||
|
|||
for (int step = 0; step < steps; step++) |
|||
{ |
|||
this.cancellationToken.ThrowIfCancellationRequested(); |
|||
this.ConvertNextStride(step); |
|||
} |
|||
} |
|||
|
|||
return this.pixelBuffer; |
|||
} |
|||
} |
|||
|
|||
public override void InjectFrameData(JpegFrame frame, IRawJpegData jpegData) |
|||
{ |
|||
MemoryAllocator allocator = this.configuration.MemoryAllocator; |
|||
|
|||
// iteration data
|
|||
IJpegComponent c0 = frame.Components[0]; |
|||
|
|||
const int blockPixelHeight = 8; |
|||
this.blockRowsPerStep = c0.SamplingFactors.Height; |
|||
this.pixelRowsPerStep = this.blockRowsPerStep * blockPixelHeight; |
|||
|
|||
// pixel buffer for resulting image
|
|||
this.pixelBuffer = allocator.Allocate2D<TPixel>(frame.PixelWidth, frame.PixelHeight, AllocationOptions.Clean); |
|||
|
|||
// component processors from spectral to Rgba32
|
|||
var postProcessorBufferSize = new Size(c0.SizeInBlocks.Width * 8, this.pixelRowsPerStep); |
|||
this.componentProcessors = new JpegComponentPostProcessor[frame.Components.Length]; |
|||
for (int i = 0; i < this.componentProcessors.Length; i++) |
|||
{ |
|||
this.componentProcessors[i] = new JpegComponentPostProcessor(allocator, frame, jpegData, postProcessorBufferSize, frame.Components[i]); |
|||
} |
|||
|
|||
// single 'stride' rgba32 buffer for conversion between spectral and TPixel
|
|||
this.rgbaBuffer = allocator.Allocate<Vector4>(frame.PixelWidth); |
|||
|
|||
// color converter from Rgba32 to TPixel
|
|||
this.colorConverter = JpegColorConverter.GetConverter(jpegData.ColorSpace, frame.Precision); |
|||
} |
|||
|
|||
public override void ConvertStrideBaseline() |
|||
{ |
|||
// Convert next pixel stride using single spectral `stride'
|
|||
// Note that zero passing eliminates the need of virtual call from JpegComponentPostProcessor
|
|||
this.ConvertNextStride(spectralStep: 0); |
|||
|
|||
// Clear spectral stride - this is VERY important as jpeg possibly won't fill entire buffer each stride
|
|||
// Which leads to decoding artifacts
|
|||
// Note that this code clears all buffers of the post processors, it's their responsibility to allocate only single stride
|
|||
foreach (JpegComponentPostProcessor cpp in this.componentProcessors) |
|||
{ |
|||
cpp.ClearSpectralBuffers(); |
|||
} |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
if (this.componentProcessors != null) |
|||
{ |
|||
foreach (JpegComponentPostProcessor cpp in this.componentProcessors) |
|||
{ |
|||
cpp.Dispose(); |
|||
} |
|||
} |
|||
|
|||
this.rgbaBuffer?.Dispose(); |
|||
} |
|||
|
|||
private void ConvertNextStride(int spectralStep) |
|||
{ |
|||
int maxY = Math.Min(this.pixelBuffer.Height, this.pixelRowCounter + this.pixelRowsPerStep); |
|||
|
|||
var buffers = new Buffer2D<float>[this.componentProcessors.Length]; |
|||
for (int i = 0; i < this.componentProcessors.Length; i++) |
|||
{ |
|||
this.componentProcessors[i].CopyBlocksToColorBuffer(spectralStep); |
|||
buffers[i] = this.componentProcessors[i].ColorBuffer; |
|||
} |
|||
|
|||
for (int yy = this.pixelRowCounter; yy < maxY; yy++) |
|||
{ |
|||
int y = yy - this.pixelRowCounter; |
|||
|
|||
var values = new JpegColorConverter.ComponentValues(buffers, y); |
|||
this.colorConverter.ConvertToRgba(values, this.rgbaBuffer.GetSpan()); |
|||
|
|||
Span<TPixel> destRow = this.pixelBuffer.GetRowSpan(yy); |
|||
|
|||
// TODO: Investigate if slicing is actually necessary
|
|||
PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, this.rgbaBuffer.GetSpan().Slice(0, destRow.Length), destRow); |
|||
} |
|||
|
|||
this.pixelRowCounter += this.pixelRowsPerStep; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,58 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Buffers.Binary; |
|||
using SixLabors.ImageSharp.Memory; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation |
|||
{ |
|||
/// <summary>
|
|||
/// Implements the 'RGB' photometric interpretation with 16 bits for each channel.
|
|||
/// </summary>
|
|||
internal class Rgb161616TiffColor<TPixel> : TiffBaseColorDecoder<TPixel> |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
{ |
|||
private readonly bool isBigEndian; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="Rgb161616TiffColor{TPixel}" /> class.
|
|||
/// </summary>
|
|||
/// <param name="isBigEndian">if set to <c>true</c> decodes the pixel data as big endian, otherwise as little endian.</param>
|
|||
public Rgb161616TiffColor(bool isBigEndian) => this.isBigEndian = isBigEndian; |
|||
|
|||
/// <inheritdoc/>
|
|||
public override void Decode(ReadOnlySpan<byte> data, Buffer2D<TPixel> pixels, int left, int top, int width, int height) |
|||
{ |
|||
var color = default(TPixel); |
|||
|
|||
int offset = 0; |
|||
|
|||
var rgba = default(Rgba64); |
|||
for (int y = top; y < top + height; y++) |
|||
{ |
|||
Span<TPixel> pixelRow = pixels.GetRowSpan(y); |
|||
|
|||
for (int x = left; x < left + width; x++) |
|||
{ |
|||
ulong r = this.ConvertToShort(data.Slice(offset, 2)); |
|||
offset += 2; |
|||
ulong g = this.ConvertToShort(data.Slice(offset, 2)); |
|||
offset += 2; |
|||
ulong b = this.ConvertToShort(data.Slice(offset, 2)); |
|||
offset += 2; |
|||
|
|||
rgba.PackedValue = r | (g << 16) | (b << 32) | (0xfffful << 48); |
|||
color.FromRgba64(rgba); |
|||
|
|||
pixelRow[x] = color; |
|||
} |
|||
} |
|||
} |
|||
|
|||
private ushort ConvertToShort(ReadOnlySpan<byte> buffer) => this.isBigEndian |
|||
? BinaryPrimitives.ReadUInt16BigEndian(buffer) |
|||
: BinaryPrimitives.ReadUInt16LittleEndian(buffer); |
|||
} |
|||
} |
|||
@ -0,0 +1,69 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using BenchmarkDotNet.Attributes; |
|||
|
|||
namespace SixLabors.ImageSharp.Benchmarks.LoadResizeSave |
|||
{ |
|||
// See README.md for instructions about initialization.
|
|||
[MemoryDiagnoser] |
|||
[ShortRunJob] |
|||
public class LoadResizeSaveStressBenchmarks |
|||
{ |
|||
private LoadResizeSaveStressRunner runner; |
|||
|
|||
// private const JpegKind Filter = JpegKind.Progressive;
|
|||
private const JpegKind Filter = JpegKind.Any; |
|||
|
|||
[GlobalSetup] |
|||
public void Setup() |
|||
{ |
|||
this.runner = new LoadResizeSaveStressRunner() |
|||
{ |
|||
ImageCount = Environment.ProcessorCount, |
|||
Filter = Filter |
|||
}; |
|||
Console.WriteLine($"ImageCount: {this.runner.ImageCount} Filter: {Filter}"); |
|||
this.runner.Init(); |
|||
} |
|||
|
|||
private void ForEachImage(Action<string> action, int maxDegreeOfParallelism) |
|||
{ |
|||
this.runner.MaxDegreeOfParallelism = maxDegreeOfParallelism; |
|||
this.runner.ForEachImageParallel(action); |
|||
} |
|||
|
|||
public int[] ParallelismValues { get; } = |
|||
{ |
|||
Environment.ProcessorCount, |
|||
Environment.ProcessorCount / 2, |
|||
Environment.ProcessorCount / 4, |
|||
1 |
|||
}; |
|||
|
|||
[Benchmark(Baseline = true)] |
|||
[ArgumentsSource(nameof(ParallelismValues))] |
|||
public void SystemDrawing(int maxDegreeOfParallelism) => this.ForEachImage(this.runner.SystemDrawingResize, maxDegreeOfParallelism); |
|||
|
|||
[Benchmark] |
|||
[ArgumentsSource(nameof(ParallelismValues))] |
|||
public void ImageSharp(int maxDegreeOfParallelism) => this.ForEachImage(this.runner.ImageSharpResize, maxDegreeOfParallelism); |
|||
|
|||
[Benchmark] |
|||
[ArgumentsSource(nameof(ParallelismValues))] |
|||
public void Magick(int maxDegreeOfParallelism) => this.ForEachImage(this.runner.MagickResize, maxDegreeOfParallelism); |
|||
|
|||
[Benchmark] |
|||
[ArgumentsSource(nameof(ParallelismValues))] |
|||
public void MagicScaler(int maxDegreeOfParallelism) => this.ForEachImage(this.runner.MagicScalerResize, maxDegreeOfParallelism); |
|||
|
|||
[Benchmark] |
|||
[ArgumentsSource(nameof(ParallelismValues))] |
|||
public void SkiaBitmap(int maxDegreeOfParallelism) => this.ForEachImage(this.runner.SkiaBitmapResize, maxDegreeOfParallelism); |
|||
|
|||
[Benchmark] |
|||
[ArgumentsSource(nameof(ParallelismValues))] |
|||
public void NetVips(int maxDegreeOfParallelism) => this.ForEachImage(this.runner.NetVipsResize, maxDegreeOfParallelism); |
|||
} |
|||
} |
|||
@ -0,0 +1,280 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Drawing; |
|||
using System.Drawing.Drawing2D; |
|||
using System.Drawing.Imaging; |
|||
using System.IO; |
|||
using System.Linq; |
|||
using System.Runtime.InteropServices; |
|||
using System.Threading.Tasks; |
|||
using ImageMagick; |
|||
using PhotoSauce.MagicScaler; |
|||
using SixLabors.ImageSharp.Formats.Jpeg; |
|||
using SixLabors.ImageSharp.Processing; |
|||
using SixLabors.ImageSharp.Tests; |
|||
using SkiaSharp; |
|||
using ImageSharpImage = SixLabors.ImageSharp.Image; |
|||
using ImageSharpSize = SixLabors.ImageSharp.Size; |
|||
using NetVipsImage = NetVips.Image; |
|||
using SystemDrawingImage = System.Drawing.Image; |
|||
|
|||
namespace SixLabors.ImageSharp.Benchmarks.LoadResizeSave |
|||
{ |
|||
public enum JpegKind |
|||
{ |
|||
Baseline = 1, |
|||
Progressive = 2, |
|||
Any = Baseline | Progressive |
|||
} |
|||
|
|||
public class LoadResizeSaveStressRunner |
|||
{ |
|||
private const int ThumbnailSize = 150; |
|||
private const int Quality = 75; |
|||
private const string ImageSharp = nameof(ImageSharp); |
|||
private const string SystemDrawing = nameof(SystemDrawing); |
|||
private const string MagickNET = nameof(MagickNET); |
|||
private const string NetVips = nameof(NetVips); |
|||
private const string MagicScaler = nameof(MagicScaler); |
|||
private const string SkiaSharpCanvas = nameof(SkiaSharpCanvas); |
|||
private const string SkiaSharpBitmap = nameof(SkiaSharpBitmap); |
|||
|
|||
// Set the quality for ImagSharp
|
|||
private readonly JpegEncoder imageSharpJpegEncoder = new () { Quality = Quality }; |
|||
private readonly ImageCodecInfo systemDrawingJpegCodec = |
|||
ImageCodecInfo.GetImageEncoders().First(codec => codec.FormatID == ImageFormat.Jpeg.Guid); |
|||
|
|||
public string[] Images { get; private set; } |
|||
|
|||
public double TotalProcessedMegapixels { get; private set; } |
|||
|
|||
private string outputDirectory; |
|||
|
|||
public int ImageCount { get; set; } = int.MaxValue; |
|||
|
|||
public int MaxDegreeOfParallelism { get; set; } = -1; |
|||
|
|||
public JpegKind Filter { get; set; } |
|||
|
|||
private static readonly string[] ProgressiveFiles = |
|||
{ |
|||
"ancyloscelis-apiformis-m-paraguay-face_2014-08-08-095255-zs-pmax_15046500892_o.jpg", |
|||
"acanthopus-excellens-f-face-brasil_2014-08-06-132105-zs-pmax_14792513890_o.jpg", |
|||
"bee-ceratina-monster-f-ukraine-face_2014-08-09-123342-zs-pmax_15068816101_o.jpg", |
|||
"bombus-eximias-f-tawain-face_2014-08-10-094449-zs-pmax_15155452565_o.jpg", |
|||
"ceratina-14507h1-m-vietnam-face_2014-08-09-163218-zs-pmax_15096718245_o.jpg", |
|||
"ceratina-buscki-f-panama-face_2014-11-25-140413-zs-pmax_15923736081_o.jpg", |
|||
"ceratina-tricolor-f-panama-face2_2014-08-29-160402-zs-pmax_14906318297_o.jpg", |
|||
"ceratina-tricolor-f-panama-face_2014-08-29-160001-zs-pmax_14906300608_o.jpg", |
|||
"ceratina-tricolor-m-panama-face_2014-08-29-162821-zs-pmax_15069878876_o.jpg", |
|||
"coelioxys-cayennensis-f-argentina-face_2014-08-09-171932-zs-pmax_14914109737_o.jpg", |
|||
"ctenocolletes-smaragdinus-f-australia-face_2014-08-08-134825-zs-pmax_14865269708_o.jpg", |
|||
"diphaglossa-gayi-f-face-chile_2014-08-04-180547-zs-pmax_14918891472_o.jpg", |
|||
"hylaeus-nubilosus-f-australia-face_2014-08-14-121100-zs-pmax_15049602149_o.jpg", |
|||
"hypanthidioides-arenaria-f-face-brazil_2014-08-06-061201-zs-pmax_14770371360_o.jpg", |
|||
"megachile-chalicodoma-species-f-morocco-face_2014-08-14-124840-zs-pmax_15217084686_o.jpg", |
|||
"megachile-species-f-15266b06-face-kenya_2014-08-06-161044-zs-pmax_14994381392_o.jpg", |
|||
"megalopta-genalis-m-face-panama-barocolorado_2014-09-19-164939-zs-pmax_15121397069_o.jpg", |
|||
"melitta-haemorrhoidalis-m--england-face_2014-11-02-014026-zs-pmax-recovered_15782113675_o.jpg", |
|||
"nomia-heart-antennae-m-15266b02-face-kenya_2014-08-04-195216-zs-pmax_14922843736_o.jpg", |
|||
"nomia-species-m-oman-face_2014-08-09-192602-zs-pmax_15128732411_o.jpg", |
|||
"nomia-spiney-m-vietnam-face_2014-08-09-213126-zs-pmax_15191389705_o.jpg", |
|||
"ochreriades-fasciata-m-face-israel_2014-08-06-084407-zs-pmax_14965515571_o.jpg", |
|||
"osmia-brevicornisf-jaw-kyrgystan_2014-08-08-103333-zs-pmax_14865267787_o.jpg", |
|||
"pachyanthidium-aff-benguelense-f-6711f07-face_2014-08-07-112830-zs-pmax_15018069042_o.jpg", |
|||
"pachymelus-bicolor-m-face-madagascar_2014-08-06-134930-zs-pmax_14801667477_o.jpg", |
|||
"psaenythia-species-m-argentina-face_2014-08-07-163754-zs-pmax_15007018976_o.jpg", |
|||
"stingless-bee-1-f-face-peru_2014-07-30-123322-zs-pmax_15633797167_o.jpg", |
|||
"triepeolus-simplex-m-face-md-kent-county_2014-07-22-100937-zs-pmax_14805405233_o.jpg", |
|||
"washed-megachile-f-face-chile_2014-08-06-103414-zs-pmax_14977843152_o.jpg", |
|||
"xylocopa-balck-violetwing-f-kyrgystan-angle_2014-08-09-182433-zs-pmax_15123416061_o.jpg", |
|||
"xylocopa-india-yellow-m-india-face_2014-08-10-111701-zs-pmax_15166559172_o.jpg", |
|||
}; |
|||
|
|||
public void Init() |
|||
{ |
|||
if (RuntimeInformation.OSArchitecture is Architecture.X86 or Architecture.X64) |
|||
{ |
|||
// Workaround ImageMagick issue
|
|||
OpenCL.IsEnabled = false; |
|||
} |
|||
|
|||
string imageDirectory = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, "MemoryStress"); |
|||
if (!Directory.Exists(imageDirectory) || !Directory.EnumerateFiles(imageDirectory).Any()) |
|||
{ |
|||
throw new DirectoryNotFoundException($"Copy stress images to: {imageDirectory}"); |
|||
} |
|||
|
|||
// Get at most this.ImageCount images from there
|
|||
bool FilterFunc(string f) => this.Filter.HasFlag(GetJpegType(f)); |
|||
|
|||
this.Images = Directory.EnumerateFiles(imageDirectory).Where(FilterFunc).Take(this.ImageCount).ToArray(); |
|||
|
|||
// Create the output directory next to the images directory
|
|||
this.outputDirectory = TestEnvironment.CreateOutputDirectory("MemoryStress"); |
|||
|
|||
static JpegKind GetJpegType(string f) => |
|||
ProgressiveFiles.Any(p => f.EndsWith(p, StringComparison.OrdinalIgnoreCase)) |
|||
? JpegKind.Progressive |
|||
: JpegKind.Baseline; |
|||
} |
|||
|
|||
public void ForEachImageParallel(Action<string> action) => Parallel.ForEach( |
|||
this.Images, |
|||
new ParallelOptions { MaxDegreeOfParallelism = this.MaxDegreeOfParallelism }, |
|||
action); |
|||
|
|||
private void IncreaseTotalMegapixels(int width, int height) |
|||
{ |
|||
double pixels = width * (double)height; |
|||
this.TotalProcessedMegapixels += pixels / 1_000_000.0; |
|||
} |
|||
|
|||
private string OutputPath(string inputPath, string postfix) => |
|||
Path.Combine( |
|||
this.outputDirectory, |
|||
Path.GetFileNameWithoutExtension(inputPath) + "-" + postfix + Path.GetExtension(inputPath)); |
|||
|
|||
private (int width, int height) ScaledSize(int inWidth, int inHeight, int outSize) |
|||
{ |
|||
int width, height; |
|||
if (inWidth > inHeight) |
|||
{ |
|||
width = outSize; |
|||
height = (int)Math.Round(inHeight * outSize / (double)inWidth); |
|||
} |
|||
else |
|||
{ |
|||
width = (int)Math.Round(inWidth * outSize / (double)inHeight); |
|||
height = outSize; |
|||
} |
|||
|
|||
return (width, height); |
|||
} |
|||
|
|||
public void SystemDrawingResize(string input) |
|||
{ |
|||
using var image = SystemDrawingImage.FromFile(input, true); |
|||
this.IncreaseTotalMegapixels(image.Width, image.Height); |
|||
|
|||
(int width, int height) scaled = this.ScaledSize(image.Width, image.Height, ThumbnailSize); |
|||
var resized = new Bitmap(scaled.width, scaled.height); |
|||
using var graphics = Graphics.FromImage(resized); |
|||
using var attributes = new ImageAttributes(); |
|||
attributes.SetWrapMode(WrapMode.TileFlipXY); |
|||
graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; |
|||
graphics.CompositingMode = CompositingMode.SourceCopy; |
|||
graphics.CompositingQuality = CompositingQuality.AssumeLinear; |
|||
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; |
|||
graphics.DrawImage(image, System.Drawing.Rectangle.FromLTRB(0, 0, resized.Width, resized.Height), 0, 0, image.Width, image.Height, GraphicsUnit.Pixel, attributes); |
|||
|
|||
// Save the results
|
|||
using var encoderParams = new EncoderParameters(1); |
|||
using var qualityParam = new EncoderParameter(Encoder.Quality, (long)Quality); |
|||
encoderParams.Param[0] = qualityParam; |
|||
resized.Save(this.OutputPath(input, SystemDrawing), this.systemDrawingJpegCodec, encoderParams); |
|||
} |
|||
|
|||
public void ImageSharpResize(string input) |
|||
{ |
|||
using FileStream output = File.Open(this.OutputPath(input, ImageSharp), FileMode.Create); |
|||
|
|||
// Resize it to fit a 150x150 square
|
|||
using var image = ImageSharpImage.Load(input); |
|||
this.IncreaseTotalMegapixels(image.Width, image.Height); |
|||
|
|||
image.Mutate(i => i.Resize(new ResizeOptions |
|||
{ |
|||
Size = new ImageSharpSize(ThumbnailSize, ThumbnailSize), |
|||
Mode = ResizeMode.Max |
|||
})); |
|||
|
|||
// Reduce the size of the file
|
|||
image.Metadata.ExifProfile = null; |
|||
|
|||
// Save the results
|
|||
image.Save(output, this.imageSharpJpegEncoder); |
|||
} |
|||
|
|||
public void MagickResize(string input) |
|||
{ |
|||
using var image = new MagickImage(input); |
|||
this.IncreaseTotalMegapixels(image.Width, image.Height); |
|||
|
|||
// Resize it to fit a 150x150 square
|
|||
image.Resize(ThumbnailSize, ThumbnailSize); |
|||
|
|||
// Reduce the size of the file
|
|||
image.Strip(); |
|||
|
|||
// Set the quality
|
|||
image.Quality = Quality; |
|||
|
|||
// Save the results
|
|||
image.Write(this.OutputPath(input, MagickNET)); |
|||
} |
|||
|
|||
public void MagicScalerResize(string input) |
|||
{ |
|||
var settings = new ProcessImageSettings() |
|||
{ |
|||
Width = ThumbnailSize, |
|||
Height = ThumbnailSize, |
|||
ResizeMode = CropScaleMode.Max, |
|||
SaveFormat = FileFormat.Jpeg, |
|||
JpegQuality = Quality, |
|||
JpegSubsampleMode = ChromaSubsampleMode.Subsample420 |
|||
}; |
|||
|
|||
// TODO: Is there a way to capture input dimensions for IncreaseTotalMegapixels?
|
|||
using var output = new FileStream(this.OutputPath(input, MagicScaler), FileMode.Create); |
|||
MagicImageProcessor.ProcessImage(input, output, settings); |
|||
} |
|||
|
|||
public void SkiaCanvasResize(string input) |
|||
{ |
|||
using var original = SKBitmap.Decode(input); |
|||
this.IncreaseTotalMegapixels(original.Width, original.Height); |
|||
(int width, int height) scaled = this.ScaledSize(original.Width, original.Height, ThumbnailSize); |
|||
using var surface = SKSurface.Create(new SKImageInfo(scaled.width, scaled.height, original.ColorType, original.AlphaType)); |
|||
using var paint = new SKPaint() { FilterQuality = SKFilterQuality.High }; |
|||
SKCanvas canvas = surface.Canvas; |
|||
canvas.Scale((float)scaled.width / original.Width); |
|||
canvas.DrawBitmap(original, 0, 0, paint); |
|||
canvas.Flush(); |
|||
|
|||
using FileStream output = File.OpenWrite(this.OutputPath(input, SkiaSharpCanvas)); |
|||
surface.Snapshot() |
|||
.Encode(SKEncodedImageFormat.Jpeg, Quality) |
|||
.SaveTo(output); |
|||
} |
|||
|
|||
public void SkiaBitmapResize(string input) |
|||
{ |
|||
using var original = SKBitmap.Decode(input); |
|||
this.IncreaseTotalMegapixels(original.Width, original.Height); |
|||
(int width, int height) scaled = this.ScaledSize(original.Width, original.Height, ThumbnailSize); |
|||
using var resized = original.Resize(new SKImageInfo(scaled.width, scaled.height), SKFilterQuality.High); |
|||
if (resized == null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
using var image = SKImage.FromBitmap(resized); |
|||
using FileStream output = File.OpenWrite(this.OutputPath(input, SkiaSharpBitmap)); |
|||
image.Encode(SKEncodedImageFormat.Jpeg, Quality) |
|||
.SaveTo(output); |
|||
} |
|||
|
|||
public void NetVipsResize(string input) |
|||
{ |
|||
// Thumbnail to fit a 150x150 square
|
|||
using var thumb = NetVipsImage.Thumbnail(input, ThumbnailSize, ThumbnailSize); |
|||
|
|||
// Save the results
|
|||
thumb.Jpegsave(this.OutputPath(input, NetVips), q: Quality, strip: true); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
The benchmarks have been adapted from the |
|||
[PhotoSauce's MemoryStress project](https://github.com/saucecontrol/core-imaging-playground/tree/beeees/MemoryStress). |
|||
|
|||
### Setup |
|||
|
|||
Download the [Bee Heads album](https://www.flickr.com/photos/usgsbiml/albums/72157633925491877) from the USGS Bee Inventory flickr |
|||
and extract to folder `<solution-dir>\tests\Images\ActualOutput\MemoryStress\`. |
|||
|
|||
|
|||
@ -0,0 +1,142 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Diagnostics; |
|||
using System.Text; |
|||
using SixLabors.ImageSharp.Benchmarks.LoadResizeSave; |
|||
|
|||
namespace SixLabors.ImageSharp.Tests.ProfilingSandbox |
|||
{ |
|||
// See ImageSharp.Benchmarks/LoadResizeSave/README.md
|
|||
internal class LoadResizeSaveParallelMemoryStress |
|||
{ |
|||
private readonly LoadResizeSaveStressRunner benchmarks; |
|||
|
|||
private LoadResizeSaveParallelMemoryStress() |
|||
{ |
|||
this.benchmarks = new LoadResizeSaveStressRunner() |
|||
{ |
|||
// MaxDegreeOfParallelism = 10,
|
|||
// Filter = JpegKind.Baseline
|
|||
}; |
|||
this.benchmarks.Init(); |
|||
} |
|||
|
|||
private double TotalProcessedMegapixels => this.benchmarks.TotalProcessedMegapixels; |
|||
|
|||
public static void Run() |
|||
{ |
|||
Console.WriteLine(@"Choose a library for image resizing stress test:
|
|||
|
|||
1. System.Drawing |
|||
2. ImageSharp |
|||
3. MagicScaler |
|||
4. SkiaSharp |
|||
5. NetVips |
|||
6. ImageMagick |
|||
");
|
|||
|
|||
ConsoleKey key = Console.ReadKey().Key; |
|||
if (key < ConsoleKey.D1 || key > ConsoleKey.D6) |
|||
{ |
|||
Console.WriteLine("Unrecognized command."); |
|||
return; |
|||
} |
|||
|
|||
try |
|||
{ |
|||
var lrs = new LoadResizeSaveParallelMemoryStress(); |
|||
|
|||
Console.WriteLine($"\nEnvironment.ProcessorCount={Environment.ProcessorCount}"); |
|||
Console.WriteLine($"Running with MaxDegreeOfParallelism={lrs.benchmarks.MaxDegreeOfParallelism} ..."); |
|||
var timer = Stopwatch.StartNew(); |
|||
|
|||
switch (key) |
|||
{ |
|||
case ConsoleKey.D1: |
|||
lrs.SystemDrawingBenchmarkParallel(); |
|||
break; |
|||
case ConsoleKey.D2: |
|||
lrs.ImageSharpBenchmarkParallel(); |
|||
break; |
|||
case ConsoleKey.D3: |
|||
lrs.MagicScalerBenchmarkParallel(); |
|||
break; |
|||
case ConsoleKey.D4: |
|||
lrs.SkiaBitmapBenchmarkParallel(); |
|||
break; |
|||
case ConsoleKey.D5: |
|||
lrs.NetVipsBenchmarkParallel(); |
|||
break; |
|||
case ConsoleKey.D6: |
|||
lrs.MagickBenchmarkParallel(); |
|||
break; |
|||
} |
|||
|
|||
timer.Stop(); |
|||
var stats = new Stats(timer, lrs.TotalProcessedMegapixels); |
|||
Console.WriteLine("Done. TotalProcessedMegapixels: " + lrs.TotalProcessedMegapixels); |
|||
Console.WriteLine(stats.GetMarkdown()); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
Console.WriteLine(ex.ToString()); |
|||
} |
|||
} |
|||
|
|||
private struct Stats |
|||
{ |
|||
public double TotalSeconds { get; } |
|||
|
|||
public double TotalMegapixels { get; } |
|||
|
|||
public double MegapixelsPerSec { get; } |
|||
|
|||
public double MegapixelsPerSecPerCpu { get; } |
|||
|
|||
public Stats(Stopwatch sw, double totalMegapixels) |
|||
{ |
|||
this.TotalMegapixels = totalMegapixels; |
|||
this.TotalSeconds = sw.ElapsedMilliseconds / 1000.0; |
|||
this.MegapixelsPerSec = totalMegapixels / this.TotalSeconds; |
|||
this.MegapixelsPerSecPerCpu = this.MegapixelsPerSec / Environment.ProcessorCount; |
|||
} |
|||
|
|||
public string GetMarkdown() |
|||
{ |
|||
var bld = new StringBuilder(); |
|||
bld.AppendLine($"| {nameof(this.TotalSeconds)} | {nameof(this.MegapixelsPerSec)} | {nameof(this.MegapixelsPerSecPerCpu)} |"); |
|||
bld.AppendLine( |
|||
$"| {L(nameof(this.TotalSeconds))} | {L(nameof(this.MegapixelsPerSec))} | {L(nameof(this.MegapixelsPerSecPerCpu))} |"); |
|||
|
|||
bld.Append("| "); |
|||
bld.AppendFormat(F(nameof(this.TotalSeconds)), this.TotalSeconds); |
|||
bld.Append(" | "); |
|||
bld.AppendFormat(F(nameof(this.MegapixelsPerSec)), this.MegapixelsPerSec); |
|||
bld.Append(" | "); |
|||
bld.AppendFormat(F(nameof(this.MegapixelsPerSecPerCpu)), this.MegapixelsPerSecPerCpu); |
|||
bld.AppendLine(" |"); |
|||
|
|||
return bld.ToString(); |
|||
|
|||
static string L(string header) => new ('-', header.Length); |
|||
static string F(string column) => $"{{0,{column.Length}:f3}}"; |
|||
} |
|||
} |
|||
|
|||
private void ForEachImage(Action<string> action) => this.benchmarks.ForEachImageParallel(action); |
|||
|
|||
private void SystemDrawingBenchmarkParallel() => this.ForEachImage(this.benchmarks.SystemDrawingResize); |
|||
|
|||
private void ImageSharpBenchmarkParallel() => this.ForEachImage(this.benchmarks.ImageSharpResize); |
|||
|
|||
private void MagickBenchmarkParallel() => this.ForEachImage(this.benchmarks.MagickResize); |
|||
|
|||
private void MagicScalerBenchmarkParallel() => this.ForEachImage(this.benchmarks.MagicScalerResize); |
|||
|
|||
private void SkiaBitmapBenchmarkParallel() => this.ForEachImage(this.benchmarks.SkiaBitmapResize); |
|||
|
|||
private void NetVipsBenchmarkParallel() => this.ForEachImage(this.benchmarks.NetVipsResize); |
|||
} |
|||
} |
|||
@ -1,97 +0,0 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using SixLabors.ImageSharp.Formats.Jpeg; |
|||
using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using SixLabors.ImageSharp.Tests.Formats.Jpg.Utils; |
|||
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; |
|||
|
|||
using Xunit; |
|||
using Xunit.Abstractions; |
|||
|
|||
namespace SixLabors.ImageSharp.Tests.Formats.Jpg |
|||
{ |
|||
[Trait("Format", "Jpg")] |
|||
public class JpegImagePostProcessorTests |
|||
{ |
|||
public static string[] BaselineTestJpegs = |
|||
{ |
|||
TestImages.Jpeg.Baseline.Calliphora, |
|||
TestImages.Jpeg.Baseline.Cmyk, |
|||
TestImages.Jpeg.Baseline.Ycck, |
|||
TestImages.Jpeg.Baseline.Jpeg400, |
|||
TestImages.Jpeg.Baseline.Testorig420, |
|||
TestImages.Jpeg.Baseline.Jpeg444, |
|||
}; |
|||
|
|||
public JpegImagePostProcessorTests(ITestOutputHelper output) |
|||
{ |
|||
this.Output = output; |
|||
} |
|||
|
|||
private ITestOutputHelper Output { get; } |
|||
|
|||
private static void SaveBuffer<TPixel>(JpegComponentPostProcessor cp, TestImageProvider<TPixel> provider) |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
{ |
|||
using (Image<Rgba32> image = cp.ColorBuffer.ToGrayscaleImage(1f / 255f)) |
|||
{ |
|||
image.DebugSave(provider, $"-C{cp.Component.Index}-"); |
|||
} |
|||
} |
|||
|
|||
[Theory] |
|||
[WithFile(TestImages.Jpeg.Baseline.Calliphora, PixelTypes.Rgba32)] |
|||
[WithFile(TestImages.Jpeg.Baseline.Testorig420, PixelTypes.Rgba32)] |
|||
public void DoProcessorStep<TPixel>(TestImageProvider<TPixel> provider) |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
{ |
|||
string imageFile = provider.SourceFileOrDescription; |
|||
using (JpegDecoderCore decoder = JpegFixture.ParseJpegStream(imageFile)) |
|||
using (var pp = new JpegImagePostProcessor(Configuration.Default, decoder)) |
|||
using (var imageFrame = new ImageFrame<Rgba32>(Configuration.Default, decoder.ImageWidth, decoder.ImageHeight)) |
|||
{ |
|||
pp.DoPostProcessorStep(imageFrame); |
|||
|
|||
JpegComponentPostProcessor[] cp = pp.ComponentProcessors; |
|||
|
|||
SaveBuffer(cp[0], provider); |
|||
SaveBuffer(cp[1], provider); |
|||
SaveBuffer(cp[2], provider); |
|||
} |
|||
} |
|||
|
|||
[Theory] |
|||
[WithFileCollection(nameof(BaselineTestJpegs), PixelTypes.Rgba32)] |
|||
public void PostProcess<TPixel>(TestImageProvider<TPixel> provider) |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
{ |
|||
string imageFile = provider.SourceFileOrDescription; |
|||
using (JpegDecoderCore decoder = JpegFixture.ParseJpegStream(imageFile)) |
|||
using (var pp = new JpegImagePostProcessor(Configuration.Default, decoder)) |
|||
using (var image = new Image<Rgba32>(decoder.ImageWidth, decoder.ImageHeight)) |
|||
{ |
|||
pp.PostProcess(image.Frames.RootFrame, default); |
|||
|
|||
image.DebugSave(provider); |
|||
|
|||
ImagingTestCaseUtility testUtil = provider.Utility; |
|||
testUtil.TestGroupName = nameof(JpegDecoderTests); |
|||
testUtil.TestName = JpegDecoderTests.DecodeBaselineJpegOutputName; |
|||
|
|||
using (Image<TPixel> referenceImage = |
|||
provider.GetReferenceOutputImage<TPixel>(appendPixelTypeToFileName: false)) |
|||
{ |
|||
ImageSimilarityReport report = ImageComparer.Exact.CompareImagesOrFrames(referenceImage, image); |
|||
|
|||
this.Output.WriteLine($"*** {imageFile} ***"); |
|||
this.Output.WriteLine($"Difference: {report.DifferencePercentageString}"); |
|||
|
|||
// ReSharper disable once PossibleInvalidOperationException
|
|||
Assert.True(report.TotalNormalizedDifference.Value < 0.005f); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,69 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System.IO; |
|||
using System.Linq; |
|||
using SixLabors.ImageSharp.Formats.Jpeg; |
|||
using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; |
|||
using SixLabors.ImageSharp.IO; |
|||
using SixLabors.ImageSharp.Metadata; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; |
|||
using Xunit; |
|||
using Xunit.Abstractions; |
|||
|
|||
namespace SixLabors.ImageSharp.Tests.Formats.Jpg |
|||
{ |
|||
[Trait("Format", "Jpg")] |
|||
public class SpectralToPixelConversionTests |
|||
{ |
|||
public static readonly string[] BaselineTestJpegs = |
|||
{ |
|||
TestImages.Jpeg.Baseline.Calliphora, TestImages.Jpeg.Baseline.Cmyk, TestImages.Jpeg.Baseline.Jpeg400, |
|||
TestImages.Jpeg.Baseline.Jpeg444, TestImages.Jpeg.Baseline.Testorig420, |
|||
TestImages.Jpeg.Baseline.Jpeg420Small, TestImages.Jpeg.Baseline.Bad.BadEOF, |
|||
TestImages.Jpeg.Baseline.MultiScanBaselineCMYK |
|||
}; |
|||
|
|||
public SpectralToPixelConversionTests(ITestOutputHelper output) |
|||
{ |
|||
this.Output = output; |
|||
} |
|||
|
|||
private ITestOutputHelper Output { get; } |
|||
|
|||
[Theory] |
|||
[WithFileCollection(nameof(BaselineTestJpegs), PixelTypes.Rgba32)] |
|||
public void Decoder_PixelBufferComparison<TPixel>(TestImageProvider<TPixel> provider) |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
{ |
|||
// Stream
|
|||
byte[] sourceBytes = TestFile.Create(provider.SourceFileOrDescription).Bytes; |
|||
using var ms = new MemoryStream(sourceBytes); |
|||
using var bufferedStream = new BufferedReadStream(Configuration.Default, ms); |
|||
|
|||
// Decoding
|
|||
using var converter = new SpectralConverter<TPixel>(Configuration.Default, cancellationToken: default); |
|||
var decoder = new JpegDecoderCore(Configuration.Default, new JpegDecoder()); |
|||
var scanDecoder = new HuffmanScanDecoder(bufferedStream, converter, cancellationToken: default); |
|||
decoder.ParseStream(bufferedStream, scanDecoder, cancellationToken: default); |
|||
|
|||
// Test metadata
|
|||
provider.Utility.TestGroupName = nameof(JpegDecoderTests); |
|||
provider.Utility.TestName = JpegDecoderTests.DecodeBaselineJpegOutputName; |
|||
|
|||
// Comparison
|
|||
using (Image<TPixel> image = new Image<TPixel>(Configuration.Default, converter.PixelBuffer, new ImageMetadata())) |
|||
using (Image<TPixel> referenceImage = provider.GetReferenceOutputImage<TPixel>(appendPixelTypeToFileName: false)) |
|||
{ |
|||
ImageSimilarityReport report = ImageComparer.Exact.CompareImagesOrFrames(referenceImage, image); |
|||
|
|||
this.Output.WriteLine($"*** {provider.SourceFileOrDescription} ***"); |
|||
this.Output.WriteLine($"Difference: {report.DifferencePercentageString}"); |
|||
|
|||
// ReSharper disable once PossibleInvalidOperationException
|
|||
Assert.True(report.TotalNormalizedDifference.Value < 0.005f); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,3 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:a76832570111a868ea6cb6e8287aae1976c575c94c63880c74346a4b5db5d305 |
|||
size 27007 |
|||
oid sha256:2b5e1d91fb6dc1ddb696fbee63331ba9c6ef3548b619c005887e60c5b01f4981 |
|||
size 27303 |
|||
|
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:fbb6acd612cdb09825493d04ec7c6aba8ef2a94cc9a86c6b16218720adfb8f5c |
|||
size 58065 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:8720a9ccf118c3f55407aa250ee490d583286c7e40c8c62a6f8ca449ca3ddff3 |
|||
size 58067 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:c734dd489c65fb77bd7a35cd663aa16ce986df2c2ab8c7ca43d8b65db9d47c03 |
|||
size 6666162 |
|||
Loading…
Reference in new issue