diff --git a/src/ImageSharp/Formats/WebP/BitWriter/Vp8LBitWriter.cs b/src/ImageSharp/Formats/WebP/BitWriter/Vp8LBitWriter.cs
index afb91b43a1..26b58377f9 100644
--- a/src/ImageSharp/Formats/WebP/BitWriter/Vp8LBitWriter.cs
+++ b/src/ImageSharp/Formats/WebP/BitWriter/Vp8LBitWriter.cs
@@ -2,6 +2,7 @@
// Licensed under the GNU Affero General Public License, Version 3.
using System;
+using System.Buffers.Binary;
using SixLabors.ImageSharp.Formats.WebP.Lossless;
namespace SixLabors.ImageSharp.Formats.WebP.BitWriter
@@ -49,13 +50,12 @@ namespace SixLabors.ImageSharp.Formats.WebP.BitWriter
public Vp8LBitWriter(int expectedSize)
{
this.buffer = new byte[expectedSize];
+ this.end = this.buffer.Length;
}
///
/// This function writes bits into bytes in increasing addresses (little endian),
- /// and within a byte least-significant-bit first.
- /// This function can write up to 32 bits in one go, but VP8LBitReader can only
- /// read 24 bits max (VP8L_MAX_NUM_BIT_READ).
+ /// and within a byte least-significant-bit first. This function can write up to 32 bits in one go.
///
public void PutBits(uint bits, int nBits)
{
@@ -85,6 +85,11 @@ namespace SixLabors.ImageSharp.Formats.WebP.BitWriter
this.PutBits((uint)((bits << depth) | symbol), depth + nBits);
}
+ public int NumBytes()
+ {
+ return this.cur + ((this.used + 7) >> 3);
+ }
+
///
/// Internal function for PutBits flushing 32 bits from the written state.
///
@@ -101,7 +106,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.BitWriter
}
}
- //*(vp8l_wtype_t*)bw->cur_ = (vp8l_wtype_t)WSWAP((vp8l_wtype_t)bw->bits_);
+ BinaryPrimitives.WriteUInt64LittleEndian(this.buffer.AsSpan(this.cur), this.bits);
this.cur += WriterBytes;
this.bits >>= WriterBits;
this.used -= WriterBits;
@@ -109,6 +114,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.BitWriter
private bool BitWriterResize(int extraSize)
{
+ // TODO: resize buffer
return true;
}
}
diff --git a/src/ImageSharp/Formats/WebP/Lossless/BackwardReferenceEncoder.cs b/src/ImageSharp/Formats/WebP/Lossless/BackwardReferenceEncoder.cs
index d9532c91b5..048ddde997 100644
--- a/src/ImageSharp/Formats/WebP/Lossless/BackwardReferenceEncoder.cs
+++ b/src/ImageSharp/Formats/WebP/Lossless/BackwardReferenceEncoder.cs
@@ -232,22 +232,20 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
/// Evaluates best possible backward references for specified quality.
/// The input cache_bits to 'VP8LGetBackwardReferences' sets the maximum cache
/// bits to use (passing 0 implies disabling the local color cache).
- /// The optimal cache bits is evaluated and set for the *cache_bits parameter.
+ /// The optimal cache bits is evaluated and set for the cacheBits parameter.
/// The return value is the pointer to the best of the two backward refs viz,
/// refs[0] or refs[1].
///
public static Vp8LBackwardRefs GetBackwardReferences(int width, int height, Span bgra, int quality,
- int lz77TypesToTry, int cacheBits, Vp8LHashChain hashChain, Vp8LBackwardRefs best, Vp8LBackwardRefs worst)
+ int lz77TypesToTry, ref int cacheBits, Vp8LHashChain hashChain, Vp8LBackwardRefs best, Vp8LBackwardRefs worst)
{
var histo = new Vp8LHistogram[WebPConstants.MaxColorCacheBits];
- int lz77Type = 0;
int lz77TypeBest = 0;
double bitCostBest = -1;
int cacheBitsInitial = cacheBits;
Vp8LHashChain hashChainBox = null;
- for (lz77Type = 1; lz77TypesToTry > 0; lz77TypesToTry &= ~lz77Type, lz77Type <<= 1)
+ for (int lz77Type = 1; lz77TypesToTry > 0; lz77TypesToTry &= ~lz77Type, lz77Type <<= 1)
{
- int res = 0;
double bitCost;
int cacheBitsTmp = cacheBitsInitial;
if ((lz77TypesToTry & lz77Type) == 0)
@@ -312,25 +310,30 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
///
/// Evaluate optimal cache bits for the local color cache.
- /// The input *best_cache_bits sets the maximum cache bits to use (passing 0 implies disabling the local color cache).
- /// The local color cache is also disabled for the lower (<= 25) quality.
+ /// The input bestCacheBits sets the maximum cache bits to use (passing 0 implies disabling the local color cache).
+ /// The local color cache is also disabled for the lower (smaller then 25) quality.
///
private static int CalculateBestCacheSize(Span bgra, int quality, Vp8LBackwardRefs refs, int bestCacheBits)
{
int cacheBitsMax = (quality <= 25) ? 0 : bestCacheBits;
+ if (cacheBitsMax == 0)
+ {
+ // Local color cache is disabled.
+ return 0;
+ }
+
double entropyMin = MaxEntropy;
int pos = 0;
- var ccInit = new int[WebPConstants.MaxColorCacheBits + 1];
var colorCache = new ColorCache[WebPConstants.MaxColorCacheBits + 1];
var histos = new Vp8LHistogram[WebPConstants.MaxColorCacheBits + 1];
- if (cacheBitsMax == 0)
+ for (int i = 0; i < WebPConstants.MaxColorCacheBits + 1; i++)
{
- // Local color cache is disabled.
- return 0;
+ histos[i] = new Vp8LHistogram();
+ colorCache[i] = new ColorCache();
+ colorCache[i].Init(i);
}
- // Find the cache_bits giving the lowest entropy. The search is done in a
- // brute-force way as the function (entropy w.r.t cache_bits) can be anything in practice.
+ // Find the cache_bits giving the lowest entropy.
using List.Enumerator c = refs.Refs.GetEnumerator();
while (c.MoveNext())
{
@@ -346,13 +349,13 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
// The keys of the caches can be derived from the longest one.
int key = ColorCache.HashPix(pix, 32 - cacheBitsMax);
- // Do not use the color cache for cache_bits = 0.
+ // Do not use the color cache for cacheBits = 0.
++histos[0].Blue[b];
++histos[0].Literal[g];
++histos[0].Red[r];
++histos[0].Alpha[a];
- // Deal with cache_bits > 0.
+ // Deal with cacheBits > 0.
for (int i = cacheBitsMax; i >= 1; i--, key >>= 1)
{
if (colorCache[i].Lookup(key) == pix)
@@ -371,7 +374,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
}
else
{
- // We should compute the contribution of the (distance,length)
+ // We should compute the contribution of the (distance, length)
// histograms but those are the same independently from the cache size.
// As those constant contributions are in the end added to the other
// histogram contributions, we can safely ignore them.
@@ -437,7 +440,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
colorCache.Init(cacheBits);
}
- // TODO: VP8LClearBackwardRefs(refs);
+ refs.Refs.Clear();
for (int i = 0; i < pixCount;)
{
// Alternative #1: Code the pixels starting at 'i' using backward reference.
@@ -641,7 +644,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
break;
}
- // The same color is repeated counts_pos times at j_offset and j.
+ // The same color is repeated counts_pos times at jOffset and j.
currLength += countsJOffset;
jOffset += countsJOffset;
j += countsJOffset;
@@ -693,7 +696,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
colorCache.Init(cacheBits);
}
- // VP8LClearBackwardRefs(refs);
+ refs.Refs.Clear();
// Add first pixel as literal.
AddSingleLiteral(bgra[0], useColorCache, colorCache, refs);
@@ -708,8 +711,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
refs.Add(PixOrCopy.CreateCopy(1, (short)rleLen));
// We don't need to update the color cache here since it is always the
- // same pixel being copied, and that does not change the color cache
- // state.
+ // same pixel being copied, and that does not change the color cache state.
i += rleLen;
}
else if (prevRowLen >= MinLength)
@@ -734,7 +736,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
if (useColorCache)
{
- // VP8LColorCacheClear();
+ // TODO: VP8LColorCacheClear();
}
}
@@ -756,7 +758,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
int ix = colorCache.Contains(bgraLiteral);
if (ix >= 0)
{
- // color cache contains bgraLiteral
+ // Color cache contains bgraLiteral
v = PixOrCopy.CreateCacheIdx(ix);
}
else
@@ -776,7 +778,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
}
}
- // VP8LColorCacheClear(colorCache);
+ // TODO: VP8LColorCacheClear(colorCache);
}
private static void BackwardReferences2DLocality(int xSize, Vp8LBackwardRefs refs)
@@ -835,10 +837,10 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
///
/// Returns the exact index where array1 and array2 are different. For an index
- /// inferior or equal to best_len_match, the return value just has to be strictly
- /// inferior to best_len_match. The current behavior is to return 0 if this index
- /// is best_len_match, and the index itself otherwise.
- /// If no two elements are the same, it returns max_limit.
+ /// inferior or equal to bestLenMatch, the return value just has to be strictly
+ /// inferior to best_lenMatch. The current behavior is to return 0 if this index
+ /// is bestLenMatch, and the index itself otherwise.
+ /// If no two elements are the same, it returns maxLimit.
///
private static int FindMatchLength(Span array1, Span array2, int bestLenMatch, int maxLimit)
{
diff --git a/src/ImageSharp/Formats/WebP/Lossless/DominantCostRange.cs b/src/ImageSharp/Formats/WebP/Lossless/DominantCostRange.cs
new file mode 100644
index 0000000000..f81cc2c9f4
--- /dev/null
+++ b/src/ImageSharp/Formats/WebP/Lossless/DominantCostRange.cs
@@ -0,0 +1,92 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the GNU Affero General Public License, Version 3.
+
+namespace SixLabors.ImageSharp.Formats.WebP.Lossless
+{
+ ///
+ /// Data container to keep track of cost range for the three dominant entropy symbols.
+ ///
+ internal class DominantCostRange
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public DominantCostRange()
+ {
+ this.LiteralMax = 0.0d;
+ this.LiteralMin = double.MaxValue;
+ this.RedMax = 0.0d;
+ this.RedMin = double.MaxValue;
+ this.BlueMax = 0.0d;
+ this.BlueMin = double.MaxValue;
+ }
+
+ public double LiteralMax { get; set; }
+
+ public double LiteralMin { get; set; }
+
+ public double RedMax { get; set; }
+
+ public double RedMin { get; set; }
+
+ public double BlueMax { get; set; }
+
+ public double BlueMin { get; set; }
+
+ public void UpdateDominantCostRange(Vp8LHistogram h)
+ {
+ if (this.LiteralMax < h.LiteralCost)
+ {
+ this.LiteralMax = h.LiteralCost;
+ }
+
+ if (this.LiteralMin > h.LiteralCost)
+ {
+ this.LiteralMin = h.LiteralCost;
+ }
+
+ if (this.RedMax < h.RedCost)
+ {
+ this.RedMax = h.RedCost;
+ }
+
+ if (this.RedMin > h.RedCost)
+ {
+ this.RedMin = h.RedCost;
+ }
+
+ if (this.BlueMax < h.BlueCost)
+ {
+ this.BlueMax = h.BlueCost;
+ }
+
+ if (this.BlueMin > h.BlueCost)
+ {
+ this.BlueMin = h.BlueCost;
+ }
+ }
+
+ public int GetHistoBinIndex(Vp8LHistogram h, int numPartitions)
+ {
+ int binId = GetBinIdForEntropy(this.LiteralMin, this.LiteralMax, h.LiteralCost, numPartitions);
+ binId = (binId * numPartitions) + GetBinIdForEntropy(this.RedMin, this.RedMax, h.RedCost, numPartitions);
+ binId = (binId * numPartitions) + GetBinIdForEntropy(this.BlueMin, this.BlueMax, h.BlueCost, numPartitions);
+
+ return binId;
+ }
+
+ private static int GetBinIdForEntropy(double min, double max, double val, int numPartitions)
+ {
+ double range = max - min;
+ if (range > 0.0d)
+ {
+ double delta = val - min;
+ return (int)((numPartitions - 1e-6) * delta / range);
+ }
+ else
+ {
+ return 0;
+ }
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/WebP/Lossless/HistogramEncoder.cs b/src/ImageSharp/Formats/WebP/Lossless/HistogramEncoder.cs
new file mode 100644
index 0000000000..330d3afb2f
--- /dev/null
+++ b/src/ImageSharp/Formats/WebP/Lossless/HistogramEncoder.cs
@@ -0,0 +1,627 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the GNU Affero General Public License, Version 3.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace SixLabors.ImageSharp.Formats.WebP.Lossless
+{
+ internal class HistogramEncoder
+ {
+ ///
+ /// Number of partitions for the three dominant (literal, red and blue) symbol costs.
+ ///
+ private const int NumPartitions = 4;
+
+ ///
+ /// The size of the bin-hash corresponding to the three dominant costs.
+ ///
+ private const int BinSize = NumPartitions * NumPartitions * NumPartitions;
+
+ ///
+ /// Maximum number of histograms allowed in greedy combining algorithm.
+ ///
+ private const int MaxHistoGreedy = 100;
+
+ private const uint NonTrivialSym = 0xffffffff;
+
+ public static void GetHistoImageSymbols(int xSize, int ySize, Vp8LBackwardRefs refs, int quality, int histoBits, int cacheBits, List imageHisto, Vp8LHistogram tmpHisto, short[] histogramSymbols)
+ {
+ int histoXSize = histoBits > 0 ? LosslessUtils.SubSampleSize(xSize, histoBits) : 1;
+ int histoYSize = histoBits > 0 ? LosslessUtils.SubSampleSize(ySize, histoBits) : 1;
+ int imageHistoRawSize = histoXSize * histoYSize;
+ int entropyCombineNumBins = BinSize;
+ short[] mapTmp = new short[imageHistoRawSize];
+ short[] clusterMappings = new short[imageHistoRawSize];
+ int numUsed = imageHistoRawSize;
+ var origHisto = new List(imageHistoRawSize);
+ for (int i = 0; i < imageHistoRawSize; i++)
+ {
+ origHisto.Add(new Vp8LHistogram(cacheBits));
+ }
+
+ // Construct the histograms from backward references.
+ HistogramBuild(xSize, histoBits, refs, origHisto);
+
+ // Copies the histograms and computes its bit_cost. histogramSymbols is optimized.
+ HistogramCopyAndAnalyze(origHisto, imageHisto, ref numUsed, histogramSymbols);
+
+ var entropyCombine = (numUsed > entropyCombineNumBins * 2) && (quality < 100);
+ if (entropyCombine)
+ {
+ var binMap = mapTmp;
+ var numClusters = numUsed;
+ double combineCostFactor = GetCombineCostFactor(imageHistoRawSize, quality);
+ HistogramAnalyzeEntropyBin(imageHisto, binMap);
+
+ // Collapse histograms with similar entropy.
+ HistogramCombineEntropyBin(imageHisto, ref numUsed, histogramSymbols, clusterMappings, tmpHisto, binMap, entropyCombineNumBins, combineCostFactor);
+
+ OptimizeHistogramSymbols(imageHisto, clusterMappings, numClusters, mapTmp, histogramSymbols);
+ }
+
+ if (!entropyCombine)
+ {
+ float x = quality / 100.0f;
+
+ // Cubic ramp between 1 and MaxHistoGreedy:
+ int thresholdSize = (int)(1 + (x * x * x * (MaxHistoGreedy - 1)));
+ bool doGreedy = HistogramCombineStochastic(imageHisto, ref numUsed, thresholdSize);
+ if (doGreedy)
+ {
+ HistogramCombineGreedy(imageHisto, ref numUsed);
+ }
+ }
+ }
+
+ ///
+ /// Construct the histograms from backward references.
+ ///
+ private static void HistogramBuild(int xSize, int histoBits, Vp8LBackwardRefs backwardRefs, List histograms)
+ {
+ int x = 0, y = 0;
+ int histoXSize = LosslessUtils.SubSampleSize(xSize, histoBits);
+ using List.Enumerator backwardRefsEnumerator = backwardRefs.Refs.GetEnumerator();
+ while (backwardRefsEnumerator.MoveNext())
+ {
+ PixOrCopy v = backwardRefsEnumerator.Current;
+ int ix = ((y >> histoBits) * histoXSize) + (x >> histoBits);
+ histograms[ix].AddSinglePixOrCopy(v, false);
+ x += v.Len;
+ while (x >= xSize)
+ {
+ x -= xSize;
+ y++;
+ }
+ }
+ }
+
+ ///
+ /// Partition histograms to different entropy bins for three dominant (literal,
+ /// red and blue) symbol costs and compute the histogram aggregate bitCost.
+ ///
+ private static void HistogramAnalyzeEntropyBin(List histograms, short[] binMap)
+ {
+ int histoSize = histograms.Count;
+ var costRange = new DominantCostRange();
+
+ // Analyze the dominant (literal, red and blue) entropy costs.
+ for (int i = 0; i < histoSize; i++)
+ {
+ costRange.UpdateDominantCostRange(histograms[i]);
+ }
+
+ // bin-hash histograms on three of the dominant (literal, red and blue)
+ // symbol costs and store the resulting bin_id for each histogram.
+ for (int i = 0; i < histoSize; i++)
+ {
+ binMap[i] = (short)costRange.GetHistoBinIndex(histograms[i], NumPartitions);
+ }
+ }
+
+ private static void HistogramCopyAndAnalyze(List origHistograms, List histograms, ref int numUsed, short[] histogramSymbols)
+ {
+ int numUsedOrig = numUsed;
+ var indicesToRemove = new List();
+ for (int clusterId = 0, i = 0; i < origHistograms.Count; i++)
+ {
+ Vp8LHistogram histo = origHistograms[i];
+ histo.UpdateHistogramCost();
+
+ // Skip the histogram if it is completely empty, which can happen for tiles
+ // with no information (when they are skipped because of LZ77).
+ if (!histo.IsUsed[0] && !histo.IsUsed[1] && !histo.IsUsed[2] && !histo.IsUsed[3] && !histo.IsUsed[4])
+ {
+ indicesToRemove.Add(i);
+ }
+ else
+ {
+ // TODO: HistogramCopy(histo, histograms[i]);
+ histogramSymbols[i] = (short)clusterId++;
+ }
+ }
+
+ foreach (int indice in indicesToRemove.OrderByDescending(v => v))
+ {
+ origHistograms.RemoveAt(indice);
+ histograms.RemoveAt(indice);
+ }
+ }
+
+ private static void HistogramCombineEntropyBin(List histograms, ref int numUsed, short[] clusters, short[] clusterMappings, Vp8LHistogram curCombo, short[] binMap, int numBins, double combineCostFactor)
+ {
+ for (int idx = 0; idx < histograms.Count; idx++)
+ {
+ clusterMappings[idx] = (short)idx;
+ }
+ }
+
+ ///
+ /// Given a Histogram set, the mapping of clusters 'clusterMapping' and the
+ /// current assignment of the cells in 'symbols', merge the clusters and
+ /// assign the smallest possible clusters values.
+ ///
+ private static void OptimizeHistogramSymbols(List histograms, short[] clusterMappings, int numClusters, short[] clusterMappingsTmp, short[] symbols)
+ {
+ int clusterMax;
+ bool doContinue = true;
+
+ // First, assign the lowest cluster to each pixel.
+ while (doContinue)
+ {
+ doContinue = false;
+ for (int i = 0; i < numClusters; i++)
+ {
+ int k;
+ k = clusterMappings[i];
+ while (k != clusterMappings[k])
+ {
+ clusterMappings[k] = clusterMappings[clusterMappings[k]];
+ k = clusterMappings[k];
+ }
+
+ if (k != clusterMappings[i])
+ {
+ doContinue = true;
+ clusterMappings[i] = (short)k;
+ }
+ }
+ }
+
+ // Create a mapping from a cluster id to its minimal version.
+ clusterMax = 0;
+ clusterMappingsTmp.AsSpan().Fill(0);
+
+ // Re-map the ids.
+ for (int i = 0; i < histograms.Count; i++)
+ {
+ int cluster;
+ cluster = clusterMappings[symbols[i]];
+ if (cluster > 0 && clusterMappingsTmp[cluster] == 0)
+ {
+ clusterMax++;
+ clusterMappingsTmp[cluster] = (short)clusterMax;
+ }
+
+ symbols[i] = clusterMappingsTmp[cluster];
+ }
+
+ // Make sure all cluster values are used.
+ clusterMax = 0;
+ for (int i = 0; i < histograms.Count; i++)
+ {
+ if (symbols[i] <= clusterMax)
+ {
+ continue;
+ }
+
+ clusterMax++;
+ }
+ }
+
+ ///
+ /// Perform histogram aggregation using a stochastic approach.
+ ///
+ /// true if a greedy approach needs to be performed afterwards, false otherwise.
+ private static bool HistogramCombineStochastic(List histograms, ref int numUsed, int minClusterSize)
+ {
+ var rand = new Random();
+ int triesWithNoSuccess = 0;
+ int outerIters = numUsed;
+ int numTriesNoSuccess = outerIters / 2;
+
+ // Priority queue of histogram pairs. Its size impacts the quality of the compression and the speed:
+ // the smaller the faster but the worse for the compression.
+ var histoPriorityList = new List();
+ int histoQueueMaxSize = histograms.Count * histograms.Count;
+
+ // Fill the initial mapping.
+ int[] mappings = new int[histograms.Count];
+ for (int j = 0, iter = 0; iter < histograms.Count; iter++)
+ {
+ mappings[j++] = iter;
+ }
+
+ // Collapse similar histograms
+ for (int iter = 0; iter < outerIters && numUsed >= minClusterSize && ++triesWithNoSuccess < numTriesNoSuccess; iter++)
+ {
+ double bestCost = (histoPriorityList.Count == 0) ? 0.0d : histoPriorityList[0].CostDiff;
+ int bestIdx1 = -1;
+ int bestIdx2 = 1;
+ int numTries = numUsed / 2; // TODO: should that be histogram.Count/2?
+ uint randRange = (uint)((numUsed - 1) * numUsed);
+
+ // Pick random samples.
+ for (int j = 0; numUsed >= 2 && j < numTries; j++)
+ {
+ // Choose two different histograms at random and try to combine them.
+ uint tmp = (uint)(rand.Next() % randRange);
+ double currCost;
+ int idx1 = (int)(tmp / (numUsed - 1));
+ int idx2 = (int)(tmp % (numUsed - 1));
+ if (idx2 >= idx1)
+ {
+ idx2++;
+ }
+
+ idx1 = mappings[idx1];
+ idx2 = mappings[idx2];
+
+ // Calculate cost reduction on combination.
+ currCost = HistoQueuePush(histoPriorityList, histoQueueMaxSize, histograms, idx1, idx2, bestCost);
+
+ // Found a better pair?
+ if (currCost < 0)
+ {
+ bestCost = currCost;
+
+ // Empty the queue if we reached full capacity.
+ if (histoPriorityList.Count == histoQueueMaxSize)
+ {
+ break;
+ }
+ }
+ }
+
+ if (histoPriorityList.Count == 0)
+ {
+ continue;
+ }
+
+ // Get the best histograms.
+ bestIdx1 = histoPriorityList[0].Idx1;
+ bestIdx2 = histoPriorityList[0].Idx2;
+
+ // Pop bestIdx2 from mappings.
+ var mappingIndex = Array.BinarySearch(mappings, bestIdx2);
+ // TODO: memmove(mapping_index, mapping_index + 1, sizeof(*mapping_index) *((*num_used) - (mapping_index - mappings) - 1));
+
+ // Merge the histograms and remove bestIdx2 from the queue.
+ HistogramAdd(histograms[bestIdx2], histograms[bestIdx1], histograms[bestIdx1]);
+ histograms.ElementAt(bestIdx1).BitCost = histoPriorityList[0].CostCombo;
+ histograms.RemoveAt(bestIdx2);
+ numUsed--;
+
+ var indicesToRemove = new List();
+ int lastIndex = histoPriorityList.Count - 1;
+ for (int j = 0; j < histoPriorityList.Count;)
+ {
+ HistogramPair p = histoPriorityList.ElementAt(j);
+ bool isIdx1Best = p.Idx1 == bestIdx1 || p.Idx1 == bestIdx2;
+ bool isIdx2Best = p.Idx2 == bestIdx1 || p.Idx2 == bestIdx2;
+ bool doEval = false;
+
+ // The front pair could have been duplicated by a random pick so
+ // check for it all the time nevertheless.
+ if (isIdx1Best && isIdx2Best)
+ {
+ indicesToRemove.Add(lastIndex);
+ numUsed--;
+ lastIndex--;
+ continue;
+ }
+
+ // Any pair containing one of the two best indices should only refer to
+ // best_idx1. Its cost should also be updated.
+ if (isIdx1Best)
+ {
+ p.Idx1 = bestIdx1;
+ doEval = true;
+ }
+ else if (isIdx2Best)
+ {
+ p.Idx2 = bestIdx1;
+ doEval = true;
+ }
+
+ // Make sure the index order is respected.
+ if (p.Idx1 > p.Idx2)
+ {
+ int tmp = p.Idx2;
+ p.Idx2 = p.Idx1;
+ p.Idx1 = tmp;
+ }
+
+ if (doEval)
+ {
+ // Re-evaluate the cost of an updated pair.
+ HistoQueueUpdatePair(histograms[p.Idx1], histograms[p.Idx2], 0.0d, p);
+ if (p.CostDiff >= 0.0d)
+ {
+ indicesToRemove.Add(lastIndex);
+ lastIndex--;
+ numUsed--;
+ continue;
+ }
+ }
+
+ HistoQueueUpdateHead(histoPriorityList, p);
+ j++;
+ }
+
+ triesWithNoSuccess = 0;
+ }
+
+ bool doGreedy = numUsed <= minClusterSize;
+
+ return doGreedy;
+ }
+
+ private static void HistogramCombineGreedy(List histograms, ref int numUsed)
+ {
+ int histoSize = histograms.Count;
+
+ // Priority list of histogram pairs.
+ var histoPriorityList = new List();
+ int maxHistoQueueSize = histoSize * histoSize;
+
+ for (int i = 0; i < histograms.Count; i++)
+ {
+ for (int j = i + 1; j < histograms.Count; j++)
+ {
+ // Initialize queue.
+ HistoQueuePush(histoPriorityList, maxHistoQueueSize, histograms, i, j, 0.0d);
+ }
+ }
+
+ while (histoPriorityList.Count > 0)
+ {
+ int idx1 = histoPriorityList[0].Idx1;
+ int idx2 = histoPriorityList[0].Idx2;
+ HistogramAdd(histograms[idx2], histograms[idx1], histograms[idx1]);
+ histograms[idx1].BitCost = histoPriorityList[0].CostCombo;
+
+ // Remove merged histogram.
+ histograms.RemoveAt(idx2);
+ numUsed--;
+
+ // Remove pairs intersecting the just combined best pair.
+ for (int i = 0; i < histoPriorityList.Count;)
+ {
+ HistogramPair p = histoPriorityList.ElementAt(i);
+ if (p.Idx1 == idx1 || p.Idx2 == idx1 || p.Idx1 == idx2 || p.Idx2 == idx2)
+ {
+ // Remove last entry from the queue.
+ p = histoPriorityList.ElementAt(histoPriorityList.Count - 1);
+ histoPriorityList.RemoveAt(histoPriorityList.Count - 1); // TODO: use list instead Queue?
+ }
+ else
+ {
+ HistoQueueUpdateHead(histoPriorityList, p);
+ i++;
+ }
+ }
+
+ // Push new pairs formed with combined histogram to the queue.
+ for (int i = 0; i < histograms.Count; i++)
+ {
+ if (i == idx1)
+ {
+ continue;
+ }
+
+ HistoQueuePush(histoPriorityList, maxHistoQueueSize, histograms, idx1, i, 0.0d);
+ }
+ }
+ }
+
+ ///
+ /// // Create a pair from indices "idx1" and "idx2" provided its cost
+ /// is inferior to "threshold", a negative entropy.
+ ///
+ /// The cost of the pair, or 0. if it superior to threshold.
+ private static double HistoQueuePush(List histoQueue, int queueMaxSize, List histograms, int idx1, int idx2, double threshold)
+ {
+ var pair = new HistogramPair();
+
+ // Stop here if the queue is full.
+ if (histoQueue.Count == queueMaxSize)
+ {
+ return 0.0d;
+ }
+
+ if (idx1 > idx2)
+ {
+ int tmp = idx2;
+ idx2 = idx1;
+ idx1 = tmp;
+ }
+
+ pair.Idx1 = idx1;
+ pair.Idx2 = idx2;
+ Vp8LHistogram h1 = histograms[idx1];
+ Vp8LHistogram h2 = histograms[idx2];
+
+ HistoQueueUpdatePair(h1, h2, threshold, pair);
+
+ // Do not even consider the pair if it does not improve the entropy.
+ if (pair.CostDiff >= threshold)
+ {
+ return 0.0d;
+ }
+
+ histoQueue.Add(pair);
+
+ HistoQueueUpdateHead(histoQueue, pair);
+
+ return pair.CostDiff;
+ }
+
+ ///
+ /// Update the cost diff and combo of a pair of histograms. This needs to be
+ /// called when the the histograms have been merged with a third one.
+ ///
+ private static void HistoQueueUpdatePair(Vp8LHistogram h1, Vp8LHistogram h2, double threshold, HistogramPair pair)
+ {
+ double sumCost = h1.BitCost + h2.BitCost;
+ pair.CostCombo = GetCombinedHistogramEntropy(h1, h2, sumCost + threshold);
+ pair.CostDiff = pair.CostCombo - sumCost;
+ }
+
+ private static double GetCombinedHistogramEntropy(Vp8LHistogram a, Vp8LHistogram b, double costThreshold)
+ {
+ double cost = 0.0d;
+ int paletteCodeBits = a.PaletteCodeBits;
+ bool trivialAtEnd = false;
+
+ cost += GetCombinedEntropy(a.Literal, b.Literal, Vp8LHistogram.HistogramNumCodes(paletteCodeBits), a.IsUsed[0], b.IsUsed[0], false);
+
+ cost += ExtraCostCombined(a.Literal.AsSpan(WebPConstants.NumLiteralCodes), b.Literal.AsSpan(WebPConstants.NumLiteralCodes), WebPConstants.NumLengthCodes);
+
+ if (cost > costThreshold)
+ {
+ return 0;
+ }
+
+ if (a.TrivialSymbol != NonTrivialSym && a.TrivialSymbol == b.TrivialSymbol)
+ {
+ // A, R and B are all 0 or 0xff.
+ uint color_a = (a.TrivialSymbol >> 24) & 0xff;
+ uint color_r = (a.TrivialSymbol >> 16) & 0xff;
+ uint color_b = (a.TrivialSymbol >> 0) & 0xff;
+ if ((color_a == 0 || color_a == 0xff) &&
+ (color_r == 0 || color_r == 0xff) &&
+ (color_b == 0 || color_b == 0xff))
+ {
+ trivialAtEnd = true;
+ }
+ }
+
+ cost += GetCombinedEntropy(a.Red, b.Red, WebPConstants.NumLiteralCodes, a.IsUsed[1], b.IsUsed[1], trivialAtEnd);
+
+ return cost;
+ }
+
+ private static double GetCombinedEntropy(uint[] x, uint[] y, int length, bool isXUsed, bool isYUsed, bool trivialAtEnd)
+ {
+ var stats = new Vp8LStreaks();
+ if (trivialAtEnd)
+ {
+ // This configuration is due to palettization that transforms an indexed
+ // pixel into 0xff000000 | (pixel << 8) in BundleColorMap.
+ // BitsEntropyRefine is 0 for histograms with only one non-zero value.
+ // Only FinalHuffmanCost needs to be evaluated.
+
+ // Deal with the non-zero value at index 0 or length-1.
+ stats.Streaks[1][0] = 1;
+
+ // Deal with the following/previous zero streak.
+ stats.Counts[0] = 1;
+ stats.Streaks[0][1] = length - 1;
+
+ return stats.FinalHuffmanCost();
+ }
+
+ var bitEntropy = new Vp8LBitEntropy();
+ if (isXUsed)
+ {
+ if (isYUsed)
+ {
+ bitEntropy.GetCombinedEntropyUnrefined(x, y, length, stats);
+ }
+ else
+ {
+ bitEntropy.GetEntropyUnrefined(x, length, stats);
+ }
+ }
+ else
+ {
+ if (isYUsed)
+ {
+ bitEntropy.GetEntropyUnrefined(y, length, stats);
+ }
+ else
+ {
+ stats.Counts[0] = 1;
+ stats.Streaks[0][length > 3 ? 1 : 0] = length;
+ bitEntropy.Init();
+ }
+ }
+
+ return bitEntropy.BitsEntropyRefine() + stats.FinalHuffmanCost();
+ }
+
+ private static double ExtraCostCombined(Span x, Span y, int length)
+ {
+ double cost = 0.0d;
+ for (int i = 2; i < length - 2; i++)
+ {
+ int xy = (int)(x[i + 2] + y[i + 2]);
+ cost += (i >> 1) * xy;
+ }
+
+ return cost;
+ }
+
+ private static void HistogramAdd(Vp8LHistogram a, Vp8LHistogram b, Vp8LHistogram output)
+ {
+ // TODO: VP8LHistogramAdd(a, b, out);
+ output.TrivialSymbol = (a.TrivialSymbol == b.TrivialSymbol)
+ ? a.TrivialSymbol
+ : NonTrivialSym;
+ }
+
+ ///
+ /// Check whether a pair in the list should be updated as head or not.
+ ///
+ private static void HistoQueueUpdateHead(List histoQueue, HistogramPair pair)
+ {
+ if (pair.CostDiff < histoQueue[0].CostDiff)
+ {
+ // Replace the best pair.
+ histoQueue.RemoveAt(0);
+ histoQueue.Insert(0, pair);
+ }
+ }
+
+ private static double GetCombineCostFactor(int histoSize, int quality)
+ {
+ double combineCostFactor = 0.16d;
+ if (quality < 90)
+ {
+ if (histoSize > 256)
+ {
+ combineCostFactor /= 2.0d;
+ }
+
+ if (histoSize > 512)
+ {
+ combineCostFactor /= 2.0d;
+ }
+
+ if (histoSize > 1024)
+ {
+ combineCostFactor /= 2.0d;
+ }
+
+ if (quality <= 50)
+ {
+ combineCostFactor /= 2.0d;
+ }
+ }
+
+ return combineCostFactor;
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/WebP/Lossless/HistogramPair.cs b/src/ImageSharp/Formats/WebP/Lossless/HistogramPair.cs
new file mode 100644
index 0000000000..8e314c561b
--- /dev/null
+++ b/src/ImageSharp/Formats/WebP/Lossless/HistogramPair.cs
@@ -0,0 +1,19 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the GNU Affero General Public License, Version 3.
+
+namespace SixLabors.ImageSharp.Formats.WebP.Lossless
+{
+ ///
+ /// Pair of histograms. Negative Idx1 value means that pair is out-of-date.
+ ///
+ internal class HistogramPair
+ {
+ public int Idx1 { get; set; }
+
+ public int Idx2 { get; set; }
+
+ public double CostDiff { get; set; }
+
+ public double CostCombo { get; set; }
+ }
+}
diff --git a/src/ImageSharp/Formats/WebP/Lossless/LosslessUtils.cs b/src/ImageSharp/Formats/WebP/Lossless/LosslessUtils.cs
index 4132991a7b..94510efd23 100644
--- a/src/ImageSharp/Formats/WebP/Lossless/LosslessUtils.cs
+++ b/src/ImageSharp/Formats/WebP/Lossless/LosslessUtils.cs
@@ -437,22 +437,22 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
return (float)retVal;
}
- public static sbyte TransformColorRed(sbyte greenToRed, uint argb)
+ public static byte TransformColorRed(sbyte greenToRed, uint argb)
{
sbyte green = U32ToS8(argb >> 8);
int newRed = (int)(argb >> 16);
newRed -= ColorTransformDelta(greenToRed, green);
- return (sbyte)(newRed & 0xff);
+ return (byte)(newRed & 0xff);
}
- public static sbyte TransformColorBlue(sbyte greenToBlue, sbyte redToBlue, uint argb)
+ public static byte 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);
+ return (byte)(newBlue & 0xff);
}
///
diff --git a/src/ImageSharp/Formats/WebP/Lossless/PredictorEncoder.cs b/src/ImageSharp/Formats/WebP/Lossless/PredictorEncoder.cs
index c954e18b79..467bba031c 100644
--- a/src/ImageSharp/Formats/WebP/Lossless/PredictorEncoder.cs
+++ b/src/ImageSharp/Formats/WebP/Lossless/PredictorEncoder.cs
@@ -112,7 +112,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
///
/// Returns best predictor and updates the accumulated histogram.
- /// If max_quantization > 1, assumes that near lossless processing will be
+ /// If maxQuantization > 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).
@@ -184,10 +184,10 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
upperRow = currentRow;
currentRow = tmp;
- // Read current_row. Include a pixel to the left if it exists; include a
+ // Read currentRow. 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).
+ // not exist in the currentRow).
Span src = argb.Slice((y * width) + contextStartX, maxX + haveLeft + ((y + 1) < height ? 1 : 0));
Span dst = currentRow.Slice(contextStartX);
src.CopyTo(dst);
@@ -476,7 +476,8 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
Span tmp32 = upperRow;
upperRow = currentRow;
currentRow = tmp32;
- argb.Slice(y * width, width + y + (1 < height ? 1 : 0)).CopyTo(currentRow);
+ Span src = argb.Slice(y * width, width + ((y + 1) < height ? 1 : 0));
+ src.CopyTo(currentRow);
if (maxQuantization > 1)
{
// Compute max_diffs for the lower row now, because that needs the
@@ -659,7 +660,11 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
while (yScan-- > 0)
{
LosslessUtils.TransformColor(colorTransform, argb, xScan);
- argb = argb.Slice(xSize);
+
+ if (argb.Length > xSize)
+ {
+ argb = argb.Slice(xSize);
+ }
}
}
@@ -820,15 +825,16 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
private static void CollectColorRedTransforms(Span argb, int stride, int tileWidth, int tileHeight, int greenToRed, int[] histo)
{
- int pos = 0;
+ int startIdx = 0;
while (tileHeight-- > 0)
{
for (int x = 0; x < tileWidth; x++)
{
- ++histo[LosslessUtils.TransformColorRed((sbyte)greenToRed, argb[pos + x])];
+ int idx = LosslessUtils.TransformColorRed((sbyte)greenToRed, argb[startIdx + x]);
+ ++histo[idx];
}
- pos += stride;
+ startIdx += stride;
}
}
@@ -839,7 +845,8 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
{
for (int x = 0; x < tileWidth; x++)
{
- ++histo[LosslessUtils.TransformColorBlue((sbyte)greenToBlue, (sbyte)redToBlue, argb[pos + x])];
+ int idx = LosslessUtils.TransformColorBlue((sbyte)greenToBlue, (sbyte)redToBlue, argb[pos + x]);
+ ++histo[idx];
}
pos += stride;
diff --git a/src/ImageSharp/Formats/WebP/Lossless/Vp8LBitEntropy.cs b/src/ImageSharp/Formats/WebP/Lossless/Vp8LBitEntropy.cs
index c16ff52974..8e5864146f 100644
--- a/src/ImageSharp/Formats/WebP/Lossless/Vp8LBitEntropy.cs
+++ b/src/ImageSharp/Formats/WebP/Lossless/Vp8LBitEntropy.cs
@@ -52,6 +52,15 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
///
public uint NoneZeroCode { get; set; }
+ public void Init()
+ {
+ this.Entropy = 0.0d;
+ this.Sum = 0;
+ this.NoneZeros = 0;
+ this.MaxVal = 0;
+ this.NoneZeroCode = NonTrivialSym;
+ }
+
public double BitsEntropyRefine()
{
double mix;
@@ -95,6 +104,8 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
public void BitsEntropyUnrefined(Span array, int n)
{
+ this.Init();
+
for (int i = 0; i < n; i++)
{
if (array[i] != 0)
@@ -121,6 +132,9 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
int i;
int iPrev = 0;
uint xPrev = x[0];
+
+ this.Init();
+
for (i = 1; i < length; ++i)
{
uint xi = x[i];
@@ -135,6 +149,50 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
this.Entropy += LosslessUtils.FastSLog2(this.Sum);
}
+ public void GetCombinedEntropyUnrefined(uint[] x, uint[] y, int length, Vp8LStreaks stats)
+ {
+ int i;
+ int iPrev = 0;
+ uint xyPrev = x[0] + y[0];
+
+ this.Init();
+
+ for (i = 1; i < length; i++)
+ {
+ uint xy = x[i] + y[i];
+ if (xy != xyPrev)
+ {
+ this.GetEntropyUnrefined(xy, i, ref xyPrev, ref iPrev, stats);
+ }
+ }
+
+ this.GetEntropyUnrefined(0, i, ref xyPrev, ref iPrev, stats);
+
+ this.Entropy += LosslessUtils.FastSLog2(this.Sum);
+ }
+
+ public void GetEntropyUnrefined(uint[] x, int length, Vp8LStreaks stats)
+ {
+ int i;
+ int iPrev = 0;
+ uint xPrev = x[0];
+
+ this.Init();
+
+ for (i = 1; i < length; i++)
+ {
+ uint xi = x[i];
+ if (xi != xPrev)
+ {
+ this.GetEntropyUnrefined(xi, i, ref xPrev, ref iPrev, stats);
+ }
+ }
+
+ this.GetEntropyUnrefined(0, i, ref xPrev, ref iPrev, stats);
+
+ this.Entropy += LosslessUtils.FastSLog2(this.Sum);
+ }
+
private void GetEntropyUnrefined(uint val, int i, ref uint valPrev, ref int iPrev, Vp8LStreaks stats)
{
// Gather info for the bit entropy.
diff --git a/src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs
index 0f54285a54..32cc7d7712 100644
--- a/src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs
+++ b/src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs
@@ -22,6 +22,9 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
///
private const int MinBlockSize = 256;
+ ///
+ /// The to use for buffer allocations.
+ ///
private MemoryAllocator memoryAllocator;
///
@@ -135,6 +138,18 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
this.BgraScratch = this.memoryAllocator.Allocate(argbScratchSize);
this.TransformData = this.memoryAllocator.Allocate(transformDataSize);
+ this.CurrentWidth = width;
+ }
+
+ ///
+ /// Clears the backward references.
+ ///
+ public void ClearRefs()
+ {
+ for (int i = 0; i < this.Refs.Length; i++)
+ {
+ this.Refs[i].Refs.Clear();
+ }
}
///
diff --git a/src/ImageSharp/Formats/WebP/Lossless/Vp8LHistogram.cs b/src/ImageSharp/Formats/WebP/Lossless/Vp8LHistogram.cs
index ec326905a7..fe47e45486 100644
--- a/src/ImageSharp/Formats/WebP/Lossless/Vp8LHistogram.cs
+++ b/src/ImageSharp/Formats/WebP/Lossless/Vp8LHistogram.cs
@@ -8,6 +8,13 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
{
internal class Vp8LHistogram
{
+ private const uint NonTrivialSym = 0xffffffff;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The backward references to initialize the histogram with.
+ /// The palette code bits.
public Vp8LHistogram(Vp8LBackwardRefs refs, int paletteCodeBits)
: this()
{
@@ -19,12 +26,19 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
this.StoreRefs(refs);
}
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The palette code bits.
public Vp8LHistogram(int paletteCodeBits)
: this()
{
this.PaletteCodeBits = paletteCodeBits;
}
+ ///
+ /// Initializes a new instance of the class.
+ ///
public Vp8LHistogram()
{
this.Red = new uint[WebPConstants.NumLiteralCodes + 1];
@@ -45,24 +59,24 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
public int PaletteCodeBits { get; }
///
- /// Gets the cached value of bit cost.
+ /// Gets or sets the cached value of bit cost.
///
- public double BitCost { get; }
+ public double BitCost { get; set; }
///
- /// Gets the cached value of literal entropy costs.
+ /// Gets or sets the cached value of literal entropy costs.
///
- public double LiteralCost { get; }
+ public double LiteralCost { get; set; }
///
- /// Gets the cached value of red entropy costs.
+ /// Gets or sets the cached value of red entropy costs.
///
- public double RedCost { get; }
+ public double RedCost { get; set; }
///
- /// Gets the cached value of blue entropy costs.
+ /// Gets or sets the cached value of blue entropy costs.
///
- public double BlueCost { get; }
+ public double BlueCost { get; set; }
public uint[] Red { get; }
@@ -74,8 +88,14 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
public uint[] Distance { get; }
+ public uint TrivialSymbol { get; set; }
+
public bool[] IsUsed { get; }
+ ///
+ /// Collect all the references into a histogram (without reset).
+ ///
+ /// The backward references.
public void StoreRefs(Vp8LBackwardRefs refs)
{
using List.Enumerator c = refs.Refs.GetEnumerator();
@@ -85,6 +105,11 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
}
}
+ ///
+ /// Accumulate a token 'v' into a histogram.
+ ///
+ /// The token to add.
+ /// Indicates whether to use the distance modifier.
public void AddSinglePixOrCopy(PixOrCopy v, bool useDistanceModifier)
{
if (v.IsLiteral())
@@ -122,22 +147,48 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
return WebPConstants.NumLiteralCodes + WebPConstants.NumLengthCodes + ((this.PaletteCodeBits > 0) ? (1 << this.PaletteCodeBits) : 0);
}
+ ///
+ /// Estimate how many bits the combined entropy of literals and distance approximately maps to.
+ ///
+ /// Estimated bits.
public double EstimateBits()
{
+ uint notUsed = 0;
return
- PopulationCost(this.Literal, this.NumCodes(), ref this.IsUsed[0])
- + PopulationCost(this.Red, WebPConstants.NumLiteralCodes, ref this.IsUsed[1])
- + PopulationCost(this.Blue, WebPConstants.NumLiteralCodes, ref this.IsUsed[2])
- + PopulationCost(this.Alpha, WebPConstants.NumLiteralCodes, ref this.IsUsed[3])
- + PopulationCost(this.Distance, WebPConstants.NumDistanceCodes, ref this.IsUsed[4])
+ PopulationCost(this.Literal, this.NumCodes(), ref notUsed, ref this.IsUsed[0])
+ + PopulationCost(this.Red, WebPConstants.NumLiteralCodes, ref notUsed, ref this.IsUsed[1])
+ + PopulationCost(this.Blue, WebPConstants.NumLiteralCodes, ref notUsed, ref this.IsUsed[2])
+ + PopulationCost(this.Alpha, WebPConstants.NumLiteralCodes, ref notUsed, ref this.IsUsed[3])
+ + PopulationCost(this.Distance, WebPConstants.NumDistanceCodes, ref notUsed, ref this.IsUsed[4])
+ ExtraCost(this.Literal.AsSpan(WebPConstants.NumLiteralCodes), WebPConstants.NumLengthCodes)
+ ExtraCost(this.Distance, WebPConstants.NumDistanceCodes);
}
+ public void UpdateHistogramCost()
+ {
+ uint alphaSym = 0, redSym = 0, blueSym = 0;
+ uint notUsed = 0;
+ double alphaCost = PopulationCost(this.Alpha, WebPConstants.NumLiteralCodes, ref alphaSym, ref this.IsUsed[3]);
+ double distanceCost = PopulationCost(this.Distance, WebPConstants.NumDistanceCodes, ref notUsed, ref this.IsUsed[4]) + ExtraCost(this.Distance, WebPConstants.NumDistanceCodes);
+ int numCodes = HistogramNumCodes(this.PaletteCodeBits);
+ this.LiteralCost = PopulationCost(this.Literal, numCodes, ref notUsed, ref this.IsUsed[0]) + ExtraCost(this.Literal.AsSpan(WebPConstants.NumLiteralCodes), WebPConstants.NumLengthCodes);
+ this.RedCost = PopulationCost(this.Red, WebPConstants.NumLiteralCodes, ref redSym, ref this.IsUsed[1]);
+ this.BlueCost = PopulationCost(this.Blue, WebPConstants.NumLiteralCodes, ref blueSym, ref this.IsUsed[2]);
+ this.BitCost = this.LiteralCost + this.RedCost + this.BlueCost + alphaCost + distanceCost;
+ if ((alphaSym | redSym | blueSym) == NonTrivialSym)
+ {
+ this.TrivialSymbol = NonTrivialSym;
+ }
+ else
+ {
+ this.TrivialSymbol = ((uint)alphaSym << 24) | (redSym << 16) | (blueSym << 0);
+ }
+ }
+
///
/// Get the symbol entropy for the distribution 'population'.
///
- private static double PopulationCost(uint[] population, int length, ref bool isUsed)
+ private static double PopulationCost(uint[] population, int length, ref uint trivialSym, ref bool isUsed)
{
var bitEntropy = new Vp8LBitEntropy();
var stats = new Vp8LStreaks();
@@ -146,42 +197,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
// The histogram is used if there is at least one non-zero streak.
isUsed = stats.Streaks[1][0] != 0 || stats.Streaks[1][1] != 0;
- return bitEntropy.BitsEntropyRefine() + FinalHuffmanCost(stats);
- }
-
- ///
- /// Finalize the Huffman cost based on streak numbers and length type (<3 or >=3).
- ///
- private static double FinalHuffmanCost(Vp8LStreaks stats)
- {
- // The constants in this function are experimental and got rounded from
- // their original values in 1/8 when switched to 1/1024.
- double retval = InitialHuffmanCost();
-
- // Second coefficient: Many zeros in the histogram are covered efficiently
- // by a run-length encode. Originally 2/8.
- retval += (stats.Counts[0] * 1.5625) + (0.234375 * stats.Streaks[0][1]);
-
- // Second coefficient: Constant values are encoded less efficiently, but still
- // RLE'ed. Originally 6/8.
- retval += (stats.Counts[1] * 2.578125) + 0.703125 * stats.Streaks[1][1];
-
- // 0s are usually encoded more efficiently than non-0s.
- // Originally 15/8.
- retval += 1.796875 * stats.Streaks[0][0];
-
- // Originally 26/8.
- retval += 3.28125 * stats.Streaks[1][0];
-
- return retval;
- }
-
- private static double InitialHuffmanCost()
- {
- // Small bias because Huffman code length is typically not stored in full length.
- int huffmanCodeOfHuffmanCodeSize = WebPConstants.CodeLengthCodes * 3;
- double smallBias = 9.1;
- return huffmanCodeOfHuffmanCodeSize - smallBias;
+ return bitEntropy.BitsEntropyRefine() + stats.FinalHuffmanCost();
}
private static double ExtraCost(Span population, int length)
@@ -194,5 +210,10 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
return cost;
}
+
+ public static int HistogramNumCodes(int paletteCodeBits)
+ {
+ return WebPConstants.NumLiteralCodes + WebPConstants.NumLengthCodes + ((paletteCodeBits > 0) ? (1 << paletteCodeBits) : 0);
+ }
}
}
diff --git a/src/ImageSharp/Formats/WebP/Lossless/Vp8LStreaks.cs b/src/ImageSharp/Formats/WebP/Lossless/Vp8LStreaks.cs
index 41947ae682..728e4e893d 100644
--- a/src/ImageSharp/Formats/WebP/Lossless/Vp8LStreaks.cs
+++ b/src/ImageSharp/Formats/WebP/Lossless/Vp8LStreaks.cs
@@ -22,5 +22,37 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
/// [zero/non-zero][streak < 3 / streak >= 3].
///
public int[][] Streaks { get; }
+
+ public double FinalHuffmanCost()
+ {
+ // The constants in this function are experimental and got rounded from
+ // their original values in 1/8 when switched to 1/1024.
+ double retval = InitialHuffmanCost();
+
+ // Second coefficient: Many zeros in the histogram are covered efficiently
+ // by a run-length encode. Originally 2/8.
+ retval += (this.Counts[0] * 1.5625) + (0.234375 * this.Streaks[0][1]);
+
+ // Second coefficient: Constant values are encoded less efficiently, but still
+ // RLE'ed. Originally 6/8.
+ retval += (this.Counts[1] * 2.578125) + (0.703125 * this.Streaks[1][1]);
+
+ // 0s are usually encoded more efficiently than non-0s.
+ // Originally 15/8.
+ retval += 1.796875 * this.Streaks[0][0];
+
+ // Originally 26/8.
+ retval += 3.28125 * this.Streaks[1][0];
+
+ return retval;
+ }
+
+ private static double InitialHuffmanCost()
+ {
+ // Small bias because Huffman code length is typically not stored in full length.
+ int huffmanCodeOfHuffmanCodeSize = WebPConstants.CodeLengthCodes * 3;
+ double smallBias = 9.1;
+ return huffmanCodeOfHuffmanCodeSize - smallBias;
+ }
}
}
diff --git a/src/ImageSharp/Formats/WebP/WebPConstants.cs b/src/ImageSharp/Formats/WebP/WebPConstants.cs
index 5bf313917a..bbc736b590 100644
--- a/src/ImageSharp/Formats/WebP/WebPConstants.cs
+++ b/src/ImageSharp/Formats/WebP/WebPConstants.cs
@@ -95,7 +95,7 @@ namespace SixLabors.ImageSharp.Formats.WebP
///
/// Maximum number of color cache bits.
///
- public const int MaxColorCacheBits = 11;
+ public const int MaxColorCacheBits = 10;
///
/// The maximum number of allowed transforms in a VP8L bitstream.
diff --git a/src/ImageSharp/Formats/WebP/WebPEncoderCore.cs b/src/ImageSharp/Formats/WebP/WebPEncoderCore.cs
index f53636cca7..ee47e33d81 100644
--- a/src/ImageSharp/Formats/WebP/WebPEncoderCore.cs
+++ b/src/ImageSharp/Formats/WebP/WebPEncoderCore.cs
@@ -68,7 +68,7 @@ namespace SixLabors.ImageSharp.Formats.WebP
int width = image.Width;
int height = image.Height;
- int initialSize = width * height;
+ int initialSize = width * height * 2;
this.bitWriter = new Vp8LBitWriter(initialSize);
// Write image size.
@@ -113,34 +113,13 @@ namespace SixLabors.ImageSharp.Formats.WebP
private void EncodeStream(Image image)
where TPixel : unmanaged, IPixel
{
- var encoder = new Vp8LEncoder(this.memoryAllocator, image.Width, image.Height);
-
- // Analyze image (entropy, num_palettes etc).
- this.EncoderAnalyze(image, encoder);
- }
-
- ///
- /// Analyzes the image and decides what transforms should be used.
- ///
- private void EncoderAnalyze(Image image, Vp8LEncoder enc)
- where TPixel : unmanaged, IPixel
- {
- 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;
-
- // Check if we only deal with a small number of colors and should use a palette.
- var usePalette = this.AnalyzeAndCreatePalette(image, enc);
-
- // Empirical bit sizes.
- enc.HistoBits = GetHistoBits(method, usePalette, width, height);
- enc.TransformBits = GetTransformBits(method, enc.HistoBits);
+ int bytePosition = this.bitWriter.NumBytes();
+ var enc = new Vp8LEncoder(this.memoryAllocator, width, height);
// Convert image pixels to bgra array.
- using System.Buffers.IMemoryOwner bgraBuffer = this.memoryAllocator.Allocate(width * height);
- Span bgra = bgraBuffer.Memory.Span;
+ Span bgra = enc.Bgra.GetSpan();
int idx = 0;
for (int y = 0; y < height; y++)
{
@@ -151,15 +130,25 @@ namespace SixLabors.ImageSharp.Formats.WebP
}
}
- // Try out multiple LZ77 on images with few colors.
- var nlz77s = (enc.PaletteSize > 0 && enc.PaletteSize <= 16) ? 2 : 1;
- EntropyIx entropyIdx = this.AnalyzeEntropy(image, usePalette, enc.PaletteSize, enc.TransformBits, out bool redAndBlueAlwaysZero);
+ // Analyze image (entropy, numPalettes etc).
+ this.EncoderAnalyze(image, enc, bgra);
+
+ var entropyIdx = 3; // TODO: hardcoded for now.
+ int quality = 75; // TODO: quality is hardcoded for now.
+ bool useCache = true; // TODO: useCache is hardcoded for now.
+ bool redAndBlueAlwaysZero = false;
- enc.UsePalette = entropyIdx == EntropyIx.Palette;
- enc.UseSubtractGreenTransform = (entropyIdx == EntropyIx.SubGreen) || (entropyIdx == EntropyIx.SpatialSubGreen);
- enc.UsePredictorTransform = (entropyIdx == EntropyIx.Spatial) || (entropyIdx == EntropyIx.SpatialSubGreen);
+ enc.UsePalette = entropyIdx == (int)EntropyIx.Palette;
+ enc.UseSubtractGreenTransform = (entropyIdx == (int)EntropyIx.SubGreen) || (entropyIdx == (int)EntropyIx.SpatialSubGreen);
+ enc.UsePredictorTransform = (entropyIdx == (int)EntropyIx.Spatial) || (entropyIdx == (int)EntropyIx.SpatialSubGreen);
enc.UseCrossColorTransform = redAndBlueAlwaysZero ? false : enc.UsePredictorTransform;
+ enc.AllocateTransformBuffer(width, height);
+
+ // Reset any parameter in the encoder that is set in the previous iteration.
enc.CacheBits = 0;
+ enc.ClearRefs();
+
+ // TODO: Apply near-lossless preprocessing.
// Encode palette.
if (enc.UsePalette)
@@ -167,8 +156,7 @@ namespace SixLabors.ImageSharp.Formats.WebP
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 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;
@@ -194,7 +182,143 @@ namespace SixLabors.ImageSharp.Formats.WebP
this.bitWriter.PutBits(0, 1); // No more transforms.
// Encode and write the transformed image.
- //EncodeImageInternal();
+ this.EncodeImage(bgra, enc.HashChain, enc.Refs, enc.CurrentWidth, height, quality, useCache, enc.CacheBits, enc.HistoBits, bytePosition);
+ }
+
+ ///
+ /// Analyzes the image and decides what transforms should be used.
+ ///
+ private void EncoderAnalyze(Image image, Vp8LEncoder enc, Span bgra)
+ where TPixel : unmanaged, IPixel
+ {
+ int method = 4; // TODO: method hardcoded to 4 for now.
+ int width = image.Width;
+ int height = image.Height;
+
+ // Check if we only deal with a small number of colors and should use a palette.
+ var usePalette = this.AnalyzeAndCreatePalette(image, enc);
+
+ // Empirical bit sizes.
+ enc.HistoBits = GetHistoBits(method, usePalette, width, height);
+ enc.TransformBits = GetTransformBits(method, enc.HistoBits);
+
+ // Try out multiple LZ77 on images with few colors.
+ var nlz77s = (enc.PaletteSize > 0 && enc.PaletteSize <= 16) ? 2 : 1;
+ EntropyIx entropyIdx = this.AnalyzeEntropy(image, usePalette, enc.PaletteSize, enc.TransformBits, out bool redAndBlueAlwaysZero);
+
+ // TODO: Fill CrunchConfig
+ }
+
+ private void EncodeImage(Span bgra, Vp8LHashChain hashChain, Vp8LBackwardRefs[] refsArray, int width, int height, int quality, bool useCache, int cacheBits, int histogramBits, int initBytePosition)
+ {
+ int lz77sTypesToTrySize = 1; // TODO: harcoded for now.
+ int[] lz77sTypesToTry = { 3 };
+ int histogramImageXySize = LosslessUtils.SubSampleSize(width, histogramBits) * LosslessUtils.SubSampleSize(height, histogramBits);
+ short[] histogramSymbols = new short[histogramImageXySize];
+ var huffTree = new HuffmanTree[3 * WebPConstants.CodeLengthCodes];
+
+ if (useCache)
+ {
+ if (cacheBits == 0)
+ {
+ cacheBits = WebPConstants.MaxColorCacheBits;
+ }
+ }
+ else
+ {
+ cacheBits = 0;
+ }
+
+ // Calculate backward references from ARGB image.
+ BackwardReferenceEncoder.HashChainFill(hashChain, bgra, quality, width, height);
+ // TODO: BitWriterInit(&bw_best, 0)
+ // BitWriterClone(bw, &bw_best))
+
+ for (int lz77sIdx = 0; lz77sIdx < lz77sTypesToTrySize; lz77sIdx++)
+ {
+ Vp8LBackwardRefs refsBest = BackwardReferenceEncoder.GetBackwardReferences(width, height, bgra, quality, lz77sTypesToTry[lz77sIdx], ref cacheBits, hashChain, refsArray[0], refsArray[1]);
+
+ // Keep the best references aside and use the other element from the first
+ // two as a temporary for later usage.
+ Vp8LBackwardRefs refsTmp = refsArray[refsBest.Equals(refsArray[0]) ? 1 : 0];
+
+ var tmpHisto = new Vp8LHistogram(cacheBits);
+ var histogramImage = new List(histogramImageXySize);
+ for (int i = 0; i < histogramImageXySize; i++)
+ {
+ histogramImage.Add(new Vp8LHistogram(cacheBits));
+ }
+
+ // Build histogram image and symbols from backward references.
+ HistogramEncoder.GetHistoImageSymbols(width, height, refsBest, quality, histogramBits, cacheBits, histogramImage, tmpHisto, histogramSymbols);
+
+ // Create Huffman bit lengths and codes for each histogram image.
+ var histogramImageSize = histogramImage.Count;
+ var bitArraySize = 5 * histogramImageSize;
+ var huffmanCodes = new HuffmanTreeCode[bitArraySize];
+
+ GetHuffBitLengthsAndCodes(histogramImage, huffmanCodes);
+
+ // Color Cache parameters.
+ if (cacheBits > 0)
+ {
+ this.bitWriter.PutBits(1, 1);
+ this.bitWriter.PutBits((uint)cacheBits, 4);
+ }
+ else
+ {
+ this.bitWriter.PutBits(0, 1);
+ }
+
+ // Huffman image + meta huffman.
+ bool writeHistogramImage = histogramImageSize > 1;
+ this.bitWriter.PutBits((uint)(writeHistogramImage ? 1 : 0), 1);
+ if (writeHistogramImage)
+ {
+ using System.Buffers.IMemoryOwner histogramArgbBuffer = this.memoryAllocator.Allocate(histogramImageXySize);
+ Span histogramArgb = histogramArgbBuffer.GetSpan();
+ int maxIndex = 0;
+ for (int i = 0; i < histogramImageXySize; i++)
+ {
+ int symbolIndex = histogramSymbols[i] & 0xffff;
+ histogramArgb[i] = (uint)(symbolIndex << 8);
+ if (symbolIndex >= maxIndex)
+ {
+ maxIndex = symbolIndex + 1;
+ }
+ }
+
+ histogramImageSize = maxIndex;
+ this.bitWriter.PutBits((uint)(histogramBits - 2), 3);
+ this.EncodeImageNoHuffman(histogramArgb, hashChain, refsTmp, refsArray[2], LosslessUtils.SubSampleSize(width, histogramBits), LosslessUtils.SubSampleSize(height, histogramBits), quality);
+ }
+
+ // Store Huffman codes.
+ // Find maximum number of symbols for the huffman tree-set.
+ int maxTokens = 0;
+ for (int i = 0; i < 5 * histogramImageSize; i++)
+ {
+ HuffmanTreeCode codes = huffmanCodes[i];
+ if (maxTokens < codes.NumSymbols)
+ {
+ maxTokens = codes.NumSymbols;
+ }
+ }
+
+ var tokens = new HuffmanTreeToken[maxTokens];
+ for (int i = 0; i < 5 * histogramImageSize; i++)
+ {
+ HuffmanTreeCode codes = huffmanCodes[i];
+ this.StoreHuffmanCode(huffTree, tokens, codes);
+ ClearHuffmanTreeIfOnlyOneSymbol(codes);
+ }
+
+ // Store actual literals.
+ var hdrSizeTmp = (int)(this.bitWriter.NumBytes() - initBytePosition);
+ this.StoreImageToBitMask(width, histogramBits, refsBest, histogramSymbols, huffmanCodes);
+
+ // TODO: Keep track of the smallest image so far.
+ }
}
///
@@ -236,7 +360,7 @@ namespace SixLabors.ImageSharp.Formats.WebP
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.
+ bool exact = false; // TODO: always false for now.
int predBits = enc.TransformBits;
int transformWidth = LosslessUtils.SubSampleSize(width, predBits);
int transformHeight = LosslessUtils.SubSampleSize(height, predBits);
@@ -281,7 +405,7 @@ namespace SixLabors.ImageSharp.Formats.WebP
huffTree[i] = new HuffmanTree();
}
- // Calculate backward references from ARGB image.
+ // Calculate backward references from the image pixels.
BackwardReferenceEncoder.HashChainFill(hashChain, bgra, quality, width, height);
Vp8LBackwardRefs refs = BackwardReferenceEncoder.GetBackwardReferences(
@@ -290,7 +414,7 @@ namespace SixLabors.ImageSharp.Formats.WebP
bgra,
quality,
(int)Vp8LLz77Type.Lz77Standard | (int)Vp8LLz77Type.Lz77Rle,
- cacheBits,
+ ref cacheBits,
hashChain,
refsTmp1,
refsTmp2);
@@ -823,8 +947,6 @@ namespace SixLabors.ImageSharp.Formats.WebP
xBits = (paletteSize <= 16) ? 1 : 0;
}
- enc.AllocateTransformBuffer(LosslessUtils.SubSampleSize(width, xBits), height);
-
this.ApplyPalette(src, srcStride, dst, enc.CurrentWidth, palette, paletteSize, width, height, xBits);
}