Browse Source

Merge pull request #188 from JimBobSquarePants/accurate-jpeg

Use same conversion methods as libjpeg
af/merge-core
James Jackson-South 9 years ago
committed by GitHub
parent
commit
e372d29829
  1. 109
      src/ImageSharp/Formats/Jpeg/Components/Decoder/YCbCrToRgbTables.cs
  2. 131
      src/ImageSharp/Formats/Jpeg/Components/Encoder/RgbToYCbCrTables.cs
  3. 82
      src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
  4. 160
      src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
  5. 4
      tests/ImageSharp.Tests/Colors/RgbaVectorTransformTests.cs

109
src/ImageSharp/Formats/Jpeg/Components/Decoder/YCbCrToRgbTables.cs

@ -0,0 +1,109 @@
// <copyright file="YCbCrToRgbTables.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Formats.Jpg
{
using System.Runtime.CompilerServices;
using ImageSharp.PixelFormats;
/// <summary>
/// Provides 8-bit lookup tables for converting from YCbCr to Rgb colorspace.
/// Methods to build the tables are based on libjpeg implementation.
/// </summary>
internal unsafe struct YCbCrToRgbTables
{
/// <summary>
/// The red red-chrominance table
/// </summary>
public fixed int CrRTable[256];
/// <summary>
/// The blue blue-chrominance table
/// </summary>
public fixed int CbBTable[256];
/// <summary>
/// The green red-chrominance table
/// </summary>
public fixed int CrGTable[256];
/// <summary>
/// The green blue-chrominance table
/// </summary>
public fixed int CbGTable[256];
// Speediest right-shift on some machines and gives us enough accuracy at 4 decimal places.
private const int ScaleBits = 16;
private const int Half = 1 << (ScaleBits - 1);
/// <summary>
/// Initializes the YCbCr tables
/// </summary>
/// <returns>The intialized <see cref="YCbCrToRgbTables"/></returns>
public static YCbCrToRgbTables Create()
{
YCbCrToRgbTables tables = default(YCbCrToRgbTables);
for (int i = 0, x = -128; i <= 255; i++, x++)
{
// i is the actual input pixel value, in the range 0..255
// The Cb or Cr value we are thinking of is x = i - 128
// Cr=>R value is nearest int to 1.402 * x
tables.CrRTable[i] = RightShift((Fix(1.402F) * x) + Half);
// Cb=>B value is nearest int to 1.772 * x
tables.CbBTable[i] = RightShift((Fix(1.772F) * x) + Half);
// Cr=>G value is scaled-up -0.714136286
tables.CrGTable[i] = (-Fix(0.714136286F)) * x;
// Cb => G value is scaled - up - 0.344136286 * x
// We also add in Half so that need not do it in inner loop
tables.CbGTable[i] = ((-Fix(0.344136286F)) * x) + Half;
}
return tables;
}
/// <summary>
/// Optimized method to pack bytes to the image from the YCbCr color space.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="packed">The packed pixel.</param>
/// <param name="tables">The reference to the tables instance.</param>
/// <param name="y">The y luminance component.</param>
/// <param name="cb">The cb chroma component.</param>
/// <param name="cr">The cr chroma component.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Pack<TPixel>(ref TPixel packed, YCbCrToRgbTables* tables, byte y, byte cb, byte cr)
where TPixel : struct, IPixel<TPixel>
{
// float r = MathF.Round(y + (1.402F * cr), MidpointRounding.AwayFromZero);
byte r = (byte)(y + tables->CrRTable[cr]).Clamp(0, 255);
// float g = MathF.Round(y - (0.344136F * cb) - (0.714136F * cr), MidpointRounding.AwayFromZero);
// The values for the G calculation are left scaled up, since we must add them together before rounding.
byte g = (byte)(y + RightShift(tables->CbGTable[cb] + tables->CrGTable[cr])).Clamp(0, 255);
// float b = MathF.Round(y + (1.772F * cb), MidpointRounding.AwayFromZero);
byte b = (byte)(y + tables->CbBTable[cb]).Clamp(0, 255);
packed.PackFromBytes(r, g, b, byte.MaxValue);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int Fix(float x)
{
return (int)((x * (1L << ScaleBits)) + 0.5F);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int RightShift(int x)
{
return x >> ScaleBits;
}
}
}

131
src/ImageSharp/Formats/Jpeg/Components/Encoder/RgbToYCbCrTables.cs

@ -0,0 +1,131 @@
// <copyright file="RgbToYCbCrTables.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Formats.Jpg
{
using System.Runtime.CompilerServices;
/// <summary>
/// Provides 8-bit lookup tables for converting from Rgb to YCbCr colorspace.
/// Methods to build the tables are based on libjpeg implementation.
/// </summary>
internal unsafe struct RgbToYCbCrTables
{
/// <summary>
/// The red luminance table
/// </summary>
public fixed int YRTable[256];
/// <summary>
/// The green luminance table
/// </summary>
public fixed int YGTable[256];
/// <summary>
/// The blue luminance table
/// </summary>
public fixed int YBTable[256];
/// <summary>
/// The red blue-chrominance table
/// </summary>
public fixed int CbRTable[256];
/// <summary>
/// The green blue-chrominance table
/// </summary>
public fixed int CbGTable[256];
/// <summary>
/// The blue blue-chrominance table
/// B=>Cb and R=>Cr are the same
/// </summary>
public fixed int CbBTable[256];
/// <summary>
/// The green red-chrominance table
/// </summary>
public fixed int CrGTable[256];
/// <summary>
/// The blue red-chrominance table
/// </summary>
public fixed int CrBTable[256];
// Speediest right-shift on some machines and gives us enough accuracy at 4 decimal places.
private const int ScaleBits = 16;
private const int CBCrOffset = 128 << ScaleBits;
private const int Half = 1 << (ScaleBits - 1);
/// <summary>
/// Initializes the YCbCr tables
/// </summary>
/// <returns>The intialized <see cref="RgbToYCbCrTables"/></returns>
public static RgbToYCbCrTables Create()
{
RgbToYCbCrTables tables = default(RgbToYCbCrTables);
for (int i = 0; i <= 255; i++)
{
// The values for the calculations are left scaled up since we must add them together before rounding.
tables.YRTable[i] = Fix(0.299F) * i;
tables.YGTable[i] = Fix(0.587F) * i;
tables.YBTable[i] = (Fix(0.114F) * i) + Half;
tables.CbRTable[i] = (-Fix(0.168735892F)) * i;
tables.CbGTable[i] = (-Fix(0.331264108F)) * i;
// We use a rounding fudge - factor of 0.5 - epsilon for Cb and Cr.
// This ensures that the maximum output will round to 255
// not 256, and thus that we don't have to range-limit.
//
// B=>Cb and R=>Cr tables are the same
tables.CbBTable[i] = (Fix(0.5F) * i) + CBCrOffset + Half - 1;
tables.CrGTable[i] = (-Fix(0.418687589F)) * i;
tables.CrBTable[i] = (-Fix(0.081312411F)) * i;
}
return tables;
}
/// <summary>
/// Optimized method to allocates the correct y, cb, and cr values to the DCT blocks from the given r, g, b values.
/// </summary>
/// <param name="yBlockRaw">The The luminance block.</param>
/// <param name="cbBlockRaw">The red chroma block.</param>
/// <param name="crBlockRaw">The blue chroma block.</param>
/// <param name="tables">The reference to the tables instance.</param>
/// <param name="index">The current index.</param>
/// <param name="r">The red value.</param>
/// <param name="g">The green value.</param>
/// <param name="b">The blue value.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Allocate(ref float* yBlockRaw, ref float* cbBlockRaw, ref float* crBlockRaw, ref RgbToYCbCrTables* tables, int index, int r, int g, int b)
{
// float y = (0.299F * r) + (0.587F * g) + (0.114F * b);
yBlockRaw[index] = (tables->YRTable[r] + tables->YGTable[g] + tables->YBTable[b]) >> ScaleBits;
// float cb = 128F + ((-0.168736F * r) - (0.331264F * g) + (0.5F * b));
cbBlockRaw[index] = (tables->CbRTable[r] + tables->CbGTable[g] + tables->CbBTable[b]) >> ScaleBits;
// float b = MathF.Round(y + (1.772F * cb), MidpointRounding.AwayFromZero);
crBlockRaw[index] = (tables->CbBTable[r] + tables->CrGTable[g] + tables->CrBTable[b]) >> ScaleBits;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int Fix(float x)
{
return (int)((x * (1L << ScaleBits)) + 0.5F);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int RightShift(int x)
{
return x >> ScaleBits;
}
}
}

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

@ -38,6 +38,11 @@ namespace ImageSharp.Formats
public InputProcessor InputProcessor;
#pragma warning restore SA401
/// <summary>
/// Lookup tables for converting YCbCr to Rgb
/// </summary>
private static YCbCrToRgbTables yCbCrToRgbTables = YCbCrToRgbTables.Create();
/// <summary>
/// The decoder options.
/// </summary>
@ -251,35 +256,6 @@ namespace ImageSharp.Formats
}
}
/// <summary>
/// Optimized method to pack bytes to the image from the YCbCr color space.
/// This is faster than implicit casting as it avoids double packing.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="packed">The packed pixel.</param>
/// <param name="y">The y luminance component.</param>
/// <param name="cb">The cb chroma component.</param>
/// <param name="cr">The cr chroma component.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void PackYcbCr<TPixel>(ref TPixel packed, byte y, byte cb, byte cr)
where TPixel : struct, IPixel<TPixel>
{
int ccb = cb - 128;
int ccr = cr - 128;
// Speed up the algorithm by removing floating point calculation
// Scale by 65536, add .5F and truncate value. We use bit shifting to divide the result
int r0 = 91881 * ccr; // (1.402F * 65536) + .5F
int g0 = 22554 * ccb; // (0.34414F * 65536) + .5F
int g1 = 46802 * ccr; // (0.71414F * 65536) + .5F
int b0 = 116130 * ccb; // (1.772F * 65536) + .5F
byte r = (byte)(y + (r0 >> 16)).Clamp(0, 255);
byte g = (byte)(y - (g0 >> 16) - (g1 >> 16)).Clamp(0, 255);
byte b = (byte)(y + (b0 >> 16)).Clamp(0, 255);
packed.PackFromBytes(r, g, b, 255);
}
/// <summary>
/// Read metadata from stream and read the blocks in the scans into <see cref="DecodedBlocks"/>.
/// </summary>
@ -705,26 +681,34 @@ namespace ImageSharp.Formats
using (PixelAccessor<TPixel> pixels = image.Lock())
{
Parallel.For(
0,
image.Height,
image.Configuration.ParallelOptions,
y =>
{
// TODO: Simplify + optimize + share duplicate code across converter methods
int yo = this.ycbcrImage.GetRowYOffset(y);
int co = this.ycbcrImage.GetRowCOffset(y);
for (int x = 0; x < image.Width; x++)
{
byte yy = this.ycbcrImage.YChannel.Pixels[yo + x];
byte cb = this.ycbcrImage.CbChannel.Pixels[co + (x / scale)];
byte cr = this.ycbcrImage.CrChannel.Pixels[co + (x / scale)];
TPixel packed = default(TPixel);
PackYcbCr<TPixel>(ref packed, yy, cb, cr);
pixels[x, y] = packed;
}
});
0,
image.Height,
image.Configuration.ParallelOptions,
y =>
{
// TODO. This Parallel loop doesn't give us the boost it should.
ref byte ycRef = ref this.ycbcrImage.YChannel.Pixels[0];
ref byte cbRef = ref this.ycbcrImage.CbChannel.Pixels[0];
ref byte crRef = ref this.ycbcrImage.CrChannel.Pixels[0];
fixed (YCbCrToRgbTables* tables = &yCbCrToRgbTables)
{
// TODO: Simplify + optimize + share duplicate code across converter methods
int yo = this.ycbcrImage.GetRowYOffset(y);
int co = this.ycbcrImage.GetRowCOffset(y);
for (int x = 0; x < image.Width; x++)
{
int cOff = co + (x / scale);
byte yy = Unsafe.Add(ref ycRef, yo + x);
byte cb = Unsafe.Add(ref cbRef, cOff);
byte cr = Unsafe.Add(ref crRef, cOff);
TPixel packed = default(TPixel);
YCbCrToRgbTables.Pack(ref packed, tables, yy, cb, cr);
pixels[x, y] = packed;
}
}
});
}
this.AssignResolution(image);

160
src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs

@ -5,11 +5,9 @@
namespace ImageSharp.Formats
{
using System;
using System.Buffers;
using System.IO;
using System.Runtime.CompilerServices;
using ImageSharp.Formats.Jpg;
using ImageSharp.Formats.Jpg.Components;
using ImageSharp.PixelFormats;
@ -103,6 +101,11 @@ namespace ImageSharp.Formats
}
};
/// <summary>
/// Lookup tables for converting Rgb to YCbCr
/// </summary>
private static RgbToYCbCrTables rgbToYCbCrTables = RgbToYCbCrTables.Create();
/// <summary>
/// A scratch buffer to reduce allocations.
/// </summary>
@ -285,6 +288,7 @@ namespace ImageSharp.Formats
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="pixels">The pixel accessor.</param>
/// <param name="tables">The reference to the tables instance.</param>
/// <param name="x">The x-position within the image.</param>
/// <param name="y">The y-position within the image.</param>
/// <param name="yBlock">The luminance block.</param>
@ -293,6 +297,7 @@ namespace ImageSharp.Formats
/// <param name="rgbBytes">Temporal <see cref="PixelArea{TPixel}"/> provided by the caller</param>
private static void ToYCbCr<TPixel>(
PixelAccessor<TPixel> pixels,
RgbToYCbCrTables* tables,
int x,
int y,
Block8x8F* yBlock,
@ -321,29 +326,9 @@ namespace ImageSharp.Formats
int g = Unsafe.Add(ref data0, dataIdx + 1);
int b = Unsafe.Add(ref data0, dataIdx + 2);
// Speed up the algorithm by removing floating point calculation
// Scale by 65536, add .5F and truncate value. We use bit shifting to divide the result
int y0 = 19595 * r; // (0.299F * 65536) + .5F
int y1 = 38470 * g; // (0.587F * 65536) + .5F
int y2 = 7471 * b; // (0.114F * 65536) + .5F
int cb0 = -11057 * r; // (-0.168736F * 65536) + .5F
int cb1 = 21710 * g; // (0.331264F * 65536) + .5F
int cb2 = 32768 * b; // (0.5F * 65536) + .5F
int cr0 = 32768 * r; // (0.5F * 65536) + .5F
int cr1 = 27439 * g; // (0.418688F * 65536) + .5F
int cr2 = 5329 * b; // (0.081312F * 65536) + .5F
float yy = (y0 + y1 + y2) >> 16;
float cb = 128 + ((cb0 - cb1 + cb2) >> 16);
float cr = 128 + ((cr0 - cr1 - cr2) >> 16);
int index = j8 + i;
yBlockRaw[index] = yy;
cbBlockRaw[index] = cb;
crBlockRaw[index] = cr;
RgbToYCbCrTables.Allocate(ref yBlockRaw, ref cbBlockRaw, ref crBlockRaw, ref tables, index, r, g, b);
dataIdx += 3;
}
@ -464,38 +449,41 @@ namespace ImageSharp.Formats
// ReSharper disable once InconsistentNaming
int prevDCY = 0, prevDCCb = 0, prevDCCr = 0;
using (PixelArea<TPixel> rgbBytes = new PixelArea<TPixel>(8, 8, ComponentOrder.Xyz))
fixed (RgbToYCbCrTables* tables = &rgbToYCbCrTables)
{
for (int y = 0; y < pixels.Height; y += 8)
using (PixelArea<TPixel> rgbBytes = new PixelArea<TPixel>(8, 8, ComponentOrder.Xyz))
{
for (int x = 0; x < pixels.Width; x += 8)
for (int y = 0; y < pixels.Height; y += 8)
{
ToYCbCr(pixels, x, y, &b, &cb, &cr, rgbBytes);
prevDCY = this.WriteBlock(
QuantIndex.Luminance,
prevDCY,
&b,
&temp1,
&temp2,
&onStackLuminanceQuantTable,
unzig.Data);
prevDCCb = this.WriteBlock(
QuantIndex.Chrominance,
prevDCCb,
&cb,
&temp1,
&temp2,
&onStackChrominanceQuantTable,
unzig.Data);
prevDCCr = this.WriteBlock(
QuantIndex.Chrominance,
prevDCCr,
&cr,
&temp1,
&temp2,
&onStackChrominanceQuantTable,
unzig.Data);
for (int x = 0; x < pixels.Width; x += 8)
{
ToYCbCr(pixels, tables, x, y, &b, &cb, &cr, rgbBytes);
prevDCY = this.WriteBlock(
QuantIndex.Luminance,
prevDCY,
&b,
&temp1,
&temp2,
&onStackLuminanceQuantTable,
unzig.Data);
prevDCCb = this.WriteBlock(
QuantIndex.Chrominance,
prevDCCb,
&cb,
&temp1,
&temp2,
&onStackChrominanceQuantTable,
unzig.Data);
prevDCCr = this.WriteBlock(
QuantIndex.Chrominance,
prevDCCr,
&cr,
&temp1,
&temp2,
&onStackChrominanceQuantTable,
unzig.Data);
}
}
}
}
@ -837,49 +825,51 @@ namespace ImageSharp.Formats
// ReSharper disable once InconsistentNaming
int prevDCY = 0, prevDCCb = 0, prevDCCr = 0;
using (PixelArea<TPixel> rgbBytes = new PixelArea<TPixel>(8, 8, ComponentOrder.Xyz))
fixed (RgbToYCbCrTables* tables = &rgbToYCbCrTables)
{
for (int y = 0; y < pixels.Height; y += 16)
using (PixelArea<TPixel> rgbBytes = new PixelArea<TPixel>(8, 8, ComponentOrder.Xyz))
{
for (int x = 0; x < pixels.Width; x += 16)
for (int y = 0; y < pixels.Height; y += 16)
{
for (int i = 0; i < 4; i++)
for (int x = 0; x < pixels.Width; x += 16)
{
int xOff = (i & 1) * 8;
int yOff = (i & 2) * 4;
ToYCbCr(pixels, x + xOff, y + yOff, &b, cbPtr + i, crPtr + i, rgbBytes);
for (int i = 0; i < 4; i++)
{
int xOff = (i & 1) * 8;
int yOff = (i & 2) * 4;
ToYCbCr(pixels, tables, x + xOff, y + yOff, &b, cbPtr + i, crPtr + i, rgbBytes);
prevDCY = this.WriteBlock(
QuantIndex.Luminance,
prevDCY,
&b,
&temp1,
&temp2,
&onStackLuminanceQuantTable,
unzig.Data);
}
Block8x8F.Scale16X16To8X8(&b, cbPtr);
prevDCCb = this.WriteBlock(
QuantIndex.Chrominance,
prevDCCb,
&b,
&temp1,
&temp2,
&onStackChrominanceQuantTable,
unzig.Data);
prevDCY = this.WriteBlock(
QuantIndex.Luminance,
prevDCY,
Block8x8F.Scale16X16To8X8(&b, crPtr);
prevDCCr = this.WriteBlock(
QuantIndex.Chrominance,
prevDCCr,
&b,
&temp1,
&temp2,
&onStackLuminanceQuantTable,
&onStackChrominanceQuantTable,
unzig.Data);
}
Block8x8F.Scale16X16To8X8(&b, cbPtr);
prevDCCb = this.WriteBlock(
QuantIndex.Chrominance,
prevDCCb,
&b,
&temp1,
&temp2,
&onStackChrominanceQuantTable,
unzig.Data);
Block8x8F.Scale16X16To8X8(&b, crPtr);
prevDCCr = this.WriteBlock(
QuantIndex.Chrominance,
prevDCCr,
&b,
&temp1,
&temp2,
&onStackChrominanceQuantTable,
unzig.Data);
}
}
}

4
tests/ImageSharp.Tests/Colors/RgbaVectorTransformTests.cs

@ -36,13 +36,13 @@ namespace ImageSharp.Tests.Colors
[Fact]
public void Multiply()
{
Assert.Equal(RgbaVector.Multiply(Backdrop, RgbaVector.Black).ToVector4(), Rgba32.Black.ToVector4(), FloatComparer);
Assert.Equal(RgbaVector.Multiply(Backdrop, RgbaVector.Black).ToVector4(), RgbaVector.Black.ToVector4(), FloatComparer);
Assert.Equal(RgbaVector.Multiply(Backdrop, RgbaVector.White).ToVector4(), Backdrop.ToVector4(), FloatComparer);
RgbaVector multiply = RgbaVector.Multiply(Backdrop, Source);
Assert.Equal(multiply.ToVector4(), new RgbaVector(0, 41, 0).ToVector4(), FloatComparer);
}
[Fact]
public void Screen()
{

Loading…
Cancel
Save