Browse Source

Merge pull request #337 from SixLabors/antonfirsov/jpeg-optimization

Accuracy bugfix + further optimizations on JpegImagePostprocessor
af/merge-core
Anton Firsov 9 years ago
committed by GitHub
parent
commit
a7aec0b1d3
  1. 4
      src/ImageSharp/Common/Extensions/SimdUtils.cs
  2. 98
      src/ImageSharp/Common/Tuples/Tuple8.cs
  3. 75
      src/ImageSharp/Common/Tuples/Vector4Pair.cs
  4. 9
      src/ImageSharp/Formats/Jpeg/Common/Block8x8.cs
  5. 146
      src/ImageSharp/Formats/Jpeg/Common/Block8x8F.CopyTo.cs
  6. 166
      src/ImageSharp/Formats/Jpeg/Common/Block8x8F.Generated.cs
  7. 60
      src/ImageSharp/Formats/Jpeg/Common/Block8x8F.Generated.tt
  8. 296
      src/ImageSharp/Formats/Jpeg/Common/Block8x8F.cs
  9. 155
      src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegBlockPostProcessor.cs
  10. 70
      src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.FromYCbCr.cs
  11. 7
      src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegComponentPostProcessor.cs
  12. 7
      src/ImageSharp/Formats/Jpeg/Common/FastFloatingPointDCT.cs
  13. 24
      src/ImageSharp/Formats/Jpeg/Common/ZigZag.cs
  14. 4
      src/ImageSharp/Formats/Jpeg/GolangPort/Components/Decoder/OrigJpegScanDecoder.ComputationData.cs
  15. 6
      src/ImageSharp/Formats/Jpeg/GolangPort/JpegEncoderCore.cs
  16. 10
      tests/ImageSharp.Tests/Common/SimdUtilsTests.cs
  17. 189
      tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs
  18. 2
      tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs
  19. 126
      tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
  20. 4
      tests/ImageSharp.Tests/Formats/Jpg/JpegProfilingBenchmarks.cs
  21. 21
      tests/ImageSharp.Tests/Formats/Jpg/Utils/ReferenceImplementations.cs
  22. 2
      tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs
  23. 2
      tests/ImageSharp.Tests/TestUtilities/ApproximateFloatComparer.cs
  24. 21
      tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageSimilarityReport.cs
  25. 13
      tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs
  26. 10
      tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs

4
src/ImageSharp/Common/Extensions/SimdUtils.cs

@ -15,9 +15,9 @@ namespace SixLabors.ImageSharp
internal static class SimdUtils
{
/// <summary>
/// Indicates AVX2 architecture where both float and integer registers are of size 256 byte.
/// Gets a value indicating whether the code is being executed on AVX2 CPU where both float and integer registers are of size 256 byte.
/// </summary>
public static readonly bool IsAvx2CompatibleArchitecture = Vector<float>.Count == 8 && Vector<int>.Count == 8;
public static bool IsAvx2CompatibleArchitecture => Vector<float>.Count == 8 && Vector<int>.Count == 8;
internal static void GuardAvx2(string operation)
{

98
src/ImageSharp/Common/Tuples/Tuple8.cs

@ -0,0 +1,98 @@
using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.Common.Tuples
{
/// <summary>
/// Contains value type tuples of 8 elements.
/// TODO: We should T4 this stuff to be DRY
/// </summary>
internal static class Tuple8
{
/// <summary>
/// Value type tuple of 8 <see cref="uint"/>-s
/// </summary>
[StructLayout(LayoutKind.Explicit, Size = 8 * sizeof(uint))]
public struct OfUInt32
{
[FieldOffset(0 * sizeof(uint))]
public uint V0;
[FieldOffset(1 * sizeof(uint))]
public uint V1;
[FieldOffset(2 * sizeof(uint))]
public uint V2;
[FieldOffset(3 * sizeof(uint))]
public uint V3;
[FieldOffset(4 * sizeof(uint))]
public uint V4;
[FieldOffset(5 * sizeof(uint))]
public uint V5;
[FieldOffset(6 * sizeof(uint))]
public uint V6;
[FieldOffset(7 * sizeof(uint))]
public uint V7;
public override string ToString()
{
return $"[{this.V0},{this.V1},{this.V2},{this.V3},{this.V4},{this.V5},{this.V6},{this.V7}]";
}
}
/// <summary>
/// Value type tuple of 8 <see cref="byte"/>-s
/// </summary>
[StructLayout(LayoutKind.Explicit, Size = 8)]
public struct OfByte
{
[FieldOffset(0)]
public byte V0;
[FieldOffset(1)]
public byte V1;
[FieldOffset(2)]
public byte V2;
[FieldOffset(3)]
public byte V3;
[FieldOffset(4)]
public byte V4;
[FieldOffset(5)]
public byte V5;
[FieldOffset(6)]
public byte V6;
[FieldOffset(7)]
public byte V7;
public override string ToString()
{
return $"[{this.V0},{this.V1},{this.V2},{this.V3},{this.V4},{this.V5},{this.V6},{this.V7}]";
}
/// <summary>
/// Sets the values of this tuple by casting all elements of the given <see cref="OfUInt32"/> tuple to <see cref="byte"/>.
/// </summary>
public void LoadFrom(ref OfUInt32 i)
{
this.V0 = (byte)i.V0;
this.V1 = (byte)i.V1;
this.V2 = (byte)i.V2;
this.V3 = (byte)i.V3;
this.V4 = (byte)i.V4;
this.V5 = (byte)i.V5;
this.V6 = (byte)i.V6;
this.V7 = (byte)i.V7;
}
}
}
}

75
src/ImageSharp/Common/Tuples/Vector4Pair.cs

@ -0,0 +1,75 @@
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.Common.Tuples
{
/// <summary>
/// Its faster to process multiple Vector4-s together, so let's pair them!
/// On AVX2 this pair should be convertible to <see cref="Vector{T}"/> of <see cref="float"/>!
/// </summary>
[StructLayout(LayoutKind.Sequential)]
internal struct Vector4Pair
{
public Vector4 A;
public Vector4 B;
private static readonly Vector4 Scale = new Vector4(1 / 255f);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void MultiplyInplace(float value)
{
this.A *= value;
this.B *= value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddInplace(Vector4 value)
{
this.A += value;
this.B += value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddInplace(ref Vector4Pair other)
{
this.A += other.A;
this.B += other.B;
}
/// <summary>
/// Downscale method, specific to Jpeg color conversion. Works only if Vector{float}.Count == 4!
/// TODO: Move it somewhere else.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void RoundAndDownscalePreAvx2()
{
ref Vector<float> a = ref Unsafe.As<Vector4, Vector<float>>(ref this.A);
a = a.FastRound();
ref Vector<float> b = ref Unsafe.As<Vector4, Vector<float>>(ref this.B);
b = b.FastRound();
// Downscale by 1/255
this.A *= Scale;
this.B *= Scale;
}
/// <summary>
/// AVX2-only Downscale method, specific to Jpeg color conversion.
/// TODO: Move it somewhere else.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void RoundAndDownscaleAvx2()
{
ref Vector<float> self = ref Unsafe.As<Vector4Pair, Vector<float>>(ref this);
Vector<float> v = self;
v = v.FastRound();
// Downscale by 1/255
v *= new Vector<float>(1 / 255f);
self = v;
}
}
}

9
src/ImageSharp/Formats/Jpeg/Common/Block8x8.cs

@ -176,17 +176,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common
}
/// <summary>
/// Convert into <see cref="Block8x8F"/>
/// Convert to <see cref="Block8x8F"/>
/// </summary>
public Block8x8F AsFloatBlock()
{
// TODO: Optimize this
var result = default(Block8x8F);
for (int i = 0; i < Size; i++)
{
result[i] = this[i];
}
result.LoadFrom(ref this);
return result;
}

146
src/ImageSharp/Formats/Jpeg/Common/Block8x8F.CopyTo.cs

@ -0,0 +1,146 @@
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Memory;
// ReSharper disable InconsistentNaming
namespace SixLabors.ImageSharp.Formats.Jpeg.Common
{
internal partial struct Block8x8F
{
/// <summary>
/// Copy block data into the destination color buffer pixel area with the provided horizontal and vertical.
/// </summary>
public void CopyTo(BufferArea<float> area, int horizontalScale, int verticalScale)
{
if (horizontalScale == 1 && verticalScale == 1)
{
this.CopyTo(area);
return;
}
else if (horizontalScale == 2 && verticalScale == 2)
{
this.CopyTo2x2(area);
return;
}
// TODO: Optimize: implement all the cases with loopless special code! (T4?)
for (int y = 0; y < 8; y++)
{
int yy = y * verticalScale;
int y8 = y * 8;
for (int x = 0; x < 8; x++)
{
int xx = x * horizontalScale;
float value = this[y8 + x];
for (int i = 0; i < verticalScale; i++)
{
for (int j = 0; j < horizontalScale; j++)
{
area[xx + j, yy + i] = value;
}
}
}
}
}
// [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void CopyTo(BufferArea<float> area)
{
ref byte selfBase = ref Unsafe.As<Block8x8F, byte>(ref this);
ref byte destBase = ref Unsafe.As<float, byte>(ref area.GetReferenceToOrigo());
int destStride = area.Stride * sizeof(float);
CopyRowImpl(ref selfBase, ref destBase, destStride, 0);
CopyRowImpl(ref selfBase, ref destBase, destStride, 1);
CopyRowImpl(ref selfBase, ref destBase, destStride, 2);
CopyRowImpl(ref selfBase, ref destBase, destStride, 3);
CopyRowImpl(ref selfBase, ref destBase, destStride, 4);
CopyRowImpl(ref selfBase, ref destBase, destStride, 5);
CopyRowImpl(ref selfBase, ref destBase, destStride, 6);
CopyRowImpl(ref selfBase, ref destBase, destStride, 7);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void CopyRowImpl(ref byte selfBase, ref byte destBase, int destStride, int row)
{
ref byte s = ref Unsafe.Add(ref selfBase, row * 8 * sizeof(float));
ref byte d = ref Unsafe.Add(ref destBase, row * destStride);
Unsafe.CopyBlock(ref d, ref s, 8 * sizeof(float));
}
private void CopyTo2x2(BufferArea<float> area)
{
ref float destBase = ref area.GetReferenceToOrigo();
int destStride = area.Stride;
this.WidenCopyImpl2x2(ref destBase, 0, destStride);
this.WidenCopyImpl2x2(ref destBase, 1, destStride);
this.WidenCopyImpl2x2(ref destBase, 2, destStride);
this.WidenCopyImpl2x2(ref destBase, 3, destStride);
this.WidenCopyImpl2x2(ref destBase, 4, destStride);
this.WidenCopyImpl2x2(ref destBase, 5, destStride);
this.WidenCopyImpl2x2(ref destBase, 6, destStride);
this.WidenCopyImpl2x2(ref destBase, 7, destStride);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void WidenCopyImpl2x2(ref float destBase, int row, int destStride)
{
ref Vector4 selfLeft = ref Unsafe.Add(ref this.V0L, 2 * row);
ref Vector4 selfRight = ref Unsafe.Add(ref selfLeft, 1);
ref float destLocalOrigo = ref Unsafe.Add(ref destBase, row * 2 * destStride);
Unsafe.Add(ref destLocalOrigo, 0) = selfLeft.X;
Unsafe.Add(ref destLocalOrigo, 1) = selfLeft.X;
Unsafe.Add(ref destLocalOrigo, 2) = selfLeft.Y;
Unsafe.Add(ref destLocalOrigo, 3) = selfLeft.Y;
Unsafe.Add(ref destLocalOrigo, 4) = selfLeft.Z;
Unsafe.Add(ref destLocalOrigo, 5) = selfLeft.Z;
Unsafe.Add(ref destLocalOrigo, 6) = selfLeft.W;
Unsafe.Add(ref destLocalOrigo, 7) = selfLeft.W;
Unsafe.Add(ref Unsafe.Add(ref destLocalOrigo, 8), 0) = selfRight.X;
Unsafe.Add(ref Unsafe.Add(ref destLocalOrigo, 8), 1) = selfRight.X;
Unsafe.Add(ref Unsafe.Add(ref destLocalOrigo, 8), 2) = selfRight.Y;
Unsafe.Add(ref Unsafe.Add(ref destLocalOrigo, 8), 3) = selfRight.Y;
Unsafe.Add(ref Unsafe.Add(ref destLocalOrigo, 8), 4) = selfRight.Z;
Unsafe.Add(ref Unsafe.Add(ref destLocalOrigo, 8), 5) = selfRight.Z;
Unsafe.Add(ref Unsafe.Add(ref destLocalOrigo, 8), 6) = selfRight.W;
Unsafe.Add(ref Unsafe.Add(ref destLocalOrigo, 8), 7) = selfRight.W;
Unsafe.Add(ref Unsafe.Add(ref destLocalOrigo, destStride), 0) = selfLeft.X;
Unsafe.Add(ref Unsafe.Add(ref destLocalOrigo, destStride), 1) = selfLeft.X;
Unsafe.Add(ref Unsafe.Add(ref destLocalOrigo, destStride), 2) = selfLeft.Y;
Unsafe.Add(ref Unsafe.Add(ref destLocalOrigo, destStride), 3) = selfLeft.Y;
Unsafe.Add(ref Unsafe.Add(ref destLocalOrigo, destStride), 4) = selfLeft.Z;
Unsafe.Add(ref Unsafe.Add(ref destLocalOrigo, destStride), 5) = selfLeft.Z;
Unsafe.Add(ref Unsafe.Add(ref destLocalOrigo, destStride), 6) = selfLeft.W;
Unsafe.Add(ref Unsafe.Add(ref destLocalOrigo, destStride), 7) = selfLeft.W;
Unsafe.Add(ref Unsafe.Add(ref destLocalOrigo, destStride + 8), 0) = selfRight.X;
Unsafe.Add(ref Unsafe.Add(ref destLocalOrigo, destStride + 8), 1) = selfRight.X;
Unsafe.Add(ref Unsafe.Add(ref destLocalOrigo, destStride + 8), 2) = selfRight.Y;
Unsafe.Add(ref Unsafe.Add(ref destLocalOrigo, destStride + 8), 3) = selfRight.Y;
Unsafe.Add(ref Unsafe.Add(ref destLocalOrigo, destStride + 8), 4) = selfRight.Z;
Unsafe.Add(ref Unsafe.Add(ref destLocalOrigo, destStride + 8), 5) = selfRight.Z;
Unsafe.Add(ref Unsafe.Add(ref destLocalOrigo, destStride + 8), 6) = selfRight.W;
Unsafe.Add(ref Unsafe.Add(ref destLocalOrigo, destStride + 8), 7) = selfRight.W;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WidenCopyImpl(ref Vector4 s, ref float destBase)
{
Unsafe.Add(ref destBase, 0) = s.X;
Unsafe.Add(ref destBase, 1) = s.X;
Unsafe.Add(ref destBase, 2) = s.Y;
Unsafe.Add(ref destBase, 3) = s.Y;
Unsafe.Add(ref destBase, 4) = s.Z;
Unsafe.Add(ref destBase, 5) = s.Z;
Unsafe.Add(ref destBase, 6) = s.W;
Unsafe.Add(ref destBase, 7) = s.W;
}
}
}

166
src/ImageSharp/Formats/Jpeg/Common/Block8x8F.Generated.cs

@ -96,51 +96,139 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common
/// <summary>
/// Level shift by +128, clip to [0, 255]
/// </summary>
/// <param name="d">The destination block</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void NormalizeColorsInto(ref Block8x8F d)
public void NormalizeColorsInplace()
{
d.V0L = Vector4.Clamp(V0L + COff4, CMin4, CMax4);
d.V0R = Vector4.Clamp(V0R + COff4, CMin4, CMax4);
d.V1L = Vector4.Clamp(V1L + COff4, CMin4, CMax4);
d.V1R = Vector4.Clamp(V1R + COff4, CMin4, CMax4);
d.V2L = Vector4.Clamp(V2L + COff4, CMin4, CMax4);
d.V2R = Vector4.Clamp(V2R + COff4, CMin4, CMax4);
d.V3L = Vector4.Clamp(V3L + COff4, CMin4, CMax4);
d.V3R = Vector4.Clamp(V3R + COff4, CMin4, CMax4);
d.V4L = Vector4.Clamp(V4L + COff4, CMin4, CMax4);
d.V4R = Vector4.Clamp(V4R + COff4, CMin4, CMax4);
d.V5L = Vector4.Clamp(V5L + COff4, CMin4, CMax4);
d.V5R = Vector4.Clamp(V5R + COff4, CMin4, CMax4);
d.V6L = Vector4.Clamp(V6L + COff4, CMin4, CMax4);
d.V6R = Vector4.Clamp(V6R + COff4, CMin4, CMax4);
d.V7L = Vector4.Clamp(V7L + COff4, CMin4, CMax4);
d.V7R = Vector4.Clamp(V7R + COff4, CMin4, CMax4);
this.V0L = Vector4.Clamp(this.V0L + COff4, CMin4, CMax4);
this.V0R = Vector4.Clamp(this.V0R + COff4, CMin4, CMax4);
this.V1L = Vector4.Clamp(this.V1L + COff4, CMin4, CMax4);
this.V1R = Vector4.Clamp(this.V1R + COff4, CMin4, CMax4);
this.V2L = Vector4.Clamp(this.V2L + COff4, CMin4, CMax4);
this.V2R = Vector4.Clamp(this.V2R + COff4, CMin4, CMax4);
this.V3L = Vector4.Clamp(this.V3L + COff4, CMin4, CMax4);
this.V3R = Vector4.Clamp(this.V3R + COff4, CMin4, CMax4);
this.V4L = Vector4.Clamp(this.V4L + COff4, CMin4, CMax4);
this.V4R = Vector4.Clamp(this.V4R + COff4, CMin4, CMax4);
this.V5L = Vector4.Clamp(this.V5L + COff4, CMin4, CMax4);
this.V5R = Vector4.Clamp(this.V5R + COff4, CMin4, CMax4);
this.V6L = Vector4.Clamp(this.V6L + COff4, CMin4, CMax4);
this.V6R = Vector4.Clamp(this.V6R + COff4, CMin4, CMax4);
this.V7L = Vector4.Clamp(this.V7L + COff4, CMin4, CMax4);
this.V7R = Vector4.Clamp(this.V7R + COff4, CMin4, CMax4);
}
/// <summary>
/// Level shift by +128, clip to [0, 255]
/// AVX2-only variant for executing <see cref="NormalizeColorsInplace"/> and <see cref="RoundInplace"/> in one step.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void NormalizeColorsInplace()
public void NormalizeColorsAndRoundInplaceAvx2()
{
Vector<float> off = new Vector<float>(128f);
Vector<float> max = new Vector<float>(255F);
ref Vector<float> row0 = ref Unsafe.As<Vector4, Vector<float>>(ref this.V0L);
row0 = NormalizeAndRound(row0, off, max);
ref Vector<float> row1 = ref Unsafe.As<Vector4, Vector<float>>(ref this.V1L);
row1 = NormalizeAndRound(row1, off, max);
ref Vector<float> row2 = ref Unsafe.As<Vector4, Vector<float>>(ref this.V2L);
row2 = NormalizeAndRound(row2, off, max);
ref Vector<float> row3 = ref Unsafe.As<Vector4, Vector<float>>(ref this.V3L);
row3 = NormalizeAndRound(row3, off, max);
ref Vector<float> row4 = ref Unsafe.As<Vector4, Vector<float>>(ref this.V4L);
row4 = NormalizeAndRound(row4, off, max);
ref Vector<float> row5 = ref Unsafe.As<Vector4, Vector<float>>(ref this.V5L);
row5 = NormalizeAndRound(row5, off, max);
ref Vector<float> row6 = ref Unsafe.As<Vector4, Vector<float>>(ref this.V6L);
row6 = NormalizeAndRound(row6, off, max);
ref Vector<float> row7 = ref Unsafe.As<Vector4, Vector<float>>(ref this.V7L);
row7 = NormalizeAndRound(row7, off, max);
}
/// <summary>
/// Fill the block from 'source' doing short -> float conversion.
/// </summary>
public void LoadFrom(ref Block8x8 source)
{
this.V0L = Vector4.Clamp(V0L + COff4, CMin4, CMax4);
this.V0R = Vector4.Clamp(V0R + COff4, CMin4, CMax4);
this.V1L = Vector4.Clamp(V1L + COff4, CMin4, CMax4);
this.V1R = Vector4.Clamp(V1R + COff4, CMin4, CMax4);
this.V2L = Vector4.Clamp(V2L + COff4, CMin4, CMax4);
this.V2R = Vector4.Clamp(V2R + COff4, CMin4, CMax4);
this.V3L = Vector4.Clamp(V3L + COff4, CMin4, CMax4);
this.V3R = Vector4.Clamp(V3R + COff4, CMin4, CMax4);
this.V4L = Vector4.Clamp(V4L + COff4, CMin4, CMax4);
this.V4R = Vector4.Clamp(V4R + COff4, CMin4, CMax4);
this.V5L = Vector4.Clamp(V5L + COff4, CMin4, CMax4);
this.V5R = Vector4.Clamp(V5R + COff4, CMin4, CMax4);
this.V6L = Vector4.Clamp(V6L + COff4, CMin4, CMax4);
this.V6R = Vector4.Clamp(V6R + COff4, CMin4, CMax4);
this.V7L = Vector4.Clamp(V7L + COff4, CMin4, CMax4);
this.V7R = Vector4.Clamp(V7R + COff4, CMin4, CMax4);
ref short selfRef = ref Unsafe.As<Block8x8, short>(ref source);
this.V0L.X = Unsafe.Add(ref selfRef, 0);
this.V0L.Y = Unsafe.Add(ref selfRef, 1);
this.V0L.Z = Unsafe.Add(ref selfRef, 2);
this.V0L.W = Unsafe.Add(ref selfRef, 3);
this.V0R.X = Unsafe.Add(ref selfRef, 4);
this.V0R.Y = Unsafe.Add(ref selfRef, 5);
this.V0R.Z = Unsafe.Add(ref selfRef, 6);
this.V0R.W = Unsafe.Add(ref selfRef, 7);
this.V1L.X = Unsafe.Add(ref selfRef, 8);
this.V1L.Y = Unsafe.Add(ref selfRef, 9);
this.V1L.Z = Unsafe.Add(ref selfRef, 10);
this.V1L.W = Unsafe.Add(ref selfRef, 11);
this.V1R.X = Unsafe.Add(ref selfRef, 12);
this.V1R.Y = Unsafe.Add(ref selfRef, 13);
this.V1R.Z = Unsafe.Add(ref selfRef, 14);
this.V1R.W = Unsafe.Add(ref selfRef, 15);
this.V2L.X = Unsafe.Add(ref selfRef, 16);
this.V2L.Y = Unsafe.Add(ref selfRef, 17);
this.V2L.Z = Unsafe.Add(ref selfRef, 18);
this.V2L.W = Unsafe.Add(ref selfRef, 19);
this.V2R.X = Unsafe.Add(ref selfRef, 20);
this.V2R.Y = Unsafe.Add(ref selfRef, 21);
this.V2R.Z = Unsafe.Add(ref selfRef, 22);
this.V2R.W = Unsafe.Add(ref selfRef, 23);
this.V3L.X = Unsafe.Add(ref selfRef, 24);
this.V3L.Y = Unsafe.Add(ref selfRef, 25);
this.V3L.Z = Unsafe.Add(ref selfRef, 26);
this.V3L.W = Unsafe.Add(ref selfRef, 27);
this.V3R.X = Unsafe.Add(ref selfRef, 28);
this.V3R.Y = Unsafe.Add(ref selfRef, 29);
this.V3R.Z = Unsafe.Add(ref selfRef, 30);
this.V3R.W = Unsafe.Add(ref selfRef, 31);
this.V4L.X = Unsafe.Add(ref selfRef, 32);
this.V4L.Y = Unsafe.Add(ref selfRef, 33);
this.V4L.Z = Unsafe.Add(ref selfRef, 34);
this.V4L.W = Unsafe.Add(ref selfRef, 35);
this.V4R.X = Unsafe.Add(ref selfRef, 36);
this.V4R.Y = Unsafe.Add(ref selfRef, 37);
this.V4R.Z = Unsafe.Add(ref selfRef, 38);
this.V4R.W = Unsafe.Add(ref selfRef, 39);
this.V5L.X = Unsafe.Add(ref selfRef, 40);
this.V5L.Y = Unsafe.Add(ref selfRef, 41);
this.V5L.Z = Unsafe.Add(ref selfRef, 42);
this.V5L.W = Unsafe.Add(ref selfRef, 43);
this.V5R.X = Unsafe.Add(ref selfRef, 44);
this.V5R.Y = Unsafe.Add(ref selfRef, 45);
this.V5R.Z = Unsafe.Add(ref selfRef, 46);
this.V5R.W = Unsafe.Add(ref selfRef, 47);
this.V6L.X = Unsafe.Add(ref selfRef, 48);
this.V6L.Y = Unsafe.Add(ref selfRef, 49);
this.V6L.Z = Unsafe.Add(ref selfRef, 50);
this.V6L.W = Unsafe.Add(ref selfRef, 51);
this.V6R.X = Unsafe.Add(ref selfRef, 52);
this.V6R.Y = Unsafe.Add(ref selfRef, 53);
this.V6R.Z = Unsafe.Add(ref selfRef, 54);
this.V6R.W = Unsafe.Add(ref selfRef, 55);
this.V7L.X = Unsafe.Add(ref selfRef, 56);
this.V7L.Y = Unsafe.Add(ref selfRef, 57);
this.V7L.Z = Unsafe.Add(ref selfRef, 58);
this.V7L.W = Unsafe.Add(ref selfRef, 59);
this.V7R.X = Unsafe.Add(ref selfRef, 60);
this.V7R.Y = Unsafe.Add(ref selfRef, 61);
this.V7R.Z = Unsafe.Add(ref selfRef, 62);
this.V7R.W = Unsafe.Add(ref selfRef, 63);
}
}
}
}

60
src/ImageSharp/Formats/Jpeg/Common/Block8x8F.Generated.tt

@ -61,9 +61,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common
/// <summary>
/// Level shift by +128, clip to [0, 255]
/// </summary>
/// <param name="d">The destination block</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void TransformByteConvetibleColorValuesInto(ref Block8x8F d)
public void NormalizeColorsInplace()
{
<#
@ -74,7 +72,61 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common
for (int j = 0; j < 2; j++)
{
char side = j == 0 ? 'L' : 'R';
Write($"d.V{i}{side} = Vector4.Clamp(V{i}{side} + COff4, CMin4, CMax4);\r\n");
Write($"this.V{i}{side} = Vector4.Clamp(this.V{i}{side} + COff4, CMin4, CMax4);\r\n");
}
}
PopIndent();
#>
}
/// <summary>
/// AVX2-only variant for executing <see cref="NormalizeColorsInplace"/> and <see cref="RoundInplace"/> in one step.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void NormalizeColorsAndRoundInplaceAvx2()
{
Vector<float> off = new Vector<float>(128f);
Vector<float> max = new Vector<float>(255F);
<#
for (int i = 0; i < 8; i++)
{
#>
ref Vector<float> row<#=i#> = ref Unsafe.As<Vector4, Vector<float>>(ref this.V<#=i#>L);
row<#=i#> = NormalizeAndRound(row<#=i#>, off, max);
<#
}
#>
}
/// <summary>
/// Fill the block from 'source' doing short -> float conversion.
/// </summary>
public void LoadFrom(ref Block8x8 source)
{
ref short selfRef = ref Unsafe.As<Block8x8, short>(ref source);
<#
PushIndent(" ");
for (int j = 0; j < 8; j++)
{
for (int i = 0; i < 8; i++)
{
char destCoord = coordz[i % 4];
char destSide = (i / 4) % 2 == 0 ? 'L' : 'R';
if(j > 0 && i == 0){
WriteLine("");
}
char srcCoord = coordz[j % 4];
char srcSide = (j / 4) % 2 == 0 ? 'L' : 'R';
string expression = $"this.V{j}{destSide}.{destCoord} = Unsafe.Add(ref selfRef, {j*8+i});\r\n";
Write(expression);
}
}
PopIndent();

296
src/ImageSharp/Formats/Jpeg/Common/Block8x8F.cs

@ -7,7 +7,6 @@ using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using SixLabors.ImageSharp.Memory;
// ReSharper disable InconsistentNaming
namespace SixLabors.ImageSharp.Formats.Jpeg.Common
@ -17,16 +16,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common
/// </summary>
internal partial struct Block8x8F
{
// Most of the static methods of this struct are instance methods by actual semantics: they use Block8x8F* as their first parameter.
// Example: GetScalarAt() and SetScalarAt() are really just other (optimized) versions of the indexer.
// It's much cleaner, easier and safer to work with the code, if the methods with same semantics are next to each other.
#pragma warning disable SA1204 // StaticElementsMustAppearBeforeInstanceElements
/// <summary>
/// Vector count
/// </summary>
public const int VectorCount = 16;
/// <summary>
/// A number of scalar coefficients in a <see cref="Block8x8F"/>
/// </summary>
@ -157,36 +146,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common
return result;
}
/// <summary>
/// Pointer-based "Indexer" (getter part)
/// </summary>
/// <param name="blockPtr">Block pointer</param>
/// <param name="idx">Index</param>
/// <returns>The scaleVec value at the specified index</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static unsafe float GetScalarAt(Block8x8F* blockPtr, int idx)
{
GuardBlockIndex(idx);
float* fp = (float*)blockPtr;
return fp[idx];
}
/// <summary>
/// Pointer-based "Indexer" (setter part)
/// </summary>
/// <param name="blockPtr">Block pointer</param>
/// <param name="idx">Index</param>
/// <param name="value">Value</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static unsafe void SetScalarAt(Block8x8F* blockPtr, int idx, float value)
{
GuardBlockIndex(idx);
float* fp = (float*)blockPtr;
fp[idx] = value;
}
/// <summary>
/// Fill the block with defaults (zeroes)
/// </summary>
@ -242,7 +201,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common
/// </summary>
/// <param name="dest">Destination</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe void CopyTo(Span<float> dest)
public void CopyTo(Span<float> dest)
{
ref byte d = ref Unsafe.As<float, byte>(ref dest.DangerousGetPinnableReference());
ref byte s = ref Unsafe.As<Block8x8F, byte>(ref this);
@ -306,109 +265,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common
}
}
// [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void CopyTo(BufferArea<float> area)
{
ref byte selfBase = ref Unsafe.As<Block8x8F, byte>(ref this);
ref byte destBase = ref Unsafe.As<float, byte>(ref area.GetReferenceToOrigo());
int destStride = area.Stride * sizeof(float);
CopyRowImpl(ref selfBase, ref destBase, destStride, 0);
CopyRowImpl(ref selfBase, ref destBase, destStride, 1);
CopyRowImpl(ref selfBase, ref destBase, destStride, 2);
CopyRowImpl(ref selfBase, ref destBase, destStride, 3);
CopyRowImpl(ref selfBase, ref destBase, destStride, 4);
CopyRowImpl(ref selfBase, ref destBase, destStride, 5);
CopyRowImpl(ref selfBase, ref destBase, destStride, 6);
CopyRowImpl(ref selfBase, ref destBase, destStride, 7);
}
public void CopyTo(BufferArea<float> area, int horizontalScale, int verticalScale)
{
if (horizontalScale == 1 && verticalScale == 1)
{
this.CopyTo(area);
return;
}
else if (horizontalScale == 2 && verticalScale == 2)
{
this.CopyTo2x2(area);
return;
}
// TODO: Optimize: implement all the cases with loopless special code! (T4?)
for (int y = 0; y < 8; y++)
{
int yy = y * verticalScale;
int y8 = y * 8;
for (int x = 0; x < 8; x++)
{
int xx = x * horizontalScale;
float value = this[y8 + x];
for (int i = 0; i < verticalScale; i++)
{
for (int j = 0; j < horizontalScale; j++)
{
area[xx + j, yy + i] = value;
}
}
}
}
}
private void CopyTo2x2(BufferArea<float> area)
{
ref float destBase = ref area.GetReferenceToOrigo();
int destStride = area.Stride;
this.CopyRow2x2Impl(ref destBase, 0, destStride);
this.CopyRow2x2Impl(ref destBase, 1, destStride);
this.CopyRow2x2Impl(ref destBase, 2, destStride);
this.CopyRow2x2Impl(ref destBase, 3, destStride);
this.CopyRow2x2Impl(ref destBase, 4, destStride);
this.CopyRow2x2Impl(ref destBase, 5, destStride);
this.CopyRow2x2Impl(ref destBase, 6, destStride);
this.CopyRow2x2Impl(ref destBase, 7, destStride);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void CopyRowImpl(ref byte selfBase, ref byte destBase, int destStride, int row)
{
ref byte s = ref Unsafe.Add(ref selfBase, row * 8 * sizeof(float));
ref byte d = ref Unsafe.Add(ref destBase, row * destStride);
Unsafe.CopyBlock(ref d, ref s, 8 * sizeof(float));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void CopyRow2x2Impl(ref float destBase, int row, int destStride)
{
ref Vector4 selfLeft = ref Unsafe.Add(ref this.V0L, 2 * row);
ref Vector4 selfRight = ref Unsafe.Add(ref selfLeft, 1);
ref float destLocalOrigo = ref Unsafe.Add(ref destBase, row * 2 * destStride);
Stride2VectorCopyImpl(ref selfLeft, ref destLocalOrigo);
Stride2VectorCopyImpl(ref selfRight, ref Unsafe.Add(ref destLocalOrigo, 8));
Stride2VectorCopyImpl(ref selfLeft, ref Unsafe.Add(ref destLocalOrigo, destStride));
Stride2VectorCopyImpl(ref selfRight, ref Unsafe.Add(ref destLocalOrigo, destStride + 8));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void Stride2VectorCopyImpl(ref Vector4 s, ref float destBase)
{
Unsafe.Add(ref destBase, 0) = s.X;
Unsafe.Add(ref destBase, 1) = s.X;
Unsafe.Add(ref destBase, 2) = s.Y;
Unsafe.Add(ref destBase, 3) = s.Y;
Unsafe.Add(ref destBase, 4) = s.Z;
Unsafe.Add(ref destBase, 5) = s.Z;
Unsafe.Add(ref destBase, 6) = s.W;
Unsafe.Add(ref destBase, 7) = s.W;
}
public float[] ToArray()
{
float[] result = new float[Size];
@ -419,26 +275,50 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common
/// <summary>
/// Multiply all elements of the block.
/// </summary>
/// <param name="scaleVec">Vector to multiply by</param>
/// <param name="value">The value to multiply by</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void MultiplyAllInplace(float scaleVec)
{
this.V0L *= scaleVec;
this.V0R *= scaleVec;
this.V1L *= scaleVec;
this.V1R *= scaleVec;
this.V2L *= scaleVec;
this.V2R *= scaleVec;
this.V3L *= scaleVec;
this.V3R *= scaleVec;
this.V4L *= scaleVec;
this.V4R *= scaleVec;
this.V5L *= scaleVec;
this.V5R *= scaleVec;
this.V6L *= scaleVec;
this.V6R *= scaleVec;
this.V7L *= scaleVec;
this.V7R *= scaleVec;
public void MultiplyInplace(float value)
{
this.V0L *= value;
this.V0R *= value;
this.V1L *= value;
this.V1R *= value;
this.V2L *= value;
this.V2R *= value;
this.V3L *= value;
this.V3R *= value;
this.V4L *= value;
this.V4R *= value;
this.V5L *= value;
this.V5R *= value;
this.V6L *= value;
this.V6R *= value;
this.V7L *= value;
this.V7R *= value;
}
/// <summary>
/// Multiply all elements of the block by the corresponding elements of 'other'
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void MultiplyInplace(ref Block8x8F other)
{
this.V0L *= other.V0L;
this.V0R *= other.V0R;
this.V1L *= other.V1L;
this.V1R *= other.V1R;
this.V2L *= other.V2L;
this.V2R *= other.V2R;
this.V3L *= other.V3L;
this.V3R *= other.V3R;
this.V4L *= other.V4L;
this.V4R *= other.V4R;
this.V5L *= other.V5L;
this.V5R *= other.V5R;
this.V6L *= other.V6L;
this.V6R *= other.V6R;
this.V7L *= other.V7L;
this.V7R *= other.V7R;
}
/// <summary>
@ -472,56 +352,32 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common
/// <param name="blockPtr">Block pointer</param>
/// <param name="qtPtr">Qt pointer</param>
/// <param name="unzigPtr">Unzig pointer</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
// [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static unsafe void DequantizeBlock(Block8x8F* blockPtr, Block8x8F* qtPtr, int* unzigPtr)
{
float* b = (float*)blockPtr;
float* qtp = (float*)qtPtr;
for (int zig = 0; zig < Size; zig++)
for (int qtIndex = 0; qtIndex < Size; qtIndex++)
{
float* unzigPos = b + unzigPtr[zig];
int blockIndex = unzigPtr[qtIndex];
float* unzigPos = b + blockIndex;
float val = *unzigPos;
val *= qtp[zig];
val *= qtp[qtIndex];
*unzigPos = val;
}
}
/// <summary>
/// Level shift by +128, clip to [0, 255], and write to buffer.
/// </summary>
/// <param name="destinationBuffer">Color buffer</param>
/// <param name="stride">Stride offset</param>
/// <param name="tempBlockPtr">Temp Block pointer</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe void CopyColorsTo(Span<byte> destinationBuffer, int stride, Block8x8F* tempBlockPtr)
{
this.NormalizeColorsInto(ref *tempBlockPtr);
ref byte d = ref destinationBuffer.DangerousGetPinnableReference();
float* src = (float*)tempBlockPtr;
for (int i = 0; i < 8; i++)
{
ref byte dRow = ref Unsafe.Add(ref d, i * stride);
Unsafe.Add(ref dRow, 0) = (byte)src[0];
Unsafe.Add(ref dRow, 1) = (byte)src[1];
Unsafe.Add(ref dRow, 2) = (byte)src[2];
Unsafe.Add(ref dRow, 3) = (byte)src[3];
Unsafe.Add(ref dRow, 4) = (byte)src[4];
Unsafe.Add(ref dRow, 5) = (byte)src[5];
Unsafe.Add(ref dRow, 6) = (byte)src[6];
Unsafe.Add(ref dRow, 7) = (byte)src[7];
src += 8;
}
}
/// <summary>
/// Quantize 'block' into 'dest' using the 'qt' quantization table:
/// Unzig the elements of block into dest, while dividing them by elements of qt and "pre-rounding" the values.
/// To finish the rounding it's enough to (int)-cast these values.
/// </summary>
/// <param name="block">Source block</param>
/// <param name="dest">Destination block</param>
/// <param name="qt">The quantization table</param>
/// <param name="unzigPtr">Pointer to elements of <see cref="UnzigData"/></param>
public static unsafe void UnzigDivRound(
/// <param name="unzigPtr">Pointer to elements of <see cref="ZigZag"/></param>
public static unsafe void Quantize(
Block8x8F* block,
Block8x8F* dest,
Block8x8F* qt,
@ -610,34 +466,26 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common
return result;
}
public void RoundInplace()
/// <summary>
/// Level shift by +128, clip to [0..255], and round all the values in the block.
/// </summary>
public void NormalizeColorsAndRoundInplace()
{
if (Vector<float>.Count == 8 && Vector<int>.Count == 8)
if (SimdUtils.IsAvx2CompatibleArchitecture)
{
ref Vector<float> row0 = ref Unsafe.As<Vector4, Vector<float>>(ref this.V0L);
row0 = row0.FastRound();
ref Vector<float> row1 = ref Unsafe.As<Vector4, Vector<float>>(ref this.V1L);
row1 = row1.FastRound();
ref Vector<float> row2 = ref Unsafe.As<Vector4, Vector<float>>(ref this.V2L);
row2 = row2.FastRound();
ref Vector<float> row3 = ref Unsafe.As<Vector4, Vector<float>>(ref this.V3L);
row3 = row3.FastRound();
ref Vector<float> row4 = ref Unsafe.As<Vector4, Vector<float>>(ref this.V4L);
row4 = row4.FastRound();
ref Vector<float> row5 = ref Unsafe.As<Vector4, Vector<float>>(ref this.V5L);
row5 = row5.FastRound();
ref Vector<float> row6 = ref Unsafe.As<Vector4, Vector<float>>(ref this.V6L);
row6 = row6.FastRound();
ref Vector<float> row7 = ref Unsafe.As<Vector4, Vector<float>>(ref this.V7L);
row7 = row7.FastRound();
this.NormalizeColorsAndRoundInplaceAvx2();
}
else
{
this.RoundInplaceSlow();
this.NormalizeColorsInplace();
this.RoundInplace();
}
}
private void RoundInplaceSlow()
/// <summary>
/// Rounds all values in the block.
/// </summary>
public void RoundInplace()
{
for (int i = 0; i < Size; i++)
{
@ -663,6 +511,15 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common
return bld.ToString();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector<float> NormalizeAndRound(Vector<float> row, Vector<float> off, Vector<float> max)
{
row += off;
row = Vector.Max(row, Vector<float>.Zero);
row = Vector.Min(row, max);
return row.FastRound();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector4 DivideRound(Vector4 dividend, Vector4 divisor)
{
@ -679,10 +536,5 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common
DebugGuard.MustBeLessThan(idx, Size, nameof(idx));
DebugGuard.MustBeGreaterThanOrEqualTo(idx, 0, nameof(idx));
}
[StructLayout(LayoutKind.Explicit, Size = 8 * sizeof(float))]
private struct Row
{
}
}
}

155
src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegBlockPostProcessor.cs

@ -11,145 +11,72 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder
/// Encapsulates the implementation of processing "raw" <see cref="Buffer{T}"/>-s into Jpeg image channels.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
internal unsafe struct JpegBlockPostProcessor
internal struct JpegBlockPostProcessor
{
/// <summary>
/// The <see cref="ComputationData"/>
/// Source block
/// </summary>
private ComputationData data;
public Block8x8F SourceBlock;
/// <summary>
/// Pointers to elements of <see cref="data"/>
/// Temporal block 1 to store intermediate and/or final computation results
/// </summary>
private DataPointers pointers;
public Block8x8F WorkspaceBlock1;
/// <summary>
/// Initialize the <see cref="JpegBlockPostProcessor"/> instance on the stack.
/// Temporal block 2 to store intermediate and/or final computation results
/// </summary>
/// <param name="postProcessor">The <see cref="JpegBlockPostProcessor"/> instance</param>
public static void Init(JpegBlockPostProcessor* postProcessor)
{
postProcessor->data = ComputationData.Create();
postProcessor->pointers = new DataPointers(&postProcessor->data);
}
public void QuantizeAndTransform(IRawJpegData decoder, IJpegComponent component, ref Block8x8 sourceBlock)
{
this.data.SourceBlock = sourceBlock.AsFloatBlock();
int qtIndex = component.QuantizationTableIndex;
this.data.QuantiazationTable = decoder.QuantizationTables[qtIndex];
Block8x8F* b = this.pointers.SourceBlock;
Block8x8F.DequantizeBlock(b, this.pointers.QuantiazationTable, this.pointers.Unzig);
FastFloatingPointDCT.TransformIDCT(ref *b, ref this.data.WorkspaceBlock1, ref this.data.WorkspaceBlock2);
}
public Block8x8F WorkspaceBlock2;
public void ProcessBlockColorsInto(
IRawJpegData decoder,
IJpegComponent component,
ref Block8x8 sourceBlock,
BufferArea<float> destArea)
{
this.QuantizeAndTransform(decoder, component, ref sourceBlock);
this.data.WorkspaceBlock1.NormalizeColorsInplace();
// To conform better to libjpeg we actually NEED TO loose precision here.
// This is because they store blocks as Int16 between all the operations.
// Unfortunately, we need to emulate this to be "more accurate" :(
this.data.WorkspaceBlock1.RoundInplace();
/// <summary>
/// The quantization table as <see cref="Block8x8F"/>
/// </summary>
public Block8x8F DequantiazationTable;
Size divs = component.SubSamplingDivisors;
this.data.WorkspaceBlock1.CopyTo(destArea, divs.Width, divs.Height);
}
/// <summary>
/// Defines the horizontal and vertical scale we need to apply to the 8x8 sized block.
/// </summary>
private Size subSamplingDivisors;
/// <summary>
/// Holds the "large" data blocks needed for computations.
/// Initializes a new instance of the <see cref="JpegBlockPostProcessor"/> struct.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct ComputationData
public JpegBlockPostProcessor(IRawJpegData decoder, IJpegComponent component)
{
/// <summary>
/// Source block
/// </summary>
public Block8x8F SourceBlock;
/// <summary>
/// Temporal block 1 to store intermediate and/or final computation results
/// </summary>
public Block8x8F WorkspaceBlock1;
/// <summary>
/// Temporal block 2 to store intermediate and/or final computation results
/// </summary>
public Block8x8F WorkspaceBlock2;
/// <summary>
/// The quantization table as <see cref="Block8x8F"/>
/// </summary>
public Block8x8F QuantiazationTable;
/// <summary>
/// The jpeg unzig data
/// </summary>
public UnzigData Unzig;
int qtIndex = component.QuantizationTableIndex;
this.DequantiazationTable = ZigZag.CreateDequantizationTable(ref decoder.QuantizationTables[qtIndex]);
this.subSamplingDivisors = component.SubSamplingDivisors;
/// <summary>
/// Creates and initializes a new <see cref="ComputationData"/> instance
/// </summary>
/// <returns>The <see cref="ComputationData"/></returns>
public static ComputationData Create()
{
var data = default(ComputationData);
data.Unzig = UnzigData.Create();
return data;
}
this.SourceBlock = default(Block8x8F);
this.WorkspaceBlock1 = default(Block8x8F);
this.WorkspaceBlock2 = default(Block8x8F);
}
/// <summary>
/// Contains pointers to the memory regions of <see cref="ComputationData"/> so they can be easily passed around to pointer based utility methods of <see cref="Block8x8F"/>
/// Processes 'sourceBlock' producing Jpeg color channel values from spectral values:
/// - Dequantize
/// - Applying IDCT
/// - Level shift by +128, clip to [0, 255]
/// - Copy the resultin color values into 'destArea' scaling up the block by amount defined in <see cref="subSamplingDivisors"/>
/// </summary>
public struct DataPointers
public void ProcessBlockColorsInto(
ref Block8x8 sourceBlock,
BufferArea<float> destArea)
{
/// <summary>
/// Pointer to <see cref="ComputationData.SourceBlock"/>
/// </summary>
public Block8x8F* SourceBlock;
/// <summary>
/// Pointer to <see cref="ComputationData.WorkspaceBlock1"/>
/// </summary>
public Block8x8F* WorkspaceBlock1;
ref Block8x8F b = ref this.SourceBlock;
b.LoadFrom(ref sourceBlock);
/// <summary>
/// Pointer to <see cref="ComputationData.WorkspaceBlock2"/>
/// </summary>
public Block8x8F* WorkspaceBlock2;
// Dequantize:
b.MultiplyInplace(ref this.DequantiazationTable);
/// <summary>
/// Pointer to <see cref="ComputationData.QuantiazationTable"/>
/// </summary>
public Block8x8F* QuantiazationTable;
FastFloatingPointDCT.TransformIDCT(ref b, ref this.WorkspaceBlock1, ref this.WorkspaceBlock2);
/// <summary>
/// Pointer to <see cref="ComputationData.Unzig"/> as int*
/// </summary>
public int* Unzig;
// 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!
this.WorkspaceBlock1.NormalizeColorsAndRoundInplace();
/// <summary>
/// Initializes a new instance of the <see cref="DataPointers" /> struct.
/// </summary>
/// <param name="dataPtr">Pointer to <see cref="ComputationData"/></param>
internal DataPointers(ComputationData* dataPtr)
{
this.SourceBlock = &dataPtr->SourceBlock;
this.WorkspaceBlock1 = &dataPtr->WorkspaceBlock1;
this.WorkspaceBlock2 = &dataPtr->WorkspaceBlock2;
this.QuantiazationTable = &dataPtr->QuantiazationTable;
this.Unzig = dataPtr->Unzig.Data;
}
this.WorkspaceBlock1.CopyTo(destArea, this.subSamplingDivisors.Width, this.subSamplingDivisors.Height);
}
}
}

70
src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegColorConverter.FromYCbCr.cs

@ -1,6 +1,7 @@
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Common.Tuples;
namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder
{
@ -121,9 +122,25 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder
tmp.MultiplyInplace(1.772F);
b.AddInplace(ref tmp);
r.RoundAndDownscale();
g.RoundAndDownscale();
b.RoundAndDownscale();
if (Vector<float>.Count == 4)
{
// TODO: Find a way to properly run & test this path on AVX2 PC-s! (Have I already mentioned that Vector<T> is terrible?)
r.RoundAndDownscalePreAvx2();
g.RoundAndDownscalePreAvx2();
b.RoundAndDownscalePreAvx2();
}
else if (SimdUtils.IsAvx2CompatibleArchitecture)
{
r.RoundAndDownscaleAvx2();
g.RoundAndDownscaleAvx2();
b.RoundAndDownscaleAvx2();
}
else
{
// TODO: Run fallback scalar code here
// However, no issues expected before someone implements this: https://github.com/dotnet/coreclr/issues/12007
throw new NotImplementedException("Your CPU architecture is too modern!");
}
// Collect (r0,r1...r8) (g0,g1...g8) (b0,b1...b8) vector values in the expected (r0,g0,g1,1), (r1,g1,g2,1) ... order:
ref Vector4Octet destination = ref Unsafe.Add(ref resultBase, i);
@ -131,53 +148,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder
}
}
/// <summary>
/// Its faster to process multiple Vector4-s together
/// </summary>
private struct Vector4Pair
{
public Vector4 A;
public Vector4 B;
private static readonly Vector4 Scale = new Vector4(1 / 255f);
private static readonly Vector4 Half = new Vector4(0.5f);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RoundAndDownscale()
{
// Emulate rounding:
this.A += Half;
this.B += Half;
// Downscale by 1/255
this.A *= Scale;
this.B *= Scale;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void MultiplyInplace(float value)
{
this.A *= value;
this.B *= value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddInplace(Vector4 value)
{
this.A += value;
this.B += value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddInplace(ref Vector4Pair other)
{
this.A += other.A;
this.B += other.B;
}
}
private struct Vector4Octet
{
#pragma warning disable SA1132 // Do not combine fields

7
src/ImageSharp/Formats/Jpeg/Common/Decoder/JpegComponentPostProcessor.cs

@ -66,10 +66,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder
/// <summary>
/// Invoke <see cref="JpegBlockPostProcessor"/> for <see cref="BlockRowsPerStep"/> block rows, copy the result into <see cref="ColorBuffer"/>.
/// </summary>
public unsafe void CopyBlocksToColorBuffer()
public void CopyBlocksToColorBuffer()
{
var blockPp = default(JpegBlockPostProcessor);
JpegBlockPostProcessor.Init(&blockPp);
var blockPp = new JpegBlockPostProcessor(this.ImagePostProcessor.RawJpeg, this.Component);
for (int y = 0; y < this.BlockRowsPerStep; y++)
{
@ -95,7 +94,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder
this.blockAreaSize.Width,
this.blockAreaSize.Height);
blockPp.ProcessBlockColorsInto(this.ImagePostProcessor.RawJpeg, this.Component, ref block, destArea);
blockPp.ProcessBlockColorsInto(ref block, destArea);
}
}

7
src/ImageSharp/Formats/Jpeg/Common/FastFloatingPointDCT.cs

@ -50,7 +50,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common
/// <param name="temp">Temporary block provided by the caller</param>
public static void TransformIDCT(ref Block8x8F src, ref Block8x8F dest, ref Block8x8F temp)
{
// TODO: Transpose is a bottleneck now. We need full AVX support to optimize it:
// https://github.com/dotnet/corefx/issues/22940
src.TransposeInto(ref temp);
IDCT8x4_LeftPart(ref temp, ref dest);
IDCT8x4_RightPart(ref temp, ref dest);
@ -60,7 +63,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common
IDCT8x4_RightPart(ref temp, ref dest);
// TODO: What if we leave the blocks in a scaled-by-x8 state until final color packing?
dest.MultiplyAllInplace(C_0_125);
dest.MultiplyInplace(C_0_125);
}
/// <summary>
@ -334,7 +337,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common
FDCT8x4_LeftPart(ref temp, ref dest);
FDCT8x4_RightPart(ref temp, ref dest);
dest.MultiplyAllInplace(C_0_125);
dest.MultiplyInplace(C_0_125);
}
}
}

24
src/ImageSharp/Formats/Jpeg/Common/UnzigData.cs → src/ImageSharp/Formats/Jpeg/Common/ZigZag.cs

@ -6,13 +6,12 @@ using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.Formats.Jpeg.Common
{
/// <summary>
/// TODO: This should be simply just a <see cref="Block8x8"/>!
/// Holds the Jpeg UnZig array in a value/stack type.
/// Unzig maps from the zigzag ordering to the natural ordering. For example,
/// unzig[3] is the column and row of the fourth element in zigzag order. The
/// value is 16, which means first column (16%8 == 0) and third row (16/8 == 2).
/// </summary>
internal unsafe struct UnzigData
internal unsafe struct ZigZag
{
/// <summary>
/// Copy of <see cref="Unzig"/> in a value type
@ -33,15 +32,30 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common
};
/// <summary>
/// Creates and fills an instance of <see cref="UnzigData"/> with Jpeg unzig indices
/// Creates and fills an instance of <see cref="ZigZag"/> with Jpeg unzig indices
/// </summary>
/// <returns>The new instance</returns>
public static UnzigData Create()
public static ZigZag CreateUnzigTable()
{
UnzigData result = default(UnzigData);
ZigZag result = default(ZigZag);
int* unzigPtr = result.Data;
Marshal.Copy(Unzig, 0, (IntPtr)unzigPtr, 64);
return result;
}
/// <summary>
/// Apply Zigging to the given quantization table, so it will be sufficient to multiply blocks for dequantizing them.
/// </summary>
public static Block8x8F CreateDequantizationTable(ref Block8x8F qt)
{
Block8x8F result = default(Block8x8F);
for (int i = 0; i < 64; i++)
{
result[Unzig[i]] = qt[i];
}
return result;
}
}
}

4
src/ImageSharp/Formats/Jpeg/GolangPort/Components/Decoder/OrigJpegScanDecoder.ComputationData.cs

@ -26,7 +26,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort.Components.Decoder
/// <summary>
/// The jpeg unzig data
/// </summary>
public UnzigData Unzig;
public ZigZag Unzig;
/// <summary>
/// The buffer storing the <see cref="OrigComponentScan"/>-s for each component
@ -45,7 +45,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort.Components.Decoder
public static ComputationData Create()
{
ComputationData data = default(ComputationData);
data.Unzig = UnzigData.Create();
data.Unzig = ZigZag.CreateUnzigTable();
return data;
}
}

6
src/ImageSharp/Formats/Jpeg/GolangPort/JpegEncoderCore.cs

@ -455,7 +455,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort
Block8x8F onStackLuminanceQuantTable = this.luminanceQuantTable;
Block8x8F onStackChrominanceQuantTable = this.chrominanceQuantTable;
UnzigData unzig = UnzigData.Create();
ZigZag unzig = ZigZag.CreateUnzigTable();
// ReSharper disable once InconsistentNaming
int prevDCY = 0, prevDCCb = 0, prevDCCr = 0;
@ -564,7 +564,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort
{
FastFloatingPointDCT.TransformFDCT(ref *src, ref *tempDest1, ref *tempDest2);
Block8x8F.UnzigDivRound(tempDest1, tempDest2, quant, unzigPtr);
Block8x8F.Quantize(tempDest1, tempDest2, quant, unzigPtr);
float* unziggedDestPtr = (float*)tempDest2;
int dc = (int)unziggedDestPtr[0];
@ -912,7 +912,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort
Block8x8F onStackLuminanceQuantTable = this.luminanceQuantTable;
Block8x8F onStackChrominanceQuantTable = this.chrominanceQuantTable;
UnzigData unzig = UnzigData.Create();
ZigZag unzig = ZigZag.CreateUnzigTable();
// ReSharper disable once InconsistentNaming
int prevDCY = 0, prevDCCb = 0, prevDCCr = 0;

10
tests/ImageSharp.Tests/Common/SimdUtilsTests.cs

@ -8,6 +8,8 @@ namespace SixLabors.ImageSharp.Tests.Common
using System.Linq;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Common.Tuples;
using Xunit.Abstractions;
using Xunit.Sdk;
@ -243,15 +245,15 @@ namespace SixLabors.ImageSharp.Tests.Common
x = (x * scale) + magick;
SimdUtils.Octet.OfUInt32 ii = default(SimdUtils.Octet.OfUInt32);
Tuple8.OfUInt32 ii = default(Tuple8.OfUInt32);
ref Vector<float> iiRef = ref Unsafe.As<SimdUtils.Octet.OfUInt32, Vector<float>>(ref ii);
ref Vector<float> iiRef = ref Unsafe.As<Tuple8.OfUInt32, Vector<float>>(ref ii);
iiRef = x;
//SimdUtils.Octet.OfUInt32 ii = Unsafe.As<Vector<float>, SimdUtils.Octet.OfUInt32>(ref x);
//Tuple8.OfUInt32 ii = Unsafe.As<Vector<float>, Tuple8.OfUInt32>(ref x);
ref SimdUtils.Octet.OfByte d = ref dest.NonPortableCast<byte, SimdUtils.Octet.OfByte>()[0];
ref Tuple8.OfByte d = ref dest.NonPortableCast<byte, Tuple8.OfByte>()[0];
d.LoadFrom(ref ii);
this.Output.WriteLine(ii.ToString());

189
tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs

@ -32,32 +32,18 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
{
}
[Fact]
public void Indexer()
private bool SkipOnNonAvx2Runner()
{
float sum = 0;
this.Measure(
Times,
() =>
{
var block = new Block8x8F();
for (int i = 0; i < Block8x8F.Size; i++)
{
block[i] = i;
}
sum = 0;
for (int i = 0; i < Block8x8F.Size; i++)
{
sum += block[i];
}
});
Assert.Equal(sum, 64f * 63f * 0.5f);
if (!SimdUtils.IsAvx2CompatibleArchitecture)
{
this.Output.WriteLine("AVX2 not supported, skipping!");
return true;
}
return false;
}
[Fact]
public unsafe void Indexer_GetScalarAt_SetScalarAt()
public void Indexer()
{
float sum = 0;
this.Measure(
@ -68,18 +54,18 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
for (int i = 0; i < Block8x8F.Size; i++)
{
Block8x8F.SetScalarAt(&block, i, i);
block[i] = i;
}
sum = 0;
for (int i = 0; i < Block8x8F.Size; i++)
{
sum += Block8x8F.GetScalarAt(&block, i);
sum += block[i];
}
});
Assert.Equal(sum, 64f * 63f * 0.5f);
}
[Fact]
public void Indexer_ReferenceBenchmarkWithArray()
{
@ -222,33 +208,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
this.Output.WriteLine($"TranposeInto_PinningImpl_Benchmark finished in {sw.ElapsedMilliseconds} ms");
}
[Fact]
public unsafe void CopyColorsTo()
{
float[] data = Create8x8FloatData();
var block = new Block8x8F();
block.LoadFrom(data);
block.MultiplyAllInplace(5);
int stride = 256;
int height = 42;
int offset = height * 10 + 20;
byte[] colorsExpected = new byte[stride * height];
byte[] colorsActual = new byte[stride * height];
var temp = new Block8x8F();
ReferenceImplementations.CopyColorsTo(ref block, new Span<byte>(colorsExpected, offset), stride);
block.CopyColorsTo(new Span<byte>(colorsActual, offset), stride, &temp);
// Output.WriteLine("******* EXPECTED: *********");
// PrintLinearData(colorsExpected);
// Output.WriteLine("******** ACTUAL: **********");
Assert.Equal(colorsExpected, colorsActual);
}
private static float[] Create8x8ColorCropTestData()
{
float[] result = new float[64];
@ -263,30 +222,18 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
return result;
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void NormalizeColors(bool inplace)
[Fact]
public void NormalizeColors()
{
var block = default(Block8x8F);
float[] input = Create8x8ColorCropTestData();
block.LoadFrom(input);
this.Output.WriteLine("Input:");
this.PrintLinearData(input);
var dest = default(Block8x8F);
if (inplace)
{
dest = block;
dest.NormalizeColorsInplace();
}
else
{
block.NormalizeColorsInto(ref dest);
}
Block8x8F dest = block;
dest.NormalizeColorsInplace();
float[] array = new float[64];
dest.CopyTo(array);
this.Output.WriteLine("Result:");
@ -297,11 +244,35 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
}
}
[Theory]
[InlineData(1)]
[InlineData(2)]
public void NormalizeColorsAndRoundAvx2(int seed)
{
if (this.SkipOnNonAvx2Runner())
{
return;
}
Block8x8F source = CreateRandomFloatBlock(-200, 200, seed);
Block8x8F expected = source;
expected.NormalizeColorsInplace();
expected.RoundInplace();
Block8x8F actual = source;
actual.NormalizeColorsAndRoundInplaceAvx2();
this.Output.WriteLine(expected.ToString());
this.Output.WriteLine(actual.ToString());
this.CompareBlocks(expected, actual, 0);
}
[Theory]
[InlineData(1)]
[InlineData(2)]
public unsafe void UnzigDivRound(int seed)
public unsafe void Quantize(int seed)
{
var block = new Block8x8F();
block.LoadFrom(Create8x8RoundedRandomFloatData(-2000, 2000, seed));
@ -309,14 +280,14 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
var qt = new Block8x8F();
qt.LoadFrom(Create8x8RoundedRandomFloatData(-2000, 2000, seed));
var unzig = UnzigData.Create();
var unzig = ZigZag.CreateUnzigTable();
int* expectedResults = stackalloc int[Block8x8F.Size];
ReferenceImplementations.UnZigDivRoundRational(&block, expectedResults, &qt, unzig.Data);
ReferenceImplementations.QuantizeRational(&block, expectedResults, &qt, unzig.Data);
var actualResults = default(Block8x8F);
Block8x8F.UnzigDivRound(&block, &actualResults, &qt, unzig.Data);
Block8x8F.Quantize(&block, &actualResults, &qt, unzig.Data);
for (int i = 0; i < Block8x8F.Size; i++)
{
@ -352,7 +323,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
public void RoundInplace(int seed)
public void RoundInplaceSlow(int seed)
{
Block8x8F s = CreateRandomFloatBlock(-500, 500, seed);
@ -370,5 +341,77 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
Assert.Equal(expected, actual);
}
}
[Fact]
public void MultiplyInplace_ByOtherBlock()
{
Block8x8F original = CreateRandomFloatBlock(-500, 500, 42);
Block8x8F m = CreateRandomFloatBlock(-500, 500, 42);
Block8x8F actual = original;
actual.MultiplyInplace(ref m);
for (int i = 0; i < Block8x8F.Size; i++)
{
Assert.Equal(original[i]*m[i], actual[i]);
}
}
[Theory]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
public unsafe void DequantizeBlock(int seed)
{
Block8x8F original = CreateRandomFloatBlock(-500, 500, seed);
Block8x8F qt = CreateRandomFloatBlock(0, 10, seed + 42);
var unzig = ZigZag.CreateUnzigTable();
Block8x8F expected = original;
Block8x8F actual = original;
ReferenceImplementations.DequantizeBlock(&expected, &qt, unzig.Data);
Block8x8F.DequantizeBlock(&actual, &qt, unzig.Data);
this.CompareBlocks(expected, actual, 0);
}
[Theory]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
public unsafe void ZigZag_CreateDequantizationTable_MultiplicationShouldQuantize(int seed)
{
Block8x8F original = CreateRandomFloatBlock(-500, 500, seed);
Block8x8F qt = CreateRandomFloatBlock(0, 10, seed + 42);
var unzig = ZigZag.CreateUnzigTable();
Block8x8F zigQt = ZigZag.CreateDequantizationTable(ref qt);
Block8x8F expected = original;
Block8x8F actual = original;
ReferenceImplementations.DequantizeBlock(&expected, &qt, unzig.Data);
actual.MultiplyInplace(ref zigQt);
this.CompareBlocks(expected, actual, 0);
}
[Fact]
public void MultiplyInplace_ByScalar()
{
Block8x8F original = CreateRandomFloatBlock(-500, 500);
Block8x8F actual = original;
actual.MultiplyInplace(42f);
for (int i = 0; i < 64; i++)
{
Assert.Equal(original[i]*42f, actual[i]);
}
}
}
}

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

@ -14,7 +14,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
{
public class JpegColorConverterTests
{
private const float Precision = 1/255f;
private const float Precision = 0.1f / 255;
public static readonly TheoryData<int, int, int> CommonConversionData =
new TheoryData<int, int, int>

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

@ -9,6 +9,7 @@ using System;
namespace SixLabors.ImageSharp.Tests.Formats.Jpg
{
using System.Collections.Generic;
using System.IO;
using System.Linq;
@ -24,6 +25,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
using Xunit;
using Xunit.Abstractions;
// TODO: Scatter test cases into multiple test classes
public class JpegDecoderTests
{
public static string[] BaselineTestJpegs =
@ -50,16 +52,43 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
TestImages.Jpeg.Issues.MissingFF00ProgressiveGirl159,
};
private static readonly Dictionary<string, float> CustomToleranceValues = new Dictionary<string, float>
{
// Baseline:
[TestImages.Jpeg.Baseline.Calliphora] = 0.00002f / 100,
[TestImages.Jpeg.Baseline.Bad.ExifUndefType] = 0.011f / 100,
[TestImages.Jpeg.Baseline.Bad.BadEOF] = 0.38f / 100,
[TestImages.Jpeg.Baseline.Testorig420] = 0.38f / 100,
// Progressive:
[TestImages.Jpeg.Issues.MissingFF00ProgressiveGirl159] = 0.34f / 100,
[TestImages.Jpeg.Issues.BadCoeffsProgressive178] = 0.38f / 100,
[TestImages.Jpeg.Progressive.Bad.BadEOF] = 0.3f / 100,
[TestImages.Jpeg.Progressive.Festzug] = 0.02f / 100,
[TestImages.Jpeg.Progressive.Fb] = 0.16f / 100,
[TestImages.Jpeg.Progressive.Progress] = 0.31f / 100,
};
public const PixelTypes CommonNonDefaultPixelTypes = PixelTypes.Rgba32 | PixelTypes.Argb32 | PixelTypes.RgbaVector;
// TODO: We should make this comparer less tolerant ...
private static readonly ImageComparer VeryTolerantJpegComparer =
ImageComparer.Tolerant(0.005f, perPixelManhattanThreshold: 4);
private const float BaselineTolerance_Orig = 0.001f / 100;
private const float BaselineTolerance_PdfJs = 0.005f;
// BUG: PDF.js output is wrong on spectral level!
private static readonly ImageComparer PdfJsProgressiveComparer =
ImageComparer.Tolerant(0.015f, perPixelManhattanThreshold: 4);
private const float ProgressiveTolerance_Orig = 0.2f / 100;
private const float ProgressiveTolerance_PdfJs = 1.5f / 100; // PDF.js Progressive output is wrong on spectral level!
private ImageComparer GetImageComparerForOrigDecoder<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
string file = provider.SourceFileOrDescription;
if (!CustomToleranceValues.TryGetValue(file, out float tolerance))
{
tolerance = file.ToLower().Contains("baseline") ? BaselineTolerance_Orig : ProgressiveTolerance_Orig;
}
return ImageComparer.Tolerant(tolerance);
}
public JpegDecoderTests(ITestOutputHelper output)
{
@ -71,7 +100,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
private static IImageDecoder OrigJpegDecoder => new OrigJpegDecoder();
private static IImageDecoder PdfJsJpegDecoder => new PdfJsJpegDecoder();
[Fact]
public void ParseStream_BasicPropertiesAreCorrect1_PdfJs()
{
@ -84,50 +113,56 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
VerifyJpeg.VerifyComponentSizes3(decoder.Frame.Components, 43, 61, 22, 31, 22, 31);
}
}
public const string DecodeBaselineJpegOutputName = "DecodeBaselineJpeg";
[Theory]
[WithFileCollection(nameof(BaselineTestJpegs), PixelTypes.Rgba32)]
public void DecodeBaselineJpeg_PdfJs<TPixel>(TestImageProvider<TPixel> provider)
[WithFile(TestImages.Jpeg.Baseline.Calliphora, CommonNonDefaultPixelTypes, false)]
[WithFile(TestImages.Jpeg.Baseline.Calliphora, CommonNonDefaultPixelTypes, true)]
public void JpegDecoder_IsNotBoundToSinglePixelType<TPixel>(TestImageProvider<TPixel> provider, bool useOldDecoder)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage(PdfJsJpegDecoder))
IImageDecoder decoder = useOldDecoder ? OrigJpegDecoder : PdfJsJpegDecoder;
using (Image<TPixel> image = provider.GetImage(decoder))
{
image.DebugSave(provider);
provider.Utility.TestName = DecodeBaselineJpegOutputName;
image.CompareToReferenceOutput(provider, VeryTolerantJpegComparer, appendPixelTypeToFileName: false);
image.CompareToReferenceOutput(provider, ImageComparer.Tolerant(BaselineTolerance_PdfJs), appendPixelTypeToFileName: false);
}
}
[Theory]
[WithFile(TestImages.Jpeg.Baseline.Calliphora, CommonNonDefaultPixelTypes, false)]
[WithFile(TestImages.Jpeg.Baseline.Calliphora, CommonNonDefaultPixelTypes, true)]
public void JpegDecoder_IsNotBoundToSinglePixelType<TPixel>(TestImageProvider<TPixel> provider, bool useOldDecoder)
[WithFileCollection(nameof(BaselineTestJpegs), PixelTypes.Rgba32)]
public void DecodeBaselineJpeg_Orig<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
IImageDecoder decoder = useOldDecoder ? OrigJpegDecoder : PdfJsJpegDecoder;
using (Image<TPixel> image = provider.GetImage(decoder))
using (Image<TPixel> image = provider.GetImage(OrigJpegDecoder))
{
image.DebugSave(provider);
provider.Utility.TestName = DecodeBaselineJpegOutputName;
image.CompareToReferenceOutput(provider, VeryTolerantJpegComparer, appendPixelTypeToFileName: false);
image.CompareToReferenceOutput(
provider,
this.GetImageComparerForOrigDecoder(provider),
appendPixelTypeToFileName: false);
}
}
[Theory]
[WithFileCollection(nameof(BaselineTestJpegs), PixelTypes.Rgba32)]
public void DecodeBaselineJpeg_Orig<TPixel>(TestImageProvider<TPixel> provider)
public void DecodeBaselineJpeg_PdfJs<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage(OrigJpegDecoder))
using (Image<TPixel> image = provider.GetImage(PdfJsJpegDecoder))
{
image.DebugSave(provider);
provider.Utility.TestName = DecodeBaselineJpegOutputName;
image.CompareToReferenceOutput(provider, VeryTolerantJpegComparer, appendPixelTypeToFileName: false);
image.CompareToReferenceOutput(
provider,
ImageComparer.Tolerant(BaselineTolerance_PdfJs),
appendPixelTypeToFileName: false);
}
}
@ -144,37 +179,43 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
[Theory]
[WithFileCollection(nameof(ProgressiveTestJpegs), PixelTypes.Rgba32)]
public void DecodeProgressiveJpeg_PdfJs<TPixel>(TestImageProvider<TPixel> provider)
public void DecodeProgressiveJpeg_Orig<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage(PdfJsJpegDecoder))
using (Image<TPixel> image = provider.GetImage(OrigJpegDecoder))
{
image.DebugSave(provider);
provider.Utility.TestName = DecodeProgressiveJpegOutputName;
image.CompareToReferenceOutput(provider, PdfJsProgressiveComparer, appendPixelTypeToFileName: false);
image.CompareToReferenceOutput(
provider,
this.GetImageComparerForOrigDecoder(provider),
appendPixelTypeToFileName: false);
}
}
[Theory]
[WithFileCollection(nameof(ProgressiveTestJpegs), PixelTypes.Rgba32)]
public void DecodeProgressiveJpeg_Orig<TPixel>(TestImageProvider<TPixel> provider)
public void DecodeProgressiveJpeg_PdfJs<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage(OrigJpegDecoder))
using (Image<TPixel> image = provider.GetImage(PdfJsJpegDecoder))
{
image.DebugSave(provider);
provider.Utility.TestName = DecodeProgressiveJpegOutputName;
image.CompareToReferenceOutput(provider, VeryTolerantJpegComparer, appendPixelTypeToFileName: false);
image.CompareToReferenceOutput(
provider,
ImageComparer.Tolerant(ProgressiveTolerance_PdfJs),
appendPixelTypeToFileName: false);
}
}
private float GetDifferenceInPercents<TPixel>(Image<TPixel> image, TestImageProvider<TPixel> provider)
private string GetDifferenceInPercentageString<TPixel>(Image<TPixel> image, TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
var reportingComparer = ImageComparer.Tolerant(0, 0);
ImageSimilarityReport report = image.GetReferenceOutputSimilarityReports(
provider,
reportingComparer,
@ -183,10 +224,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
if (report != null && report.TotalNormalizedDifference.HasValue)
{
return report.TotalNormalizedDifference.Value * 100;
return report.DifferencePercentageString;
}
return 0;
return "0%";
}
private void CompareJpegDecodersImpl<TPixel>(TestImageProvider<TPixel> provider, string testName)
@ -202,14 +243,15 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
using (Image<TPixel> image = provider.GetImage(OrigJpegDecoder))
{
double d = this.GetDifferenceInPercents(image, provider);
this.Output.WriteLine($"Difference using ORIGINAL decoder: {d:0.0000}%");
string d = this.GetDifferenceInPercentageString(image, provider);
this.Output.WriteLine($"Difference using ORIGINAL decoder: {d}");
}
using (Image<TPixel> image = provider.GetImage(PdfJsJpegDecoder))
{
double d = this.GetDifferenceInPercents(image, provider);
this.Output.WriteLine($"Difference using PDFJS decoder: {d:0.0000}%");
string d = this.GetDifferenceInPercentageString(image, provider);
this.Output.WriteLine($"Difference using PDFJS decoder: {d}");
}
}
@ -228,7 +270,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
{
this.CompareJpegDecodersImpl(provider, DecodeProgressiveJpegOutputName);
}
[Theory]
[WithSolidFilledImages(16, 16, 255, 0, 0, PixelTypes.Rgba32, JpegSubsample.Ratio420, 75)]
[WithSolidFilledImages(16, 16, 255, 0, 0, PixelTypes.Rgba32, JpegSubsample.Ratio420, 100)]
@ -256,7 +298,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
var mirror = Image.Load<TPixel>(data, OrigJpegDecoder);
mirror.DebugSave(provider, $"_{subsample}_Q{quality}");
}
[Fact]
public void Decoder_Reads_Correct_Resolution_From_Jfif()
{
@ -314,7 +356,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
// into "\tests\Images\ActualOutput\JpegDecoderTests\"
//[Theory]
//[WithFile(TestImages.Jpeg.Progressive.Progress, PixelTypes.Rgba32, "PdfJsOriginal_progress.png")]
public void ValidateProgressivePdfJsOutput<TPixel>(TestImageProvider<TPixel> provider,
public void ValidateProgressivePdfJsOutput<TPixel>(TestImageProvider<TPixel> provider,
string pdfJsOriginalResultImage)
where TPixel : struct, IPixel<TPixel>
{
@ -335,7 +377,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
{
ImageSimilarityReport originalReport = comparer.CompareImagesOrFrames(expectedImage, pdfJsOriginalResult);
ImageSimilarityReport portReport = comparer.CompareImagesOrFrames(expectedImage, pdfJsPortResult);
this.Output.WriteLine($"Difference for PDF.js ORIGINAL: {originalReport.DifferencePercentageString}");
this.Output.WriteLine($"Difference for PORT: {portReport.DifferencePercentageString}");
}

4
tests/ImageSharp.Tests/Formats/Jpg/JpegProfilingBenchmarks.cs

@ -34,8 +34,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
TestImages.Jpeg.Baseline.Jpeg444,
};
// [Theory] // Benchmark, enable manually
// [MemberData(nameof(DecodeJpegData))]
//[Theory] // Benchmark, enable manually
//[MemberData(nameof(DecodeJpegData))]
public void DecodeJpeg_Original(string fileName)
{
this.DecodeJpegBenchmarkImpl(fileName, new OrigJpegDecoder());

21
tests/ImageSharp.Tests/Formats/Jpg/Utils/ReferenceImplementations.cs

@ -19,6 +19,21 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg.Utils
/// </summary>
internal static partial class ReferenceImplementations
{
public static unsafe void DequantizeBlock(Block8x8F* blockPtr, Block8x8F* qtPtr, int* unzigPtr)
{
float* b = (float*)blockPtr;
float* qtp = (float*)qtPtr;
for (int qtIndex = 0; qtIndex < Block8x8F.Size; qtIndex++)
{
int i = unzigPtr[qtIndex];
float* unzigPos = b + i;
float val = *unzigPos;
val *= qtp[qtIndex];
*unzigPos = val;
}
}
/// <summary>
/// Transpose 8x8 block stored linearly in a <see cref="Span{T}"/> (inplace)
/// </summary>
@ -93,14 +108,14 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg.Utils
}
/// <summary>
/// Reference implementation to test <see cref="Block8x8F.UnzigDivRound"/>.
/// Reference implementation to test <see cref="Block8x8F.Quantize"/>.
/// Rounding is done used an integer-based algorithm defined in <see cref="RationalRound(int,int)"/>.
/// </summary>
/// <param name="src">The input block</param>
/// <param name="dest">The destination block of integers</param>
/// <param name="qt">The quantization table</param>
/// <param name="unzigPtr">Pointer to <see cref="UnzigData.Data"/> </param>
public static unsafe void UnZigDivRoundRational(Block8x8F* src, int* dest, Block8x8F* qt, int* unzigPtr)
/// <param name="unzigPtr">Pointer to <see cref="ZigZag.Data"/> </param>
public static unsafe void QuantizeRational(Block8x8F* src, int* dest, Block8x8F* qt, int* unzigPtr)
{
float* s = (float*)src;
float* q = (float*)qt;

2
tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs

@ -44,7 +44,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
{
SizeF newSize = image.Size() * ratio;
image.Mutate(x => x.Resize((Size)newSize, sampler, false));
string details = $"{name}-{ratio}";
string details = $"{name}-{ratio.ToString(System.Globalization.CultureInfo.InvariantCulture)}";
image.DebugSave(provider, details);
image.CompareToReferenceOutput(provider, details);

2
tests/ImageSharp.Tests/TestUtilities/ApproximateFloatComparer.cs

@ -20,7 +20,7 @@ namespace SixLabors.ImageSharp.Tests
{
float d = x - y;
return d > -this.Eps && d < this.Eps;
return d >= -this.Eps && d <= this.Eps;
}
public int GetHashCode(float obj)

21
tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageSimilarityReport.cs

@ -26,9 +26,24 @@
// TODO: This should not be a nullable value!
public float? TotalNormalizedDifference { get; }
public string DifferencePercentageString => this.TotalNormalizedDifference.HasValue
? $"{this.TotalNormalizedDifference.Value * 100:0.0000}%"
: "?";
public string DifferencePercentageString
{
get
{
if (!this.TotalNormalizedDifference.HasValue)
{
return "?";
}
else if (this.TotalNormalizedDifference == 0)
{
return "0%";
}
else
{
return $"{this.TotalNormalizedDifference.Value * 100:0.0000}%";
}
}
}
public PixelDifference[] Differences { get; }

13
tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs

@ -105,15 +105,20 @@ namespace SixLabors.ImageSharp.Tests
private static readonly ConcurrentDictionary<Key, Image<TPixel>> cache = new ConcurrentDictionary<Key, Image<TPixel>>();
public FileProvider(string filePath)
// Needed for deserialization!
// ReSharper disable once UnusedMember.Local
public FileProvider()
{
this.FilePath = filePath;
}
public FileProvider()
public FileProvider(string filePath)
{
this.FilePath = filePath;
}
/// <summary>
/// Gets the file path relative to the "~/tests/images" folder
/// </summary>
public string FilePath { get; private set; }
public override string SourceFileOrDescription => this.FilePath;

10
tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs

@ -89,6 +89,8 @@ namespace SixLabors.ImageSharp.Tests
return $"{this.GetTestOutputDir()}/{this.TestName}{pixName}{fn}{details}{extension}";
}
private static string Inv(FormattableString formattable) => System.FormattableString.Invariant(formattable);
/// <summary>
/// Gets the recommended file name for the output of the test
/// </summary>
@ -111,13 +113,17 @@ namespace SixLabors.ImageSharp.Tests
TypeInfo info = type.GetTypeInfo();
if (info.IsPrimitive || info.IsEnum || type == typeof(decimal))
{
detailsString = testOutputDetails.ToString();
detailsString = Inv($"{testOutputDetails}");
}
else
{
IEnumerable<PropertyInfo> properties = testOutputDetails.GetType().GetRuntimeProperties();
detailsString = String.Join("_", properties.ToDictionary(x => x.Name, x => x.GetValue(testOutputDetails)).Select(x => $"{x.Key}-{x.Value}"));
detailsString = string.Join(
"_",
properties.ToDictionary(x => x.Name, x => x.GetValue(testOutputDetails))
.Select(x => Inv($"{x.Key}-{x.Value}"))
);
}
}
return this.GetTestOutputFileNameImpl(extension, detailsString, appendPixelTypeToFileName);

Loading…
Cancel
Save