diff --git a/src/ImageSharp/Formats/WebP/BitWriter/Vp8BitWriter.cs b/src/ImageSharp/Formats/WebP/BitWriter/Vp8BitWriter.cs
index 4e6260aa23..1150168eb7 100644
--- a/src/ImageSharp/Formats/WebP/BitWriter/Vp8BitWriter.cs
+++ b/src/ImageSharp/Formats/WebP/BitWriter/Vp8BitWriter.cs
@@ -55,7 +55,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.BitWriter
public int PutCoeffs(int ctx, Vp8Residual residual)
{
int n = residual.First;
- Vp8ProbaArray p = residual.Prob[n][ctx];
+ Vp8ProbaArray p = residual.Prob[n].Probabilities[ctx];
if (!this.PutBit(residual.Last >= 0, p.Probabilities[0]))
{
return 0;
@@ -68,13 +68,13 @@ namespace SixLabors.ImageSharp.Formats.WebP.BitWriter
int v = sign ? -c : c;
if (!this.PutBit(v != 0, p.Probabilities[1]))
{
- p = residual.Prob[WebPConstants.Bands[n]][0];
+ p = residual.Prob[WebPConstants.Bands[n]].Probabilities[0];
continue;
}
if (!this.PutBit(v > 1, p.Probabilities[2]))
{
- p = residual.Prob[WebPConstants.Bands[n]][1];
+ p = residual.Prob[WebPConstants.Bands[n]].Probabilities[1];
}
else
{
@@ -150,7 +150,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.BitWriter
}
}
- p = residual.Prob[WebPConstants.Bands[n]][2];
+ p = residual.Prob[WebPConstants.Bands[n]].Probabilities[2];
}
this.PutBitUniform(sign ? 1 : 0);
diff --git a/src/ImageSharp/Formats/WebP/Lossy/Vp8CostArray.cs b/src/ImageSharp/Formats/WebP/Lossy/Vp8CostArray.cs
new file mode 100644
index 0000000000..f0ff859898
--- /dev/null
+++ b/src/ImageSharp/Formats/WebP/Lossy/Vp8CostArray.cs
@@ -0,0 +1,18 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+namespace SixLabors.ImageSharp.Formats.WebP.Lossy
+{
+ internal class Vp8CostArray
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public Vp8CostArray()
+ {
+ this.Costs = new ushort[WebPConstants.NumCtx * (67 + 1)];
+ }
+
+ public ushort[] Costs { get; }
+ }
+}
diff --git a/src/ImageSharp/Formats/WebP/Lossy/Vp8EncProba.cs b/src/ImageSharp/Formats/WebP/Lossy/Vp8EncProba.cs
new file mode 100644
index 0000000000..8ae019ef6c
--- /dev/null
+++ b/src/ImageSharp/Formats/WebP/Lossy/Vp8EncProba.cs
@@ -0,0 +1,169 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+
+namespace SixLabors.ImageSharp.Formats.WebP.Lossy
+{
+ internal class Vp8EncProba
+ {
+ ///
+ /// Last (inclusive) level with variable cost.
+ ///
+ private const int MaxVariableLevel = 67;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public Vp8EncProba()
+ {
+ this.Dirty = true;
+ this.UseSkipProba = false;
+ this.Segments = new byte[3];
+ this.Coeffs = new Vp8BandProbas[WebPConstants.NumTypes][];
+ for (int i = 0; i < this.Coeffs.Length; i++)
+ {
+ this.Coeffs[i] = new Vp8BandProbas[WebPConstants.NumBands];
+ for (int j = 0; j < this.Coeffs[i].Length; j++)
+ {
+ this.Coeffs[i][j] = new Vp8BandProbas();
+ }
+ }
+
+ this.LevelCost = new Vp8CostArray[WebPConstants.NumTypes][];
+ for (int i = 0; i < this.LevelCost.Length; i++)
+ {
+ this.LevelCost[i] = new Vp8CostArray[WebPConstants.NumBands];
+ for (int j = 0; j < this.LevelCost[i].Length; j++)
+ {
+ this.LevelCost[i][j] = new Vp8CostArray();
+ }
+ }
+
+ this.RemappedCosts = new Vp8CostArray[WebPConstants.NumTypes][];
+ for (int i = 0; i < this.RemappedCosts.Length; i++)
+ {
+ this.RemappedCosts[i] = new Vp8CostArray[16];
+ for (int j = 0; j < this.RemappedCosts[i].Length; j++)
+ {
+ this.RemappedCosts[i][j] = new Vp8CostArray();
+ }
+ }
+
+ // Initialize with default probabilities.
+ this.Segments.AsSpan().Fill(255);
+ for (int t = 0; t < WebPConstants.NumTypes; ++t)
+ {
+ for (int b = 0; b < WebPConstants.NumBands; ++b)
+ {
+ for (int c = 0; c < WebPConstants.NumCtx; ++c)
+ {
+ Vp8ProbaArray dst = this.Coeffs[t][b].Probabilities[c];
+ for (int p = 0; p < WebPConstants.NumProbas; ++p)
+ {
+ dst.Probabilities[p] = WebPLookupTables.DefaultCoeffsProba[t, b, c, p];
+ }
+ }
+ }
+ }
+ }
+
+ ///
+ /// Gets the probabilities for segment tree.
+ ///
+ public byte[] Segments { get; }
+
+ ///
+ /// Gets the final probability of being skipped.
+ ///
+ public byte SkipProba { get; }
+
+ ///
+ /// Gets a value indicating whether to use the skip probability. Note: we always use SkipProba for now.
+ ///
+ public bool UseSkipProba { get; }
+
+ public Vp8BandProbas[][] Coeffs { get; }
+
+ public Vp8CostArray[][] LevelCost { get; }
+
+ public Vp8CostArray[][] RemappedCosts { get; }
+
+ ///
+ /// Gets or sets the number of skipped blocks.
+ ///
+ public int NbSkip { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether CalculateLevelCosts() needs to be called.
+ ///
+ public bool Dirty { get; set; }
+
+ public void CalculateLevelCosts()
+ {
+ if (!this.Dirty)
+ {
+ return; // nothing to do.
+ }
+
+ for (int ctype = 0; ctype < WebPConstants.NumTypes; ++ctype)
+ {
+ for (int band = 0; band < WebPConstants.NumBands; ++band)
+ {
+ for (int ctx = 0; ctx < WebPConstants.NumCtx; ++ctx)
+ {
+ Vp8ProbaArray p = this.Coeffs[ctype][band].Probabilities[ctx];
+ Span table = this.LevelCost[ctype][band].Costs.AsSpan(ctx * MaxVariableLevel);
+ int cost0 = (ctx > 0) ? this.BitCost(1, p.Probabilities[0]) : 0;
+ int costBase = this.BitCost(1, p.Probabilities[1]) + cost0;
+ int v;
+ table[0] = (ushort)(this.BitCost(0, p.Probabilities[1]) + cost0);
+ for (v = 1; v <= MaxVariableLevel; ++v)
+ {
+ table[v] = (ushort)(costBase + this.VariableLevelCost(v, p.Probabilities));
+ }
+
+ // Starting at level 67 and up, the variable part of the cost is actually constant.
+ }
+ }
+
+ for (int n = 0; n < 16; ++n)
+ {
+ for (int ctx = 0; ctx < WebPConstants.NumCtx; ++ctx)
+ {
+ Span dst = this.RemappedCosts[ctype][n].Costs.AsSpan(ctx * MaxVariableLevel, MaxVariableLevel);
+ Span src = this.LevelCost[ctype][WebPConstants.Bands[n]].Costs.AsSpan(ctx * MaxVariableLevel, MaxVariableLevel);
+ src.CopyTo(dst);
+ }
+ }
+ }
+
+ this.Dirty = false;
+ }
+
+ private int VariableLevelCost(int level, Span probas)
+ {
+ int pattern = WebPLookupTables.Vp8LevelCodes[level - 1][0];
+ int bits = WebPLookupTables.Vp8LevelCodes[level - 1][1];
+ int cost = 0;
+ for (int i = 2; pattern != 0; ++i)
+ {
+ if ((pattern & 1) != 0)
+ {
+ cost += this.BitCost(bits & 1, probas[i]);
+ }
+
+ bits >>= 1;
+ pattern >>= 1;
+ }
+
+ return cost;
+ }
+
+ // Cost of coding one event with probability 'proba'.
+ private int BitCost(int bit, byte proba)
+ {
+ return bit == 0 ? WebPLookupTables.Vp8EntropyCost[proba] : WebPLookupTables.Vp8EntropyCost[255 - proba];
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/WebP/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/WebP/Lossy/Vp8Encoder.cs
index fd3f541c70..c9426af16b 100644
--- a/src/ImageSharp/Formats/WebP/Lossy/Vp8Encoder.cs
+++ b/src/ImageSharp/Formats/WebP/Lossy/Vp8Encoder.cs
@@ -62,6 +62,11 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
///
private Vp8MacroBlockInfo[] mbInfo;
+ ///
+ /// Probabilities.
+ ///
+ private Vp8EncProba proba;
+
private int dqUvDc;
private int dqUvAc;
@@ -130,6 +135,8 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
this.mbInfo[i] = new Vp8MacroBlockInfo();
}
+ this.proba = new Vp8EncProba();
+
// this.Preds = this.memoryAllocator.Allocate(predSize);
this.Preds = this.memoryAllocator.Allocate(predSize * 2); // TODO: figure out how much mem we need here. This is too much.
this.predsWidth = (4 * this.mbw) + 1;
@@ -223,6 +230,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
this.AssignSegments(segmentInfos, alphas);
this.SetSegmentParams(segmentInfos);
this.SetSegmentProbas(segmentInfos);
+ this.ResetStats();
it.Init();
it.InitFilter();
do
@@ -288,10 +296,14 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
var distAccum = new int[NumMbSegments];
// Bracket the input.
- for (n = 0; n <= WebPConstants.MaxAlpha && alphas[n] == 0; ++n) { }
+ for (n = 0; n <= WebPConstants.MaxAlpha && alphas[n] == 0; ++n)
+ {
+ }
minA = n;
- for (n = WebPConstants.MaxAlpha; n > minA && alphas[n] == 0; --n) { }
+ for (n = WebPConstants.MaxAlpha; n > minA && alphas[n] == 0; --n)
+ {
+ }
maxA = n;
rangeA = maxA - minA;
@@ -407,8 +419,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
int nb = this.segmentHeader.NumSegments;
int snsStrength = 50; // TODO: Spatial Noise Shaping, hardcoded for now.
double amp = WebPConstants.SnsToDq * snsStrength / 100.0d / 128.0d;
- double Q = this.quality / 100.0d;
- double cBase = this.QualityToCompression(Q);
+ double cBase = this.QualityToCompression(this.quality / 100.0d);
for (int i = 0; i < nb; ++i)
{
// We modulate the base coefficient to accommodate for the quantization
@@ -430,6 +441,12 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
// and make it safe.
this.dqUvAc = this.Clip(this.dqUvAc, WebPConstants.QuantEncMinDqUv, WebPConstants.QuantEncMaxDqUv);
+ // We also boost the dc-uv-quant a little, based on sns-strength, since
+ // U/V channels are quite more reactive to high quants (flat DC-blocks
+ // tend to appear, and are unpleasant).
+ this.dqUvDc = -4 * snsStrength / 100;
+ this.dqUvDc = this.Clip(this.dqUvDc, -15, 15); // 4bit-signed max allowed
+
this.SetupMatrices(dqm);
}
@@ -441,6 +458,13 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
// TODO: SetSegmentProbas
}
+ private void ResetStats()
+ {
+ Vp8EncProba proba = this.proba;
+ proba.CalculateLevelCosts();
+ proba.NbSkip = 0;
+ }
+
private void SetupMatrices(Vp8SegmentInfo[] dqm)
{
for (int i = 0; i < dqm.Length; ++i)
@@ -687,15 +711,15 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
var pos1 = this.bitWriter.Pos;
if (i16)
{
- residual.Init(0, 1);
+ residual.Init(0, 1, this.proba);
residual.SetCoeffs(rd.YDcLevels);
int res = this.bitWriter.PutCoeffs(it.TopNz[8] + it.LeftNz[8], residual);
it.TopNz[8] = it.LeftNz[8] = res;
- residual.Init(1, 0);
+ residual.Init(1, 0, this.proba);
}
else
{
- residual.Init(0, 3);
+ residual.Init(0, 3, this.proba);
}
// luma-AC
@@ -713,7 +737,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
var pos2 = this.bitWriter.Pos;
// U/V
- residual.Init(0, 2);
+ residual.Init(0, 2, this.proba);
for (ch = 0; ch <= 2; ch += 2)
{
for (y = 0; y < 2; ++y)
@@ -789,11 +813,12 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
Span src = it.YuvIn.AsSpan(Vp8EncIterator.YOffEnc);
int nz = 0;
int n;
- var tmp = new short[8* 16];
+ var tmp = new short[8 * 16];
for (n = 0; n < 8; n += 2)
{
- this.FTransform2(src.Slice(WebPLookupTables.Vp8ScanUv[n]),
+ this.FTransform2(
+ src.Slice(WebPLookupTables.Vp8ScanUv[n]),
reference.Slice(WebPLookupTables.Vp8ScanUv[n]),
tmp.AsSpan(n * 16, 16),
tmp.AsSpan((n + 1) * 16, 16));
diff --git a/src/ImageSharp/Formats/WebP/Lossy/Vp8Residual.cs b/src/ImageSharp/Formats/WebP/Lossy/Vp8Residual.cs
index ccc4471eee..96efe7f4f2 100644
--- a/src/ImageSharp/Formats/WebP/Lossy/Vp8Residual.cs
+++ b/src/ImageSharp/Formats/WebP/Lossy/Vp8Residual.cs
@@ -10,18 +10,6 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
///
internal class Vp8Residual
{
- ///
- /// Initializes a new instance of the class.
- ///
- public Vp8Residual()
- {
- this.Prob = new Vp8ProbaArray[3][];
- for (int i = 0; i < 3; i++)
- {
- this.Prob[i] = new Vp8ProbaArray[11];
- }
- }
-
public int First { get; set; }
public int Last { get; set; }
@@ -30,15 +18,15 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
public short[] Coeffs { get; set; }
- public Vp8ProbaArray[][] Prob { get; }
+ public Vp8BandProbas[] Prob { get; set; }
- public void Init(int first, int coeffType)
+ public void Init(int first, int coeffType, Vp8EncProba prob)
{
this.First = first;
this.CoeffType = coeffType;
+ this.Prob = prob.Coeffs[this.CoeffType];
// TODO:
- // res->prob = enc->proba_.coeffs_[coeff_type];
// res->stats = enc->proba_.stats_[coeff_type];
// res->costs = enc->proba_.remapped_costs_[coeff_type];
}
diff --git a/src/ImageSharp/Formats/WebP/WebPLookupTables.cs b/src/ImageSharp/Formats/WebP/WebPLookupTables.cs
index 6474d812d5..a550903e09 100644
--- a/src/ImageSharp/Formats/WebP/WebPLookupTables.cs
+++ b/src/ImageSharp/Formats/WebP/WebPLookupTables.cs
@@ -81,6 +81,57 @@ namespace SixLabors.ImageSharp.Formats.WebP
241, 243, 245, 247, 249, 251, 253, 127
};
+ public static readonly ushort[] Vp8EntropyCost =
+ {
+ 1792, 1792, 1792, 1536, 1536, 1408, 1366, 1280, 1280, 1216,
+ 1178, 1152, 1110, 1076, 1061, 1024, 1024, 992, 968, 951,
+ 939, 911, 896, 878, 871, 854, 838, 820, 811, 794,
+ 786, 768, 768, 752, 740, 732, 720, 709, 704, 690,
+ 683, 672, 666, 655, 647, 640, 631, 622, 615, 607,
+ 598, 592, 586, 576, 572, 564, 559, 555, 547, 541,
+ 534, 528, 522, 512, 512, 504, 500, 494, 488, 483,
+ 477, 473, 467, 461, 458, 452, 448, 443, 438, 434,
+ 427, 424, 419, 415, 410, 406, 403, 399, 394, 390,
+ 384, 384, 377, 374, 370, 366, 362, 359, 355, 351,
+ 347, 342, 342, 336, 333, 330, 326, 323, 320, 316,
+ 312, 308, 305, 302, 299, 296, 293, 288, 287, 283,
+ 280, 277, 274, 272, 268, 266, 262, 256, 256, 256,
+ 251, 248, 245, 242, 240, 237, 234, 232, 228, 226,
+ 223, 221, 218, 216, 214, 211, 208, 205, 203, 201,
+ 198, 196, 192, 191, 188, 187, 183, 181, 179, 176,
+ 175, 171, 171, 168, 165, 163, 160, 159, 156, 154,
+ 152, 150, 148, 146, 144, 142, 139, 138, 135, 133,
+ 131, 128, 128, 125, 123, 121, 119, 117, 115, 113,
+ 111, 110, 107, 105, 103, 102, 100, 98, 96, 94,
+ 92, 91, 89, 86, 86, 83, 82, 80, 77, 76,
+ 74, 73, 71, 69, 67, 66, 64, 63, 61, 59,
+ 57, 55, 54, 52, 51, 49, 47, 46, 44, 43,
+ 41, 40, 38, 36, 35, 33, 32, 30, 29, 27,
+ 25, 24, 22, 21, 19, 18, 16, 15, 13, 12,
+ 10, 9, 7, 6, 4, 3
+ };
+
+ public static readonly ushort[][] Vp8LevelCodes =
+ {
+ new ushort[] { 0x001, 0x000 }, new ushort[] { 0x007, 0x001 }, new ushort[] { 0x00f, 0x005 },
+ new ushort[] { 0x00f, 0x00d }, new ushort[] { 0x033, 0x003 }, new ushort[] { 0x033, 0x003 }, new ushort[] { 0x033, 0x023 },
+ new ushort[] { 0x033, 0x023 }, new ushort[] { 0x033, 0x023 }, new ushort[] { 0x033, 0x023 }, new ushort[] { 0x0d3, 0x013 },
+ new ushort[] { 0x0d3, 0x013 }, new ushort[] { 0x0d3, 0x013 }, new ushort[] { 0x0d3, 0x013 }, new ushort[] { 0x0d3, 0x013 },
+ new ushort[] { 0x0d3, 0x013 }, new ushort[] { 0x0d3, 0x013 }, new ushort[] { 0x0d3, 0x013 }, new ushort[] { 0x0d3, 0x093 },
+ new ushort[] { 0x0d3, 0x093 }, new ushort[] { 0x0d3, 0x093 }, new ushort[] { 0x0d3, 0x093 }, new ushort[] { 0x0d3, 0x093 },
+ new ushort[] { 0x0d3, 0x093 }, new ushort[] { 0x0d3, 0x093 }, new ushort[] { 0x0d3, 0x093 }, new ushort[] { 0x0d3, 0x093 },
+ new ushort[] { 0x0d3, 0x093 }, new ushort[] { 0x0d3, 0x093 }, new ushort[] { 0x0d3, 0x093 }, new ushort[] { 0x0d3, 0x093 },
+ new ushort[] { 0x0d3, 0x093 }, new ushort[] { 0x0d3, 0x093 }, new ushort[] { 0x0d3, 0x093 }, new ushort[] { 0x153, 0x053 },
+ new ushort[] { 0x153, 0x053 }, new ushort[] { 0x153, 0x053 }, new ushort[] { 0x153, 0x053 }, new ushort[] { 0x153, 0x053 },
+ new ushort[] { 0x153, 0x053 }, new ushort[] { 0x153, 0x053 }, new ushort[] { 0x153, 0x053 }, new ushort[] { 0x153, 0x053 },
+ new ushort[] { 0x153, 0x053 }, new ushort[] { 0x153, 0x053 }, new ushort[] { 0x153, 0x053 }, new ushort[] { 0x153, 0x053 },
+ new ushort[] { 0x153, 0x053 }, new ushort[] { 0x153, 0x053 }, new ushort[] { 0x153, 0x053 }, new ushort[] { 0x153, 0x053 },
+ new ushort[] { 0x153, 0x053 }, new ushort[] { 0x153, 0x053 }, new ushort[] { 0x153, 0x053 }, new ushort[] { 0x153, 0x053 },
+ new ushort[] { 0x153, 0x053 }, new ushort[] { 0x153, 0x053 }, new ushort[] { 0x153, 0x053 }, new ushort[] { 0x153, 0x053 },
+ new ushort[] { 0x153, 0x053 }, new ushort[] { 0x153, 0x053 }, new ushort[] { 0x153, 0x053 }, new ushort[] { 0x153, 0x053 },
+ new ushort[] { 0x153, 0x053 }, new ushort[] { 0x153, 0x053 }, new ushort[] { 0x153, 0x053 }, new ushort[] { 0x153, 0x153 },
+ };
+
///
/// Lookup table for small values of log2(int).
///