Browse Source

Unified spectral conversion for direct and downscaled routines

Fixed warnings & added intermediate benchmark results
pull/2076/head
Dmitry Pentin 4 years ago
parent
commit
1ce994af9c
  1. 17
      src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterBase.cs
  2. 47
      src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/ComponentProcessor.cs
  3. 68
      src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DirectComponentProcessor.cs
  4. 52
      src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor8.cs
  5. 124
      src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConversion/JpegComponentPostProcessor.cs
  6. 139
      src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConversion/JpegComponentPostProcessor8.cs
  7. 261
      src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConversion/ResizingSpectralConverter{TPixel}.cs
  8. 21
      src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConversion/SpectralConverter{TPixel}.cs
  9. 0
      src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs
  10. 184
      src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs
  11. 14
      src/ImageSharp/Formats/Jpeg/JpegDecoder.cs
  12. 32
      src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
  13. 2
      src/ImageSharp/Formats/Tiff/Compression/Decompressors/GrayJpegSpectralConverter.cs
  14. 6
      src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs
  15. 2
      src/ImageSharp/Formats/Tiff/Compression/Decompressors/RgbJpegSpectralConverter.cs
  16. 17
      tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpeg.cs
  17. 2
      tests/ImageSharp.Tests/Formats/Jpg/SpectralToPixelConversionTests.cs

17
src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverterBase.cs

@ -228,22 +228,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters
/// </summary>
/// <param name="processors">List of component color processors.</param>
/// <param name="row">Row to convert</param>
public ComponentValues(IReadOnlyList<JpegComponentPostProcessor> processors, int row)
{
DebugGuard.MustBeGreaterThan(processors.Count, 0, nameof(processors));
this.ComponentCount = processors.Count;
this.Component0 = processors[0].GetColorBufferRowSpan(row);
// In case of grayscale, Component1 and Component2 point to Component0 memory area
this.Component1 = this.ComponentCount > 1 ? processors[1].GetColorBufferRowSpan(row) : this.Component0;
this.Component2 = this.ComponentCount > 2 ? processors[2].GetColorBufferRowSpan(row) : this.Component0;
this.Component3 = this.ComponentCount > 3 ? processors[3].GetColorBufferRowSpan(row) : Span<float>.Empty;
}
// TODO: experimental
public ComponentValues(IReadOnlyList<JpegComponentPostProcessor8> processors, int row)
public ComponentValues(IReadOnlyList<ComponentProcessor> processors, int row)
{
DebugGuard.MustBeGreaterThan(processors.Count, 0, nameof(processors));

47
src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/ComponentProcessor.cs

@ -0,0 +1,47 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
{
internal abstract class ComponentProcessor : IDisposable
{
public ComponentProcessor(MemoryAllocator memoryAllocator, JpegFrame frame, Size postProcessorBufferSize, IJpegComponent component, int blockSize)
{
this.Frame = frame;
this.Component = component;
this.BlockAreaSize = component.SubSamplingDivisors * blockSize;
this.ColorBuffer = memoryAllocator.Allocate2DOveraligned<float>(
postProcessorBufferSize.Width,
postProcessorBufferSize.Height,
this.BlockAreaSize.Height);
}
protected JpegFrame Frame { get; }
protected IJpegComponent Component { get; }
protected Buffer2D<float> ColorBuffer { get; }
protected Size BlockAreaSize { get; }
public abstract void CopyBlocksToColorBuffer(int spectralStep);
public void ClearSpectralBuffers()
{
Buffer2D<Block8x8> spectralBlocks = this.Component.SpectralBlocks;
for (int i = 0; i < spectralBlocks.Height; i++)
{
spectralBlocks.DangerousGetRowSpan(i).Clear();
}
}
public Span<float> GetColorBufferRowSpan(int row) =>
this.ColorBuffer.DangerousGetRowSpan(row);
public void Dispose() => this.ColorBuffer.Dispose();
}
}

68
src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DirectComponentProcessor.cs

@ -0,0 +1,68 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
{
internal sealed class DirectComponentProcessor : ComponentProcessor
{
private readonly IRawJpegData rawJpeg;
public DirectComponentProcessor(MemoryAllocator memoryAllocator, JpegFrame frame, IRawJpegData rawJpeg, Size postProcessorBufferSize, IJpegComponent component)
: base(memoryAllocator, frame, postProcessorBufferSize, component, blockSize: 8)
=> this.rawJpeg = rawJpeg;
public override void CopyBlocksToColorBuffer(int spectralStep)
{
Buffer2D<Block8x8> spectralBuffer = this.Component.SpectralBlocks;
float maximumValue = this.Frame.MaxColorChannelValue;
int destAreaStride = this.ColorBuffer.Width;
int blocksRowsPerStep = this.Component.SamplingFactors.Height;
int yBlockStart = spectralStep * blocksRowsPerStep;
Size subSamplingDivisors = this.Component.SubSamplingDivisors;
Block8x8F dequantTable = this.rawJpeg.QuantizationTables[this.Component.QuantizationTableIndex];
Block8x8F workspaceBlock = default;
for (int y = 0; y < blocksRowsPerStep; y++)
{
int yBuffer = y * this.BlockAreaSize.Height;
Span<float> colorBufferRow = this.ColorBuffer.DangerousGetRowSpan(yBuffer);
Span<Block8x8> blockRow = spectralBuffer.DangerousGetRowSpan(yBlockStart + y);
for (int xBlock = 0; xBlock < spectralBuffer.Width; xBlock++)
{
// Integer to float
workspaceBlock.LoadFrom(ref blockRow[xBlock]);
// Dequantize
workspaceBlock.MultiplyInPlace(ref dequantTable);
// Convert from spectral to color
FastFloatingPointDCT.TransformIDCT(ref workspaceBlock);
// To conform better to libjpeg we actually NEED TO loose precision here.
// This is because they store blocks as Int16 between all the operations.
// To be "more accurate", we need to emulate this by rounding!
workspaceBlock.NormalizeColorsAndRoundInPlace(maximumValue);
// Write to color buffer acording to sampling factors
int xColorBufferStart = xBlock * this.BlockAreaSize.Width;
workspaceBlock.ScaledCopyTo(
ref colorBufferRow[xColorBufferStart],
destAreaStride,
subSamplingDivisors.Width,
subSamplingDivisors.Height);
}
}
}
}
}

52
src/ImageSharp/Formats/Jpeg/Components/Decoder/ComponentProcessors/DownScalingComponentProcessor8.cs

@ -0,0 +1,52 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
{
internal sealed class DownScalingComponentProcessor8 : ComponentProcessor
{
private readonly float dcDequantizer;
public DownScalingComponentProcessor8(MemoryAllocator memoryAllocator, JpegFrame frame, IRawJpegData rawJpeg, Size postProcessorBufferSize, IJpegComponent component)
: base(memoryAllocator, frame, postProcessorBufferSize, component, 1)
=> this.dcDequantizer = rawJpeg.QuantizationTables[component.QuantizationTableIndex][0];
public override void CopyBlocksToColorBuffer(int spectralStep)
{
Buffer2D<Block8x8> spectralBuffer = this.Component.SpectralBlocks;
float maximumValue = this.Frame.MaxColorChannelValue;
int blocksRowsPerStep = this.Component.SamplingFactors.Height;
int yBlockStart = spectralStep * blocksRowsPerStep;
for (int y = 0; y < blocksRowsPerStep; y++)
{
int yBuffer = y * this.BlockAreaSize.Height;
Span<float> colorBufferRow = this.ColorBuffer.DangerousGetRowSpan(yBuffer);
Span<Block8x8> blockRow = spectralBuffer.DangerousGetRowSpan(yBlockStart + y);
for (int xBlock = 0; xBlock < spectralBuffer.Width; xBlock++)
{
// get Direct current term - averaged 8x8 pixel value
float dc = blockRow[xBlock][0];
// dequantization
dc *= this.dcDequantizer;
// Normalize & round
dc = (float)Math.Round(Numerics.Clamp(dc + MathF.Ceiling(maximumValue / 2), 0, maximumValue));
// Save to the intermediate buffer
int xColorBufferStart = xBlock * this.BlockAreaSize.Width;
colorBufferRow[xColorBufferStart] = dc;
}
}
}
}
}

124
src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConversion/JpegComponentPostProcessor.cs

@ -1,124 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
{
/// <summary>
/// Encapsulates spectral data to rgba32 processing for one component.
/// </summary>
internal class JpegComponentPostProcessor : IDisposable
{
/// <summary>
/// The size of the area in <see cref="ColorBuffer"/> corresponding to one 8x8 Jpeg block
/// </summary>
private readonly Size blockAreaSize;
/// <summary>
/// Jpeg frame instance containing required decoding metadata.
/// </summary>
private readonly JpegFrame frame;
/// <summary>
/// Gets the <see cref="IJpegComponent"/> component containing decoding meta information.
/// </summary>
private readonly IJpegComponent component;
/// <summary>
/// Gets the <see cref="IRawJpegData"/> instance containing decoding meta information.
/// </summary>
private readonly IRawJpegData rawJpeg;
/// <summary>
/// Initializes a new instance of the <see cref="JpegComponentPostProcessor"/> class.
/// </summary>
public JpegComponentPostProcessor(MemoryAllocator memoryAllocator, JpegFrame frame, IRawJpegData rawJpeg, Size postProcessorBufferSize, IJpegComponent component)
{
this.frame = frame;
this.component = component;
this.rawJpeg = rawJpeg;
this.blockAreaSize = this.component.SubSamplingDivisors * 8;
this.ColorBuffer = memoryAllocator.Allocate2DOveraligned<float>(
postProcessorBufferSize.Width,
postProcessorBufferSize.Height,
this.blockAreaSize.Height);
}
/// <summary>
/// Gets the temporary working buffer of color values.
/// </summary>
public Buffer2D<float> ColorBuffer { get; }
/// <inheritdoc />
public void Dispose() => this.ColorBuffer.Dispose();
/// <summary>
/// Convert raw spectral DCT data to color data and copy it to the color buffer <see cref="ColorBuffer"/>.
/// </summary>
public void CopyBlocksToColorBuffer(int spectralStep)
{
Buffer2D<Block8x8> spectralBuffer = this.component.SpectralBlocks;
float maximumValue = this.frame.MaxColorChannelValue;
int destAreaStride = this.ColorBuffer.Width;
int blocksRowsPerStep = this.component.SamplingFactors.Height;
int yBlockStart = spectralStep * blocksRowsPerStep;
Size subSamplingDivisors = this.component.SubSamplingDivisors;
Block8x8F dequantTable = this.rawJpeg.QuantizationTables[this.component.QuantizationTableIndex];
Block8x8F workspaceBlock = default;
for (int y = 0; y < blocksRowsPerStep; y++)
{
int yBuffer = y * this.blockAreaSize.Height;
Span<float> colorBufferRow = this.ColorBuffer.DangerousGetRowSpan(yBuffer);
Span<Block8x8> blockRow = spectralBuffer.DangerousGetRowSpan(yBlockStart + y);
for (int xBlock = 0; xBlock < spectralBuffer.Width; xBlock++)
{
// Integer to float
workspaceBlock.LoadFrom(ref blockRow[xBlock]);
// Dequantize
workspaceBlock.MultiplyInPlace(ref dequantTable);
// Convert from spectral to color
FastFloatingPointDCT.TransformIDCT(ref workspaceBlock);
// To conform better to libjpeg we actually NEED TO loose precision here.
// This is because they store blocks as Int16 between all the operations.
// To be "more accurate", we need to emulate this by rounding!
workspaceBlock.NormalizeColorsAndRoundInPlace(maximumValue);
// Write to color buffer acording to sampling factors
int xColorBufferStart = xBlock * this.blockAreaSize.Width;
workspaceBlock.ScaledCopyTo(
ref colorBufferRow[xColorBufferStart],
destAreaStride,
subSamplingDivisors.Width,
subSamplingDivisors.Height);
}
}
}
public void ClearSpectralBuffers()
{
Buffer2D<Block8x8> spectralBlocks = this.component.SpectralBlocks;
for (int i = 0; i < spectralBlocks.Height; i++)
{
spectralBlocks.DangerousGetRowSpan(i).Clear();
}
}
public Span<float> GetColorBufferRowSpan(int row) =>
this.ColorBuffer.DangerousGetRowSpan(row);
}
}

139
src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConversion/JpegComponentPostProcessor8.cs

@ -1,139 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
{
/// <summary>
/// Encapsulates spectral data to rgba32 processing for one component.
/// </summary>
internal class JpegComponentPostProcessor8 : IDisposable
{
/// <summary>
/// The size of the area in <see cref="ColorBuffer"/> corresponding to one 8x8 Jpeg block
/// </summary>
private readonly Size blockAreaSize;
/// <summary>
/// Jpeg frame instance containing required decoding metadata.
/// </summary>
private readonly JpegFrame frame;
/// <summary>
/// Gets the <see cref="IJpegComponent"/> component containing decoding meta information.
/// </summary>
private readonly IJpegComponent component;
/// <summary>
/// Gets the <see cref="IRawJpegData"/> instance containing decoding meta information.
/// </summary>
private readonly IRawJpegData rawJpeg;
private readonly float dcDequantizer;
/// <summary>
/// Initializes a new instance of the <see cref="JpegComponentPostProcessor8"/> class.
/// </summary>
public JpegComponentPostProcessor8(MemoryAllocator memoryAllocator, JpegFrame frame, IRawJpegData rawJpeg, Size postProcessorBufferSize, IJpegComponent component)
{
// TODO: this must be a variable depending on dct scale factor
const int blockSize = 1;
this.frame = frame;
this.component = component;
this.rawJpeg = rawJpeg;
this.dcDequantizer = rawJpeg.QuantizationTables[this.component.QuantizationTableIndex][0];
this.blockAreaSize = this.component.SubSamplingDivisors * blockSize;
this.ColorBuffer = memoryAllocator.Allocate2DOveraligned<float>(
postProcessorBufferSize.Width,
postProcessorBufferSize.Height,
this.blockAreaSize.Height);
}
/// <summary>
/// Gets the temporary working buffer of color values.
/// </summary>
public Buffer2D<float> ColorBuffer { get; }
/// <inheritdoc />
public void Dispose() => this.ColorBuffer.Dispose();
/// <summary>
/// Convert raw spectral DCT data to color data and copy it to the color buffer <see cref="ColorBuffer"/>.
/// </summary>
public void CopyBlocksToColorBuffer(int spectralStep)
{
Buffer2D<Block8x8> spectralBuffer = this.component.SpectralBlocks;
float maximumValue = this.frame.MaxColorChannelValue;
int blocksRowsPerStep = this.component.SamplingFactors.Height;
int yBlockStart = spectralStep * blocksRowsPerStep;
for (int y = 0; y < blocksRowsPerStep; y++)
{
int yBuffer = y * this.blockAreaSize.Height;
Span<float> colorBufferRow = this.ColorBuffer.DangerousGetRowSpan(yBuffer);
Span<Block8x8> blockRow = spectralBuffer.DangerousGetRowSpan(yBlockStart + y);
for (int xBlock = 0; xBlock < spectralBuffer.Width; xBlock++)
{
// get DC - averaged 8x8 pixel value
float DC = blockRow[xBlock][0];
// dequantization
DC *= this.dcDequantizer;
// Normalize & round
DC = (float)Math.Round(Numerics.Clamp(DC + MathF.Ceiling(maximumValue / 2), 0, maximumValue));
// Save to the intermediate buffer
int xColorBufferStart = xBlock * this.blockAreaSize.Width;
colorBufferRow[xColorBufferStart] = DC;
//// Integer to float
//workspaceBlock.LoadFrom(ref blockRow[xBlock]);
//// Dequantize
//workspaceBlock.MultiplyInPlace(ref dequantTable);
//// Convert from spectral to color
//FastFloatingPointDCT.TransformIDCT(ref workspaceBlock);
//// To conform better to libjpeg we actually NEED TO loose precision here.
//// This is because they store blocks as Int16 between all the operations.
//// To be "more accurate", we need to emulate this by rounding!
//workspaceBlock.NormalizeColorsAndRoundInPlace(maximumValue);
//// Write to color buffer acording to sampling factors
//int xColorBufferStart = xBlock * this.blockAreaSize.Width;
//workspaceBlock.ScaledCopyTo(
// ref colorBufferRow[xColorBufferStart],
// destAreaStride,
// subSamplingDivisors.Width,
// subSamplingDivisors.Height);
}
}
}
public void ClearSpectralBuffers()
{
Buffer2D<Block8x8> spectralBlocks = this.component.SpectralBlocks;
for (int i = 0; i < spectralBlocks.Height; i++)
{
spectralBlocks.DangerousGetRowSpan(i).Clear();
}
}
public Span<float> GetColorBufferRowSpan(int row) =>
this.ColorBuffer.DangerousGetRowSpan(row);
}
}

261
src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConversion/ResizingSpectralConverter{TPixel}.cs

@ -1,261 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.Linq;
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
{
// TODO: docs
internal class ResizingSpectralConverter<TPixel> : SpectralConverter<TPixel>, IDisposable
where TPixel : unmanaged, IPixel<TPixel>
{
/// <summary>
/// <see cref="Configuration"/> instance associated with current
/// decoding routine.
/// </summary>
private readonly Configuration configuration;
/// <summary>
/// Target image size after scaled decoding.
/// </summary>
private readonly Size targetSize;
/// <summary>
/// Jpeg component converters from decompressed spectral to color data.
/// </summary>
private JpegComponentPostProcessor8[] componentProcessors;
/// <summary>
/// Resulting 2D pixel buffer.
/// </summary>
private Buffer2D<TPixel> pixelBuffer;
/// <summary>
/// How many pixel rows are processed in one 'stride'.
/// </summary>
private int pixelRowsPerStep;
/// <summary>
/// How many pixel rows were processed.
/// </summary>
private int pixelRowCounter;
/// <summary>
/// Intermediate buffer of RGB components used in color conversion.
/// </summary>
private IMemoryOwner<byte> rgbBuffer;
/// <summary>
/// Proxy buffer used in packing from RGB to target TPixel pixels.
/// </summary>
private IMemoryOwner<TPixel> paddedProxyPixelRow;
/// <summary>
/// Color converter from jpeg color space to target pixel color space.
/// </summary>
private JpegColorConverterBase colorConverter;
public ResizingSpectralConverter(Configuration configuration, Size targetSize)
{
this.configuration = configuration;
this.targetSize = targetSize;
}
public override void InjectFrameData(JpegFrame frame, IRawJpegData jpegData)
{
MemoryAllocator allocator = this.configuration.MemoryAllocator;
(int width, int height, int scaleDenominator) = GetScaledImageDimensions(frame.PixelWidth, frame.PixelHeight, this.targetSize.Width, this.targetSize.Height);
// iteration data
int majorBlockWidth = frame.Components.Max((component) => component.SizeInBlocks.Width);
int majorVerticalSamplingFactor = frame.Components.Max((component) => component.SamplingFactors.Height);
this.pixelBuffer = allocator.Allocate2D<TPixel>(
width,
height,
this.configuration.PreferContiguousImageBuffers);
this.paddedProxyPixelRow = allocator.Allocate<TPixel>(width + 3);
// single 'stride' rgba32 buffer for conversion between spectral and TPixel
this.rgbBuffer = allocator.Allocate<byte>(width * 3);
// component processors from spectral to Rgba32
int blockPixelSize = 8 / scaleDenominator;
this.pixelRowsPerStep = majorVerticalSamplingFactor * blockPixelSize;
// color converter
JpegColorConverterBase converter = this.GetColorConverter(frame, jpegData);
this.colorConverter = converter;
int bufferWidth = majorBlockWidth * blockPixelSize;
int batchSize = converter.ElementsPerBatch;
int correctedBufferWidth = bufferWidth + (batchSize - (bufferWidth % batchSize));
var postProcessorBufferSize = new Size(correctedBufferWidth, this.pixelRowsPerStep);
this.componentProcessors = new JpegComponentPostProcessor8[frame.Components.Length];
for (int i = 0; i < this.componentProcessors.Length; i++)
{
this.componentProcessors[i] = new JpegComponentPostProcessor8(allocator, frame, jpegData, postProcessorBufferSize, frame.Components[i]);
}
}
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.ConvertStride(spectralStep: 0);
foreach (JpegComponentPostProcessor8 cpp in this.componentProcessors)
{
cpp.ClearSpectralBuffers();
}
}
/// <summary>
/// Converts single spectral jpeg stride to color stride.
/// </summary>
/// <param name="spectralStep">Spectral stride index.</param>
private void ConvertStride(int spectralStep)
{
int maxY = Math.Min(this.pixelBuffer.Height, this.pixelRowCounter + this.pixelRowsPerStep);
for (int i = 0; i < this.componentProcessors.Length; i++)
{
this.componentProcessors[i].CopyBlocksToColorBuffer(spectralStep);
}
int width = this.pixelBuffer.Width;
for (int yy = this.pixelRowCounter; yy < maxY; yy++)
{
int y = yy - this.pixelRowCounter;
var values = new JpegColorConverterBase.ComponentValues(this.componentProcessors, y);
this.colorConverter.ConvertToRgbInplace(values);
values = values.Slice(0, width); // slice away Jpeg padding
Span<byte> r = this.rgbBuffer.Slice(0, width);
Span<byte> g = this.rgbBuffer.Slice(width, width);
Span<byte> b = this.rgbBuffer.Slice(width * 2, width);
SimdUtils.NormalizedFloatToByteSaturate(values.Component0.Slice(0, width), r);
SimdUtils.NormalizedFloatToByteSaturate(values.Component1.Slice(0, width), g);
SimdUtils.NormalizedFloatToByteSaturate(values.Component2.Slice(0, width), b);
// PackFromRgbPlanes expects the destination to be padded, so try to get padded span containing extra elements from the next row.
// If we can't get such a padded row because we are on a MemoryGroup boundary or at the last row,
// pack pixels to a temporary, padded proxy buffer, then copy the relevant values to the destination row.
if (this.pixelBuffer.DangerousTryGetPaddedRowSpan(yy, 3, out Span<TPixel> destRow))
{
PixelOperations<TPixel>.Instance.PackFromRgbPlanes(this.configuration, r, g, b, destRow);
}
else
{
Span<TPixel> proxyRow = this.paddedProxyPixelRow.GetSpan();
PixelOperations<TPixel>.Instance.PackFromRgbPlanes(this.configuration, r, g, b, proxyRow);
proxyRow.Slice(0, width).CopyTo(this.pixelBuffer.DangerousGetRowSpan(yy));
}
}
this.pixelRowCounter += this.pixelRowsPerStep;
}
public override Buffer2D<TPixel> GetPixelBuffer(CancellationToken cancellationToken)
{
if (!this.Converted)
{
int steps = (int)Math.Ceiling(this.pixelBuffer.Height / (float)this.pixelRowsPerStep);
for (int step = 0; step < steps; step++)
{
cancellationToken.ThrowIfCancellationRequested();
this.ConvertStride(step);
}
}
return this.pixelBuffer;
}
public void Dispose()
{
if (this.componentProcessors != null)
{
foreach (JpegComponentPostProcessor8 cpp in this.componentProcessors)
{
cpp.Dispose();
}
}
this.rgbBuffer?.Dispose();
this.paddedProxyPixelRow?.Dispose();
}
// TODO: docs, code formatting
private static readonly (int Num, int Denom)[] ScalingFactors = new (int, int)[]
{
/* upscaling factors */
// (16, 8),
// (15, 8),
// (14, 8),
// (13, 8),
// (12, 8),
// (11, 8),
// (10, 8),
// (9, 8),
/* no scaling */
(8, 8),
/* downscaling factors */
// (7, 8), // 8 => 7
// (6, 8), // 8 => 6
// (5, 8), // 8 => 5
// (4, 8), // 1/2 dct scaling - currently not supported
// (3, 8), // 8 => 3
// (2, 8), // 1/4 dct scaling - currently not supported
(1, 8), // 1/8 dct scaling
};
/// <summary>
/// TODO: docs, code formatting
/// </summary>
/// <param name="iWidth">Initial image width.</param>
/// <param name="iHeight">Initial image height.</param>
/// <param name="tWidth">Target image width.</param>
/// <param name="tHeight">Target image height.</param>
private static (int Width, int Height, int ScaleDenominator) GetScaledImageDimensions(int iWidth, int iHeight, int tWidth, int tHeight)
{
int output_width = iWidth;
int output_height = iHeight;
int dct_scale = 8;
for (int i = 1; i < ScalingFactors.Length; i++)
{
(int num, int denom) = ScalingFactors[i];
int scaledw = (int)Numerics.DivideCeil((uint)(iWidth * num), (uint)denom);
int scaledh = (int)Numerics.DivideCeil((uint)(iHeight * num), (uint)denom);
if (scaledw < tWidth || scaledh < tHeight)
{
dct_scale = 8 / ScalingFactors[i - 1].Num;
break;
}
output_width = scaledw;
output_height = scaledh;
}
return (output_width, output_height, dct_scale);
}
}
}

21
src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConversion/SpectralConverter{TPixel}.cs

@ -1,21 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System.Threading;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
{
// TODO: docs
internal abstract class SpectralConverter<TPixel> : SpectralConverter
where TPixel : unmanaged, IPixel<TPixel>
{
/// <summary>
/// Gets converted pixel buffer.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Pixel buffer.</returns>
public abstract Buffer2D<TPixel> GetPixelBuffer(CancellationToken cancellationToken);
}
}

0
src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConversion/SpectralConverter.cs → src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter.cs

184
src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConversion/DirectSpectralConverter{TPixel}.cs → src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs

@ -22,9 +22,21 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// </listheader>
/// </list>
/// </remarks>
internal class DirectSpectralConverter<TPixel> : SpectralConverter<TPixel>, IDisposable
internal class SpectralConverter<TPixel> : SpectralConverter, IDisposable
where TPixel : unmanaged, IPixel<TPixel>
{
/// <summary>
/// Supported scaling factors for DCT jpeg scaling.
/// </summary>
private static readonly int[] ScalingFactors = new int[]
{
// 8 => 8, no scaling
8,
// 8 => 1, 1/8 of the original size
1,
};
/// <summary>
/// <see cref="Configuration"/> instance associated with current
/// decoding routine.
@ -34,7 +46,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// <summary>
/// Jpeg component converters from decompressed spectral to color data.
/// </summary>
private JpegComponentPostProcessor[] componentProcessors;
private ComponentProcessor[] componentProcessors;
/// <summary>
/// Color converter from jpeg color space to target pixel color space.
@ -67,18 +79,35 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
private int pixelRowCounter;
/// <summary>
/// Initializes a new instance of the <see cref="DirectSpectralConverter{TPixel}" /> class.
/// Represent target size after decoding for scaling decoding mode.
/// </summary>
/// <remarks>
/// Null if no scaling is required.
/// </remarks>
private Size? targetSize;
/// <summary>
/// Initializes a new instance of the <see cref="SpectralConverter{TPixel}" /> class.
/// </summary>
/// <param name="configuration">The configuration.</param>
public DirectSpectralConverter(Configuration configuration) =>
/// <param name="targetSize">Optional target size for decoded image.</param>
public SpectralConverter(Configuration configuration, Size? targetSize = null)
{
this.configuration = configuration;
/// <inheritdoc/>
this.targetSize = targetSize;
}
/// <summary>
/// Gets converted pixel buffer.
/// </summary>
/// <remarks>
/// For non-baseline interleaved jpeg this method does a 'lazy' spectral
/// conversion from spectral to color.
/// </remarks>
public override Buffer2D<TPixel> GetPixelBuffer(CancellationToken cancellationToken)
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Pixel buffer.</returns>
public Buffer2D<TPixel> GetPixelBuffer(CancellationToken cancellationToken)
{
if (!this.Converted)
{
@ -94,52 +123,45 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
return this.pixelBuffer;
}
/// <inheritdoc/>
public override void InjectFrameData(JpegFrame frame, IRawJpegData jpegData)
/// <summary>
/// Calculates resulting image size and jpeg block scaling.
/// </summary>
/// <param name="nativeSize">Native size of the image.</param>
/// <param name="blockPixelSize">Resulting jpeg block pixel size.</param>
/// <returns>Scaled jpeg image size.</returns>
private Size GetResultingImageSize(Size nativeSize, out int blockPixelSize)
{
MemoryAllocator allocator = this.configuration.MemoryAllocator;
// iteration data
int majorBlockWidth = frame.Components.Max((component) => component.SizeInBlocks.Width);
int majorVerticalSamplingFactor = frame.Components.Max((component) => component.SamplingFactors.Height);
const int blockPixelHeight = 8;
this.pixelRowsPerStep = majorVerticalSamplingFactor * blockPixelHeight;
// pixel buffer for resulting image
this.pixelBuffer = allocator.Allocate2D<TPixel>(
frame.PixelWidth,
frame.PixelHeight,
this.configuration.PreferContiguousImageBuffers);
this.paddedProxyPixelRow = allocator.Allocate<TPixel>(frame.PixelWidth + 3);
// component processors from spectral to Rgba32
const int blockPixelWidth = 8;
var postProcessorBufferSize = new Size(majorBlockWidth * blockPixelWidth, this.pixelRowsPerStep);
this.componentProcessors = new JpegComponentPostProcessor[frame.Components.Length];
for (int i = 0; i < this.componentProcessors.Length; i++)
if (this.targetSize == null)
{
this.componentProcessors[i] = new JpegComponentPostProcessor(allocator, frame, jpegData, postProcessorBufferSize, frame.Components[i]);
blockPixelSize = 8;
return nativeSize;
}
else
{
const uint jpegBlockPixelSize = 8;
// single 'stride' rgba32 buffer for conversion between spectral and TPixel
this.rgbBuffer = allocator.Allocate<byte>(frame.PixelWidth * 3);
Size targetSize = this.targetSize.Value;
int outputWidth = nativeSize.Width;
int outputHeight = nativeSize.Height;
blockPixelSize = 1;
// color converter from Rgba32 to TPixel
this.colorConverter = this.GetColorConverter(frame, jpegData);
}
/// <inheritdoc/>
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.ConvertStride(spectralStep: 0);
for (int i = 1; i < ScalingFactors.Length; i++)
{
int scale = ScalingFactors[i];
int scaledw = (int)Numerics.DivideCeil((uint)(nativeSize.Width * scale), jpegBlockPixelSize);
int scaledh = (int)Numerics.DivideCeil((uint)(nativeSize.Height * scale), jpegBlockPixelSize);
if (scaledw < targetSize.Width || scaledh < targetSize.Height)
{
blockPixelSize = ScalingFactors[i - 1];
break;
}
outputWidth = scaledw;
outputHeight = scaledh;
}
foreach (JpegComponentPostProcessor cpp in this.componentProcessors)
{
cpp.ClearSpectralBuffers();
return new Size(outputWidth, outputHeight);
}
}
@ -193,12 +215,80 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
this.pixelRowCounter += this.pixelRowsPerStep;
}
/// <inheritdoc/>
public override void InjectFrameData(JpegFrame frame, IRawJpegData jpegData)
{
MemoryAllocator allocator = this.configuration.MemoryAllocator;
// color converter from RGB to TPixel
JpegColorConverterBase converter = this.GetColorConverter(frame, jpegData);
this.colorConverter = converter;
// Resulting image size
Size pixelSize = this.GetResultingImageSize(frame.PixelSize, out int blockPixelSize);
// iteration data
int majorBlockWidth = frame.Components.Max((component) => component.SizeInBlocks.Width);
int majorVerticalSamplingFactor = frame.Components.Max((component) => component.SamplingFactors.Height);
this.pixelRowsPerStep = majorVerticalSamplingFactor * blockPixelSize;
// pixel buffer for resulting image
this.pixelBuffer = allocator.Allocate2D<TPixel>(
pixelSize.Width,
pixelSize.Height,
this.configuration.PreferContiguousImageBuffers);
this.paddedProxyPixelRow = allocator.Allocate<TPixel>(pixelSize.Width + 3);
// component processors from spectral to RGB
int bufferWidth = majorBlockWidth * blockPixelSize;
int batchSize = converter.ElementsPerBatch;
int converterAlignedBufferWidth = bufferWidth + (batchSize - (bufferWidth % batchSize));
var postProcessorBufferSize = new Size(converterAlignedBufferWidth, this.pixelRowsPerStep);
this.componentProcessors = new ComponentProcessor[frame.Components.Length];
switch (blockPixelSize)
{
case 8:
for (int i = 0; i < this.componentProcessors.Length; i++)
{
this.componentProcessors[i] = new DirectComponentProcessor(allocator, frame, jpegData, postProcessorBufferSize, frame.Components[i]);
}
break;
case 1:
for (int i = 0; i < this.componentProcessors.Length; i++)
{
this.componentProcessors[i] = new DownScalingComponentProcessor8(allocator, frame, jpegData, postProcessorBufferSize, frame.Components[i]);
}
break;
// TODO: default?
}
// single 'stride' rgba32 buffer for conversion between spectral and TPixel
this.rgbBuffer = allocator.Allocate<byte>(pixelSize.Width * 3);
}
/// <inheritdoc/>
public override void ConvertStrideBaseline()
{
// Convert next pixel stride using single spectral `stride'
// Note that zero passing eliminates extra virtual call
this.ConvertStride(spectralStep: 0);
foreach (ComponentProcessor cpp in this.componentProcessors)
{
cpp.ClearSpectralBuffers();
}
}
/// <inheritdoc/>
public void Dispose()
{
if (this.componentProcessors != null)
{
foreach (JpegComponentPostProcessor cpp in this.componentProcessors)
foreach (ComponentProcessor cpp in this.componentProcessors)
{
cpp.Dispose();
}

14
src/ImageSharp/Formats/Jpeg/JpegDecoder.cs

@ -32,8 +32,16 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
public Image Decode(Configuration configuration, Stream stream, CancellationToken cancellationToken)
=> this.Decode<Rgb24>(configuration, stream, cancellationToken);
// TODO: this implementation is experimental
public Image<TPixel> experimental__DecodeInto<TPixel>(Configuration configuration, Stream stream, Size targetSize, CancellationToken cancellationToken)
/// <summary>
/// TODO: this implementation is experimental
/// </summary>
/// <typeparam name="TPixel">Placeholder1</typeparam>
/// <param name="configuration">Placeholder2</param>
/// <param name="stream">Placeholder3</param>
/// <param name="targetSize">Placeholder4</param>
/// <param name="cancellationToken">Placeholder5</param>
/// <returns>Placeholder6</returns>
public Image<TPixel> Experimental__DecodeInto<TPixel>(Configuration configuration, Stream stream, Size targetSize, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
Guard.NotNull(stream, nameof(stream));
@ -45,7 +53,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
using var bufferedReadStream = new BufferedReadStream(configuration, stream);
try
{
return decoder.experimental__DecodeInto<TPixel>(bufferedReadStream, targetSize, cancellationToken);
return decoder.Experimental__DecodeInto<TPixel>(bufferedReadStream, targetSize, cancellationToken);
}
catch (InvalidMemoryOperationException ex)
{

32
src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs

@ -182,6 +182,22 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
return new JpegFileMarker(marker[1], stream.Position - 2, true);
}
/// <inheritdoc/>
public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
using var spectralConverter = new SpectralConverter<TPixel>(this.Configuration);
return this.Decode(stream, spectralConverter, cancellationToken);
}
// TODO: docs
public Image<TPixel> Experimental__DecodeInto<TPixel>(BufferedReadStream stream, Size targetSize, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
using var spectralConverter = new SpectralConverter<TPixel>(this.Configuration, targetSize);
return this.Decode(stream, spectralConverter, cancellationToken);
}
// TODO: docs
private Image<TPixel> Decode<TPixel>(BufferedReadStream stream, SpectralConverter<TPixel> converter, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
@ -201,22 +217,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.Metadata);
}
// TODO: docs
public Image<TPixel> experimental__DecodeInto<TPixel>(BufferedReadStream stream, Size targetSize, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
using var converter = new ResizingSpectralConverter<TPixel>(this.Configuration, targetSize);
return this.Decode(stream, converter, cancellationToken);
}
/// <inheritdoc/>
public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
using var converter = new DirectSpectralConverter<TPixel>(this.Configuration);
return this.Decode(stream, converter, cancellationToken);
}
/// <inheritdoc/>
public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
{

2
src/ImageSharp/Formats/Tiff/Compression/Decompressors/GrayJpegSpectralConverter.cs

@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors
/// Spectral converter for gray TIFF's which use the JPEG compression.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
internal sealed class GrayJpegSpectralConverter<TPixel> : DirectSpectralConverter<TPixel>
internal sealed class GrayJpegSpectralConverter<TPixel> : SpectralConverter<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
/// <summary>

6
src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs

@ -59,7 +59,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors
case TiffPhotometricInterpretation.BlackIsZero:
case TiffPhotometricInterpretation.WhiteIsZero:
{
using DirectSpectralConverter<L8> spectralConverterGray = new GrayJpegSpectralConverter<L8>(this.configuration);
using SpectralConverter<L8> spectralConverterGray = new GrayJpegSpectralConverter<L8>(this.configuration);
var scanDecoderGray = new HuffmanScanDecoder(stream, spectralConverterGray, CancellationToken.None);
jpegDecoder.LoadTables(this.jpegTables, scanDecoderGray);
jpegDecoder.ParseStream(stream, scanDecoderGray, CancellationToken.None);
@ -74,8 +74,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors
case TiffPhotometricInterpretation.YCbCr:
case TiffPhotometricInterpretation.Rgb:
{
using DirectSpectralConverter<Rgb24> spectralConverter = this.photometricInterpretation == TiffPhotometricInterpretation.YCbCr ?
new RgbJpegSpectralConverter<Rgb24>(this.configuration) : new DirectSpectralConverter<Rgb24>(this.configuration);
using SpectralConverter<Rgb24> spectralConverter = this.photometricInterpretation == TiffPhotometricInterpretation.YCbCr ?
new RgbJpegSpectralConverter<Rgb24>(this.configuration) : new SpectralConverter<Rgb24>(this.configuration);
var scanDecoder = new HuffmanScanDecoder(stream, spectralConverter, CancellationToken.None);
jpegDecoder.LoadTables(this.jpegTables, scanDecoder);
jpegDecoder.ParseStream(stream, scanDecoder, CancellationToken.None);

2
src/ImageSharp/Formats/Tiff/Compression/Decompressors/RgbJpegSpectralConverter.cs

@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors
/// The jpeg data should be always treated as RGB color space.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
internal sealed class RgbJpegSpectralConverter<TPixel> : DirectSpectralConverter<TPixel>
internal sealed class RgbJpegSpectralConverter<TPixel> : SpectralConverter<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
/// <summary>

17
tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpeg.cs

@ -79,4 +79,21 @@ Intel Core i7-6700K CPU 4.00GHz (Skylake), 1 CPU, 8 logical and 4 physical cores
| 'Baseline 4:2:0 Interleaved' | 8.458 ms | 0.0289 ms | 0.0256 ms |
| 'Baseline 4:0:0 (grayscale)' | 1.550 ms | 0.0050 ms | 0.0044 ms |
| 'Progressive 4:2:0 Non-Interleaved' | 13.220 ms | 0.0449 ms | 0.0398 ms |
FRESH BENCHMARKS FOR NEW SPECTRAL CONVERSION SETUP
BenchmarkDotNet=v0.13.0, OS=Windows 10.0.19044
Intel Core i7-6700K CPU 4.00GHz (Skylake), 1 CPU, 8 logical and 4 physical cores
.NET SDK=6.0.100-preview.3.21202.5
[Host] : .NET Core 3.1.21 (CoreCLR 4.700.21.51404, CoreFX 4.700.21.51508), X64 RyuJIT
DefaultJob : .NET Core 3.1.21 (CoreCLR 4.700.21.51404, CoreFX 4.700.21.51508), X64 RyuJIT
| Method | Mean | Error | StdDev |
|------------------------------------ |----------:|----------:|----------:|
| 'Baseline 4:4:4 Interleaved' | 10.734 ms | 0.0287 ms | 0.0254 ms |
| 'Baseline 4:2:0 Interleaved' | 8.517 ms | 0.0401 ms | 0.0356 ms |
| 'Baseline 4:0:0 (grayscale)' | 1.442 ms | 0.0051 ms | 0.0045 ms |
| 'Progressive 4:2:0 Non-Interleaved' | 12.740 ms | 0.0832 ms | 0.0730 ms |
*/

2
tests/ImageSharp.Tests/Formats/Jpg/SpectralToPixelConversionTests.cs

@ -44,7 +44,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
using var bufferedStream = new BufferedReadStream(Configuration.Default, ms);
// Decoding
using var converter = new DirectSpectralConverter<TPixel>(Configuration.Default);
using var converter = new SpectralConverter<TPixel>(Configuration.Default);
using var decoder = new JpegDecoderCore(Configuration.Default, new JpegDecoder());
var scanDecoder = new HuffmanScanDecoder(bufferedStream, converter, cancellationToken: default);
decoder.ParseStream(bufferedStream, scanDecoder, cancellationToken: default);

Loading…
Cancel
Save