Browse Source

Apply transformations

pull/1552/head
Brian Popow 6 years ago
parent
commit
3517697659
  1. 309
      src/ImageSharp/Formats/WebP/Lossless/LosslessUtils.cs
  2. 908
      src/ImageSharp/Formats/WebP/Lossless/PredictorEncoder.cs
  3. 58
      src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs
  4. 14
      src/ImageSharp/Formats/WebP/Lossless/Vp8LMultipliers.cs
  5. 5
      src/ImageSharp/Formats/WebP/WebPConstants.cs
  6. 318
      src/ImageSharp/Formats/WebP/WebPEncoderCore.cs

309
src/ImageSharp/Formats/WebP/Lossless/LosslessUtils.cs

@ -73,6 +73,18 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
}
}
public static void SubtractGreenFromBlueAndRed(Span<uint> pixelData, int numPixels)
{
for (int i = 0; i < numPixels; i++)
{
uint argb = pixelData[i];
uint green = (argb >> 8) & 0xff;
uint newR = (((argb >> 16) & 0xff) - green) & 0xff;
uint newB = (((argb >> 0) & 0xff) - green) & 0xff;
pixelData[i] = (argb & 0xff00ff00u) | (newR << 16) | newB;
}
}
/// <summary>
/// If there are not many unique pixel values, it is more efficient to create a color index array and replace the pixel values by the array's indices.
/// This will reverse the color index transform.
@ -179,6 +191,24 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
}
}
public static void TransformColor(Vp8LMultipliers m, Span<uint> data, int numPixels)
{
for (int i = 0; i < numPixels; i++)
{
uint argb = data[i];
sbyte green = U32ToS8(argb >> 8);
sbyte red = U32ToS8(argb >> 16);
int newRed = red & 0xff;
int newBlue = (int)(argb & 0xff);
newRed -= ColorTransformDelta((sbyte)m.GreenToRed, green);
newRed &= 0xff;
newBlue -= ColorTransformDelta((sbyte)m.GreenToBlue, green);
newBlue -= ColorTransformDelta((sbyte)m.RedToBlue, red);
newBlue &= 0xff;
data[i] = (uint)((argb & 0xff00ff00u) | (newRed << 16) | newBlue);
}
}
/// <summary>
/// Reverses the color space transform.
/// </summary>
@ -345,6 +375,86 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
return (alphaAndGreen & 0xff00ff00u) | (redAndBlue & 0x00ff00ffu);
}
/// <summary>
/// Bundles multiple (1, 2, 4 or 8) pixels into a single pixel.
/// </summary>
public static void BundleColorMap(Span<byte> row, int width, int xBits, Span<uint> dst)
{
int x;
if (xBits > 0)
{
int bitDepth = 1 << (3 - xBits);
int mask = (1 << xBits) - 1;
uint code = 0xff000000;
for (x = 0; x < width; x++)
{
int xsub = x & mask;
if (xsub == 0)
{
code = 0xff000000;
}
code |= (uint)(row[x] << (8 + (bitDepth * xsub)));
dst[x >> xBits] = code;
}
}
else
{
for (x = 0; x < width; x++)
{
dst[x] = (uint)(0xff000000 | (row[x] << 8));
}
}
}
/// <summary>
/// Compute the combined Shanon's entropy for distribution {X} and {X+Y}.
/// </summary>
/// <returns>Shanon entropy.</returns>
public static float CombinedShannonEntropy(int[] x, int[] y)
{
double retVal = 0.0d;
uint sumX = 0, sumXY = 0;
for (int i = 0; i < 256; i++)
{
uint xi = (uint)x[i];
if (xi != 0)
{
uint xy = xi + (uint)y[i];
sumX += xi;
retVal -= FastSLog2(xi);
sumXY += xy;
retVal -= FastSLog2(xy);
}
else if (y[i] != 0)
{
sumXY += (uint)y[i];
retVal -= FastSLog2((uint)y[i]);
}
}
retVal += FastSLog2(sumX) + FastSLog2(sumXY);
return (float)retVal;
}
public static sbyte TransformColorRed(sbyte greenToRed, uint argb)
{
sbyte green = U32ToS8(argb >> 8);
int newRed = (int)(argb >> 16);
newRed -= ColorTransformDelta(greenToRed, green);
return (sbyte)(newRed & 0xff);
}
public static sbyte TransformColorBlue(sbyte greenToBlue, sbyte redToBlue, uint argb)
{
sbyte green = U32ToS8(argb >> 8);
sbyte red = U32ToS8(argb >> 16);
int newBlue = (int)(argb & 0xff);
newBlue -= ColorTransformDelta(greenToBlue, green);
newBlue -= ColorTransformDelta(redToBlue, red);
return (sbyte)(newBlue & 0xff);
}
/// <summary>
/// Fast calculation of log2(v) for integer input.
/// </summary>
@ -362,6 +472,26 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
return (v < LogLookupIdxMax) ? WebPLookupTables.SLog2Table[v] : FastSLog2Slow(v);
}
[MethodImpl(InliningOptions.ShortMethod)]
public static void ColorCodeToMultipliers(uint colorCode, ref Vp8LMultipliers m)
{
m.GreenToRed = (byte)(colorCode & 0xff);
m.GreenToBlue = (byte)((colorCode >> 8) & 0xff);
m.RedToBlue = (byte)((colorCode >> 16) & 0xff);
}
[MethodImpl(InliningOptions.ShortMethod)]
public static int NearLosslessBits(int nearLosslessQuality)
{
// 100 -> 0
// 80..99 -> 1
// 60..79 -> 2
// 40..59 -> 3
// 20..39 -> 4
// 0..19 -> 5
return 5 - (nearLosslessQuality / 20);
}
private static float FastSLog2Slow(uint v)
{
Guard.MustBeGreaterThanOrEqualTo(v, LogLookupIdxMax, nameof(v));
@ -605,86 +735,224 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
}
[MethodImpl(InliningOptions.ShortMethod)]
private static uint Predictor2(Span<uint> top, int idx)
public static uint Predictor2(Span<uint> top, int idx)
{
return top[idx];
}
[MethodImpl(InliningOptions.ShortMethod)]
private static uint Predictor3(Span<uint> top, int idx)
public static uint Predictor3(Span<uint> top, int idx)
{
return top[idx + 1];
}
[MethodImpl(InliningOptions.ShortMethod)]
private static uint Predictor4(Span<uint> top, int idx)
public static uint Predictor4(Span<uint> top, int idx)
{
return top[idx - 1];
}
[MethodImpl(InliningOptions.ShortMethod)]
private static uint Predictor5(uint left, Span<uint> top, int idx)
public static uint Predictor5(uint left, Span<uint> top, int idx)
{
uint pred = Average3(left, top[idx], top[idx + 1]);
return pred;
}
[MethodImpl(InliningOptions.ShortMethod)]
private static uint Predictor6(uint left, Span<uint> top, int idx)
public static uint Predictor6(uint left, Span<uint> top, int idx)
{
uint pred = Average2(left, top[idx - 1]);
return pred;
}
[MethodImpl(InliningOptions.ShortMethod)]
private static uint Predictor7(uint left, Span<uint> top, int idx)
public static uint Predictor7(uint left, Span<uint> top, int idx)
{
uint pred = Average2(left, top[idx]);
return pred;
}
[MethodImpl(InliningOptions.ShortMethod)]
private static uint Predictor8(Span<uint> top, int idx)
public static uint Predictor8(Span<uint> top, int idx)
{
uint pred = Average2(top[idx - 1], top[idx]);
return pred;
}
[MethodImpl(InliningOptions.ShortMethod)]
private static uint Predictor9(Span<uint> top, int idx)
public static uint Predictor9(Span<uint> top, int idx)
{
uint pred = Average2(top[idx], top[idx + 1]);
return pred;
}
[MethodImpl(InliningOptions.ShortMethod)]
private static uint Predictor10(uint left, Span<uint> top, int idx)
public static uint Predictor10(uint left, Span<uint> top, int idx)
{
uint pred = Average4(left, top[idx - 1], top[idx], top[idx + 1]);
return pred;
}
[MethodImpl(InliningOptions.ShortMethod)]
private static uint Predictor11(uint left, Span<uint> top, int idx)
public static uint Predictor11(uint left, Span<uint> top, int idx)
{
uint pred = Select(top[idx], left, top[idx - 1]);
return pred;
}
[MethodImpl(InliningOptions.ShortMethod)]
private static uint Predictor12(uint left, Span<uint> top, int idx)
public static uint Predictor12(uint left, Span<uint> top, int idx)
{
uint pred = ClampedAddSubtractFull(left, top[idx], top[idx - 1]);
return pred;
}
[MethodImpl(InliningOptions.ShortMethod)]
private static uint Predictor13(uint left, Span<uint> top, int idx)
public static uint Predictor13(uint left, Span<uint> top, int idx)
{
uint pred = ClampedAddSubtractHalf(left, top[idx], top[idx - 1]);
return pred;
}
[MethodImpl(InliningOptions.ShortMethod)]
public static void PredictorSub0(Span<uint> input, int numPixels, Span<uint> output)
{
for (int i = 0; i < numPixels; i++)
{
output[i] = SubPixels(input[i], WebPConstants.ArgbBlack);
}
}
[MethodImpl(InliningOptions.ShortMethod)]
public static void PredictorSub1(Span<uint> input, int idx, int numPixels, Span<uint> output)
{
for (int i = 0; i < numPixels; i++)
{
output[i] = SubPixels(input[idx + i], input[idx + i - 1]);
}
}
[MethodImpl(InliningOptions.ShortMethod)]
public static void PredictorSub2(Span<uint> input, int idx, Span<uint> upper, int numPixels, Span<uint> output)
{
for (int x = 0; x < numPixels; x++)
{
uint pred = Predictor2(upper, x);
output[x] = SubPixels(input[idx + x], pred);
}
}
[MethodImpl(InliningOptions.ShortMethod)]
public static void PredictorSub3(Span<uint> input, int idx, Span<uint> upper, int numPixels, Span<uint> output)
{
for (int x = 0; x < numPixels; x++)
{
uint pred = Predictor3(upper, x);
output[x] = SubPixels(input[idx + x], pred);
}
}
[MethodImpl(InliningOptions.ShortMethod)]
public static void PredictorSub4(Span<uint> input, int idx, Span<uint> upper, int numPixels, Span<uint> output)
{
for (int x = 0; x < numPixels; x++)
{
uint pred = Predictor4(upper, x);
output[x] = SubPixels(input[idx + x], pred);
}
}
[MethodImpl(InliningOptions.ShortMethod)]
public static void PredictorSub5(Span<uint> input, int idx, Span<uint> upper, int numPixels, Span<uint> output)
{
for (int x = 0; x < numPixels; x++)
{
uint pred = Predictor5(input[idx - 1], upper, x);
output[x] = SubPixels(input[idx + x], pred);
}
}
[MethodImpl(InliningOptions.ShortMethod)]
public static void PredictorSub6(Span<uint> input, int idx, Span<uint> upper, int numPixels, Span<uint> output)
{
for (int x = 0; x < numPixels; x++)
{
uint pred = Predictor6(input[idx - 1], upper, x);
output[x] = SubPixels(input[idx + x], pred);
}
}
[MethodImpl(InliningOptions.ShortMethod)]
public static void PredictorSub7(Span<uint> input, int idx, Span<uint> upper, int numPixels, Span<uint> output)
{
for (int x = 0; x < numPixels; x++)
{
uint pred = Predictor7(input[idx - 1], upper, x);
output[x] = SubPixels(input[idx + x], pred);
}
}
[MethodImpl(InliningOptions.ShortMethod)]
public static void PredictorSub8(Span<uint> input, int idx, Span<uint> upper, int numPixels, Span<uint> output)
{
for (int x = 0; x < numPixels; x++)
{
uint pred = Predictor8(upper, x);
output[x] = SubPixels(input[idx + x], pred);
}
}
[MethodImpl(InliningOptions.ShortMethod)]
public static void PredictorSub9(Span<uint> input, int idx, Span<uint> upper, int numPixels, Span<uint> output)
{
for (int x = 0; x < numPixels; x++)
{
uint pred = Predictor9(upper, x);
output[x] = SubPixels(input[idx + x], pred);
}
}
[MethodImpl(InliningOptions.ShortMethod)]
public static void PredictorSub10(Span<uint> input, int idx, Span<uint> upper, int numPixels, Span<uint> output)
{
for (int x = 0; x < numPixels; x++)
{
uint pred = Predictor10(input[idx - 1], upper, x);
output[x] = SubPixels(input[idx + x], pred);
}
}
[MethodImpl(InliningOptions.ShortMethod)]
public static void PredictorSub11(Span<uint> input, int idx, Span<uint> upper, int numPixels, Span<uint> output)
{
for (int x = 0; x < numPixels; x++)
{
uint pred = Predictor11(input[idx - 1], upper, x);
output[x] = SubPixels(input[idx + x], pred);
}
}
[MethodImpl(InliningOptions.ShortMethod)]
public static void PredictorSub12(Span<uint> input, int idx, Span<uint> upper, int numPixels, Span<uint> output)
{
for (int x = 0; x < numPixels; x++)
{
uint pred = Predictor12(input[idx - 1], upper, x);
output[x] = SubPixels(input[idx + x], pred);
}
}
[MethodImpl(InliningOptions.ShortMethod)]
public static void PredictorSub13(Span<uint> input, int idx, Span<uint> upper, int numPixels, Span<uint> output)
{
for (int x = 0; x < numPixels; x++)
{
uint pred = Predictor13(input[idx - 1], upper, x);
output[x] = SubPixels(input[idx + x], pred);
}
}
private static uint ClampedAddSubtractFull(uint c0, uint c1, uint c2)
{
int a = AddSubtractComponentFull(
@ -780,7 +1048,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
/// Sum of each component, mod 256.
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
private static uint AddPixels(uint a, uint b)
public static uint AddPixels(uint a, uint b)
{
uint alphaAndGreen = (a & 0xff00ff00u) + (b & 0xff00ff00u);
uint redAndBlue = (a & 0x00ff00ffu) + (b & 0x00ff00ffu);
@ -800,20 +1068,9 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
}
[MethodImpl(InliningOptions.ShortMethod)]
private static void ColorCodeToMultipliers(uint colorCode, ref Vp8LMultipliers m)
private static sbyte U32ToS8(uint v)
{
m.GreenToRed = (byte)(colorCode & 0xff);
m.GreenToBlue = (byte)((colorCode >> 8) & 0xff);
m.RedToBlue = (byte)((colorCode >> 16) & 0xff);
}
internal struct Vp8LMultipliers
{
public byte GreenToRed;
public byte GreenToBlue;
public byte RedToBlue;
return (sbyte)(v & 0xff);
}
}
}

908
src/ImageSharp/Formats/WebP/Lossless/PredictorEncoder.cs

@ -0,0 +1,908 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the GNU Affero General Public License, Version 3.
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.Formats.WebP.Lossless
{
/// <summary>
/// Image transform methods for the lossless webp encoder.
/// </summary>
internal static class PredictorEncoder
{
private const int GreenRedToBlueNumAxis = 8;
private const int GreenRedToBlueMaxIters = 7;
private const float MaxDiffCost = 1e30f;
private const uint MaskAlpha = 0xff000000;
private const float SpatialPredictorBias = 15.0f;
/// <summary>
/// Finds the best predictor for each tile, and converts the image to residuals
/// with respect to predictions. If nearLosslessQuality < 100, applies
/// near lossless processing, shaving off more bits of residuals for lower qualities.
/// </summary>
public static void ResidualImage(int width, int height, int bits, Span<uint> argb, Span<uint> argbScratch, Span<uint> image, int nearLosslessQuality, bool exact, bool usedSubtractGreen)
{
int tilesPerRow = LosslessUtils.SubSampleSize(width, bits);
int tilesPerCol = LosslessUtils.SubSampleSize(height, bits);
int maxQuantization = 1 << LosslessUtils.NearLosslessBits(nearLosslessQuality);
int[][] histo = new int[4][];
for (int i = 0; i < 4; i++)
{
histo[i] = new int[256];
}
for (int tileY = 0; tileY < tilesPerCol; tileY++)
{
for (int tileX = 0; tileX < tilesPerRow; tileX++)
{
int pred = GetBestPredictorForTile(width, height, tileX, tileY, bits, histo, argbScratch, argb, maxQuantization, exact, usedSubtractGreen, image);
image[(tileY * tilesPerRow) + tileX] = (uint)(WebPConstants.ArgbBlack | (pred << 8));
}
}
CopyImageWithPrediction(width, height, bits, image, argbScratch, argb, maxQuantization, exact, usedSubtractGreen);
}
public static void ColorSpaceTransform(int width, int height, int bits, int quality, Span<uint> argb, Span<uint> image)
{
int maxTileSize = 1 << bits;
int tileXSize = LosslessUtils.SubSampleSize(width, bits);
int tileYSize = LosslessUtils.SubSampleSize(height, bits);
int[] accumulatedRedHisto = new int[256];
int[] accumulatedBlueHisto = new int[256];
var prevX = default(Vp8LMultipliers);
var prevY = default(Vp8LMultipliers);
for (int tileY = 0; tileY < tileYSize; tileY++)
{
for (int tileX = 0; tileX < tileXSize; tileX++)
{
int tileXOffset = tileX * maxTileSize;
int tileYOffset = tileY * maxTileSize;
int allXMax = GetMin(tileXOffset + maxTileSize, width);
int allYMax = GetMin(tileYOffset + maxTileSize, height);
int offset = (tileY * tileXSize) + tileX;
if (tileY != 0)
{
LosslessUtils.ColorCodeToMultipliers(image[offset - tileXSize], ref prevY);
}
prevX = GetBestColorTransformForTile(tileX, tileY, bits,
prevX, prevY,
quality, width, height,
accumulatedRedHisto,
accumulatedBlueHisto,
argb);
image[offset] = MultipliersToColorCode(prevX);
CopyTileWithColorTransform(width, height, tileXOffset, tileYOffset, maxTileSize, prevX, argb);
// Gather accumulated histogram data.
for (int y = tileYOffset; y < allYMax; y++)
{
int ix = (y * width) + tileXOffset;
int ixEnd = ix + allXMax - tileXOffset;
for (; ix < ixEnd; ix++)
{
uint pix = argb[ix];
if (ix >= 2 && pix == argb[ix - 2] && pix == argb[ix - 1])
{
continue; // Repeated pixels are handled by backward references.
}
if (ix >= width + 2 && argb[ix - 2] == argb[ix - width - 2] && argb[ix - 1] == argb[ix - width - 1] && pix == argb[ix - width])
{
continue; // Repeated pixels are handled by backward references.
}
accumulatedRedHisto[(pix >> 16) & 0xff]++;
accumulatedBlueHisto[(pix >> 0) & 0xff]++;
}
}
}
}
}
/// <summary>
/// Returns best predictor and updates the accumulated histogram.
/// If max_quantization > 1, assumes that near lossless processing will be
/// applied, quantizing residuals to multiples of quantization levels up to
/// maxQuantization (the actual quantization level depends on smoothness near
/// the given pixel).
/// </summary>
/// <returns>Best predictor.</returns>
private static int GetBestPredictorForTile(int width, int height, int tileX, int tileY,
int bits, int[][] accumulated, Span<uint> argbScratch, Span<uint> argb,
int maxQuantization, bool exact, bool usedSubtractGreen, Span<uint> modes)
{
const int numPredModes = 14;
int startX = tileX << bits;
int startY = tileY << bits;
int tileSize = 1 << bits;
int maxY = GetMin(tileSize, height - startY);
int maxX = GetMin(tileSize, width - startX);
// Whether there exist columns just outside the tile.
int haveLeft = (startX > 0) ? 1 : 0;
// Position and size of the strip covering the tile and adjacent columns if they exist.
int contextStartX = startX - haveLeft;
int contextWidth = maxX + haveLeft + (maxX < width ? 1 : 0) - startX;
int tilesPerRow = LosslessUtils.SubSampleSize(width, bits);
// Prediction modes of the left and above neighbor tiles.
int leftMode = (int)((tileX > 0) ? (modes[(tileY * tilesPerRow) + tileX - 1] >> 8) & 0xff : 0xff);
int aboveMode = (int)((tileY > 0) ? (modes[((tileY - 1) * tilesPerRow) + tileX] >> 8) & 0xff : 0xff);
// The width of upper_row and current_row is one pixel larger than image width
// to allow the top right pixel to point to the leftmost pixel of the next row
// when at the right edge.
Span<uint> upperRow = argbScratch;
Span<uint> currentRow = upperRow.Slice(width + 1);
Span<byte> maxDiffs = MemoryMarshal.Cast<uint, byte>(currentRow.Slice(width + 1));
float bestDiff = MaxDiffCost;
int bestMode = 0;
uint[] residuals = new uint[1 << WebPConstants.MaxTransformBits];
int[][] histoArgb = new int[4][];
int[][] bestHisto = new int[4][];
for (int i = 0; i < 4; i++)
{
histoArgb[i] = new int[256];
bestHisto[i] = new int[256];
}
for (int mode = 0; mode < numPredModes; mode++)
{
float curDiff;
for (int i = 0; i < 4; i++)
{
histoArgb[i].AsSpan().Fill(0);
}
if (startY > 0)
{
// Read the row above the tile which will become the first upper_row.
// Include a pixel to the left if it exists; include a pixel to the right
// in all cases (wrapping to the leftmost pixel of the next row if it does
// not exist).
Span<uint> src = argb.Slice(((startY - 1) * width) + contextStartX, maxX + haveLeft + 1);
Span<uint> dst = currentRow.Slice(contextStartX);
src.CopyTo(dst);
}
for (int relativeY = 0; relativeY < maxY; relativeY++)
{
int y = startY + relativeY;
Span<uint> tmp = upperRow;
upperRow = currentRow;
currentRow = tmp;
// Read current_row. Include a pixel to the left if it exists; include a
// pixel to the right in all cases except at the bottom right corner of
// the image (wrapping to the leftmost pixel of the next row if it does
// not exist in the current row).
Span<uint> src = argb.Slice((y * width) + contextStartX, maxX + haveLeft + ((y + 1) < height ? 1 : 0));
Span<uint> dst = currentRow.Slice(contextStartX);
src.CopyTo(dst);
if (maxQuantization > 1 && y >= 1 && y + 1 < height)
{
MaxDiffsForRow(contextWidth, width, argb.Slice((y * width) + contextStartX), maxDiffs.Slice(contextStartX), usedSubtractGreen);
}
GetResidual(width, height, upperRow, currentRow, maxDiffs, mode, startX, startX + maxX, y, maxQuantization, exact, usedSubtractGreen, residuals);
for (int relativeX = 0; relativeX < maxX; relativeX++)
{
UpdateHisto(histoArgb, residuals[relativeX]);
}
}
curDiff = PredictionCostSpatialHistogram(accumulated, histoArgb);
// Favor keeping the areas locally similar.
if (mode == leftMode)
{
curDiff -= SpatialPredictorBias;
}
if (mode == aboveMode)
{
curDiff -= SpatialPredictorBias;
}
if (curDiff < bestDiff)
{
for (int i = 0; i < 4; i++)
{
histoArgb[i].AsSpan().CopyTo(bestHisto[i]);
}
bestDiff = curDiff;
bestMode = mode;
}
}
for (int i = 0; i < 4; i++)
{
for (int j = 0; j < 256; j++)
{
accumulated[i][j] += bestHisto[i][j];
}
}
return bestMode;
}
/// <summary>
/// Stores the difference between the pixel and its prediction in "output".
/// In case of a lossy encoding, updates the source image to avoid propagating
/// the deviation further to pixels which depend on the current pixel for their
/// predictions.
/// </summary>
private static void GetResidual(int width, int height, Span<uint> upperRow, Span<uint> currentRow, Span<byte> maxDiffs, int mode, int xStart, int xEnd, int y, int maxQuantization, bool exact, bool usedSubtractGreen, Span<uint> output)
{
if (exact)
{
PredictBatch(mode, xStart, y, xEnd - xStart, currentRow, upperRow, output);
}
else
{
for (int x = xStart; x < xEnd; x++)
{
uint predict = 0;
uint residual;
if (y == 0)
{
predict = (x == 0) ? WebPConstants.ArgbBlack : currentRow[x - 1]; // Left.
}
else if (x == 0)
{
predict = upperRow[x]; // Top.
}
else
{
switch (mode)
{
case 0:
predict = WebPConstants.ArgbBlack;
break;
case 1:
predict = currentRow[x - 1];
break;
case 2:
predict = LosslessUtils.Predictor2(upperRow, x);
break;
case 3:
predict = LosslessUtils.Predictor3(upperRow, x);
break;
case 4:
predict = LosslessUtils.Predictor4(upperRow, x);
break;
case 5:
predict = LosslessUtils.Predictor5(currentRow[x - 1], upperRow, x);
break;
case 6:
predict = LosslessUtils.Predictor6(currentRow[x - 1], upperRow, x);
break;
case 7:
predict = LosslessUtils.Predictor7(currentRow[x - 1], upperRow, x);
break;
case 8:
predict = LosslessUtils.Predictor8(upperRow, x);
break;
case 9:
predict = LosslessUtils.Predictor9(upperRow, x);
break;
case 10:
predict = LosslessUtils.Predictor10(currentRow[x - 1], upperRow, x);
break;
case 11:
predict = LosslessUtils.Predictor11(currentRow[x - 1], upperRow, x);
break;
case 12:
predict = LosslessUtils.Predictor12(currentRow[x - 1], upperRow, x);
break;
case 13:
predict = LosslessUtils.Predictor13(currentRow[x - 1], upperRow, x);
break;
}
}
if (maxQuantization == 1 || mode == 0 || y == 0 || y == height - 1 || x == 0 || x == width - 1)
{
residual = LosslessUtils.SubPixels(currentRow[x], predict);
}
else
{
residual = NearLossless(currentRow[x], predict, maxQuantization, maxDiffs[x], usedSubtractGreen);
// Update the source image.
currentRow[x] = LosslessUtils.AddPixels(predict, residual);
// x is never 0 here so we do not need to update upper_row like below.
}
if ((currentRow[x] & MaskAlpha) == 0)
{
// If alpha is 0, cleanup RGB. We can choose the RGB values of the
// residual for best compression. The prediction of alpha itself can be
// non-zero and must be kept though. We choose RGB of the residual to be
// 0.
residual &= MaskAlpha;
// Update the source image.
currentRow[x] = predict & ~MaskAlpha;
// The prediction for the rightmost pixel in a row uses the leftmost
// pixel
// in that row as its top-right context pixel. Hence if we change the
// leftmost pixel of current_row, the corresponding change must be
// applied
// to upper_row as well where top-right context is being read from.
if (x == 0 && y != 0)
{
upperRow[width] = currentRow[0];
}
}
output[x - xStart] = residual;
}
}
}
/// <summary>
/// Quantize every component of the difference between the actual pixel value and
/// its prediction to a multiple of a quantization (a power of 2, not larger than
/// maxQuantization which is a power of 2, smaller than maxDiff). Take care if
/// value and predict have undergone subtract green, which means that red and
/// blue are represented as offsets from green.
/// </summary>
private static uint NearLossless(uint value, uint predict, int maxQuantization, int maxDiff, bool usedSubtractGreen)
{
int quantization;
byte newGreen = 0;
byte greenDiff = 0;
byte a, r, g, b;
if (maxDiff <= 2)
{
return LosslessUtils.SubPixels(value, predict);
}
quantization = maxQuantization;
while (quantization >= maxDiff)
{
quantization >>= 1;
}
if ((value >> 24) == 0 || (value >> 24) == 0xff)
{
// Preserve transparency of fully transparent or fully opaque pixels.
a = NearLosslessDiff((byte)((value >> 24) & 0xff), (byte)((predict >> 24) & 0xff));
}
else
{
a = NearLosslessComponent((byte)(value >> 24), (byte)(predict >> 24), 0xff, quantization);
}
g = NearLosslessComponent((byte)((value >> 8) & 0xff), (byte)((predict >> 8) & 0xff), 0xff, quantization);
if (usedSubtractGreen)
{
// The green offset will be added to red and blue components during decoding
// to obtain the actual red and blue values.
newGreen = (byte)(((predict >> 8) + g) & 0xff);
// The amount by which green has been adjusted during quantization. It is
// subtracted from red and blue for compensation, to avoid accumulating two
// quantization errors in them.
greenDiff = NearLosslessDiff(newGreen, (byte)((value >> 8) & 0xff));
}
r = NearLosslessComponent(NearLosslessDiff((byte)((value >> 16) & 0xff), greenDiff), (byte)((predict >> 16) & 0xff), (byte)(0xff - newGreen), quantization);
b = NearLosslessComponent(NearLosslessDiff((byte)(value & 0xff), greenDiff), (byte)(predict & 0xff), (byte)(0xff - newGreen), quantization);
return ((uint)a << 24) | ((uint)r << 16) | ((uint)g << 8) | b;
}
/// <summary>
/// Quantize the difference between the actual component value and its prediction
/// to a multiple of quantization, working modulo 256, taking care not to cross
/// a boundary (inclusive upper limit).
/// </summary>
private static byte NearLosslessComponent(byte value, byte predict, byte boundary, int quantization)
{
int residual = (value - predict) & 0xff;
int boundaryResidual = (boundary - predict) & 0xff;
int lower = residual & ~(quantization - 1);
int upper = lower + quantization;
// Resolve ties towards a value closer to the prediction (i.e. towards lower
// if value comes after prediction and towards upper otherwise).
int bias = ((boundary - value) & 0xff) < boundaryResidual ? 1 : 0;
if (residual - lower < upper - residual + bias)
{
// lower is closer to residual than upper.
if (residual > boundaryResidual && lower <= boundaryResidual)
{
// Halve quantization step to avoid crossing boundary. This midpoint is
// on the same side of boundary as residual because midpoint >= residual
// (since lower is closer than upper) and residual is above the boundary.
return (byte)(lower + (quantization >> 1));
}
return (byte)lower;
}
else
{
// upper is closer to residual than lower.
if (residual <= boundaryResidual && upper > boundaryResidual)
{
// Halve quantization step to avoid crossing boundary. This midpoint is
// on the same side of boundary as residual because midpoint <= residual
// (since upper is closer than lower) and residual is below the boundary.
return (byte)(lower + (quantization >> 1));
}
return (byte)(upper & 0xff);
}
}
/// <summary>
/// Converts pixels of the image to residuals with respect to predictions.
/// If max_quantization > 1, applies near lossless processing, quantizing
/// residuals to multiples of quantization levels up to max_quantization
/// (the actual quantization level depends on smoothness near the given pixel).
/// </summary>
private static void CopyImageWithPrediction(int width, int height, int bits, Span<uint> modes, Span<uint> argbScratch, Span<uint> argb, int maxQuantization, bool exact, bool usedSubtractGreen)
{
int tilesPerRow = LosslessUtils.SubSampleSize(width, bits);
// The width of upper_row and current_row is one pixel larger than image width
// to allow the top right pixel to point to the leftmost pixel of the next row
// when at the right edge.
Span<uint> upperRow = argbScratch;
Span<uint> currentRow = upperRow.Slice(width + 1);
Span<byte> currentMaxDiffs = MemoryMarshal.Cast<uint, byte>(currentRow.Slice(width + 1));
Span<byte> lowerMaxDiffs = currentMaxDiffs.Slice(width);
for (int y = 0; y < height; y++)
{
Span<uint> tmp32 = upperRow;
upperRow = currentRow;
currentRow = tmp32;
argb.Slice(y * width, width + y + (1 < height ? 1 : 0)).CopyTo(currentRow);
if (maxQuantization > 1)
{
// Compute max_diffs for the lower row now, because that needs the
// contents of argb for the current row, which we will overwrite with
// residuals before proceeding with the next row.
Span<byte> tmp8 = currentMaxDiffs;
currentMaxDiffs = lowerMaxDiffs;
lowerMaxDiffs = tmp8;
if (y + 2 < height)
{
MaxDiffsForRow(width, width, argb.Slice((y + 1) * width), lowerMaxDiffs, usedSubtractGreen);
}
}
for (int x = 0; x < width;)
{
int mode = (int)((modes[((y >> bits) * tilesPerRow) + (x >> bits)] >> 8) & 0xff);
int xEnd = x + (1 << bits);
if (xEnd > width)
{
xEnd = width;
}
GetResidual(width, height, upperRow, currentRow, currentMaxDiffs,
mode, x, xEnd, y, maxQuantization, exact,
usedSubtractGreen, argb.Slice((y * width) + x));
x = xEnd;
}
}
}
private static void PredictBatch(int mode, int xStart, int y, int numPixels, Span<uint> current, Span<uint> upper, Span<uint> output)
{
if (xStart == 0)
{
if (y == 0)
{
// ARGB_BLACK.
LosslessUtils.PredictorSub0(current, 1, output);
}
else
{
// Top one.
LosslessUtils.PredictorSub2(current, 0, upper, 1, output);
}
xStart++;
output = output.Slice(1);
numPixels--;
}
if (y == 0)
{
// Left one.
LosslessUtils.PredictorSub1(current, xStart, numPixels, output);
}
else
{
switch (mode)
{
case 0:
LosslessUtils.PredictorSub0(current, numPixels, output);
break;
case 1:
LosslessUtils.PredictorSub1(current, xStart, numPixels, output);
break;
case 2:
LosslessUtils.PredictorSub2(current, xStart, upper.Slice(xStart), numPixels, output);
break;
case 3:
LosslessUtils.PredictorSub3(current, xStart, upper.Slice(xStart), numPixels, output);
break;
case 4:
LosslessUtils.PredictorSub4(current, xStart, upper.Slice(xStart), numPixels, output);
break;
case 5:
LosslessUtils.PredictorSub5(current, xStart, upper.Slice(xStart), numPixels, output);
break;
case 6:
LosslessUtils.PredictorSub6(current, xStart, upper.Slice(xStart), numPixels, output);
break;
case 7:
LosslessUtils.PredictorSub7(current, xStart, upper.Slice(xStart), numPixels, output);
break;
case 8:
LosslessUtils.PredictorSub8(current, xStart, upper.Slice(xStart), numPixels, output);
break;
case 9:
LosslessUtils.PredictorSub9(current, xStart, upper.Slice(xStart), numPixels, output);
break;
case 10:
LosslessUtils.PredictorSub10(current, xStart, upper.Slice(xStart), numPixels, output);
break;
case 11:
LosslessUtils.PredictorSub11(current, xStart, upper.Slice(xStart), numPixels, output);
break;
case 12:
LosslessUtils.PredictorSub12(current, xStart, upper.Slice(xStart), numPixels, output);
break;
case 13:
LosslessUtils.PredictorSub13(current, xStart, upper.Slice(xStart), numPixels, output);
break;
}
}
}
private static void MaxDiffsForRow(int width, int stride, Span<uint> argb, Span<byte> maxDiffs, bool usedSubtractGreen)
{
if (width <= 2)
{
return;
}
uint current = argb[0];
uint right = argb[1];
if (usedSubtractGreen)
{
current = AddGreenToBlueAndRed(current);
right = AddGreenToBlueAndRed(right);
}
for (int x = 1; x < width - 1; x++)
{
uint up = argb[-stride + x]; // TODO: -stride!
uint down = argb[stride + x];
uint left = current;
current = right;
right = argb[x + 1];
if (usedSubtractGreen)
{
up = AddGreenToBlueAndRed(up);
down = AddGreenToBlueAndRed(down);
right = AddGreenToBlueAndRed(right);
}
maxDiffs[x] = (byte)MaxDiffAroundPixel(current, up, down, left, right);
}
}
private static int MaxDiffBetweenPixels(uint p1, uint p2)
{
int diffA = Math.Abs((int)(p1 >> 24) - (int)(p2 >> 24));
int diffR = Math.Abs((int)((p1 >> 16) & 0xff) - (int)((p2 >> 16) & 0xff));
int diffG = Math.Abs((int)((p1 >> 8) & 0xff) - (int)((p2 >> 8) & 0xff));
int diffB = Math.Abs((int)(p1 & 0xff) - (int)(p2 & 0xff));
return GetMax(GetMax(diffA, diffR), GetMax(diffG, diffB));
}
private static int MaxDiffAroundPixel(uint current, uint up, uint down, uint left, uint right)
{
int diffUp = MaxDiffBetweenPixels(current, up);
int diffDown = MaxDiffBetweenPixels(current, down);
int diffLeft = MaxDiffBetweenPixels(current, left);
int diffRight = MaxDiffBetweenPixels(current, right);
return GetMax(GetMax(diffUp, diffDown), GetMax(diffLeft, diffRight));
}
private static void UpdateHisto(int[][] histoArgb, uint argb)
{
++histoArgb[0][argb >> 24];
++histoArgb[1][(argb >> 16) & 0xff];
++histoArgb[2][(argb >> 8) & 0xff];
++histoArgb[3][argb & 0xff];
}
private static uint AddGreenToBlueAndRed(uint argb)
{
uint green = (argb >> 8) & 0xff;
uint redBlue = argb & 0x00ff00ffu;
redBlue += (green << 16) | green;
redBlue &= 0x00ff00ffu;
return (argb & 0xff00ff00u) | redBlue;
}
private static void CopyTileWithColorTransform(int xSize, int ySize, int tileX, int tileY, int maxTileSize, Vp8LMultipliers colorTransform, Span<uint> argb)
{
int xScan = GetMin(maxTileSize, xSize - tileX);
int yScan = GetMin(maxTileSize, ySize - tileY);
argb = argb.Slice((tileY * xSize) + tileX);
while (yScan-- > 0)
{
LosslessUtils.TransformColor(colorTransform, argb, xScan);
argb = argb.Slice(xSize);
}
}
private static Vp8LMultipliers GetBestColorTransformForTile(int tile_x, int tile_y, int bits, Vp8LMultipliers prevX, Vp8LMultipliers prevY, int quality, int xSize, int ySize, int[] accumulatedRedHisto, int[] accumulatedBlueHisto, Span<uint> argb)
{
int maxTileSize = 1 << bits;
int tileYOffset = tile_y * maxTileSize;
int tileXOffset = tile_x * maxTileSize;
int allXMax = GetMin(tileXOffset + maxTileSize, xSize);
int allYMax = GetMin(tileYOffset + maxTileSize, ySize);
int tileWidth = allXMax - tileXOffset;
int tileHeight = allYMax - tileYOffset;
Span<uint> tileArgb = argb.Slice((tileYOffset * xSize) + tileXOffset);
var bestTx = default(Vp8LMultipliers);
GetBestGreenToRed(tileArgb, xSize, tileWidth, tileHeight, prevX, prevY, quality, accumulatedRedHisto, ref bestTx);
GetBestGreenRedToBlue(tileArgb, xSize, tileWidth, tileHeight, prevX, prevY, quality, accumulatedBlueHisto, ref bestTx);
return bestTx;
}
private static void GetBestGreenToRed(Span<uint> argb, int stride, int tileWidth, int tileHeight, Vp8LMultipliers prevX, Vp8LMultipliers prevY, int quality, int[] accumulatedRedHisto, ref Vp8LMultipliers bestTx)
{
int maxIters = 4 + ((7 * quality) >> 8); // in range [4..6]
int greenToRedBest = 0;
float bestDiff = GetPredictionCostCrossColorRed(argb, stride, tileWidth, tileHeight, prevX, prevY, greenToRedBest, accumulatedRedHisto);
for (int iter = 0; iter < maxIters; iter++)
{
// ColorTransformDelta is a 3.5 bit fixed point, so 32 is equal to
// one in color computation. Having initial delta here as 1 is sufficient
// to explore the range of (-2, 2).
int delta = 32 >> iter;
// Try a negative and a positive delta from the best known value.
for (int offset = -delta; offset <= delta; offset += 2 * delta)
{
int greenToRedCur = offset + greenToRedBest;
float curDiff = GetPredictionCostCrossColorRed(argb, stride, tileWidth, tileHeight, prevX, prevY, greenToRedCur, accumulatedRedHisto);
if (curDiff < bestDiff)
{
bestDiff = curDiff;
greenToRedBest = greenToRedCur;
}
}
}
bestTx.GreenToRed = (byte)(greenToRedBest & 0xff);
}
private static void GetBestGreenRedToBlue(Span<uint> argb, int stride, int tileWidth, int tileHeight, Vp8LMultipliers prevX, Vp8LMultipliers prevY, int quality, int[] accumulatedBlueHisto, ref Vp8LMultipliers bestTx)
{
int iters = (quality < 25) ? 1 : (quality > 50) ? GreenRedToBlueMaxIters : 4;
int greenToBlueBest = 0;
int redToBlueBest = 0;
sbyte[][] offset = { new sbyte[] { 0, -1 }, new sbyte[] { 0, 1 }, new sbyte[] { -1, 0 }, new sbyte[] { 1, 0 }, new sbyte[] { -1, -1 }, new sbyte[] { -1, 1 }, new sbyte[] { 1, -1 }, new sbyte[] { 1, 1 } };
sbyte[] deltaLut = { 16, 16, 8, 4, 2, 2, 2 };
// Initial value at origin:
float bestDiff = GetPredictionCostCrossColorBlue(argb, stride, tileWidth, tileHeight, prevX, prevY, greenToBlueBest, redToBlueBest, accumulatedBlueHisto);
for (int iter = 0; iter < iters; iter++)
{
int delta = deltaLut[iter];
for (int axis = 0; axis < GreenRedToBlueNumAxis; axis++)
{
int greenToBlueCur = (offset[axis][0] * delta) + greenToBlueBest;
int redToBlueCur = (offset[axis][1] * delta) + redToBlueBest;
float curDiff = GetPredictionCostCrossColorBlue(argb, stride, tileWidth, tileHeight, prevX, prevY, greenToBlueCur, redToBlueCur, accumulatedBlueHisto);
if (curDiff < bestDiff)
{
bestDiff = curDiff;
greenToBlueBest = greenToBlueCur;
redToBlueBest = redToBlueCur;
}
if (quality < 25 && iter == 4)
{
// Only axis aligned diffs for lower quality.
break; // next iter.
}
}
if (delta == 2 && greenToBlueBest == 0 && redToBlueBest == 0)
{
// Further iterations would not help.
break; // out of iter-loop.
}
}
bestTx.GreenToBlue = (byte)(greenToBlueBest & 0xff);
bestTx.RedToBlue = (byte)(redToBlueBest & 0xff);
}
private static float GetPredictionCostCrossColorRed(Span<uint> argb, int stride, int tileWidth, int tileHeight, Vp8LMultipliers prevX, Vp8LMultipliers prevY, int greenToRed, int[] accumulatedRedHisto)
{
int[] histo = new int[256];
CollectColorRedTransforms(argb, stride, tileWidth, tileHeight, greenToRed, histo);
float curDiff = PredictionCostCrossColor(accumulatedRedHisto, histo);
if ((byte)greenToRed == prevX.GreenToRed)
{
curDiff -= 3; // Favor keeping the areas locally similar.
}
if ((byte)greenToRed == prevY.GreenToRed)
{
curDiff -= 3; // Favor keeping the areas locally similar.
}
if (greenToRed == 0)
{
curDiff -= 3;
}
return curDiff;
}
private static float GetPredictionCostCrossColorBlue(Span<uint> argb, int stride, int tileWidth, int tileHeight, Vp8LMultipliers prevX, Vp8LMultipliers prevY, int greenToBlue, int redToBlue, int[] accumulatedBlueHisto)
{
int[] histo = new int[256];
CollectColorBlueTransforms(argb, stride, tileWidth, tileHeight, greenToBlue, redToBlue, histo);
float curDiff = PredictionCostCrossColor(accumulatedBlueHisto, histo);
if ((byte)greenToBlue == prevX.GreenToBlue)
{
curDiff -= 3; // Favor keeping the areas locally similar.
}
if ((byte)greenToBlue == prevY.GreenToBlue)
{
curDiff -= 3; // Favor keeping the areas locally similar.
}
if ((byte)redToBlue == prevX.RedToBlue)
{
curDiff -= 3; // Favor keeping the areas locally similar.
}
if ((byte)redToBlue == prevY.RedToBlue)
{
curDiff -= 3; // Favor keeping the areas locally similar.
}
if (greenToBlue == 0)
{
curDiff -= 3;
}
if (redToBlue == 0)
{
curDiff -= 3;
}
return curDiff;
}
private static void CollectColorRedTransforms(Span<uint> argb, int stride, int tileWidth, int tileHeight, int greenToRed, int[] histo)
{
int pos = 0;
while (tileHeight-- > 0)
{
for (int x = 0; x < tileWidth; x++)
{
++histo[LosslessUtils.TransformColorRed((sbyte)greenToRed, argb[pos + x])];
}
pos += stride;
}
}
private static void CollectColorBlueTransforms(Span<uint> argb, int stride, int tileWidth, int tileHeight, int greenToBlue, int redToBlue, int[] histo)
{
int pos = 0;
while (tileHeight-- > 0)
{
for (int x = 0; x < tileWidth; x++)
{
++histo[LosslessUtils.TransformColorBlue((sbyte)greenToBlue, (sbyte)redToBlue, argb[pos + x])];
}
pos += stride;
}
}
private static float PredictionCostSpatialHistogram(int[][] accumulated, int[][] tile)
{
double retVal = 0.0d;
for (int i = 0; i < 4; i++)
{
double kExpValue = 0.94;
retVal += PredictionCostSpatial(tile[i], 1, kExpValue);
retVal += LosslessUtils.CombinedShannonEntropy(tile[i], accumulated[i]);
}
return (float)retVal;
}
private static float PredictionCostCrossColor(int[] accumulated, int[] counts)
{
// Favor low entropy, locally and globally.
// Favor small absolute values for PredictionCostSpatial.
const double expValue = 2.4d;
return LosslessUtils.CombinedShannonEntropy(counts, accumulated) + PredictionCostSpatial(counts, 3, expValue);
}
private static float PredictionCostSpatial(int[] counts, int weight0, double expVal)
{
int significantSymbols = 256 >> 4;
double expDecayFactor = 0.6;
double bits = weight0 * counts[0];
for (int i = 1; i < significantSymbols; i++)
{
bits += expVal * (counts[i] + counts[256 - i]);
expVal *= expDecayFactor;
}
return (float)(-0.1 * bits);
}
[MethodImpl(InliningOptions.ShortMethod)]
private static byte NearLosslessDiff(byte a, byte b)
{
return (byte)((a - b) & 0xff);
}
[MethodImpl(InliningOptions.ShortMethod)]
private static uint MultipliersToColorCode(Vp8LMultipliers m)
{
return 0xff000000u | ((uint)m.RedToBlue << 16) | ((uint)m.GreenToBlue << 8) | m.GreenToRed;
}
[MethodImpl(InliningOptions.ShortMethod)]
private static int GetMin(int a, int b)
{
return (a > b) ? b : a;
}
[MethodImpl(InliningOptions.ShortMethod)]
private static int GetMax(int a, int b)
{
return (a < b) ? b : a;
}
}
}

58
src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs

@ -22,15 +22,25 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
/// </summary>
private const int MinBlockSize = 256;
private MemoryAllocator memoryAllocator;
/// <summary>
/// Initializes a new instance of the <see cref="Vp8LEncoder"/> class.
/// </summary>
/// <param name="memoryAllocator">The memory allocator.</param>
/// <param name="width">The width of the input image.</param>
/// <param name="height">The height of the input image.</param>
public Vp8LEncoder(MemoryAllocator memoryAllocator, int width, int height)
{
var pixelCount = width * height;
this.Bgra = memoryAllocator.Allocate<uint>(pixelCount);
this.Palette = memoryAllocator.Allocate<uint>(WebPConstants.MaxPaletteSize);
this.Refs = new Vp8LBackwardRefs[3];
this.HashChain = new Vp8LHashChain(pixelCount);
this.memoryAllocator = memoryAllocator;
// We round the block size up, so we're guaranteed to have at most MAX_REFS_BLOCK_PER_IMAGE blocks used:
// We round the block size up, so we're guaranteed to have at most MaxRefsBlockPerImage blocks used:
int refsBlockSize = ((pixelCount - 1) / MaxRefsBlockPerImage) + 1;
for (int i = 0; i < this.Refs.Length; ++i)
{
@ -39,6 +49,21 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
}
}
/// <summary>
/// Gets transformed image data.
/// </summary>
public IMemoryOwner<uint> Bgra { get; }
/// <summary>
/// Gets the scratch memory for bgra rows used for prediction.
/// </summary>
public IMemoryOwner<uint> BgraScratch { get; set; }
/// <summary>
/// Gets or sets the packed image width.
/// </summary>
public int CurrentWidth { get; set; }
/// <summary>
/// Gets or sets the huffman image bits.
/// </summary>
@ -50,9 +75,14 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
public int TransformBits { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to use a color cache.
/// Gets or sets the transform data.
/// </summary>
public bool UseColorCache { get; set; }
public IMemoryOwner<uint> TransformData { get; set; }
/// <summary>
/// Gets or sets the cache bits. If equal to 0, don't use color cache.
/// </summary>
public int CacheBits { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to use the cross color transform.
@ -84,14 +114,36 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
/// </summary>
public IMemoryOwner<uint> Palette { get; }
/// <summary>
/// Gets the backward references.
/// </summary>
public Vp8LBackwardRefs[] Refs { get; }
/// <summary>
/// Gets the hash chain.
/// </summary>
public Vp8LHashChain HashChain { get; }
public void AllocateTransformBuffer(int width, int height)
{
int imageSize = width * height;
// VP8LResidualImage needs room for 2 scanlines of uint32 pixels with an extra
// pixel in each, plus 2 regular scanlines of bytes.
int argbScratchSize = this.UsePredictorTransform ? ((width + 1) * 2) + (((width * 2) + 4 - 1) / 4) : 0;
int transformDataSize = (this.UsePredictorTransform || this.UseCrossColorTransform) ? LosslessUtils.SubSampleSize(width, this.TransformBits) * LosslessUtils.SubSampleSize(height, this.TransformBits) : 0;
this.BgraScratch = this.memoryAllocator.Allocate<uint>(argbScratchSize);
this.TransformData = this.memoryAllocator.Allocate<uint>(transformDataSize);
}
/// <inheritdoc/>
public void Dispose()
{
this.Bgra.Dispose();
this.BgraScratch.Dispose();
this.Palette.Dispose();
this.TransformData.Dispose();
}
}
}

14
src/ImageSharp/Formats/WebP/Lossless/Vp8LMultipliers.cs

@ -0,0 +1,14 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the GNU Affero General Public License, Version 3.
namespace SixLabors.ImageSharp.Formats.WebP.Lossless
{
internal struct Vp8LMultipliers
{
public byte GreenToRed;
public byte GreenToBlue;
public byte RedToBlue;
}
}

5
src/ImageSharp/Formats/WebP/WebPConstants.cs

@ -102,6 +102,11 @@ namespace SixLabors.ImageSharp.Formats.WebP
/// </summary>
public const int MaxNumberOfTransforms = 4;
/// <summary>
/// Maximum value of transformBits in VP8LEncoder.
/// </summary>
public const int MaxTransformBits = 6;
/// <summary>
/// The bit to be written when next data to be read is a transform.
/// </summary>

318
src/ImageSharp/Formats/WebP/WebPEncoderCore.cs

@ -35,6 +35,12 @@ namespace SixLabors.ImageSharp.Formats.WebP
/// </summary>
private Vp8LBitWriter bitWriter;
private const int ApplyPaletteGreedyMax = 4;
private const int PaletteInvSizeBits = 11;
private const int PaletteInvSize = 1 << PaletteInvSizeBits;
/// <summary>
/// Initializes a new instance of the <see cref="WebPEncoderCore"/> class.
/// </summary>
@ -119,6 +125,9 @@ namespace SixLabors.ImageSharp.Formats.WebP
private void EncoderAnalyze<TPixel>(Image<TPixel> image, Vp8LEncoder enc)
where TPixel : unmanaged, IPixel<TPixel>
{
int method = 4; // TODO: method hardcoded to 4 for now.
int quality = 100; // TODO: quality is hardcoded for now.
bool useCache = true; // TODO: useCache is hardcoded for now.
int width = image.Width;
int height = image.Height;
@ -126,7 +135,6 @@ namespace SixLabors.ImageSharp.Formats.WebP
var usePalette = this.AnalyzeAndCreatePalette(image, enc);
// Empirical bit sizes.
int method = 4; // TODO: method hardcoded to 4 for now.
enc.HistoBits = GetHistoBits(method, usePalette, width, height);
enc.TransformBits = GetTransformBits(method, enc.HistoBits);
@ -151,13 +159,42 @@ namespace SixLabors.ImageSharp.Formats.WebP
enc.UseSubtractGreenTransform = (entropyIdx == EntropyIx.SubGreen) || (entropyIdx == EntropyIx.SpatialSubGreen);
enc.UsePredictorTransform = (entropyIdx == EntropyIx.Spatial) || (entropyIdx == EntropyIx.SpatialSubGreen);
enc.UseCrossColorTransform = redAndBlueAlwaysZero ? false : enc.UsePredictorTransform;
enc.UseColorCache = false;
enc.CacheBits = 0;
// Encode palette.
if (enc.UsePalette)
{
this.EncodePalette(image, bgra, enc);
this.MapImageFromPalette(enc, width, height);
// If using a color cache, do not have it bigger than the number of
// colors.
if (useCache && enc.PaletteSize < (1 << WebPConstants.MaxColorCacheBits))
{
enc.CacheBits = WebPCommonUtils.BitsLog2Floor((uint)enc.PaletteSize) + 1;
}
}
// Apply transforms and write transform data.
if (enc.UseSubtractGreenTransform)
{
this.ApplySubtractGreen(enc, enc.CurrentWidth, height);
}
if (enc.UsePredictorTransform)
{
this.ApplyPredictFilter(enc, enc.CurrentWidth, height, quality, enc.UseSubtractGreenTransform);
}
if (enc.UseCrossColorTransform)
{
this.ApplyCrossColorFilter(enc, enc.CurrentWidth, height, quality);
}
this.bitWriter.PutBits(0, 1); // No more transforms.
// Encode and write the transformed image.
//EncodeImageInternal();
}
/// <summary>
@ -183,6 +220,51 @@ namespace SixLabors.ImageSharp.Formats.WebP
this.EncodeImageNoHuffman(tmpPalette, enc.HashChain, enc.Refs[0], enc.Refs[1], width: paletteSize, height: 1, quality: 20);
}
/// <summary>
/// Applies the substract green transformation to the pixel data of the image.
/// </summary>
/// <param name="enc">The VP8 Encoder.</param>
/// <param name="width">The width of the image.</param>
/// <param name="height">The height of the image.</param>
private void ApplySubtractGreen(Vp8LEncoder enc, int width, int height)
{
this.bitWriter.PutBits(WebPConstants.TransformPresent, 1);
this.bitWriter.PutBits((uint)Vp8LTransformType.SubtractGreen, 2);
LosslessUtils.SubtractGreenFromBlueAndRed(enc.Bgra.GetSpan(), width * height);
}
private void ApplyPredictFilter(Vp8LEncoder enc, int width, int height, int quality, bool usedSubtractGreen)
{
int nearLosslessStrength = 100; // TODO: for now always 100
bool exact = true; // TODO: always true for now.
int predBits = enc.TransformBits;
int transformWidth = LosslessUtils.SubSampleSize(width, predBits);
int transformHeight = LosslessUtils.SubSampleSize(height, predBits);
PredictorEncoder.ResidualImage(width, height, predBits, enc.Bgra.GetSpan(), enc.BgraScratch.GetSpan(), enc.TransformData.GetSpan(), nearLosslessStrength, exact, usedSubtractGreen);
this.bitWriter.PutBits(WebPConstants.TransformPresent, 1);
this.bitWriter.PutBits((uint)Vp8LTransformType.PredictorTransform, 2);
this.bitWriter.PutBits((uint)(predBits - 2), 3);
this.EncodeImageNoHuffman(enc.TransformData.GetSpan(), enc.HashChain, enc.Refs[0], enc.Refs[1], transformWidth, transformHeight, quality);
}
private void ApplyCrossColorFilter(Vp8LEncoder enc, int width, int height, int quality)
{
int colorTransformBits = enc.TransformBits;
int transformWidth = LosslessUtils.SubSampleSize(width, colorTransformBits);
int transformHeight = LosslessUtils.SubSampleSize(height, colorTransformBits);
PredictorEncoder.ColorSpaceTransform(width, height, colorTransformBits, quality, enc.Bgra.GetSpan(), enc.TransformData.GetSpan());
this.bitWriter.PutBits(WebPConstants.TransformPresent, 1);
this.bitWriter.PutBits((uint)Vp8LTransformType.CrossColorTransform, 2);
this.bitWriter.PutBits((uint)(colorTransformBits - 2), 3);
this.EncodeImageNoHuffman(enc.TransformData.GetSpan(), enc.HashChain, enc.Refs[0], enc.Refs[1], transformWidth, transformHeight, quality);
}
private void EncodeImageNoHuffman(Span<uint> bgra, Vp8LHashChain hashChain, Vp8LBackwardRefs refsTmp1, Vp8LBackwardRefs refsTmp2, int width, int height, int quality)
{
int cacheBits = 0;
@ -239,7 +321,7 @@ namespace SixLabors.ImageSharp.Formats.WebP
}
var tokens = new HuffmanTreeToken[maxTokens];
for(int i = 0; i < tokens.Length; i++)
for (int i = 0; i < tokens.Length; i++)
{
tokens[i] = new HuffmanTreeToken();
}
@ -721,6 +803,208 @@ namespace SixLabors.ImageSharp.Formats.WebP
return colors.Count;
}
private void MapImageFromPalette(Vp8LEncoder enc, int width, int height)
{
Span<uint> src = enc.Bgra.GetSpan();
int srcStride = enc.CurrentWidth;
Span<uint> dst = enc.Bgra.GetSpan(); // Applying the palette will be done in place.
Span<uint> palette = enc.Palette.GetSpan();
int paletteSize = enc.PaletteSize;
int xBits;
// Replace each input pixel by corresponding palette index.
// This is done line by line.
if (paletteSize <= 4)
{
xBits = (paletteSize <= 2) ? 3 : 2;
}
else
{
xBits = (paletteSize <= 16) ? 1 : 0;
}
enc.AllocateTransformBuffer(LosslessUtils.SubSampleSize(width, xBits), height);
this.ApplyPalette(src, srcStride, dst, enc.CurrentWidth, palette, paletteSize, width, height, xBits);
}
/// <summary>
/// Remap argb values in src[] to packed palettes entries in dst[]
/// using 'row' as a temporary buffer of size 'width'.
/// We assume that all src[] values have a corresponding entry in the palette.
/// Note: src[] can be the same as dst[]
/// </summary>
private void ApplyPalette(Span<uint> src, int srcStride, Span<uint> dst, int dstStride, Span<uint> palette, int paletteSize, int width, int height, int xBits)
{
using System.Buffers.IMemoryOwner<byte> tmpRowBuffer = this.memoryAllocator.Allocate<byte>(width);
Span<byte> tmpRow = tmpRowBuffer.GetSpan();
if (paletteSize < ApplyPaletteGreedyMax)
{
// TODO: APPLY_PALETTE_FOR(SearchColorGreedy(palette, palette_size, pix));
}
else
{
uint[] buffer = new uint[PaletteInvSize];
// Try to find a perfect hash function able to go from a color to an index
// within 1 << PaletteInvSize in order to build a hash map to go from color to index in palette.
int i;
for (i = 0; i < 3; i++)
{
bool useLUT = true;
// Set each element in buffer to max value.
buffer.AsSpan().Fill(uint.MaxValue);
for (int j = 0; j < paletteSize; j++)
{
uint ind = 0;
switch (i)
{
case 0:
ind = ApplyPaletteHash0(palette[j]);
break;
case 1:
ind = ApplyPaletteHash1(palette[j]);
break;
case 2:
ind = ApplyPaletteHash2(palette[j]);
break;
}
if (buffer[ind] != uint.MaxValue)
{
useLUT = false;
break;
}
else
{
buffer[ind] = (uint)j;
}
}
if (useLUT)
{
break;
}
}
if (i == 0 || i == 1 || i == 2)
{
ApplyPaletteFor(width, height, palette, i, src, srcStride, dst, dstStride, tmpRow, buffer, xBits);
}
else
{
uint[] idxMap = new uint[paletteSize];
uint[] paletteSorted = new uint[paletteSize];
PrepareMapToPalette(palette, paletteSize, paletteSorted, idxMap);
ApplyPaletteForWithIdxMap(width, height, palette, src, srcStride, dst, dstStride, tmpRow, idxMap, xBits, paletteSorted, paletteSize);
}
}
}
private static void ApplyPaletteFor(int width, int height, Span<uint> palette, int hashIdx, Span<uint> src, int srcStride, Span<uint> dst, int dstStride, Span<byte> tmpRow, uint[] buffer, int xBits)
{
uint prevPix = palette[0];
uint prevIdx = 0;
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
uint pix = src[x];
if (pix != prevPix)
{
switch (hashIdx)
{
case 0:
prevIdx = buffer[ApplyPaletteHash0(pix)];
break;
case 1:
prevIdx = buffer[ApplyPaletteHash1(pix)];
break;
case 2:
prevIdx = buffer[ApplyPaletteHash2(pix)];
break;
}
prevPix = pix;
}
tmpRow[x] = (byte)prevIdx;
}
LosslessUtils.BundleColorMap(tmpRow, width, xBits, dst);
src = src.Slice((int)srcStride);
dst = dst.Slice((int)dstStride);
}
}
private static void ApplyPaletteForWithIdxMap(int width, int height, Span<uint> palette, Span<uint> src, int srcStride, Span<uint> dst, int dstStride, Span<byte> tmpRow, uint[] idxMap, int xBits, uint[] paletteSorted, int paletteSize)
{
uint prevPix = palette[0];
uint prevIdx = 0;
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
uint pix = src[x];
if (pix != prevPix)
{
prevIdx = idxMap[SearchColorNoIdx(paletteSorted, pix, paletteSize)];
prevPix = pix;
}
tmpRow[x] = (byte)prevIdx;
}
LosslessUtils.BundleColorMap(tmpRow, width, xBits, dst);
src = src.Slice((int)srcStride);
dst = dst.Slice((int)dstStride);
}
}
/// <summary>
/// Sort palette in increasing order and prepare an inverse mapping array.
/// </summary>
private static void PrepareMapToPalette(Span<uint> palette, int numColors, uint[] sorted, uint[] idxMap)
{
palette.Slice(numColors).CopyTo(sorted);
Array.Sort(sorted, PaletteCompareColorsForSort);
for (int i = 0; i < numColors; i++)
{
idxMap[SearchColorNoIdx(sorted, palette[i], numColors)] = (uint)i;
}
}
private static int SearchColorNoIdx(uint[] sorted, uint color, int hi)
{
int low = 0;
if (sorted[low] == color)
{
return low; // loop invariant: sorted[low] != color
}
while (true)
{
int mid = (low + hi) >> 1;
if (sorted[mid] == color)
{
return mid;
}
else if (sorted[mid] < color)
{
low = mid;
}
else
{
hi = mid;
}
}
}
private static void ClearHuffmanTreeIfOnlyOneSymbol(HuffmanTreeCode huffmanCode)
{
int count = 0;
@ -944,6 +1228,28 @@ namespace SixLabors.ImageSharp.Formats.WebP
b[(int)((p >> 0) - green) & 0xff]++;
}
[MethodImpl(InliningOptions.ShortMethod)]
private static uint ApplyPaletteHash0(uint color)
{
// Focus on the green color.
return (color >> 8) & 0xff;
}
[MethodImpl(InliningOptions.ShortMethod)]
private static uint ApplyPaletteHash1(uint color)
{
// Forget about alpha.
return ((uint)((color & 0x00ffffffu) * 4222244071ul)) >> (32 - PaletteInvSizeBits);
}
[MethodImpl(InliningOptions.ShortMethod)]
private static uint ApplyPaletteHash2(uint color)
{
// Forget about alpha.
return ((uint)((color & 0x00ffffffu) * ((1ul << 31) - 1))) >> (32 - PaletteInvSizeBits);
}
[MethodImpl(InliningOptions.ShortMethod)]
private static uint HashPix(uint pix)
{
// Note that masking with 0xffffffffu is for preventing an
@ -951,6 +1257,12 @@ namespace SixLabors.ImageSharp.Formats.WebP
return (uint)((((long)pix + (pix >> 19)) * 0x39c5fba7L) & 0xffffffffu) >> 24;
}
[MethodImpl(InliningOptions.ShortMethod)]
private static int PaletteCompareColorsForSort(uint p1, uint p2)
{
return (p1 < p2) ? -1 : 1;
}
[MethodImpl(InliningOptions.ShortMethod)]
private static uint PaletteComponentDistance(uint v)
{

Loading…
Cancel
Save