Browse Source

Encode image

pull/1552/head
Brian Popow 6 years ago
parent
commit
40f18c11e9
  1. 14
      src/ImageSharp/Formats/WebP/BitWriter/Vp8LBitWriter.cs
  2. 58
      src/ImageSharp/Formats/WebP/Lossless/BackwardReferenceEncoder.cs
  3. 92
      src/ImageSharp/Formats/WebP/Lossless/DominantCostRange.cs
  4. 627
      src/ImageSharp/Formats/WebP/Lossless/HistogramEncoder.cs
  5. 19
      src/ImageSharp/Formats/WebP/Lossless/HistogramPair.cs
  6. 8
      src/ImageSharp/Formats/WebP/Lossless/LosslessUtils.cs
  7. 25
      src/ImageSharp/Formats/WebP/Lossless/PredictorEncoder.cs
  8. 58
      src/ImageSharp/Formats/WebP/Lossless/Vp8LBitEntropy.cs
  9. 15
      src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs
  10. 121
      src/ImageSharp/Formats/WebP/Lossless/Vp8LHistogram.cs
  11. 32
      src/ImageSharp/Formats/WebP/Lossless/Vp8LStreaks.cs
  12. 2
      src/ImageSharp/Formats/WebP/WebPConstants.cs
  13. 200
      src/ImageSharp/Formats/WebP/WebPEncoderCore.cs

14
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;
}
/// <summary>
/// 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.
/// </summary>
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);
}
/// <summary>
/// Internal function for PutBits flushing 32 bits from the written state.
/// </summary>
@ -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;
}
}

58
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].
/// </summary>
public static Vp8LBackwardRefs GetBackwardReferences(int width, int height, Span<uint> 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
/// <summary>
/// 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.
/// </summary>
private static int CalculateBestCacheSize(Span<uint> 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<PixOrCopy>.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
/// <summary>
/// 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.
/// </summary>
private static int FindMatchLength(Span<uint> array1, Span<uint> array2, int bestLenMatch, int maxLimit)
{

92
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
{
/// <summary>
/// Data container to keep track of cost range for the three dominant entropy symbols.
/// </summary>
internal class DominantCostRange
{
/// <summary>
/// Initializes a new instance of the <see cref="DominantCostRange"/> class.
/// </summary>
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;
}
}
}
}

627
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
{
/// <summary>
/// Number of partitions for the three dominant (literal, red and blue) symbol costs.
/// </summary>
private const int NumPartitions = 4;
/// <summary>
/// The size of the bin-hash corresponding to the three dominant costs.
/// </summary>
private const int BinSize = NumPartitions * NumPartitions * NumPartitions;
/// <summary>
/// Maximum number of histograms allowed in greedy combining algorithm.
/// </summary>
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<Vp8LHistogram> 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<Vp8LHistogram>(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);
}
}
}
/// <summary>
/// Construct the histograms from backward references.
/// </summary>
private static void HistogramBuild(int xSize, int histoBits, Vp8LBackwardRefs backwardRefs, List<Vp8LHistogram> histograms)
{
int x = 0, y = 0;
int histoXSize = LosslessUtils.SubSampleSize(xSize, histoBits);
using List<PixOrCopy>.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++;
}
}
}
/// <summary>
/// Partition histograms to different entropy bins for three dominant (literal,
/// red and blue) symbol costs and compute the histogram aggregate bitCost.
/// </summary>
private static void HistogramAnalyzeEntropyBin(List<Vp8LHistogram> 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<Vp8LHistogram> origHistograms, List<Vp8LHistogram> histograms, ref int numUsed, short[] histogramSymbols)
{
int numUsedOrig = numUsed;
var indicesToRemove = new List<int>();
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<Vp8LHistogram> 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;
}
}
/// <summary>
/// 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.
/// </summary>
private static void OptimizeHistogramSymbols(List<Vp8LHistogram> 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++;
}
}
/// <summary>
/// Perform histogram aggregation using a stochastic approach.
/// </summary>
/// <returns>true if a greedy approach needs to be performed afterwards, false otherwise.</returns>
private static bool HistogramCombineStochastic(List<Vp8LHistogram> 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<HistogramPair>();
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>();
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<Vp8LHistogram> histograms, ref int numUsed)
{
int histoSize = histograms.Count;
// Priority list of histogram pairs.
var histoPriorityList = new List<HistogramPair>();
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);
}
}
}
/// <summary>
/// // Create a pair from indices "idx1" and "idx2" provided its cost
/// is inferior to "threshold", a negative entropy.
/// </summary>
/// <returns>The cost of the pair, or 0. if it superior to threshold.</returns>
private static double HistoQueuePush(List<HistogramPair> histoQueue, int queueMaxSize, List<Vp8LHistogram> 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;
}
/// <summary>
/// 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.
/// </summary>
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<uint> x, Span<uint> 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;
}
/// <summary>
/// Check whether a pair in the list should be updated as head or not.
/// </summary>
private static void HistoQueueUpdateHead(List<HistogramPair> 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;
}
}
}

19
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
{
/// <summary>
/// Pair of histograms. Negative Idx1 value means that pair is out-of-date.
/// </summary>
internal class HistogramPair
{
public int Idx1 { get; set; }
public int Idx2 { get; set; }
public double CostDiff { get; set; }
public double CostCombo { get; set; }
}
}

8
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);
}
/// <summary>

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

@ -112,7 +112,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
/// <summary>
/// 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<uint> src = argb.Slice((y * width) + contextStartX, maxX + haveLeft + ((y + 1) < height ? 1 : 0));
Span<uint> dst = currentRow.Slice(contextStartX);
src.CopyTo(dst);
@ -476,7 +476,8 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
Span<uint> tmp32 = upperRow;
upperRow = currentRow;
currentRow = tmp32;
argb.Slice(y * width, width + y + (1 < height ? 1 : 0)).CopyTo(currentRow);
Span<uint> 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<uint> 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;

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

@ -52,6 +52,15 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
/// </summary>
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<uint> 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.

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

@ -22,6 +22,9 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
/// </summary>
private const int MinBlockSize = 256;
/// <summary>
/// The <see cref="MemoryAllocator"/> to use for buffer allocations.
/// </summary>
private MemoryAllocator memoryAllocator;
/// <summary>
@ -135,6 +138,18 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
this.BgraScratch = this.memoryAllocator.Allocate<uint>(argbScratchSize);
this.TransformData = this.memoryAllocator.Allocate<uint>(transformDataSize);
this.CurrentWidth = width;
}
/// <summary>
/// Clears the backward references.
/// </summary>
public void ClearRefs()
{
for (int i = 0; i < this.Refs.Length; i++)
{
this.Refs[i].Refs.Clear();
}
}
/// <inheritdoc/>

121
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;
/// <summary>
/// Initializes a new instance of the <see cref="Vp8LHistogram"/> class.
/// </summary>
/// <param name="refs">The backward references to initialize the histogram with.</param>
/// <param name="paletteCodeBits">The palette code bits.</param>
public Vp8LHistogram(Vp8LBackwardRefs refs, int paletteCodeBits)
: this()
{
@ -19,12 +26,19 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
this.StoreRefs(refs);
}
/// <summary>
/// Initializes a new instance of the <see cref="Vp8LHistogram"/> class.
/// </summary>
/// <param name="paletteCodeBits">The palette code bits.</param>
public Vp8LHistogram(int paletteCodeBits)
: this()
{
this.PaletteCodeBits = paletteCodeBits;
}
/// <summary>
/// Initializes a new instance of the <see cref="Vp8LHistogram"/> class.
/// </summary>
public Vp8LHistogram()
{
this.Red = new uint[WebPConstants.NumLiteralCodes + 1];
@ -45,24 +59,24 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
public int PaletteCodeBits { get; }
/// <summary>
/// Gets the cached value of bit cost.
/// Gets or sets the cached value of bit cost.
/// </summary>
public double BitCost { get; }
public double BitCost { get; set; }
/// <summary>
/// Gets the cached value of literal entropy costs.
/// Gets or sets the cached value of literal entropy costs.
/// </summary>
public double LiteralCost { get; }
public double LiteralCost { get; set; }
/// <summary>
/// Gets the cached value of red entropy costs.
/// Gets or sets the cached value of red entropy costs.
/// </summary>
public double RedCost { get; }
public double RedCost { get; set; }
/// <summary>
/// Gets the cached value of blue entropy costs.
/// Gets or sets the cached value of blue entropy costs.
/// </summary>
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; }
/// <summary>
/// Collect all the references into a histogram (without reset).
/// </summary>
/// <param name="refs">The backward references.</param>
public void StoreRefs(Vp8LBackwardRefs refs)
{
using List<PixOrCopy>.Enumerator c = refs.Refs.GetEnumerator();
@ -85,6 +105,11 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
}
}
/// <summary>
/// Accumulate a token 'v' into a histogram.
/// </summary>
/// <param name="v">The token to add.</param>
/// <param name="useDistanceModifier">Indicates whether to use the distance modifier.</param>
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);
}
/// <summary>
/// Estimate how many bits the combined entropy of literals and distance approximately maps to.
/// </summary>
/// <returns>Estimated bits.</returns>
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);
}
}
/// <summary>
/// Get the symbol entropy for the distribution 'population'.
/// </summary>
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);
}
/// <summary>
/// Finalize the Huffman cost based on streak numbers and length type (<3 or >=3).
/// </summary>
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<uint> 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);
}
}
}

32
src/ImageSharp/Formats/WebP/Lossless/Vp8LStreaks.cs

@ -22,5 +22,37 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
/// [zero/non-zero][streak < 3 / streak >= 3].
/// </summary>
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;
}
}
}

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

@ -95,7 +95,7 @@ namespace SixLabors.ImageSharp.Formats.WebP
/// <summary>
/// Maximum number of color cache bits.
/// </summary>
public const int MaxColorCacheBits = 11;
public const int MaxColorCacheBits = 10;
/// <summary>
/// The maximum number of allowed transforms in a VP8L bitstream.

200
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<TPixel>(Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
var encoder = new Vp8LEncoder(this.memoryAllocator, image.Width, image.Height);
// Analyze image (entropy, num_palettes etc).
this.EncoderAnalyze(image, encoder);
}
/// <summary>
/// Analyzes the image and decides what transforms should be used.
/// </summary>
private void EncoderAnalyze<TPixel>(Image<TPixel> image, Vp8LEncoder enc)
where TPixel : unmanaged, IPixel<TPixel>
{
int method = 4; // TODO: method hardcoded to 4 for now.
int quality = 100; // TODO: quality is hardcoded for now.
bool useCache = true; // TODO: useCache is hardcoded for now.
int width = image.Width;
int height = image.Height;
// 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<uint> bgraBuffer = this.memoryAllocator.Allocate<uint>(width * height);
Span<uint> bgra = bgraBuffer.Memory.Span;
Span<uint> 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);
}
/// <summary>
/// Analyzes the image and decides what transforms should be used.
/// </summary>
private void EncoderAnalyze<TPixel>(Image<TPixel> image, Vp8LEncoder enc, Span<uint> bgra)
where TPixel : unmanaged, IPixel<TPixel>
{
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<uint> 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<Vp8LHistogram>(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<uint> histogramArgbBuffer = this.memoryAllocator.Allocate<uint>(histogramImageXySize);
Span<uint> 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.
}
}
/// <summary>
@ -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);
}

Loading…
Cancel
Save