diff --git a/src/ImageSharp/Formats/WebP/BitWriter/Vp8BitWriter.cs b/src/ImageSharp/Formats/WebP/BitWriter/Vp8BitWriter.cs
index 8d3fe61b4..e18a945bc 100644
--- a/src/ImageSharp/Formats/WebP/BitWriter/Vp8BitWriter.cs
+++ b/src/ImageSharp/Formats/WebP/BitWriter/Vp8BitWriter.cs
@@ -28,8 +28,6 @@ namespace SixLabors.ImageSharp.Formats.WebP.BitWriter
private int maxPos;
- // private bool error;
-
///
/// Initializes a new instance of the class.
///
@@ -43,8 +41,6 @@ namespace SixLabors.ImageSharp.Formats.WebP.BitWriter
this.nbBits = -8;
this.pos = 0;
this.maxPos = 0;
-
- // this.error = false;
}
///
@@ -69,13 +65,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]].Probabilities[0];
+ p = residual.Prob[WebPConstants.Vp8EncBands[n]].Probabilities[0];
continue;
}
if (!this.PutBit(v > 1, p.Probabilities[2]))
{
- p = residual.Prob[WebPConstants.Bands[n]].Probabilities[1];
+ p = residual.Prob[WebPConstants.Vp8EncBands[n]].Probabilities[1];
}
else
{
@@ -102,7 +98,6 @@ namespace SixLabors.ImageSharp.Formats.WebP.BitWriter
{
int mask;
byte[] tab;
- var tabIdx = 0;
if (v < 3 + (8 << 1))
{
// VP8Cat3 (3b)
@@ -111,7 +106,6 @@ namespace SixLabors.ImageSharp.Formats.WebP.BitWriter
v -= 3 + (8 << 0);
mask = 1 << 2;
tab = WebPConstants.Cat3;
- tabIdx = 0;
}
else if (v < 3 + (8 << 2))
{
@@ -121,7 +115,6 @@ namespace SixLabors.ImageSharp.Formats.WebP.BitWriter
v -= 3 + (8 << 1);
mask = 1 << 3;
tab = WebPConstants.Cat4;
- tabIdx = 0;
}
else if (v < 3 + (8 << 3))
{
@@ -131,7 +124,6 @@ namespace SixLabors.ImageSharp.Formats.WebP.BitWriter
v -= 3 + (8 << 2);
mask = 1 << 4;
tab = WebPConstants.Cat5;
- tabIdx = 0;
}
else
{
@@ -141,9 +133,9 @@ namespace SixLabors.ImageSharp.Formats.WebP.BitWriter
v -= 3 + (8 << 3);
mask = 1 << 10;
tab = WebPConstants.Cat6;
- tabIdx = 0;
}
+ var tabIdx = 0;
while (mask != 0)
{
this.PutBit(v & mask, tab[tabIdx++]);
@@ -151,7 +143,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.BitWriter
}
}
- p = residual.Prob[WebPConstants.Bands[n]].Probabilities[2];
+ p = residual.Prob[WebPConstants.Vp8EncBands[n]].Probabilities[2];
}
this.PutBitUniform(sign ? 1 : 0);
diff --git a/src/ImageSharp/Formats/WebP/Lossy/PassStats.cs b/src/ImageSharp/Formats/WebP/Lossy/PassStats.cs
new file mode 100644
index 000000000..20f18648f
--- /dev/null
+++ b/src/ImageSharp/Formats/WebP/Lossy/PassStats.cs
@@ -0,0 +1,76 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+namespace SixLabors.ImageSharp.Formats.WebP.Lossy
+{
+ ///
+ /// Class for organizing convergence in either size or PSNR.
+ ///
+ internal class PassStats
+ {
+ public PassStats(long targetSize, float targetPsnr, int qMin, int qMax, int quality)
+ {
+ bool doSizeSearch = targetSize != 0;
+
+ this.IsFirst = true;
+ this.Dq = 10.0f;
+ this.Qmin = qMin;
+ this.Qmax = qMax;
+ this.Q = quality.Clamp(qMin, qMax);
+ this.LastQ = this.Q;
+ this.Target = doSizeSearch ? targetSize
+ : (targetPsnr > 0.0f) ? targetPsnr
+ : 40.0f; // default, just in case
+ this.Value = 0.0f;
+ this.LastValue = 0.0f;
+ this.DoSizeSearch = doSizeSearch;
+ }
+
+ public bool IsFirst { get; set; }
+
+ public float Dq { get; set; }
+
+ public float Q { get; set; }
+
+ public float LastQ { get; set; }
+
+ public float Qmin { get; }
+
+ public float Qmax { get; }
+
+ public double Value { get; set; } // PSNR or size
+
+ public double LastValue { get; set; }
+
+ public double Target { get; }
+
+ public bool DoSizeSearch { get; }
+
+ public float ComputeNextQ()
+ {
+ float dq;
+ if (this.IsFirst)
+ {
+ dq = (this.Value > this.Target) ? -this.Dq : this.Dq;
+ this.IsFirst = false;
+ }
+ else if (this.Value != this.LastValue)
+ {
+ double slope = (this.Target - this.Value) / (this.LastValue - this.Value);
+ dq = (float)(slope * (this.LastQ - this.Q));
+ }
+ else
+ {
+ dq = 0.0f; // we're done?!
+ }
+
+ // Limit variable to avoid large swings.
+ this.Dq = dq.Clamp(-30.0f, 30.0f);
+ this.LastQ = this.Q;
+ this.LastValue = this.Value;
+ this.Q = (this.Q + this.Dq).Clamp(this.Qmin, this.Qmax);
+
+ return this.Q;
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/WebP/Lossy/Vp8EncProba.cs b/src/ImageSharp/Formats/WebP/Lossy/Vp8EncProba.cs
index 8ae019ef6..6fe23adb3 100644
--- a/src/ImageSharp/Formats/WebP/Lossy/Vp8EncProba.cs
+++ b/src/ImageSharp/Formats/WebP/Lossy/Vp8EncProba.cs
@@ -12,6 +12,11 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
///
private const int MaxVariableLevel = 67;
+ ///
+ /// Value below which using skipProba is OK.
+ ///
+ private const int SkipProbaThreshold = 250;
+
///
/// Initializes a new instance of the class.
///
@@ -30,6 +35,16 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
}
}
+ this.Stats = new Vp8Stats[WebPConstants.NumTypes][];
+ for (int i = 0; i < this.Coeffs.Length; i++)
+ {
+ this.Stats[i] = new Vp8Stats[WebPConstants.NumBands];
+ for (int j = 0; j < this.Stats[i].Length; j++)
+ {
+ this.Stats[i][j] = new Vp8Stats();
+ }
+ }
+
this.LevelCost = new Vp8CostArray[WebPConstants.NumTypes][];
for (int i = 0; i < this.LevelCost.Length; i++)
{
@@ -74,17 +89,19 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
public byte[] Segments { get; }
///
- /// Gets the final probability of being skipped.
+ /// Gets or sets the final probability of being skipped.
///
- public byte SkipProba { get; }
+ public byte SkipProba { get; set; }
///
- /// Gets a value indicating whether to use the skip probability. Note: we always use SkipProba for now.
+ /// Gets or sets a value indicating whether to use the skip probability. Note: we always use SkipProba for now.
///
- public bool UseSkipProba { get; }
+ public bool UseSkipProba { get; set; }
public Vp8BandProbas[][] Coeffs { get; }
+ public Vp8Stats[][] Stats { get; }
+
public Vp8CostArray[][] LevelCost { get; }
public Vp8CostArray[][] RemappedCosts { get; }
@@ -132,7 +149,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
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);
+ Span src = this.LevelCost[ctype][WebPConstants.Vp8EncBands[n]].Costs.AsSpan(ctx * MaxVariableLevel, MaxVariableLevel);
src.CopyTo(dst);
}
}
@@ -141,6 +158,87 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
this.Dirty = false;
}
+ public int FinalizeTokenProbas()
+ {
+ bool hasChanged = false;
+ int size = 0;
+ for (int t = 0; t < WebPConstants.NumTypes; ++t)
+ {
+ for (int b = 0; b < WebPConstants.NumBands; ++b)
+ {
+ for (int c = 0; c < WebPConstants.NumCtx; ++c)
+ {
+ for (int p = 0; p < WebPConstants.NumProbas; ++p)
+ {
+ var stats = this.Stats[t][b].Stats[c].Stats[p];
+ int nb = (int)((stats >> 0) & 0xffff);
+ int total = (int)((stats >> 16) & 0xffff);
+ int updateProba = WebPLookupTables.CoeffsUpdateProba[t, b, c, p];
+ int oldP = WebPLookupTables.DefaultCoeffsProba[t, b, c, p];
+ int newP = this.CalcTokenProba(nb, total);
+ int oldCost = this.BranchCost(nb, total, oldP) + this.BitCost(0, (byte)updateProba);
+ int newCost = this.BranchCost(nb, total, newP) + this.BitCost(1, (byte)updateProba) + (8 * 256);
+ bool useNewP = oldCost > newCost;
+ size += this.BitCost(useNewP ? 1 : 0, (byte)updateProba);
+ if (useNewP)
+ {
+ // Only use proba that seem meaningful enough.
+ this.Coeffs[t][b].Probabilities[c].Probabilities[p] = (byte)newP;
+ hasChanged |= newP != oldP;
+ size += 8 * 256;
+ }
+ else
+ {
+ this.Coeffs[t][b].Probabilities[c].Probabilities[p] = (byte)oldP;
+ }
+ }
+ }
+ }
+ }
+
+ this.Dirty = hasChanged;
+ return size;
+ }
+
+ public int FinalizeSkipProba(int mbw, int mbh)
+ {
+ int nbMbs = mbw * mbh;
+ int nbEvents = this.NbSkip;
+ this.SkipProba = (byte)this.CalcSkipProba(nbEvents, nbMbs);
+ this.UseSkipProba = this.SkipProba < SkipProbaThreshold;
+
+ int size = 256;
+ if (this.UseSkipProba)
+ {
+ size += (nbEvents * this.BitCost(1, this.SkipProba)) + ((nbMbs - nbEvents) * this.BitCost(0, this.SkipProba));
+ size += 8 * 256; // cost of signaling the skipProba itself.
+ }
+
+ return size;
+ }
+
+ public void ResetTokenStats()
+ {
+ for (int t = 0; t < WebPConstants.NumTypes; ++t)
+ {
+ for (int b = 0; b < WebPConstants.NumBands; ++b)
+ {
+ for (int c = 0; c < WebPConstants.NumCtx; ++c)
+ {
+ for (int p = 0; p < WebPConstants.NumProbas; ++p)
+ {
+ this.Stats[t][b].Stats[c].Stats[p] = 0;
+ }
+ }
+ }
+ }
+ }
+
+ private int CalcSkipProba(long nb, long total)
+ {
+ return (int)(total != 0 ? (total - nb) * 255 / total : 255);
+ }
+
private int VariableLevelCost(int level, Span probas)
{
int pattern = WebPLookupTables.Vp8LevelCodes[level - 1][0];
@@ -160,6 +258,19 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
return cost;
}
+ // Collect statistics and deduce probabilities for next coding pass.
+ // Return the total bit-cost for coding the probability updates.
+ private int CalcTokenProba(int nb, int total)
+ {
+ return nb != 0 ? (255 - (nb * 255 / total)) : 255;
+ }
+
+ // Cost of coding 'nb' 1's and 'total-nb' 0's using 'proba' probability.
+ private int BranchCost(int nb, int total, int proba)
+ {
+ return (nb * this.BitCost(1, (byte)proba)) + ((total - nb) * this.BitCost(0, (byte)proba));
+ }
+
// Cost of coding one event with probability 'proba'.
private int BitCost(int bit, byte proba)
{
diff --git a/src/ImageSharp/Formats/WebP/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/WebP/Lossy/Vp8Encoder.cs
index d4a21c49b..3af091d90 100644
--- a/src/ImageSharp/Formats/WebP/Lossy/Vp8Encoder.cs
+++ b/src/ImageSharp/Formats/WebP/Lossy/Vp8Encoder.cs
@@ -3,7 +3,6 @@
using System;
using System.Buffers;
-using System.Buffers.Binary;
using System.IO;
using System.Runtime.CompilerServices;
@@ -38,20 +37,25 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
///
private readonly int method;
+ ///
+ /// Number of entropy-analysis passes (in [1..10]).
+ ///
+ private readonly int entropyPasses;
+
///
/// Stride of the prediction plane (=4*mb_w + 1)
///
- private int predsWidth;
+ private readonly int predsWidth;
///
/// Macroblock width.
///
- private int mbw;
+ private readonly int mbw;
///
/// Macroblock height.
///
- private int mbh;
+ private readonly int mbh;
///
/// The segment features.
@@ -61,17 +65,21 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
///
/// Contextual macroblock infos.
///
- private Vp8MacroBlockInfo[] mbInfo;
+ private readonly Vp8MacroBlockInfo[] mbInfo;
///
/// Probabilities.
///
- private Vp8EncProba proba;
+ private readonly Vp8EncProba proba;
+
+ private readonly Vp8RdLevel rdOptLevel;
private int dqUvDc;
private int dqUvAc;
+ private int maxI4HeaderBits;
+
///
/// Global susceptibility.
///
@@ -103,6 +111,17 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
private const int MaxItersKMeans = 6;
+ // Convergence is considered reached if dq < DqLimit
+ private const float DqLimit = 0.4f;
+
+ private const ulong Partition0SizeLimit = (WebPConstants.Vp8MaxPartition0Size - 2048UL) << 11;
+
+ private const long HeaderSizeEstimate = WebPConstants.RiffHeaderSize + WebPConstants.ChunkHeaderSize + WebPConstants.Vp8FrameHeaderSize;
+
+ private const int QMin = 0;
+
+ private const int QMax = 100;
+
///
/// Initializes a new instance of the class.
///
@@ -111,11 +130,17 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
/// The height of the input image.
/// The encoding quality.
/// Quality/speed trade-off (0=fast, 6=slower-better).
- public Vp8Encoder(MemoryAllocator memoryAllocator, int width, int height, int quality, int method)
+ /// Number of entropy-analysis passes (in [1..10]).
+ public Vp8Encoder(MemoryAllocator memoryAllocator, int width, int height, int quality, int method, int entropyPasses)
{
this.memoryAllocator = memoryAllocator;
this.quality = quality.Clamp(0, 100);
this.method = method.Clamp(0, 6);
+ this.entropyPasses = entropyPasses.Clamp(1, 10);
+ this.rdOptLevel = (method >= 6) ? Vp8RdLevel.RdOptTrellisAll
+ : (method >= 5) ? Vp8RdLevel.RdOptTrellis
+ : (method >= 3) ? Vp8RdLevel.RdOptBasic
+ : Vp8RdLevel.RdOptNone;
var pixelCount = width * height;
this.mbw = (width + 15) >> 4;
@@ -130,6 +155,12 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
this.MbHeaderLimit = 256 * 510 * 8 * 1024 / (this.mbw * this.mbh);
int predSize = (((4 * this.mbw) + 1) * ((4 * this.mbh) + 1)) + this.predsWidth + 1;
+ // TODO: make partition_limit configurable?
+ int limit = 100; // original code: limit = 100 - config->partition_limit;
+ this.maxI4HeaderBits =
+ 256 * 16 * 16 * // upper bound: up to 16bit per 4x4 block
+ (limit * limit) / (100 * 100); // ... modulated with a quadratic curve.
+
this.mbInfo = new Vp8MacroBlockInfo[this.mbw * this.mbh];
for (int i = 0; i < this.mbInfo.Length; i++)
{
@@ -225,20 +256,22 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
this.alpha = this.MacroBlockAnalysis(width, height, it, y, u, v, yStride, uvStride, alphas, out this.uvAlpha);
// Analysis is done, proceed to actual encoding.
-
- // TODO: EncodeAlpha();
this.segmentHeader = new Vp8EncSegmentHeader(4);
this.AssignSegments(segmentInfos, alphas);
- this.SetSegmentParams(segmentInfos);
+ this.SetSegmentParams(segmentInfos, this.quality);
this.SetSegmentProbas(segmentInfos);
this.ResetStats();
+
+ // TODO: EncodeAlpha();
+ // Stats-collection loop.
+ this.StatLoop(width, height, yStride, uvStride, segmentInfos);
it.Init();
it.InitFilter();
do
{
var info = new Vp8ModeScore();
it.Import(y, u, v, yStride, uvStride, width, height);
- if (!this.Decimate(it, segmentInfos, info))
+ if (!this.Decimate(it, segmentInfos, info, this.rdOptLevel))
{
this.CodeResiduals(it, info);
}
@@ -266,6 +299,139 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
this.Preds.Dispose();
}
+ ///
+ /// Only collect statistics(number of skips, token usage, ...).
+ /// This is used for deciding optimal probabilities. It also modifies the
+ /// quantizer value if some target (size, PSNR) was specified.
+ ///
+ private void StatLoop(int width, int height, int yStride, int uvStride, Vp8SegmentInfo[] segmentInfos)
+ {
+ int targetSize = 0; // TODO: target size is hardcoded.
+ float targetPsnr = 0.0f; // TDOO: targetPsnr is hardcoded.
+ int method = this.method;
+ bool doSearch = false; // TODO: doSearch hardcoded for now.
+ bool fastProbe = (method == 0 || method == 3) && !doSearch;
+ int numPassLeft = this.entropyPasses;
+ Vp8RdLevel rdOpt = (method >= 3 || doSearch) ? Vp8RdLevel.RdOptBasic : Vp8RdLevel.RdOptNone;
+ int nbMbs = this.mbw * this.mbh;
+
+ var stats = new PassStats(targetSize, targetPsnr, QMin, QMax, this.quality);
+ this.proba.ResetTokenStats();
+
+ // Fast mode: quick analysis pass over few mbs. Better than nothing.
+ if (fastProbe)
+ {
+ if (method == 3)
+ {
+ // We need more stats for method 3 to be reliable.
+ nbMbs = (nbMbs > 200) ? nbMbs >> 1 : 100;
+ }
+ else
+ {
+ nbMbs = (nbMbs > 200) ? nbMbs >> 2 : 50;
+ }
+ }
+
+ while (numPassLeft-- > 0)
+ {
+ bool isLastPass = (MathF.Abs(stats.Dq) <= DqLimit) || (numPassLeft == 0) || (this.maxI4HeaderBits == 0);
+ var sizeP0 = this.OneStatPass(width, height, yStride, uvStride, rdOpt, nbMbs, stats, segmentInfos);
+ if (sizeP0 == 0)
+ {
+ return;
+ }
+
+ if (this.maxI4HeaderBits > 0 && sizeP0 > (long)Partition0SizeLimit)
+ {
+ ++numPassLeft;
+ this.maxI4HeaderBits >>= 1; // strengthen header bit limitation...
+ continue; // ...and start over
+ }
+
+ if (isLastPass)
+ {
+ break;
+ }
+
+ // If no target size: just do several pass without changing 'q'
+ if (doSearch)
+ {
+ stats.ComputeNextQ();
+ if (MathF.Abs(stats.Dq) <= DqLimit)
+ {
+ break;
+ }
+ }
+ }
+
+ if (!doSearch || !stats.DoSizeSearch)
+ {
+ // Need to finalize probas now, since it wasn't done during the search.
+ this.proba.FinalizeSkipProba(this.mbw, this.mbh);
+ this.proba.FinalizeTokenProbas();
+ }
+
+ this.proba.CalculateLevelCosts(); // finalize costs
+ }
+
+ private long OneStatPass(int width, int height, int yStride, int uvStride, Vp8RdLevel rdOpt, int nbMbs, PassStats stats, Vp8SegmentInfo[] segmentInfos)
+ {
+ Span y = this.Y.GetSpan();
+ Span u = this.U.GetSpan();
+ Span v = this.V.GetSpan();
+ var it = new Vp8EncIterator(this.YTop, this.UvTop, this.Preds, this.Nz, this.mbInfo, this.mbw, this.mbh);
+ long size = 0;
+ long sizeP0 = 0;
+ long distortion = 0;
+ long pixelCount = nbMbs * 384;
+
+ this.SetLoopParams(segmentInfos, stats.Q);
+ do
+ {
+ var info = new Vp8ModeScore();
+ it.Import(y, u, v, yStride, uvStride, width, height);
+ if (this.Decimate(it, segmentInfos, info, rdOpt))
+ {
+ // Just record the number of skips and act like skipProba is not used.
+ ++this.proba.NbSkip;
+ }
+
+ this.RecordResiduals(it, info);
+ size += info.R + info.H;
+ sizeP0 += info.H;
+ distortion += info.D;
+
+ it.SaveBoundary();
+ }
+ while (it.Next());
+
+ sizeP0 += this.segmentHeader.Size;
+ if (stats.DoSizeSearch)
+ {
+ size += this.proba.FinalizeSkipProba(this.mbw, this.mbh);
+ size += this.proba.FinalizeTokenProbas();
+ size = ((size + sizeP0 + 1024) >> 11) + HeaderSizeEstimate;
+ stats.Value = size;
+ }
+ else
+ {
+ stats.Value = this.GetPsnr(distortion, pixelCount);
+ }
+
+ return sizeP0;
+ }
+
+ private void SetLoopParams(Vp8SegmentInfo[] dqm, float q)
+ {
+ // Setup segment quantizations and filters.
+ this.SetSegmentParams(dqm, q);
+
+ // Compute segment probabilities.
+ this.SetSegmentProbas(dqm);
+
+ this.ResetStats();
+ }
+
private void ResetBoundaryPredictions()
{
Span top = this.Preds.GetSpan();
@@ -416,12 +582,12 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
}
}
- private void SetSegmentParams(Vp8SegmentInfo[] dqm)
+ private void SetSegmentParams(Vp8SegmentInfo[] dqm, float quality)
{
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 cBase = this.QualityToCompression(this.quality / 100.0d);
+ double cBase = this.QualityToCompression(quality / 100.0d);
for (int i = 0; i < nb; ++i)
{
// We modulate the base coefficient to accommodate for the quantization
@@ -488,6 +654,8 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
m.Uv.Q[1] = WebPLookupTables.AcTable[this.Clip(q + this.dqUvAc, 0, 127)];
var qi4 = m.Y1.Expand(0);
+ var qi16 = m.Y2.Expand(1);
+ var quv = m.Uv.Expand(2);
m.I4Penalty = 1000 * qi4 * qi4;
}
@@ -541,7 +709,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
return bestAlpha; // Mixed susceptibility (not just luma).
}
- private bool Decimate(Vp8EncIterator it, Vp8SegmentInfo[] segmentInfos, Vp8ModeScore rd)
+ private bool Decimate(Vp8EncIterator it, Vp8SegmentInfo[] segmentInfos, Vp8ModeScore rd, Vp8RdLevel rdOpt)
{
rd.InitScore();
@@ -730,7 +898,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
for (x = 0; x < 4; ++x)
{
int ctx = it.TopNz[x] + it.LeftNz[y];
- residual.SetCoeffs(rd.YAcLevels.AsSpan(x + (y * 4), 16));
+ residual.SetCoeffs(rd.YAcLevels.AsSpan(16 * (x + (y * 4)), 16));
int res = this.bitWriter.PutCoeffs(ctx, residual);
it.TopNz[x] = it.LeftNz[y] = res;
}
@@ -747,7 +915,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
for (x = 0; x < 2; ++x)
{
int ctx = it.TopNz[4 + ch + x] + it.LeftNz[4 + ch + y];
- residual.SetCoeffs(rd.UvLevels.AsSpan((ch * 2) + x + (y * 2), 16));
+ residual.SetCoeffs(rd.UvLevels.AsSpan(16 * ((ch * 2) + x + (y * 2)), 16));
var res = this.bitWriter.PutCoeffs(ctx, residual);
it.TopNz[4 + ch + x] = it.LeftNz[4 + ch + y] = res;
}
@@ -762,6 +930,67 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
it.BytesToNz();
}
+ ///
+ /// Same as CodeResiduals, but doesn't actually write anything.
+ /// Instead, it just records the event distribution.
+ ///
+ private void RecordResiduals(Vp8EncIterator it, Vp8ModeScore rd)
+ {
+ int x, y, ch;
+ var residual = new Vp8Residual();
+ bool i16 = it.CurrentMacroBlockInfo.MacroBlockType == Vp8MacroBlockType.I16X16;
+ int segment = it.CurrentMacroBlockInfo.Segment;
+
+ it.NzToBytes();
+
+ if (i16)
+ {
+ // i16x16
+ residual.Init(0, 1, this.proba);
+ residual.SetCoeffs(rd.YDcLevels);
+ var res = residual.RecordCoeffs(it.TopNz[8] + it.LeftNz[8]);
+ it.TopNz[8] = res;
+ it.LeftNz[8] = res;
+ residual.Init(1, 0, this.proba);
+ }
+ else
+ {
+ residual.Init(0, 3, this.proba);
+ }
+
+ // luma-AC
+ for (y = 0; y < 4; ++y)
+ {
+ for (x = 0; x < 4; ++x)
+ {
+ int ctx = it.TopNz[x] + it.LeftNz[y];
+ residual.SetCoeffs(rd.YAcLevels.AsSpan(16 * (x + (y * 4)), 16));
+ var res = residual.RecordCoeffs(ctx);
+ it.TopNz[x] = res;
+ it.LeftNz[y] = res;
+ }
+ }
+
+ // U/V
+ residual.Init(0, 2, this.proba);
+ for (ch = 0; ch <= 2; ch += 2)
+ {
+ for (y = 0; y < 2; ++y)
+ {
+ for (x = 0; x < 2; ++x)
+ {
+ int ctx = it.TopNz[4 + ch + x] + it.LeftNz[4 + ch + y];
+ residual.SetCoeffs(rd.UvLevels.AsSpan(16 * ((ch * 2) + x + (y * 2)), 16));
+ var res = residual.RecordCoeffs(ctx);
+ it.TopNz[4 + ch + x] = res;
+ it.LeftNz[4 + ch + y] = res;
+ }
+ }
+ }
+
+ it.BytesToNz();
+ }
+
private int ReconstructIntra16(Vp8EncIterator it, Vp8SegmentInfo dqm, Vp8ModeScore rd, Span yuvOut, int mode)
{
Span reference = it.YuvP.AsSpan(Vp8EncIterator.Vp8I16ModeOffsets[mode]);
@@ -785,7 +1014,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
// Zero-out the first coeff, so that: a) nz is correct below, and
// b) finding 'last' non-zero coeffs in SetResidualCoeffs() is simplified.
tmp[n * 16] = tmp[(n + 1) * 16] = 0;
- nz |= this.Quantize2Blocks(tmpSpan.Slice(n * 16), rd.YAcLevels.AsSpan(n, 32), dqm.Y1) << n;
+ nz |= this.Quantize2Blocks(tmpSpan.Slice(n * 16), rd.YAcLevels.AsSpan(n * 16, 32), dqm.Y1) << n;
}
// Transform back.
@@ -1400,6 +1629,12 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
return v;
}
+ [MethodImpl(InliningOptions.ShortMethod)]
+ private double GetPsnr(long mse, long size)
+ {
+ return (mse > 0 && size > 0) ? 10.0f * Math.Log10(255.0f * 255.0f * size / mse) : 99;
+ }
+
[MethodImpl(InliningOptions.ShortMethod)]
private int QuantDiv(uint n, uint iQ, uint b)
{
diff --git a/src/ImageSharp/Formats/WebP/Lossy/Vp8ModeScore.cs b/src/ImageSharp/Formats/WebP/Lossy/Vp8ModeScore.cs
index f9316e40b..816752085 100644
--- a/src/ImageSharp/Formats/WebP/Lossy/Vp8ModeScore.cs
+++ b/src/ImageSharp/Formats/WebP/Lossy/Vp8ModeScore.cs
@@ -17,7 +17,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
{
this.YDcLevels = new short[16];
this.YAcLevels = new short[16 * 16];
- this.UvLevels = new short[4 + (4 * 16)];
+ this.UvLevels = new short[(4 + 4) * 16];
this.ModesI4 = new byte[16];
}
diff --git a/src/ImageSharp/Formats/WebP/Lossy/Vp8RDLevel.cs b/src/ImageSharp/Formats/WebP/Lossy/Vp8RDLevel.cs
new file mode 100644
index 000000000..763e29f5b
--- /dev/null
+++ b/src/ImageSharp/Formats/WebP/Lossy/Vp8RDLevel.cs
@@ -0,0 +1,31 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+namespace SixLabors.ImageSharp.Formats.WebP.Lossy
+{
+ ///
+ /// Rate-distortion optimization levels
+ ///
+ internal enum Vp8RdLevel
+ {
+ ///
+ /// No rd-opt.
+ ///
+ RdOptNone = 0,
+
+ ///
+ /// Basic scoring (no trellis).
+ ///
+ RdOptBasic = 1,
+
+ ///
+ /// Perform trellis-quant on the final decision only.
+ ///
+ RdOptTrellis = 2,
+
+ ///
+ /// Trellis-quant for every scoring (much slower).
+ ///
+ RdOptTrellisAll = 3
+ }
+}
diff --git a/src/ImageSharp/Formats/WebP/Lossy/Vp8Residual.cs b/src/ImageSharp/Formats/WebP/Lossy/Vp8Residual.cs
index 96efe7f4f..f79e8f91d 100644
--- a/src/ImageSharp/Formats/WebP/Lossy/Vp8Residual.cs
+++ b/src/ImageSharp/Formats/WebP/Lossy/Vp8Residual.cs
@@ -10,6 +10,8 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
///
internal class Vp8Residual
{
+ private const int MaxVariableLevel = 67;
+
public int First { get; set; }
public int Last { get; set; }
@@ -20,14 +22,16 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
public Vp8BandProbas[] Prob { get; set; }
+ public Vp8Stats[] Stats { get; set; }
+
public void Init(int first, int coeffType, Vp8EncProba prob)
{
this.First = first;
this.CoeffType = coeffType;
this.Prob = prob.Coeffs[this.CoeffType];
+ this.Stats = prob.Stats[this.CoeffType];
// TODO:
- // res->stats = enc->proba_.stats_[coeff_type];
// res->costs = enc->proba_.remapped_costs_[coeff_type];
}
@@ -46,5 +50,79 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
this.Coeffs = coeffs.ToArray();
}
+
+ // Simulate block coding, but only record statistics.
+ // Note: no need to record the fixed probas.
+ public int RecordCoeffs(int ctx)
+ {
+ int n = this.First;
+ Vp8StatsArray s = this.Stats[n].Stats[ctx];
+ if (this.Last < 0)
+ {
+ this.RecordStats(0, s, 0);
+ return 0;
+ }
+
+ while (n <= this.Last)
+ {
+ int v;
+ this.RecordStats(1, s, 0); // order of record doesn't matter
+ while ((v = this.Coeffs[n++]) == 0)
+ {
+ this.RecordStats(0, s, 1);
+ s = this.Stats[WebPConstants.Vp8EncBands[n]].Stats[0];
+ this.RecordStats(1, s, 1);
+ if (this.RecordStats((v + 1) > 2u ? 1 : 0, s, 2) == 0)
+ {
+ // v = -1 or 1
+ s = this.Stats[WebPConstants.Vp8EncBands[n]].Stats[1];
+ }
+ else
+ {
+ v = Math.Abs(v);
+ if (v > MaxVariableLevel)
+ {
+ v = MaxVariableLevel;
+ }
+
+ int bits = WebPLookupTables.Vp8LevelCodes[v - 1][1];
+ int pattern = WebPLookupTables.Vp8LevelCodes[v - 1][0];
+ int i;
+ for (i = 0; (pattern >>= 1) != 0; ++i)
+ {
+ int mask = 2 << i;
+ if ((pattern & 1) != 0)
+ {
+ this.RecordStats(bits & mask, s, 3 + i);
+ }
+ }
+
+ s = this.Stats[WebPConstants.Vp8EncBands[n]].Stats[2];
+ }
+ }
+ }
+
+ if (n < 16)
+ {
+ this.RecordStats(0, s, 0);
+ }
+
+ return 1;
+ }
+
+ private int RecordStats(int bit, Vp8StatsArray statsArr, int idx)
+ {
+ // An overflow is inbound. Note we handle this at 0xfffe0000u instead of
+ // 0xffff0000u to make sure p + 1u does not overflow.
+ if (statsArr.Stats[idx] >= 0xfffe0000u)
+ {
+ statsArr.Stats[idx] = ((statsArr.Stats[idx] + 1u) >> 1) & 0x7fff7fffu; // -> divide the stats by 2.
+ }
+
+ // record bit count (lower 16 bits) and increment total count (upper 16 bits).
+ statsArr.Stats[idx] += 0x00010000u + (uint)bit;
+
+ return bit;
+ }
}
}
diff --git a/src/ImageSharp/Formats/WebP/Lossy/Vp8Stats.cs b/src/ImageSharp/Formats/WebP/Lossy/Vp8Stats.cs
new file mode 100644
index 000000000..374d37960
--- /dev/null
+++ b/src/ImageSharp/Formats/WebP/Lossy/Vp8Stats.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+namespace SixLabors.ImageSharp.Formats.WebP.Lossy
+{
+ internal class Vp8Stats
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public Vp8Stats()
+ {
+ this.Stats = new Vp8StatsArray[WebPConstants.NumCtx];
+ for (int i = 0; i < WebPConstants.NumCtx; i++)
+ {
+ this.Stats[i] = new Vp8StatsArray();
+ }
+ }
+
+ public Vp8StatsArray[] Stats { get; }
+ }
+}
diff --git a/src/ImageSharp/Formats/WebP/Lossy/Vp8StatsArray.cs b/src/ImageSharp/Formats/WebP/Lossy/Vp8StatsArray.cs
new file mode 100644
index 000000000..69c0fd5bf
--- /dev/null
+++ b/src/ImageSharp/Formats/WebP/Lossy/Vp8StatsArray.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 Vp8StatsArray
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public Vp8StatsArray()
+ {
+ this.Stats = new uint[WebPConstants.NumProbas];
+ }
+
+ public uint[] Stats { get; }
+ }
+}
diff --git a/src/ImageSharp/Formats/WebP/Lossy/WebPLossyDecoder.cs b/src/ImageSharp/Formats/WebP/Lossy/WebPLossyDecoder.cs
index 351f1a45e..53fe0b95d 100644
--- a/src/ImageSharp/Formats/WebP/Lossy/WebPLossyDecoder.cs
+++ b/src/ImageSharp/Formats/WebP/Lossy/WebPLossyDecoder.cs
@@ -1278,7 +1278,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
for (int b = 0; b < 16 + 1; ++b)
{
- proba.BandsPtr[t][b] = proba.Bands[t, WebPConstants.Bands[b]];
+ proba.BandsPtr[t][b] = proba.Bands[t, WebPConstants.Vp8EncBands[b]];
}
}
diff --git a/src/ImageSharp/Formats/WebP/WebPConstants.cs b/src/ImageSharp/Formats/WebP/WebPConstants.cs
index a53130e2a..08dac5bf0 100644
--- a/src/ImageSharp/Formats/WebP/WebPConstants.cs
+++ b/src/ImageSharp/Formats/WebP/WebPConstants.cs
@@ -6,7 +6,7 @@ using System.Collections.Generic;
namespace SixLabors.ImageSharp.Formats.WebP
{
///
- /// Constants used for decoding VP8 and VP8L bitstreams.
+ /// Constants used for encoding and decoding VP8 and VP8L bitstreams.
///
internal static class WebPConstants
{
@@ -78,11 +78,21 @@ namespace SixLabors.ImageSharp.Formats.WebP
///
public const int Vp8LImageSizeBits = 14;
+ ///
+ /// Size of the frame header within VP8 data.
+ ///
+ public const int Vp8FrameHeaderSize = 10;
+
///
/// Size of a chunk header.
///
public const int ChunkHeaderSize = 8;
+ ///
+ /// Size of the RIFF header ("RIFFnnnnWEBP").
+ ///
+ public const int RiffHeaderSize = 12;
+
///
/// Size of a chunk tag (e.g. "VP8L").
///
@@ -241,6 +251,11 @@ namespace SixLabors.ImageSharp.Formats.WebP
public const int QFix = 17;
+ ///
+ /// Max size of mode partition.
+ ///
+ public const int Vp8MaxPartition0Size = 1 << 19;
+
public static readonly short[] Vp8FixedCostsUv = { 302, 984, 439, 642 };
public static readonly short[] Vp8FixedCostsI16 = { 663, 919, 872, 919 };
@@ -258,7 +273,7 @@ namespace SixLabors.ImageSharp.Formats.WebP
public static readonly byte[] FilterExtraRows = { 0, 2, 8 };
// Paragraph 9.9
- public static readonly int[] Bands =
+ public static readonly int[] Vp8EncBands =
{
0, 1, 2, 3, 6, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6, 7, 0
};
diff --git a/src/ImageSharp/Formats/WebP/WebPEncoderCore.cs b/src/ImageSharp/Formats/WebP/WebPEncoderCore.cs
index 1158421f0..5fbae79e2 100644
--- a/src/ImageSharp/Formats/WebP/WebPEncoderCore.cs
+++ b/src/ImageSharp/Formats/WebP/WebPEncoderCore.cs
@@ -48,6 +48,11 @@ namespace SixLabors.ImageSharp.Formats.WebP
///
private readonly int method;
+ ///
+ /// The number of entropy-analysis passes (in [1..10]).
+ ///
+ private readonly int entropyPasses;
+
///
/// Initializes a new instance of the class.
///
@@ -60,6 +65,7 @@ namespace SixLabors.ImageSharp.Formats.WebP
this.lossy = options.Lossy;
this.quality = options.Quality;
this.method = options.Method;
+ this.entropyPasses = options.EntropyPasses;
}
///
@@ -79,7 +85,7 @@ namespace SixLabors.ImageSharp.Formats.WebP
if (this.lossy)
{
- var enc = new Vp8Encoder(this.memoryAllocator, image.Width, image.Height, this.quality, this.method);
+ var enc = new Vp8Encoder(this.memoryAllocator, image.Width, image.Height, this.quality, this.method, this.entropyPasses);
enc.Encode(image, stream);
}
else