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 internal static class SimdUtils
{ {
/// <summary> /// <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> /// </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) 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> /// <summary>
/// Convert into <see cref="Block8x8F"/> /// Convert to <see cref="Block8x8F"/>
/// </summary> /// </summary>
public Block8x8F AsFloatBlock() public Block8x8F AsFloatBlock()
{ {
// TODO: Optimize this
var result = default(Block8x8F); var result = default(Block8x8F);
for (int i = 0; i < Size; i++) result.LoadFrom(ref this);
{
result[i] = this[i];
}
return result; 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> /// <summary>
/// Level shift by +128, clip to [0, 255] /// Level shift by +128, clip to [0, 255]
/// </summary> /// </summary>
/// <param name="d">The destination block</param> public void NormalizeColorsInplace()
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void NormalizeColorsInto(ref Block8x8F d)
{ {
d.V0L = Vector4.Clamp(V0L + COff4, CMin4, CMax4); this.V0L = Vector4.Clamp(this.V0L + COff4, CMin4, CMax4);
d.V0R = Vector4.Clamp(V0R + COff4, CMin4, CMax4); this.V0R = Vector4.Clamp(this.V0R + COff4, CMin4, CMax4);
d.V1L = Vector4.Clamp(V1L + COff4, CMin4, CMax4); this.V1L = Vector4.Clamp(this.V1L + COff4, CMin4, CMax4);
d.V1R = Vector4.Clamp(V1R + COff4, CMin4, CMax4); this.V1R = Vector4.Clamp(this.V1R + COff4, CMin4, CMax4);
d.V2L = Vector4.Clamp(V2L + COff4, CMin4, CMax4); this.V2L = Vector4.Clamp(this.V2L + COff4, CMin4, CMax4);
d.V2R = Vector4.Clamp(V2R + COff4, CMin4, CMax4); this.V2R = Vector4.Clamp(this.V2R + COff4, CMin4, CMax4);
d.V3L = Vector4.Clamp(V3L + COff4, CMin4, CMax4); this.V3L = Vector4.Clamp(this.V3L + COff4, CMin4, CMax4);
d.V3R = Vector4.Clamp(V3R + COff4, CMin4, CMax4); this.V3R = Vector4.Clamp(this.V3R + COff4, CMin4, CMax4);
d.V4L = Vector4.Clamp(V4L + COff4, CMin4, CMax4); this.V4L = Vector4.Clamp(this.V4L + COff4, CMin4, CMax4);
d.V4R = Vector4.Clamp(V4R + COff4, CMin4, CMax4); this.V4R = Vector4.Clamp(this.V4R + COff4, CMin4, CMax4);
d.V5L = Vector4.Clamp(V5L + COff4, CMin4, CMax4); this.V5L = Vector4.Clamp(this.V5L + COff4, CMin4, CMax4);
d.V5R = Vector4.Clamp(V5R + COff4, CMin4, CMax4); this.V5R = Vector4.Clamp(this.V5R + COff4, CMin4, CMax4);
d.V6L = Vector4.Clamp(V6L + COff4, CMin4, CMax4); this.V6L = Vector4.Clamp(this.V6L + COff4, CMin4, CMax4);
d.V6R = Vector4.Clamp(V6R + COff4, CMin4, CMax4); this.V6R = Vector4.Clamp(this.V6R + COff4, CMin4, CMax4);
d.V7L = Vector4.Clamp(V7L + COff4, CMin4, CMax4); this.V7L = Vector4.Clamp(this.V7L + COff4, CMin4, CMax4);
d.V7R = Vector4.Clamp(V7R + COff4, CMin4, CMax4); this.V7R = Vector4.Clamp(this.V7R + COff4, CMin4, CMax4);
} }
/// <summary> /// <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> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [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); ref short selfRef = ref Unsafe.As<Block8x8, short>(ref source);
this.V0R = Vector4.Clamp(V0R + COff4, CMin4, CMax4);
this.V1L = Vector4.Clamp(V1L + COff4, CMin4, CMax4); this.V0L.X = Unsafe.Add(ref selfRef, 0);
this.V1R = Vector4.Clamp(V1R + COff4, CMin4, CMax4); this.V0L.Y = Unsafe.Add(ref selfRef, 1);
this.V2L = Vector4.Clamp(V2L + COff4, CMin4, CMax4); this.V0L.Z = Unsafe.Add(ref selfRef, 2);
this.V2R = Vector4.Clamp(V2R + COff4, CMin4, CMax4); this.V0L.W = Unsafe.Add(ref selfRef, 3);
this.V3L = Vector4.Clamp(V3L + COff4, CMin4, CMax4); this.V0R.X = Unsafe.Add(ref selfRef, 4);
this.V3R = Vector4.Clamp(V3R + COff4, CMin4, CMax4); this.V0R.Y = Unsafe.Add(ref selfRef, 5);
this.V4L = Vector4.Clamp(V4L + COff4, CMin4, CMax4); this.V0R.Z = Unsafe.Add(ref selfRef, 6);
this.V4R = Vector4.Clamp(V4R + COff4, CMin4, CMax4); this.V0R.W = Unsafe.Add(ref selfRef, 7);
this.V5L = Vector4.Clamp(V5L + COff4, CMin4, CMax4);
this.V5R = Vector4.Clamp(V5R + COff4, CMin4, CMax4); this.V1L.X = Unsafe.Add(ref selfRef, 8);
this.V6L = Vector4.Clamp(V6L + COff4, CMin4, CMax4); this.V1L.Y = Unsafe.Add(ref selfRef, 9);
this.V6R = Vector4.Clamp(V6R + COff4, CMin4, CMax4); this.V1L.Z = Unsafe.Add(ref selfRef, 10);
this.V7L = Vector4.Clamp(V7L + COff4, CMin4, CMax4); this.V1L.W = Unsafe.Add(ref selfRef, 11);
this.V7R = Vector4.Clamp(V7R + COff4, CMin4, CMax4); 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> /// <summary>
/// Level shift by +128, clip to [0, 255] /// Level shift by +128, clip to [0, 255]
/// </summary> /// </summary>
/// <param name="d">The destination block</param> public void NormalizeColorsInplace()
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void TransformByteConvetibleColorValuesInto(ref Block8x8F d)
{ {
<# <#
@ -74,7 +72,61 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common
for (int j = 0; j < 2; j++) for (int j = 0; j < 2; j++)
{ {
char side = j == 0 ? 'L' : 'R'; 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(); PopIndent();

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

@ -7,7 +7,6 @@ using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using SixLabors.ImageSharp.Memory;
// ReSharper disable InconsistentNaming // ReSharper disable InconsistentNaming
namespace SixLabors.ImageSharp.Formats.Jpeg.Common namespace SixLabors.ImageSharp.Formats.Jpeg.Common
@ -17,16 +16,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common
/// </summary> /// </summary>
internal partial struct Block8x8F 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> /// <summary>
/// A number of scalar coefficients in a <see cref="Block8x8F"/> /// A number of scalar coefficients in a <see cref="Block8x8F"/>
/// </summary> /// </summary>
@ -157,36 +146,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common
return result; 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> /// <summary>
/// Fill the block with defaults (zeroes) /// Fill the block with defaults (zeroes)
/// </summary> /// </summary>
@ -242,7 +201,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common
/// </summary> /// </summary>
/// <param name="dest">Destination</param> /// <param name="dest">Destination</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [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 d = ref Unsafe.As<float, byte>(ref dest.DangerousGetPinnableReference());
ref byte s = ref Unsafe.As<Block8x8F, byte>(ref this); 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() public float[] ToArray()
{ {
float[] result = new float[Size]; float[] result = new float[Size];
@ -419,26 +275,50 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common
/// <summary> /// <summary>
/// Multiply all elements of the block. /// Multiply all elements of the block.
/// </summary> /// </summary>
/// <param name="scaleVec">Vector to multiply by</param> /// <param name="value">The value to multiply by</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void MultiplyAllInplace(float scaleVec) public void MultiplyInplace(float value)
{ {
this.V0L *= scaleVec; this.V0L *= value;
this.V0R *= scaleVec; this.V0R *= value;
this.V1L *= scaleVec; this.V1L *= value;
this.V1R *= scaleVec; this.V1R *= value;
this.V2L *= scaleVec; this.V2L *= value;
this.V2R *= scaleVec; this.V2R *= value;
this.V3L *= scaleVec; this.V3L *= value;
this.V3R *= scaleVec; this.V3R *= value;
this.V4L *= scaleVec; this.V4L *= value;
this.V4R *= scaleVec; this.V4R *= value;
this.V5L *= scaleVec; this.V5L *= value;
this.V5R *= scaleVec; this.V5R *= value;
this.V6L *= scaleVec; this.V6L *= value;
this.V6R *= scaleVec; this.V6R *= value;
this.V7L *= scaleVec; this.V7L *= value;
this.V7R *= scaleVec; 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> /// <summary>
@ -472,56 +352,32 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common
/// <param name="blockPtr">Block pointer</param> /// <param name="blockPtr">Block pointer</param>
/// <param name="qtPtr">Qt pointer</param> /// <param name="qtPtr">Qt pointer</param>
/// <param name="unzigPtr">Unzig 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) public static unsafe void DequantizeBlock(Block8x8F* blockPtr, Block8x8F* qtPtr, int* unzigPtr)
{ {
float* b = (float*)blockPtr; float* b = (float*)blockPtr;
float* qtp = (float*)qtPtr; 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; float val = *unzigPos;
val *= qtp[zig]; val *= qtp[qtIndex];
*unzigPos = val; *unzigPos = val;
} }
} }
/// <summary> /// <summary>
/// Level shift by +128, clip to [0, 255], and write to buffer. /// Quantize 'block' into 'dest' using the 'qt' quantization table:
/// </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>
/// Unzig the elements of block into dest, while dividing them by elements of qt and "pre-rounding" the values. /// 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. /// To finish the rounding it's enough to (int)-cast these values.
/// </summary> /// </summary>
/// <param name="block">Source block</param> /// <param name="block">Source block</param>
/// <param name="dest">Destination block</param> /// <param name="dest">Destination block</param>
/// <param name="qt">The quantization table</param> /// <param name="qt">The quantization table</param>
/// <param name="unzigPtr">Pointer to elements of <see cref="UnzigData"/></param> /// <param name="unzigPtr">Pointer to elements of <see cref="ZigZag"/></param>
public static unsafe void UnzigDivRound( public static unsafe void Quantize(
Block8x8F* block, Block8x8F* block,
Block8x8F* dest, Block8x8F* dest,
Block8x8F* qt, Block8x8F* qt,
@ -610,34 +466,26 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common
return result; 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); this.NormalizeColorsAndRoundInplaceAvx2();
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();
} }
else 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++) for (int i = 0; i < Size; i++)
{ {
@ -663,6 +511,15 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common
return bld.ToString(); 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)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector4 DivideRound(Vector4 dividend, Vector4 divisor) 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.MustBeLessThan(idx, Size, nameof(idx));
DebugGuard.MustBeGreaterThanOrEqualTo(idx, 0, 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. /// Encapsulates the implementation of processing "raw" <see cref="Buffer{T}"/>-s into Jpeg image channels.
/// </summary> /// </summary>
[StructLayout(LayoutKind.Sequential)] [StructLayout(LayoutKind.Sequential)]
internal unsafe struct JpegBlockPostProcessor internal struct JpegBlockPostProcessor
{ {
/// <summary> /// <summary>
/// The <see cref="ComputationData"/> /// Source block
/// </summary> /// </summary>
private ComputationData data; public Block8x8F SourceBlock;
/// <summary> /// <summary>
/// Pointers to elements of <see cref="data"/> /// Temporal block 1 to store intermediate and/or final computation results
/// </summary> /// </summary>
private DataPointers pointers; public Block8x8F WorkspaceBlock1;
/// <summary> /// <summary>
/// Initialize the <see cref="JpegBlockPostProcessor"/> instance on the stack. /// Temporal block 2 to store intermediate and/or final computation results
/// </summary> /// </summary>
/// <param name="postProcessor">The <see cref="JpegBlockPostProcessor"/> instance</param> public Block8x8F WorkspaceBlock2;
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 void ProcessBlockColorsInto( /// <summary>
IRawJpegData decoder, /// The quantization table as <see cref="Block8x8F"/>
IJpegComponent component, /// </summary>
ref Block8x8 sourceBlock, public Block8x8F DequantiazationTable;
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();
Size divs = component.SubSamplingDivisors; /// <summary>
this.data.WorkspaceBlock1.CopyTo(destArea, divs.Width, divs.Height); /// Defines the horizontal and vertical scale we need to apply to the 8x8 sized block.
} /// </summary>
private Size subSamplingDivisors;
/// <summary> /// <summary>
/// Holds the "large" data blocks needed for computations. /// Initializes a new instance of the <see cref="JpegBlockPostProcessor"/> struct.
/// </summary> /// </summary>
[StructLayout(LayoutKind.Sequential)] public JpegBlockPostProcessor(IRawJpegData decoder, IJpegComponent component)
public struct ComputationData
{ {
/// <summary> int qtIndex = component.QuantizationTableIndex;
/// Source block this.DequantiazationTable = ZigZag.CreateDequantizationTable(ref decoder.QuantizationTables[qtIndex]);
/// </summary> this.subSamplingDivisors = component.SubSamplingDivisors;
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;
/// <summary> this.SourceBlock = default(Block8x8F);
/// Creates and initializes a new <see cref="ComputationData"/> instance this.WorkspaceBlock1 = default(Block8x8F);
/// </summary> this.WorkspaceBlock2 = default(Block8x8F);
/// <returns>The <see cref="ComputationData"/></returns>
public static ComputationData Create()
{
var data = default(ComputationData);
data.Unzig = UnzigData.Create();
return data;
}
} }
/// <summary> /// <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> /// </summary>
public struct DataPointers public void ProcessBlockColorsInto(
ref Block8x8 sourceBlock,
BufferArea<float> destArea)
{ {
/// <summary> ref Block8x8F b = ref this.SourceBlock;
/// Pointer to <see cref="ComputationData.SourceBlock"/> b.LoadFrom(ref sourceBlock);
/// </summary>
public Block8x8F* SourceBlock;
/// <summary>
/// Pointer to <see cref="ComputationData.WorkspaceBlock1"/>
/// </summary>
public Block8x8F* WorkspaceBlock1;
/// <summary> // Dequantize:
/// Pointer to <see cref="ComputationData.WorkspaceBlock2"/> b.MultiplyInplace(ref this.DequantiazationTable);
/// </summary>
public Block8x8F* WorkspaceBlock2;
/// <summary> FastFloatingPointDCT.TransformIDCT(ref b, ref this.WorkspaceBlock1, ref this.WorkspaceBlock2);
/// Pointer to <see cref="ComputationData.QuantiazationTable"/>
/// </summary>
public Block8x8F* QuantiazationTable;
/// <summary> // To conform better to libjpeg we actually NEED TO loose precision here.
/// Pointer to <see cref="ComputationData.Unzig"/> as int* // This is because they store blocks as Int16 between all the operations.
/// </summary> // To be "more accurate", we need to emulate this by rounding!
public int* Unzig; this.WorkspaceBlock1.NormalizeColorsAndRoundInplace();
/// <summary> this.WorkspaceBlock1.CopyTo(destArea, this.subSamplingDivisors.Width, this.subSamplingDivisors.Height);
/// 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;
}
} }
} }
} }

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

@ -1,6 +1,7 @@
using System; using System;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Common.Tuples;
namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder
{ {
@ -121,9 +122,25 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder
tmp.MultiplyInplace(1.772F); tmp.MultiplyInplace(1.772F);
b.AddInplace(ref tmp); b.AddInplace(ref tmp);
r.RoundAndDownscale(); if (Vector<float>.Count == 4)
g.RoundAndDownscale(); {
b.RoundAndDownscale(); // 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: // 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); 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 private struct Vector4Octet
{ {
#pragma warning disable SA1132 // Do not combine fields #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> /// <summary>
/// Invoke <see cref="JpegBlockPostProcessor"/> for <see cref="BlockRowsPerStep"/> block rows, copy the result into <see cref="ColorBuffer"/>. /// Invoke <see cref="JpegBlockPostProcessor"/> for <see cref="BlockRowsPerStep"/> block rows, copy the result into <see cref="ColorBuffer"/>.
/// </summary> /// </summary>
public unsafe void CopyBlocksToColorBuffer() public void CopyBlocksToColorBuffer()
{ {
var blockPp = default(JpegBlockPostProcessor); var blockPp = new JpegBlockPostProcessor(this.ImagePostProcessor.RawJpeg, this.Component);
JpegBlockPostProcessor.Init(&blockPp);
for (int y = 0; y < this.BlockRowsPerStep; y++) for (int y = 0; y < this.BlockRowsPerStep; y++)
{ {
@ -95,7 +94,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common.Decoder
this.blockAreaSize.Width, this.blockAreaSize.Width,
this.blockAreaSize.Height); 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> /// <param name="temp">Temporary block provided by the caller</param>
public static void TransformIDCT(ref Block8x8F src, ref Block8x8F dest, ref Block8x8F temp) 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); src.TransposeInto(ref temp);
IDCT8x4_LeftPart(ref temp, ref dest); IDCT8x4_LeftPart(ref temp, ref dest);
IDCT8x4_RightPart(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); IDCT8x4_RightPart(ref temp, ref dest);
// TODO: What if we leave the blocks in a scaled-by-x8 state until final color packing? // 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> /// <summary>
@ -334,7 +337,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common
FDCT8x4_LeftPart(ref temp, ref dest); FDCT8x4_LeftPart(ref temp, ref dest);
FDCT8x4_RightPart(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 namespace SixLabors.ImageSharp.Formats.Jpeg.Common
{ {
/// <summary> /// <summary>
/// TODO: This should be simply just a <see cref="Block8x8"/>!
/// Holds the Jpeg UnZig array in a value/stack type. /// Holds the Jpeg UnZig array in a value/stack type.
/// Unzig maps from the zigzag ordering to the natural ordering. For example, /// 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 /// 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). /// value is 16, which means first column (16%8 == 0) and third row (16/8 == 2).
/// </summary> /// </summary>
internal unsafe struct UnzigData internal unsafe struct ZigZag
{ {
/// <summary> /// <summary>
/// Copy of <see cref="Unzig"/> in a value type /// Copy of <see cref="Unzig"/> in a value type
@ -33,15 +32,30 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Common
}; };
/// <summary> /// <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> /// </summary>
/// <returns>The new instance</returns> /// <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; int* unzigPtr = result.Data;
Marshal.Copy(Unzig, 0, (IntPtr)unzigPtr, 64); Marshal.Copy(Unzig, 0, (IntPtr)unzigPtr, 64);
return result; 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> /// <summary>
/// The jpeg unzig data /// The jpeg unzig data
/// </summary> /// </summary>
public UnzigData Unzig; public ZigZag Unzig;
/// <summary> /// <summary>
/// The buffer storing the <see cref="OrigComponentScan"/>-s for each component /// 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() public static ComputationData Create()
{ {
ComputationData data = default(ComputationData); ComputationData data = default(ComputationData);
data.Unzig = UnzigData.Create(); data.Unzig = ZigZag.CreateUnzigTable();
return data; 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 onStackLuminanceQuantTable = this.luminanceQuantTable;
Block8x8F onStackChrominanceQuantTable = this.chrominanceQuantTable; Block8x8F onStackChrominanceQuantTable = this.chrominanceQuantTable;
UnzigData unzig = UnzigData.Create(); ZigZag unzig = ZigZag.CreateUnzigTable();
// ReSharper disable once InconsistentNaming // ReSharper disable once InconsistentNaming
int prevDCY = 0, prevDCCb = 0, prevDCCr = 0; 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); FastFloatingPointDCT.TransformFDCT(ref *src, ref *tempDest1, ref *tempDest2);
Block8x8F.UnzigDivRound(tempDest1, tempDest2, quant, unzigPtr); Block8x8F.Quantize(tempDest1, tempDest2, quant, unzigPtr);
float* unziggedDestPtr = (float*)tempDest2; float* unziggedDestPtr = (float*)tempDest2;
int dc = (int)unziggedDestPtr[0]; int dc = (int)unziggedDestPtr[0];
@ -912,7 +912,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.GolangPort
Block8x8F onStackLuminanceQuantTable = this.luminanceQuantTable; Block8x8F onStackLuminanceQuantTable = this.luminanceQuantTable;
Block8x8F onStackChrominanceQuantTable = this.chrominanceQuantTable; Block8x8F onStackChrominanceQuantTable = this.chrominanceQuantTable;
UnzigData unzig = UnzigData.Create(); ZigZag unzig = ZigZag.CreateUnzigTable();
// ReSharper disable once InconsistentNaming // ReSharper disable once InconsistentNaming
int prevDCY = 0, prevDCCb = 0, prevDCCr = 0; 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.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Common.Tuples;
using Xunit.Abstractions; using Xunit.Abstractions;
using Xunit.Sdk; using Xunit.Sdk;
@ -243,15 +245,15 @@ namespace SixLabors.ImageSharp.Tests.Common
x = (x * scale) + magick; 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; 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); d.LoadFrom(ref ii);
this.Output.WriteLine(ii.ToString()); this.Output.WriteLine(ii.ToString());

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

@ -32,32 +32,18 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
{ {
} }
[Fact] private bool SkipOnNonAvx2Runner()
public void Indexer()
{ {
float sum = 0; if (!SimdUtils.IsAvx2CompatibleArchitecture)
this.Measure( {
Times, this.Output.WriteLine("AVX2 not supported, skipping!");
() => return true;
{ }
var block = new Block8x8F(); return false;
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);
} }
[Fact] [Fact]
public unsafe void Indexer_GetScalarAt_SetScalarAt() public void Indexer()
{ {
float sum = 0; float sum = 0;
this.Measure( this.Measure(
@ -68,18 +54,18 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
for (int i = 0; i < Block8x8F.Size; i++) for (int i = 0; i < Block8x8F.Size; i++)
{ {
Block8x8F.SetScalarAt(&block, i, i); block[i] = i;
} }
sum = 0; sum = 0;
for (int i = 0; i < Block8x8F.Size; i++) for (int i = 0; i < Block8x8F.Size; i++)
{ {
sum += Block8x8F.GetScalarAt(&block, i); sum += block[i];
} }
}); });
Assert.Equal(sum, 64f * 63f * 0.5f); Assert.Equal(sum, 64f * 63f * 0.5f);
} }
[Fact] [Fact]
public void Indexer_ReferenceBenchmarkWithArray() 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"); 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() private static float[] Create8x8ColorCropTestData()
{ {
float[] result = new float[64]; float[] result = new float[64];
@ -263,30 +222,18 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
return result; return result;
} }
[Theory] [Fact]
[InlineData(false)] public void NormalizeColors()
[InlineData(true)]
public void NormalizeColors(bool inplace)
{ {
var block = default(Block8x8F); var block = default(Block8x8F);
float[] input = Create8x8ColorCropTestData(); float[] input = Create8x8ColorCropTestData();
block.LoadFrom(input); block.LoadFrom(input);
this.Output.WriteLine("Input:"); this.Output.WriteLine("Input:");
this.PrintLinearData(input); this.PrintLinearData(input);
var dest = default(Block8x8F);
if (inplace) Block8x8F dest = block;
{ dest.NormalizeColorsInplace();
dest = block;
dest.NormalizeColorsInplace();
}
else
{
block.NormalizeColorsInto(ref dest);
}
float[] array = new float[64]; float[] array = new float[64];
dest.CopyTo(array); dest.CopyTo(array);
this.Output.WriteLine("Result:"); 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] [Theory]
[InlineData(1)] [InlineData(1)]
[InlineData(2)] [InlineData(2)]
public unsafe void UnzigDivRound(int seed) public unsafe void Quantize(int seed)
{ {
var block = new Block8x8F(); var block = new Block8x8F();
block.LoadFrom(Create8x8RoundedRandomFloatData(-2000, 2000, seed)); block.LoadFrom(Create8x8RoundedRandomFloatData(-2000, 2000, seed));
@ -309,14 +280,14 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
var qt = new Block8x8F(); var qt = new Block8x8F();
qt.LoadFrom(Create8x8RoundedRandomFloatData(-2000, 2000, seed)); qt.LoadFrom(Create8x8RoundedRandomFloatData(-2000, 2000, seed));
var unzig = UnzigData.Create(); var unzig = ZigZag.CreateUnzigTable();
int* expectedResults = stackalloc int[Block8x8F.Size]; int* expectedResults = stackalloc int[Block8x8F.Size];
ReferenceImplementations.UnZigDivRoundRational(&block, expectedResults, &qt, unzig.Data); ReferenceImplementations.QuantizeRational(&block, expectedResults, &qt, unzig.Data);
var actualResults = default(Block8x8F); 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++) for (int i = 0; i < Block8x8F.Size; i++)
{ {
@ -352,7 +323,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
[InlineData(1)] [InlineData(1)]
[InlineData(2)] [InlineData(2)]
[InlineData(3)] [InlineData(3)]
public void RoundInplace(int seed) public void RoundInplaceSlow(int seed)
{ {
Block8x8F s = CreateRandomFloatBlock(-500, 500, seed); Block8x8F s = CreateRandomFloatBlock(-500, 500, seed);
@ -370,5 +341,77 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
Assert.Equal(expected, actual); 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 public class JpegColorConverterTests
{ {
private const float Precision = 1/255f; private const float Precision = 0.1f / 255;
public static readonly TheoryData<int, int, int> CommonConversionData = public static readonly TheoryData<int, int, int> CommonConversionData =
new TheoryData<int, int, int> 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 namespace SixLabors.ImageSharp.Tests.Formats.Jpg
{ {
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -24,6 +25,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
using Xunit; using Xunit;
using Xunit.Abstractions; using Xunit.Abstractions;
// TODO: Scatter test cases into multiple test classes
public class JpegDecoderTests public class JpegDecoderTests
{ {
public static string[] BaselineTestJpegs = public static string[] BaselineTestJpegs =
@ -50,16 +52,43 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
TestImages.Jpeg.Issues.MissingFF00ProgressiveGirl159, 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; public const PixelTypes CommonNonDefaultPixelTypes = PixelTypes.Rgba32 | PixelTypes.Argb32 | PixelTypes.RgbaVector;
// TODO: We should make this comparer less tolerant ... private const float BaselineTolerance_Orig = 0.001f / 100;
private static readonly ImageComparer VeryTolerantJpegComparer = private const float BaselineTolerance_PdfJs = 0.005f;
ImageComparer.Tolerant(0.005f, perPixelManhattanThreshold: 4);
// BUG: PDF.js output is wrong on spectral level! private const float ProgressiveTolerance_Orig = 0.2f / 100;
private static readonly ImageComparer PdfJsProgressiveComparer = private const float ProgressiveTolerance_PdfJs = 1.5f / 100; // PDF.js Progressive output is wrong on spectral level!
ImageComparer.Tolerant(0.015f, perPixelManhattanThreshold: 4);
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) public JpegDecoderTests(ITestOutputHelper output)
{ {
@ -71,7 +100,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
private static IImageDecoder OrigJpegDecoder => new OrigJpegDecoder(); private static IImageDecoder OrigJpegDecoder => new OrigJpegDecoder();
private static IImageDecoder PdfJsJpegDecoder => new PdfJsJpegDecoder(); private static IImageDecoder PdfJsJpegDecoder => new PdfJsJpegDecoder();
[Fact] [Fact]
public void ParseStream_BasicPropertiesAreCorrect1_PdfJs() 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); VerifyJpeg.VerifyComponentSizes3(decoder.Frame.Components, 43, 61, 22, 31, 22, 31);
} }
} }
public const string DecodeBaselineJpegOutputName = "DecodeBaselineJpeg"; public const string DecodeBaselineJpegOutputName = "DecodeBaselineJpeg";
[Theory] [Theory]
[WithFileCollection(nameof(BaselineTestJpegs), PixelTypes.Rgba32)] [WithFile(TestImages.Jpeg.Baseline.Calliphora, CommonNonDefaultPixelTypes, false)]
public void DecodeBaselineJpeg_PdfJs<TPixel>(TestImageProvider<TPixel> provider) [WithFile(TestImages.Jpeg.Baseline.Calliphora, CommonNonDefaultPixelTypes, true)]
public void JpegDecoder_IsNotBoundToSinglePixelType<TPixel>(TestImageProvider<TPixel> provider, bool useOldDecoder)
where TPixel : struct, IPixel<TPixel> 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); image.DebugSave(provider);
provider.Utility.TestName = DecodeBaselineJpegOutputName; provider.Utility.TestName = DecodeBaselineJpegOutputName;
image.CompareToReferenceOutput(provider, VeryTolerantJpegComparer, appendPixelTypeToFileName: false); image.CompareToReferenceOutput(provider, ImageComparer.Tolerant(BaselineTolerance_PdfJs), appendPixelTypeToFileName: false);
} }
} }
[Theory] [Theory]
[WithFile(TestImages.Jpeg.Baseline.Calliphora, CommonNonDefaultPixelTypes, false)] [WithFileCollection(nameof(BaselineTestJpegs), PixelTypes.Rgba32)]
[WithFile(TestImages.Jpeg.Baseline.Calliphora, CommonNonDefaultPixelTypes, true)] public void DecodeBaselineJpeg_Orig<TPixel>(TestImageProvider<TPixel> provider)
public void JpegDecoder_IsNotBoundToSinglePixelType<TPixel>(TestImageProvider<TPixel> provider, bool useOldDecoder)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
IImageDecoder decoder = useOldDecoder ? OrigJpegDecoder : PdfJsJpegDecoder; using (Image<TPixel> image = provider.GetImage(OrigJpegDecoder))
using (Image<TPixel> image = provider.GetImage(decoder))
{ {
image.DebugSave(provider); image.DebugSave(provider);
provider.Utility.TestName = DecodeBaselineJpegOutputName; provider.Utility.TestName = DecodeBaselineJpegOutputName;
image.CompareToReferenceOutput(provider, VeryTolerantJpegComparer, appendPixelTypeToFileName: false); image.CompareToReferenceOutput(
provider,
this.GetImageComparerForOrigDecoder(provider),
appendPixelTypeToFileName: false);
} }
} }
[Theory] [Theory]
[WithFileCollection(nameof(BaselineTestJpegs), PixelTypes.Rgba32)] [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> where TPixel : struct, IPixel<TPixel>
{ {
using (Image<TPixel> image = provider.GetImage(OrigJpegDecoder)) using (Image<TPixel> image = provider.GetImage(PdfJsJpegDecoder))
{ {
image.DebugSave(provider); image.DebugSave(provider);
provider.Utility.TestName = DecodeBaselineJpegOutputName; 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] [Theory]
[WithFileCollection(nameof(ProgressiveTestJpegs), PixelTypes.Rgba32)] [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> where TPixel : struct, IPixel<TPixel>
{ {
using (Image<TPixel> image = provider.GetImage(PdfJsJpegDecoder)) using (Image<TPixel> image = provider.GetImage(OrigJpegDecoder))
{ {
image.DebugSave(provider); image.DebugSave(provider);
provider.Utility.TestName = DecodeProgressiveJpegOutputName; provider.Utility.TestName = DecodeProgressiveJpegOutputName;
image.CompareToReferenceOutput(provider, PdfJsProgressiveComparer, appendPixelTypeToFileName: false); image.CompareToReferenceOutput(
provider,
this.GetImageComparerForOrigDecoder(provider),
appendPixelTypeToFileName: false);
} }
} }
[Theory] [Theory]
[WithFileCollection(nameof(ProgressiveTestJpegs), PixelTypes.Rgba32)] [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> where TPixel : struct, IPixel<TPixel>
{ {
using (Image<TPixel> image = provider.GetImage(OrigJpegDecoder)) using (Image<TPixel> image = provider.GetImage(PdfJsJpegDecoder))
{ {
image.DebugSave(provider); image.DebugSave(provider);
provider.Utility.TestName = DecodeProgressiveJpegOutputName; 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> where TPixel : struct, IPixel<TPixel>
{ {
var reportingComparer = ImageComparer.Tolerant(0, 0); var reportingComparer = ImageComparer.Tolerant(0, 0);
ImageSimilarityReport report = image.GetReferenceOutputSimilarityReports( ImageSimilarityReport report = image.GetReferenceOutputSimilarityReports(
provider, provider,
reportingComparer, reportingComparer,
@ -183,10 +224,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
if (report != null && report.TotalNormalizedDifference.HasValue) 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) 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)) using (Image<TPixel> image = provider.GetImage(OrigJpegDecoder))
{ {
double d = this.GetDifferenceInPercents(image, provider); string d = this.GetDifferenceInPercentageString(image, provider);
this.Output.WriteLine($"Difference using ORIGINAL decoder: {d:0.0000}%");
this.Output.WriteLine($"Difference using ORIGINAL decoder: {d}");
} }
using (Image<TPixel> image = provider.GetImage(PdfJsJpegDecoder)) using (Image<TPixel> image = provider.GetImage(PdfJsJpegDecoder))
{ {
double d = this.GetDifferenceInPercents(image, provider); string d = this.GetDifferenceInPercentageString(image, provider);
this.Output.WriteLine($"Difference using PDFJS decoder: {d:0.0000}%"); this.Output.WriteLine($"Difference using PDFJS decoder: {d}");
} }
} }
@ -228,7 +270,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
{ {
this.CompareJpegDecodersImpl(provider, DecodeProgressiveJpegOutputName); this.CompareJpegDecodersImpl(provider, DecodeProgressiveJpegOutputName);
} }
[Theory] [Theory]
[WithSolidFilledImages(16, 16, 255, 0, 0, PixelTypes.Rgba32, JpegSubsample.Ratio420, 75)] [WithSolidFilledImages(16, 16, 255, 0, 0, PixelTypes.Rgba32, JpegSubsample.Ratio420, 75)]
[WithSolidFilledImages(16, 16, 255, 0, 0, PixelTypes.Rgba32, JpegSubsample.Ratio420, 100)] [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); var mirror = Image.Load<TPixel>(data, OrigJpegDecoder);
mirror.DebugSave(provider, $"_{subsample}_Q{quality}"); mirror.DebugSave(provider, $"_{subsample}_Q{quality}");
} }
[Fact] [Fact]
public void Decoder_Reads_Correct_Resolution_From_Jfif() public void Decoder_Reads_Correct_Resolution_From_Jfif()
{ {
@ -314,7 +356,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
// into "\tests\Images\ActualOutput\JpegDecoderTests\" // into "\tests\Images\ActualOutput\JpegDecoderTests\"
//[Theory] //[Theory]
//[WithFile(TestImages.Jpeg.Progressive.Progress, PixelTypes.Rgba32, "PdfJsOriginal_progress.png")] //[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) string pdfJsOriginalResultImage)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
@ -335,7 +377,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
{ {
ImageSimilarityReport originalReport = comparer.CompareImagesOrFrames(expectedImage, pdfJsOriginalResult); ImageSimilarityReport originalReport = comparer.CompareImagesOrFrames(expectedImage, pdfJsOriginalResult);
ImageSimilarityReport portReport = comparer.CompareImagesOrFrames(expectedImage, pdfJsPortResult); ImageSimilarityReport portReport = comparer.CompareImagesOrFrames(expectedImage, pdfJsPortResult);
this.Output.WriteLine($"Difference for PDF.js ORIGINAL: {originalReport.DifferencePercentageString}"); this.Output.WriteLine($"Difference for PDF.js ORIGINAL: {originalReport.DifferencePercentageString}");
this.Output.WriteLine($"Difference for PORT: {portReport.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, TestImages.Jpeg.Baseline.Jpeg444,
}; };
// [Theory] // Benchmark, enable manually //[Theory] // Benchmark, enable manually
// [MemberData(nameof(DecodeJpegData))] //[MemberData(nameof(DecodeJpegData))]
public void DecodeJpeg_Original(string fileName) public void DecodeJpeg_Original(string fileName)
{ {
this.DecodeJpegBenchmarkImpl(fileName, new OrigJpegDecoder()); 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> /// </summary>
internal static partial class ReferenceImplementations 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> /// <summary>
/// Transpose 8x8 block stored linearly in a <see cref="Span{T}"/> (inplace) /// Transpose 8x8 block stored linearly in a <see cref="Span{T}"/> (inplace)
/// </summary> /// </summary>
@ -93,14 +108,14 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg.Utils
} }
/// <summary> /// <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)"/>. /// Rounding is done used an integer-based algorithm defined in <see cref="RationalRound(int,int)"/>.
/// </summary> /// </summary>
/// <param name="src">The input block</param> /// <param name="src">The input block</param>
/// <param name="dest">The destination block of integers</param> /// <param name="dest">The destination block of integers</param>
/// <param name="qt">The quantization table</param> /// <param name="qt">The quantization table</param>
/// <param name="unzigPtr">Pointer to <see cref="UnzigData.Data"/> </param> /// <param name="unzigPtr">Pointer to <see cref="ZigZag.Data"/> </param>
public static unsafe void UnZigDivRoundRational(Block8x8F* src, int* dest, Block8x8F* qt, int* unzigPtr) public static unsafe void QuantizeRational(Block8x8F* src, int* dest, Block8x8F* qt, int* unzigPtr)
{ {
float* s = (float*)src; float* s = (float*)src;
float* q = (float*)qt; 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; SizeF newSize = image.Size() * ratio;
image.Mutate(x => x.Resize((Size)newSize, sampler, false)); 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.DebugSave(provider, details);
image.CompareToReferenceOutput(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; float d = x - y;
return d > -this.Eps && d < this.Eps; return d >= -this.Eps && d <= this.Eps;
} }
public int GetHashCode(float obj) 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! // TODO: This should not be a nullable value!
public float? TotalNormalizedDifference { get; } public float? TotalNormalizedDifference { get; }
public string DifferencePercentageString => this.TotalNormalizedDifference.HasValue public string DifferencePercentageString
? $"{this.TotalNormalizedDifference.Value * 100:0.0000}%" {
: "?"; 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; } 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>>(); 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 string FilePath { get; private set; }
public override string SourceFileOrDescription => this.FilePath; 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}"; return $"{this.GetTestOutputDir()}/{this.TestName}{pixName}{fn}{details}{extension}";
} }
private static string Inv(FormattableString formattable) => System.FormattableString.Invariant(formattable);
/// <summary> /// <summary>
/// Gets the recommended file name for the output of the test /// Gets the recommended file name for the output of the test
/// </summary> /// </summary>
@ -111,13 +113,17 @@ namespace SixLabors.ImageSharp.Tests
TypeInfo info = type.GetTypeInfo(); TypeInfo info = type.GetTypeInfo();
if (info.IsPrimitive || info.IsEnum || type == typeof(decimal)) if (info.IsPrimitive || info.IsEnum || type == typeof(decimal))
{ {
detailsString = testOutputDetails.ToString(); detailsString = Inv($"{testOutputDetails}");
} }
else else
{ {
IEnumerable<PropertyInfo> properties = testOutputDetails.GetType().GetRuntimeProperties(); 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); return this.GetTestOutputFileNameImpl(extension, detailsString, appendPixelTypeToFileName);

Loading…
Cancel
Save