Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Jpeg downscaling decoding #2076

Merged
merged 34 commits into from
Jul 17, 2022
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
36180c6
playground
Mar 19, 2022
11220a3
Replaced absurdly complicated math from JpegComponentPostProcessor
Mar 19, 2022
afbf44b
Infrastructure
Mar 21, 2022
017919a
First working prototype, not optimized
Mar 22, 2022
5cca314
Added playground benchmarks
Mar 22, 2022
a10be09
Resizing converter no longer depends on avx converter only
Mar 23, 2022
1ce994a
Unified spectral conversion for direct and downscaled routines
Mar 25, 2022
4e17b69
Added second stage to the resizing decoding
Mar 26, 2022
f4d5f1a
Chroma subsampling for downscaling decoder
Mar 26, 2022
282e593
Update playground
Mar 26, 2022
8192ff2
Initial processor implementation, code base for tests
Mar 29, 2022
52f507d
Separated scaled IDCT methods
Apr 1, 2022
6eceb6c
Moved quantization table initialization to component post processors
Apr 1, 2022
03407f1
4x4 implementation, tests
Apr 2, 2022
1050cf2
Fixed bug leading to gray images after decoding
Apr 12, 2022
9575a24
Fix compilation error
Apr 12, 2022
c57ca1b
Merge branch 'main' into dp/jpeg-downscaling-decode
Apr 12, 2022
12776f0
IDCT resizing modes
Apr 23, 2022
7057245
Code cleanup, removed invalid second pass logic, marked scaled decodi…
Apr 26, 2022
bfbfdfa
Added tests for out jpeg image size getter method
Apr 30, 2022
b943f80
Restored Program.cs
Apr 30, 2022
bb82e27
Merge branch 'main' into dp/jpeg-downscaling-decode
May 1, 2022
6747339
Docs & review fixes
May 1, 2022
1aff245
Merge branch 'dp/jpeg-downscaling-decode' of https://github.com/br3ak…
May 1, 2022
f011dcc
Unsafe.Add fix
May 1, 2022
3feb7f6
Merge branch 'main' of https://github.com/SixLabors/ImageSharp into d…
May 1, 2022
95c56b0
Small bug fixes, ready for merging
May 1, 2022
ed86426
Updated load-resize-save benchmark, deleted obsolete benchmarks
May 1, 2022
9f35b78
Merge branch 'main' into dp/jpeg-downscaling-decode
May 2, 2022
2896faf
Merge branch 'main' into dp/jpeg-downscaling-decode
brianpopow May 17, 2022
3f16a68
Merge branch 'main' into dp/jpeg-downscaling-decode
Jun 26, 2022
9851315
Added resizing benchmark results
Jun 26, 2022
d0de191
Merge remote-tracking branch 'upstream/main' into dp/jpeg-downscaling…
JimBobSquarePants Jul 16, 2022
7a9cf87
Fix headers
JimBobSquarePants Jul 16, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -247,8 +247,7 @@ public void ParseEntropyCodedData(int scanComponentCount)

this.scanBuffer = new JpegBitReader(this.stream);

bool fullScan = this.frame.Progressive || this.frame.MultiScan;
this.frame.AllocateComponents(fullScan);
this.frame.AllocateComponents();

if (this.frame.Progressive)
{
Expand Down Expand Up @@ -326,11 +325,13 @@ private void ParseBaselineData()

if (this.scanComponentCount != 1)
{
this.spectralConverter.PrepareForDecoding();
Copy link
Contributor Author

@br3aker br3aker May 1, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, we got a little problem here. ITU specs allows to place markers between frame and scan markers which can screw up current architecture.

I've decided to separate SOF scan callback which now simply saves references to frame and jpeg data variables and has a separate PrepareForDecoding() callback which is called explicitly for single scan images and implicitly called by multi-scan images by SpectralConverter. HuffmanScanDecoder and ArithmeticScanDecoder are affected by this change.

I'm planning to do a refactoring PR for entire jpeg decoder after this is merged. Arithmetic decoding PR and this scaling decoding PR contains rather large changes so it's the perfect time to clean it up! :)

this.ParseBaselineDataInterleaved();
this.spectralConverter.CommitConversion();
}
else if (this.frame.ComponentCount == 1)
{
this.spectralConverter.PrepareForDecoding();
this.ParseBaselineDataSingleComponent();
this.spectralConverter.CommitConversion();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
#if SUPPORTS_RUNTIME_INTRINSICS
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;

namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters
Expand All @@ -25,7 +26,9 @@ protected JpegColorConverterAvx(JpegColorSpace colorSpace, int precision)
{
}

public override bool IsAvailable => Avx.IsSupported;
public sealed override bool IsAvailable => Avx.IsSupported;

public sealed override int ElementsPerBatch => Vector256<float>.Count;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ protected JpegColorConverterBase(JpegColorSpace colorSpace, int precision)
/// </summary>
public abstract bool IsAvailable { get; }

/// <summary>
/// Gets a value indicating how many pixels are processed in a single batch.
/// </summary>
/// <remarks>
/// This generally should be equal to register size,
/// e.g. 1 for scalar implementation, 8 for AVX implementation and so on.
/// </remarks>
public abstract int ElementsPerBatch { get; }

/// <summary>
/// Gets the <see cref="JpegColorSpace"/> of this converter.
/// </summary>
Expand Down Expand Up @@ -219,7 +228,7 @@ public ComponentValues(IReadOnlyList<Buffer2D<float>> componentBuffers, int row)
/// </summary>
/// <param name="processors">List of component color processors.</param>
/// <param name="row">Row to convert</param>
public ComponentValues(IReadOnlyList<JpegComponentPostProcessor> processors, int row)
public ComponentValues(IReadOnlyList<ComponentProcessor> processors, int row)
{
DebugGuard.MustBeGreaterThan(processors.Count, 0, nameof(processors));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ protected JpegColorConverterScalar(JpegColorSpace colorSpace, int precision)
{
}

public override bool IsAvailable => true;
public sealed override bool IsAvailable => true;

public sealed override int ElementsPerBatch => 1;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ internal abstract partial class JpegColorConverterBase
/// Even though real life data is guaranteed to be of size
/// divisible by 8 newer SIMD instructions like AVX512 won't work with
/// such data out of the box. These converters have fallback code
/// for 'remainder' data.
/// for remainder data.
/// </remarks>
internal abstract class JpegColorConverterVector : JpegColorConverterBase
{
Expand All @@ -28,7 +28,9 @@ protected JpegColorConverterVector(JpegColorSpace colorSpace, int precision)

public sealed override bool IsAvailable => Vector.IsHardwareAccelerated && Vector<float>.Count % 4 == 0;

public override void ConvertToRgbInplace(in ComponentValues values)
public sealed override int ElementsPerBatch => Vector<float>.Count;

public sealed override void ConvertToRgbInplace(in ComponentValues values)
{
DebugGuard.IsTrue(this.IsAvailable, $"{this.GetType().Name} converter is not supported on current hardware.");

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// 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>
/// Base class for processing component spectral data and converting it to raw color data.
/// </summary>
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; }

/// <summary>
/// Converts spectral data to color data accessible via <see cref="GetColorBufferRowSpan(int)"/>.
/// </summary>
/// <param name="row">Spectral row index to convert.</param>
public abstract void CopyBlocksToColorBuffer(int row);

/// <summary>
/// Clears spectral buffers.
/// </summary>
/// <remarks>
/// Should only be called during baseline interleaved decoding.
/// </remarks>
public void ClearSpectralBuffers()
{
Buffer2D<Block8x8> spectralBlocks = this.Component.SpectralBlocks;
for (int i = 0; i < spectralBlocks.Height; i++)
{
spectralBlocks.DangerousGetRowSpan(i).Clear();
}
}

/// <summary>
/// Gets converted color buffer row.
/// </summary>
/// <param name="row">Row index.</param>
/// <returns>Color buffer row.</returns>
public Span<float> GetColorBufferRowSpan(int row) =>
this.ColorBuffer.DangerousGetRowSpan(row);

public void Dispose() => this.ColorBuffer.Dispose();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// 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>
/// Processes component spectral data and converts it to color data in 1-to-1 scale.
/// </summary>
internal sealed class DirectComponentProcessor : ComponentProcessor
{
private Block8x8F dequantizationTable;

public DirectComponentProcessor(MemoryAllocator memoryAllocator, JpegFrame frame, IRawJpegData rawJpeg, Size postProcessorBufferSize, IJpegComponent component)
: base(memoryAllocator, frame, postProcessorBufferSize, component, blockSize: 8)
{
this.dequantizationTable = rawJpeg.QuantizationTables[component.QuantizationTableIndex];
FloatingPointDCT.AdjustToIDCT(ref this.dequantizationTable);
}

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 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 this.dequantizationTable);

// Convert from spectral to color
FloatingPointDCT.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);
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.

using System;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Memory;

namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
{
/// <summary>
/// Processes component spectral data and converts it to color data in 2-to-1 scale.
/// </summary>
internal sealed class DownScalingComponentProcessor2 : ComponentProcessor
{
private Block8x8F dequantizationTable;

public DownScalingComponentProcessor2(MemoryAllocator memoryAllocator, JpegFrame frame, IRawJpegData rawJpeg, Size postProcessorBufferSize, IJpegComponent component)
: base(memoryAllocator, frame, postProcessorBufferSize, component, 4)
{
this.dequantizationTable = rawJpeg.QuantizationTables[component.QuantizationTableIndex];
ScaledFloatingPointDCT.AdjustToIDCT(ref this.dequantizationTable);
}

public override void CopyBlocksToColorBuffer(int spectralStep)
{
Buffer2D<Block8x8> spectralBuffer = this.Component.SpectralBlocks;

float maximumValue = this.Frame.MaxColorChannelValue;
float normalizationValue = MathF.Ceiling(maximumValue / 2);

int destAreaStride = this.ColorBuffer.Width;

int blocksRowsPerStep = this.Component.SamplingFactors.Height;
Size subSamplingDivisors = this.Component.SubSamplingDivisors;

Block8x8F workspaceBlock = default;

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++)
{
// Integer to float
workspaceBlock.LoadFrom(ref blockRow[xBlock]);

// IDCT/Normalization/Range
ScaledFloatingPointDCT.TransformIDCT_4x4(ref workspaceBlock, ref this.dequantizationTable, normalizationValue, maximumValue);

// Save to the intermediate buffer
int xColorBufferStart = xBlock * this.BlockAreaSize.Width;
ScaledCopyTo(
ref workspaceBlock,
ref colorBufferRow[xColorBufferStart],
destAreaStride,
subSamplingDivisors.Width,
subSamplingDivisors.Height);
}
}
}

[MethodImpl(InliningOptions.ShortMethod)]
public static void ScaledCopyTo(ref Block8x8F block, ref float destRef, int destStrideWidth, int horizontalScale, int verticalScale)
{
// TODO: Optimize: implement all cases with scale-specific, loopless code!
CopyArbitraryScale(ref block, ref destRef, destStrideWidth, horizontalScale, verticalScale);

[MethodImpl(InliningOptions.ColdPath)]
static void CopyArbitraryScale(ref Block8x8F block, ref float areaOrigin, int areaStride, int horizontalScale, int verticalScale)
{
for (int y = 0; y < 4; y++)
{
int yy = y * verticalScale;
int y8 = y * 8;

for (int x = 0; x < 4; x++)
{
int xx = x * horizontalScale;

float value = block[y8 + x];

for (int i = 0; i < verticalScale; i++)
{
int baseIdx = ((yy + i) * areaStride) + xx;

for (int j = 0; j < horizontalScale; j++)
{
// area[xx + j, yy + i] = value;
Unsafe.Add(ref areaOrigin, (nint)(uint)(baseIdx + j)) = value;
}
}
}
}
}
}
}
}
Loading