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); }