Browse Source

Pool Vp8LHistogram memory.

pull/2546/head
James Jackson-South 3 years ago
parent
commit
3fabb76ab6
  1. 47
      src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs
  2. 16
      src/ImageSharp/Formats/Webp/Lossless/CostModel.cs
  3. 189
      src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs
  4. 18
      src/ImageSharp/Formats/Webp/Lossless/HuffmanUtils.cs
  5. 2
      src/ImageSharp/Formats/Webp/Lossless/PixOrCopy.cs
  6. 6
      src/ImageSharp/Formats/Webp/Lossless/Vp8LBitEntropy.cs
  7. 117
      src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
  8. 273
      src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs
  9. 122
      src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogramSet.cs
  10. 13
      tests/ImageSharp.Benchmarks/Codecs/Webp/EncodeWebp.cs
  11. 10
      tests/ImageSharp.Tests/Formats/WebP/DominantCostRangeTests.cs
  12. 14
      tests/ImageSharp.Tests/Formats/WebP/Vp8LHistogramTests.cs

47
src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs

@ -50,8 +50,8 @@ internal static class BackwardReferenceEncoder
double bitCostBest = -1;
int cacheBitsInitial = cacheBits;
Vp8LHashChain? hashChainBox = null;
var stats = new Vp8LStreaks();
var bitsEntropy = new Vp8LBitEntropy();
Vp8LStreaks stats = new();
Vp8LBitEntropy bitsEntropy = new();
for (int lz77Type = 1; lz77TypesToTry > 0; lz77TypesToTry &= ~lz77Type, lz77Type <<= 1)
{
int cacheBitsTmp = cacheBitsInitial;
@ -76,21 +76,19 @@ internal static class BackwardReferenceEncoder
}
// Next, try with a color cache and update the references.
cacheBitsTmp = CalculateBestCacheSize(bgra, quality, worst, cacheBitsTmp);
cacheBitsTmp = CalculateBestCacheSize(memoryAllocator, bgra, quality, worst, cacheBitsTmp);
if (cacheBitsTmp > 0)
{
BackwardRefsWithLocalCache(bgra, cacheBitsTmp, worst);
}
// Keep the best backward references.
var histo = new Vp8LHistogram(worst, cacheBitsTmp);
using Vp8LHistogram histo = new(memoryAllocator, worst, cacheBitsTmp);
double bitCost = histo.EstimateBits(stats, bitsEntropy);
if (lz77TypeBest == 0 || bitCost < bitCostBest)
{
Vp8LBackwardRefs tmp = worst;
worst = best;
best = tmp;
(best, worst) = (worst, best);
bitCostBest = bitCost;
cacheBits = cacheBitsTmp;
lz77TypeBest = lz77Type;
@ -102,7 +100,7 @@ internal static class BackwardReferenceEncoder
{
Vp8LHashChain hashChainTmp = lz77TypeBest == (int)Vp8LLz77Type.Lz77Standard ? hashChain : hashChainBox!;
BackwardReferencesTraceBackwards(width, height, memoryAllocator, bgra, cacheBits, hashChainTmp, best, worst);
var histo = new Vp8LHistogram(worst, cacheBits);
using Vp8LHistogram histo = new(memoryAllocator, worst, cacheBits);
double bitCostTrace = histo.EstimateBits(stats, bitsEntropy);
if (bitCostTrace < bitCostBest)
{
@ -123,7 +121,12 @@ internal static class BackwardReferenceEncoder
/// The local color cache is also disabled for the lower (smaller then 25) quality.
/// </summary>
/// <returns>Best cache size.</returns>
private static int CalculateBestCacheSize(ReadOnlySpan<uint> bgra, uint quality, Vp8LBackwardRefs refs, int bestCacheBits)
private static int CalculateBestCacheSize(
MemoryAllocator memoryAllocator,
ReadOnlySpan<uint> bgra,
uint quality,
Vp8LBackwardRefs refs,
int bestCacheBits)
{
int cacheBitsMax = quality <= 25 ? 0 : bestCacheBits;
if (cacheBitsMax == 0)
@ -134,11 +137,15 @@ internal static class BackwardReferenceEncoder
double entropyMin = MaxEntropy;
int pos = 0;
var colorCache = new ColorCache[WebpConstants.MaxColorCacheBits + 1];
var histos = new Vp8LHistogram[WebpConstants.MaxColorCacheBits + 1];
// TODO: Pass from outer loop and clear.
ColorCache[] colorCache = new ColorCache[WebpConstants.MaxColorCacheBits + 1];
// TODO: Use fixed size.
using Vp8LHistogramSet histos = new(memoryAllocator, WebpConstants.MaxColorCacheBits + 1, 0);
for (int i = 0; i <= WebpConstants.MaxColorCacheBits; i++)
{
histos[i] = new Vp8LHistogram(paletteCodeBits: i);
histos[i].PaletteCodeBits = i;
colorCache[i] = new ColorCache(i);
}
@ -149,10 +156,10 @@ internal static class BackwardReferenceEncoder
if (v.IsLiteral())
{
uint pix = bgra[pos++];
uint a = (pix >> 24) & 0xff;
uint r = (pix >> 16) & 0xff;
uint g = (pix >> 8) & 0xff;
uint b = (pix >> 0) & 0xff;
int a = (int)(pix >> 24) & 0xff;
int r = (int)(pix >> 16) & 0xff;
int g = (int)(pix >> 8) & 0xff;
int b = (int)(pix >> 0) & 0xff;
// The keys of the caches can be derived from the longest one.
int key = ColorCache.HashPix(pix, 32 - cacheBitsMax);
@ -218,8 +225,8 @@ internal static class BackwardReferenceEncoder
}
}
var stats = new Vp8LStreaks();
var bitsEntropy = new Vp8LBitEntropy();
Vp8LStreaks stats = new();
Vp8LBitEntropy bitsEntropy = new();
for (int i = 0; i <= cacheBitsMax; i++)
{
double entropy = histos[i].EstimateBits(stats, bitsEntropy);
@ -266,7 +273,7 @@ internal static class BackwardReferenceEncoder
int pixCount = xSize * ySize;
bool useColorCache = cacheBits > 0;
int literalArraySize = WebpConstants.NumLiteralCodes + WebpConstants.NumLengthCodes + (cacheBits > 0 ? 1 << cacheBits : 0);
var costModel = new CostModel(literalArraySize);
CostModel costModel = new(memoryAllocator, literalArraySize);
int offsetPrev = -1;
int lenPrev = -1;
double offsetCost = -1;
@ -280,7 +287,7 @@ internal static class BackwardReferenceEncoder
}
costModel.Build(xSize, cacheBits, refs);
using var costManager = new CostManager(memoryAllocator, distArrayBuffer, pixCount, costModel);
using CostManager costManager = new(memoryAllocator, distArrayBuffer, pixCount, costModel);
Span<float> costManagerCosts = costManager.Costs.GetSpan();
Span<ushort> distArray = distArrayBuffer.GetSpan();

16
src/ImageSharp/Formats/Webp/Lossless/CostModel.cs

@ -1,18 +1,23 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Webp.Lossless;
internal class CostModel
{
private readonly MemoryAllocator memoryAllocator;
private const int ValuesInBytes = 256;
/// <summary>
/// Initializes a new instance of the <see cref="CostModel"/> class.
/// </summary>
/// <param name="memoryAllocator">The memory allocator.</param>
/// <param name="literalArraySize">The literal array size.</param>
public CostModel(int literalArraySize)
public CostModel(MemoryAllocator memoryAllocator, int literalArraySize)
{
this.memoryAllocator = memoryAllocator;
this.Alpha = new double[ValuesInBytes];
this.Red = new double[ValuesInBytes];
this.Blue = new double[ValuesInBytes];
@ -32,13 +37,12 @@ internal class CostModel
public void Build(int xSize, int cacheBits, Vp8LBackwardRefs backwardRefs)
{
var histogram = new Vp8LHistogram(cacheBits);
using System.Collections.Generic.List<PixOrCopy>.Enumerator refsEnumerator = backwardRefs.Refs.GetEnumerator();
using Vp8LHistogram histogram = new(this.memoryAllocator, cacheBits);
// The following code is similar to HistogramCreate but converts the distance to plane code.
while (refsEnumerator.MoveNext())
for (int i = 0; i < backwardRefs.Refs.Count; i++)
{
histogram.AddSinglePixOrCopy(refsEnumerator.Current, true, xSize);
histogram.AddSinglePixOrCopy(backwardRefs.Refs[i], true, xSize);
}
ConvertPopulationCountTableToBitEstimates(histogram.NumCodes(), histogram.Literal, this.Literal);
@ -70,7 +74,7 @@ internal class CostModel
public double GetLiteralCost(uint v) => this.Alpha[v >> 24] + this.Red[(v >> 16) & 0xff] + this.Literal[(v >> 8) & 0xff] + this.Blue[v & 0xff];
private static void ConvertPopulationCountTableToBitEstimates(int numSymbols, uint[] populationCounts, double[] output)
private static void ConvertPopulationCountTableToBitEstimates(int numSymbols, Span<uint> populationCounts, double[] output)
{
uint sum = 0;
int nonzeros = 0;

189
src/ImageSharp/Formats/Webp/Lossless/HistogramEncoder.cs

@ -3,6 +3,7 @@
#nullable disable
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Webp.Lossless;
@ -27,19 +28,28 @@ internal static class HistogramEncoder
private const ushort InvalidHistogramSymbol = ushort.MaxValue;
public static void GetHistoImageSymbols(int xSize, int ySize, Vp8LBackwardRefs refs, uint quality, int histoBits, int cacheBits, List<Vp8LHistogram> imageHisto, Vp8LHistogram tmpHisto, Span<ushort> histogramSymbols)
public static void GetHistoImageSymbols(
MemoryAllocator memoryAllocator,
int xSize,
int ySize,
Vp8LBackwardRefs refs,
uint quality,
int histoBits,
int cacheBits,
Vp8LHistogramSet imageHisto,
Vp8LHistogram tmpHisto,
Span<ushort> 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;
const int entropyCombineNumBins = BinSize;
// TODO: Allocations!
ushort[] mapTmp = new ushort[imageHistoRawSize];
ushort[] clusterMappings = new ushort[imageHistoRawSize];
var origHisto = new List<Vp8LHistogram>(imageHistoRawSize);
for (int i = 0; i < imageHistoRawSize; i++)
{
origHisto.Add(new Vp8LHistogram(cacheBits));
}
using Vp8LHistogramSet origHisto = new(memoryAllocator, imageHistoRawSize, cacheBits);
// Construct the histograms from the backward references.
HistogramBuild(xSize, histoBits, refs, origHisto);
@ -61,7 +71,7 @@ internal static class HistogramEncoder
OptimizeHistogramSymbols(clusterMappings, numClusters, mapTmp, histogramSymbols);
}
float x = quality / 100.0f;
float x = quality / 100F;
// Cubic ramp between 1 and MaxHistoGreedy:
int thresholdSize = (int)(1 + (x * x * x * (MaxHistoGreedy - 1)));
@ -77,26 +87,25 @@ internal static class HistogramEncoder
HistogramRemap(origHisto, imageHisto, histogramSymbols);
}
private static void RemoveEmptyHistograms(List<Vp8LHistogram> histograms)
private static void RemoveEmptyHistograms(Vp8LHistogramSet histograms)
{
int size = 0;
for (int i = 0; i < histograms.Count; i++)
for (int i = histograms.Count - 1; i >= 0; i--)
{
if (histograms[i] == null)
{
continue;
histograms.RemoveAt(i);
}
histograms[size++] = histograms[i];
}
histograms.RemoveRange(size, histograms.Count - size);
}
/// <summary>
/// Construct the histograms from the backward references.
/// </summary>
private static void HistogramBuild(int xSize, int histoBits, Vp8LBackwardRefs backwardRefs, List<Vp8LHistogram> histograms)
private static void HistogramBuild(
int xSize,
int histoBits,
Vp8LBackwardRefs backwardRefs,
Vp8LHistogramSet histograms)
{
int x = 0, y = 0;
int histoXSize = LosslessUtils.SubSampleSize(xSize, histoBits);
@ -119,10 +128,10 @@ internal static class HistogramEncoder
/// 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, ushort[] binMap)
private static void HistogramAnalyzeEntropyBin(Vp8LHistogramSet histograms, ushort[] binMap)
{
int histoSize = histograms.Count;
var costRange = new DominantCostRange();
DominantCostRange costRange = new();
// Analyze the dominant (literal, red and blue) entropy costs.
for (int i = 0; i < histoSize; i++)
@ -148,25 +157,28 @@ internal static class HistogramEncoder
}
}
private static int HistogramCopyAndAnalyze(List<Vp8LHistogram> origHistograms, List<Vp8LHistogram> histograms, Span<ushort> histogramSymbols)
private static int HistogramCopyAndAnalyze(
Vp8LHistogramSet origHistograms,
Vp8LHistogramSet histograms,
Span<ushort> histogramSymbols)
{
var stats = new Vp8LStreaks();
var bitsEntropy = new Vp8LBitEntropy();
Vp8LStreaks stats = new();
Vp8LBitEntropy bitsEntropy = new();
for (int clusterId = 0, i = 0; i < origHistograms.Count; i++)
{
Vp8LHistogram origHistogram = origHistograms[i];
origHistogram.UpdateHistogramCost(stats, bitsEntropy);
// Skip the histogram if it is completely empty, which can happen for tiles with no information (when they are skipped because of LZ77).
if (!origHistogram.IsUsed[0] && !origHistogram.IsUsed[1] && !origHistogram.IsUsed[2] && !origHistogram.IsUsed[3] && !origHistogram.IsUsed[4])
if (!origHistogram.IsUsed(0) && !origHistogram.IsUsed(1) && !origHistogram.IsUsed(2) && !origHistogram.IsUsed(3) && !origHistogram.IsUsed(4))
{
origHistograms[i] = null;
histograms[i] = null;
origHistograms.DisposeAt(i);
histograms.DisposeAt(i);
histogramSymbols[i] = InvalidHistogramSymbol;
}
else
{
histograms[i] = (Vp8LHistogram)origHistogram.DeepClone();
origHistogram.CopyTo(histograms[i]);
histogramSymbols[i] = (ushort)clusterId++;
}
}
@ -184,7 +196,7 @@ internal static class HistogramEncoder
}
private static void HistogramCombineEntropyBin(
List<Vp8LHistogram> histograms,
Vp8LHistogramSet histograms,
Span<ushort> clusters,
ushort[] clusterMappings,
Vp8LHistogram curCombo,
@ -205,9 +217,9 @@ internal static class HistogramEncoder
clusterMappings[idx] = (ushort)idx;
}
var indicesToRemove = new List<int>();
var stats = new Vp8LStreaks();
var bitsEntropy = new Vp8LBitEntropy();
List<int> indicesToRemove = new();
Vp8LStreaks stats = new();
Vp8LBitEntropy bitsEntropy = new();
for (int idx = 0; idx < histograms.Count; idx++)
{
if (histograms[idx] == null)
@ -236,15 +248,13 @@ internal static class HistogramEncoder
// histogram pairs. In that case, we fallback to combining
// histograms as usual to avoid increasing the header size.
bool tryCombine = curCombo.TrivialSymbol != NonTrivialSym || (histograms[idx].TrivialSymbol == NonTrivialSym && histograms[first].TrivialSymbol == NonTrivialSym);
int maxCombineFailures = 32;
const int maxCombineFailures = 32;
if (tryCombine || binInfo[binId].NumCombineFailures >= maxCombineFailures)
{
// Move the (better) merged histogram to its final slot.
Vp8LHistogram tmp = curCombo;
curCombo = histograms[first];
histograms[first] = tmp;
(histograms[first], curCombo) = (curCombo, histograms[first]);
histograms[idx] = null;
histograms.DisposeAt(idx);
indicesToRemove.Add(idx);
clusterMappings[clusters[idx]] = clusters[first];
}
@ -256,9 +266,9 @@ internal static class HistogramEncoder
}
}
foreach (int index in indicesToRemove.OrderByDescending(i => i))
for (int i = indicesToRemove.Count - 1; i >= 0; i--)
{
histograms.RemoveAt(index);
histograms.RemoveAt(indicesToRemove[i]);
}
}
@ -318,15 +328,15 @@ internal static class HistogramEncoder
/// 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, int minClusterSize)
private static bool HistogramCombineStochastic(Vp8LHistogramSet histograms, int minClusterSize)
{
uint seed = 1;
int triesWithNoSuccess = 0;
int numUsed = histograms.Count(h => h != null);
int outerIters = numUsed;
int numTriesNoSuccess = (int)((uint)outerIters / 2);
var stats = new Vp8LStreaks();
var bitsEntropy = new Vp8LBitEntropy();
Vp8LStreaks stats = new();
Vp8LBitEntropy bitsEntropy = new();
if (numUsed < minClusterSize)
{
@ -335,25 +345,25 @@ internal static class HistogramEncoder
// Priority list 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 maxSize = 9;
List<HistogramPair> histoPriorityList = new();
const int maxSize = 9;
// Fill the initial mapping.
Span<int> mappings = histograms.Count <= 64 ? stackalloc int[histograms.Count] : new int[histograms.Count];
for (int j = 0, iter = 0; iter < histograms.Count; iter++)
for (int j = 0, i = 0; i < histograms.Count; i++)
{
if (histograms[iter] == null)
if (histograms[i] == null)
{
continue;
}
mappings[j++] = iter;
mappings[j++] = i;
}
// Collapse similar histograms.
for (int iter = 0; iter < outerIters && numUsed >= minClusterSize && ++triesWithNoSuccess < numTriesNoSuccess; iter++)
for (int i = 0; i < outerIters && numUsed >= minClusterSize && ++triesWithNoSuccess < numTriesNoSuccess; i++)
{
double bestCost = histoPriorityList.Count == 0 ? 0.0d : histoPriorityList[0].CostDiff;
double bestCost = histoPriorityList.Count == 0 ? 0D : histoPriorityList[0].CostDiff;
int numTries = (int)((uint)numUsed / 2);
uint randRange = (uint)((numUsed - 1) * numUsed);
@ -373,7 +383,8 @@ internal static class HistogramEncoder
idx2 = mappings[idx2];
// Calculate cost reduction on combination.
double currCost = HistoPriorityListPush(histoPriorityList, maxSize, histograms, idx1, idx2, bestCost, stats, bitsEntropy);
double currCost = 0;
currCost = HistoPriorityListPush(histoPriorityList, maxSize, histograms, idx1, idx2, bestCost, stats, bitsEntropy);
// Found a better pair?
if (currCost < 0)
@ -398,13 +409,13 @@ internal static class HistogramEncoder
int mappingIndex = mappings.IndexOf(bestIdx2);
Span<int> src = mappings.Slice(mappingIndex + 1, numUsed - mappingIndex - 1);
Span<int> dst = mappings.Slice(mappingIndex);
Span<int> dst = mappings[mappingIndex..];
src.CopyTo(dst);
// Merge the histograms and remove bestIdx2 from the list.
HistogramAdd(histograms[bestIdx2], histograms[bestIdx1], histograms[bestIdx1]);
histograms.ElementAt(bestIdx1).BitCost = histoPriorityList[0].CostCombo;
histograms[bestIdx2] = null;
histograms[bestIdx1].BitCost = histoPriorityList[0].CostCombo;
histograms.DisposeAt(bestIdx2);
numUsed--;
for (int j = 0; j < histoPriorityList.Count;)
@ -418,7 +429,7 @@ internal static class HistogramEncoder
// check for it all the time nevertheless.
if (isIdx1Best && isIdx2Best)
{
histoPriorityList[j] = histoPriorityList[histoPriorityList.Count - 1];
histoPriorityList[j] = histoPriorityList[^1];
histoPriorityList.RemoveAt(histoPriorityList.Count - 1);
continue;
}
@ -439,18 +450,17 @@ internal static class HistogramEncoder
// Make sure the index order is respected.
if (p.Idx1 > p.Idx2)
{
int tmp = p.Idx2;
p.Idx2 = p.Idx1;
p.Idx1 = tmp;
(p.Idx1, p.Idx2) = (p.Idx2, p.Idx1);
}
if (doEval)
{
// Re-evaluate the cost of an updated pair.
HistoListUpdatePair(histograms[p.Idx1], histograms[p.Idx2], stats, bitsEntropy, 0.0d, p);
if (p.CostDiff >= 0.0d)
HistoListUpdatePair(histograms[p.Idx1], histograms[p.Idx2], stats, bitsEntropy, 0D, p);
if (p.CostDiff >= 0D)
{
histoPriorityList[j] = histoPriorityList[histoPriorityList.Count - 1];
histoPriorityList[j] = histoPriorityList[^1];
histoPriorityList.RemoveAt(histoPriorityList.Count - 1);
continue;
}
@ -463,20 +473,18 @@ internal static class HistogramEncoder
triesWithNoSuccess = 0;
}
bool doGreedy = numUsed <= minClusterSize;
return doGreedy;
return numUsed <= minClusterSize;
}
private static void HistogramCombineGreedy(List<Vp8LHistogram> histograms)
private static void HistogramCombineGreedy(Vp8LHistogramSet histograms)
{
int histoSize = histograms.Count(h => h != null);
// Priority list of histogram pairs.
var histoPriorityList = new List<HistogramPair>();
List<HistogramPair> histoPriorityList = new();
int maxSize = histoSize * histoSize;
var stats = new Vp8LStreaks();
var bitsEntropy = new Vp8LBitEntropy();
Vp8LStreaks stats = new();
Vp8LBitEntropy bitsEntropy = new();
for (int i = 0; i < histoSize; i++)
{
@ -504,16 +512,17 @@ internal static class HistogramEncoder
histograms[idx1].BitCost = histoPriorityList[0].CostCombo;
// Remove merged histogram.
histograms[idx2] = null;
histograms.DisposeAt(idx2);
// Remove pairs intersecting the just combined best pair.
// TODO: Reversing this will avoid the need to remove from the end of the list.
for (int i = 0; i < histoPriorityList.Count;)
{
HistogramPair p = histoPriorityList.ElementAt(i);
HistogramPair p = histoPriorityList[i];
if (p.Idx1 == idx1 || p.Idx2 == idx1 || p.Idx1 == idx2 || p.Idx2 == idx2)
{
// Replace item at pos i with the last one and shrinking the list.
histoPriorityList[i] = histoPriorityList[histoPriorityList.Count - 1];
histoPriorityList[i] = histoPriorityList[^1];
histoPriorityList.RemoveAt(histoPriorityList.Count - 1);
}
else
@ -536,12 +545,15 @@ internal static class HistogramEncoder
}
}
private static void HistogramRemap(List<Vp8LHistogram> input, List<Vp8LHistogram> output, Span<ushort> symbols)
private static void HistogramRemap(
Vp8LHistogramSet input,
Vp8LHistogramSet output,
Span<ushort> symbols)
{
int inSize = input.Count;
int outSize = output.Count;
var stats = new Vp8LStreaks();
var bitsEntropy = new Vp8LBitEntropy();
Vp8LStreaks stats = new();
Vp8LBitEntropy bitsEntropy = new();
if (outSize > 1)
{
for (int i = 0; i < inSize; i++)
@ -577,11 +589,11 @@ internal static class HistogramEncoder
}
// Recompute each output.
int paletteCodeBits = output.First().PaletteCodeBits;
output.Clear();
int paletteCodeBits = output[0].PaletteCodeBits;
for (int i = 0; i < outSize; i++)
{
output.Add(new Vp8LHistogram(paletteCodeBits));
output[i].Clear();
output[i].PaletteCodeBits = paletteCodeBits;
}
for (int i = 0; i < inSize; i++)
@ -600,20 +612,26 @@ internal static class HistogramEncoder
/// 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 HistoPriorityListPush(List<HistogramPair> histoList, int maxSize, List<Vp8LHistogram> histograms, int idx1, int idx2, double threshold, Vp8LStreaks stats, Vp8LBitEntropy bitsEntropy)
private static double HistoPriorityListPush(
List<HistogramPair> histoList,
int maxSize,
Vp8LHistogramSet histograms,
int idx1,
int idx2,
double threshold,
Vp8LStreaks stats,
Vp8LBitEntropy bitsEntropy)
{
var pair = new HistogramPair();
HistogramPair pair = new();
if (histoList.Count == maxSize)
{
return 0.0d;
return 0D;
}
if (idx1 > idx2)
{
int tmp = idx2;
idx2 = idx1;
idx1 = tmp;
(idx1, idx2) = (idx2, idx1);
}
pair.Idx1 = idx1;
@ -637,9 +655,16 @@ internal static class HistogramEncoder
}
/// <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.
/// Update the cost diff and combo of a pair of histograms. This needs to be called when the histograms have been
/// merged with a third one.
/// </summary>
private static void HistoListUpdatePair(Vp8LHistogram h1, Vp8LHistogram h2, Vp8LStreaks stats, Vp8LBitEntropy bitsEntropy, double threshold, HistogramPair pair)
private static void HistoListUpdatePair(
Vp8LHistogram h1,
Vp8LHistogram h2,
Vp8LStreaks stats,
Vp8LBitEntropy bitsEntropy,
double threshold,
HistogramPair pair)
{
double sumCost = h1.BitCost + h2.BitCost;
pair.CostCombo = 0.0d;

18
src/ImageSharp/Formats/Webp/Lossless/HuffmanUtils.cs

@ -25,7 +25,7 @@ internal static class HuffmanUtils
0x1, 0x9, 0x5, 0xd, 0x3, 0xb, 0x7, 0xf
};
public static void CreateHuffmanTree(uint[] histogram, int treeDepthLimit, bool[] bufRle, Span<HuffmanTree> huffTree, HuffmanTreeCode huffCode)
public static void CreateHuffmanTree(Span<uint> histogram, int treeDepthLimit, bool[] bufRle, Span<HuffmanTree> huffTree, HuffmanTreeCode huffCode)
{
int numSymbols = huffCode.NumSymbols;
bufRle.AsSpan().Clear();
@ -40,7 +40,7 @@ internal static class HuffmanUtils
/// Change the population counts in a way that the consequent
/// Huffman tree compression, especially its RLE-part, give smaller output.
/// </summary>
public static void OptimizeHuffmanForRle(int length, bool[] goodForRle, uint[] counts)
public static void OptimizeHuffmanForRle(int length, bool[] goodForRle, Span<uint> counts)
{
// 1) Let's make the Huffman code more compatible with rle encoding.
for (; length >= 0; --length)
@ -116,7 +116,7 @@ internal static class HuffmanUtils
{
// We don't want to change value at counts[i],
// that is already belonging to the next stride. Thus - 1.
counts[i - k - 1] = count;
counts[(int)(i - k - 1)] = count;
}
}
@ -159,7 +159,7 @@ internal static class HuffmanUtils
/// <param name="histogramSize">The size of the histogram.</param>
/// <param name="treeDepthLimit">The tree depth limit.</param>
/// <param name="bitDepths">How many bits are used for the symbol.</param>
public static void GenerateOptimalTree(Span<HuffmanTree> tree, uint[] histogram, int histogramSize, int treeDepthLimit, byte[] bitDepths)
public static void GenerateOptimalTree(Span<HuffmanTree> tree, Span<uint> histogram, int histogramSize, int treeDepthLimit, byte[] bitDepths)
{
uint countMin;
int treeSizeOrig = 0;
@ -177,7 +177,7 @@ internal static class HuffmanUtils
return;
}
Span<HuffmanTree> treePool = tree.Slice(treeSizeOrig);
Span<HuffmanTree> treePool = tree[treeSizeOrig..];
// For block sizes with less than 64k symbols we never need to do a
// second iteration of this loop.
@ -202,7 +202,7 @@ internal static class HuffmanUtils
}
// Build the Huffman tree.
Span<HuffmanTree> treeSlice = tree.Slice(0, treeSize);
Span<HuffmanTree> treeSlice = tree[..treeSize];
treeSlice.Sort(HuffmanTree.Compare);
if (treeSize > 1)
@ -357,7 +357,7 @@ internal static class HuffmanUtils
// Special case code with only one value.
if (offsets[WebpConstants.MaxAllowedCodeLength] == 1)
{
var huffmanCode = new HuffmanCode()
HuffmanCode huffmanCode = new()
{
BitsUsed = 0,
Value = (uint)sorted[0]
@ -390,7 +390,7 @@ internal static class HuffmanUtils
for (; countsLen > 0; countsLen--)
{
var huffmanCode = new HuffmanCode()
HuffmanCode huffmanCode = new()
{
BitsUsed = len,
Value = (uint)sorted[symbol++]
@ -432,7 +432,7 @@ internal static class HuffmanUtils
};
}
var huffmanCode = new HuffmanCode
HuffmanCode huffmanCode = new()
{
BitsUsed = len - rootBits,
Value = (uint)sorted[symbol++]

2
src/ImageSharp/Formats/Webp/Lossless/PixOrCopy.cs

@ -37,7 +37,7 @@ internal sealed class PixOrCopy
Len = len
};
public uint Literal(int component) => (this.BgraOrDistance >> (component * 8)) & 0xff;
public int Literal(int component) => (int)(this.BgraOrDistance >> (component * 8)) & 0xFF;
public uint CacheIdx() => this.BgraOrDistance;

6
src/ImageSharp/Formats/Webp/Lossless/Vp8LBitEntropy.cs

@ -125,7 +125,7 @@ internal class Vp8LBitEntropy
/// <summary>
/// Get the entropy for the distribution 'X'.
/// </summary>
public void BitsEntropyUnrefined(uint[] x, int length, Vp8LStreaks stats)
public void BitsEntropyUnrefined(Span<uint> x, int length, Vp8LStreaks stats)
{
int i;
int iPrev = 0;
@ -147,7 +147,7 @@ internal class Vp8LBitEntropy
this.Entropy += LosslessUtils.FastSLog2(this.Sum);
}
public void GetCombinedEntropyUnrefined(uint[] x, uint[] y, int length, Vp8LStreaks stats)
public void GetCombinedEntropyUnrefined(Span<uint> x, Span<uint> y, int length, Vp8LStreaks stats)
{
int i;
int iPrev = 0;
@ -169,7 +169,7 @@ internal class Vp8LBitEntropy
this.Entropy += LosslessUtils.FastSLog2(this.Sum);
}
public void GetEntropyUnrefined(uint[] x, int length, Vp8LStreaks stats)
public void GetEntropyUnrefined(Span<uint> x, int length, Vp8LStreaks stats)
{
int i;
int iPrev = 0;

117
src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs

@ -589,15 +589,21 @@ internal class Vp8LEncoder : IDisposable
Vp8LBackwardRefs refsTmp = this.Refs[refsBest.Equals(this.Refs[0]) ? 1 : 0];
this.bitWriter.Reset(bwInit);
Vp8LHistogram tmpHisto = new(cacheBits);
List<Vp8LHistogram> histogramImage = new(histogramImageXySize);
for (int i = 0; i < histogramImageXySize; i++)
{
histogramImage.Add(new Vp8LHistogram(cacheBits));
}
using Vp8LHistogram tmpHisto = new(this.memoryAllocator, cacheBits);
using Vp8LHistogramSet histogramImage = new(this.memoryAllocator, histogramImageXySize, cacheBits);
// Build histogram image and symbols from backward references.
HistogramEncoder.GetHistoImageSymbols(width, height, refsBest, this.quality, this.HistoBits, cacheBits, histogramImage, tmpHisto, histogramSymbols);
HistogramEncoder.GetHistoImageSymbols(
this.memoryAllocator,
width,
height,
refsBest,
this.quality,
this.HistoBits,
cacheBits,
histogramImage,
tmpHisto,
histogramSymbols);
// Create Huffman bit lengths and codes for each histogram image.
int histogramImageSize = histogramImage.Count;
@ -678,9 +684,7 @@ internal class Vp8LEncoder : IDisposable
// Keep track of the smallest image so far.
if (isFirstIteration || (bitWriterBest != null && this.bitWriter.NumBytes() < bitWriterBest.NumBytes()))
{
Vp8LBitWriter tmp = this.bitWriter;
this.bitWriter = bitWriterBest;
bitWriterBest = tmp;
(bitWriterBest, this.bitWriter) = (this.bitWriter, bitWriterBest);
}
isFirstIteration = false;
@ -787,13 +791,8 @@ internal class Vp8LEncoder : IDisposable
refsTmp1,
refsTmp2);
List<Vp8LHistogram> histogramImage = new()
{
new(cacheBits)
};
// Build histogram image and symbols from backward references.
histogramImage[0].StoreRefs(refs);
using Vp8LHistogramSet histogramImage = new(this.memoryAllocator, refs, 1, cacheBits);
// Create Huffman bit lengths and codes for each histogram image.
GetHuffBitLengthsAndCodes(histogramImage, huffmanCodes);
@ -833,7 +832,7 @@ internal class Vp8LEncoder : IDisposable
private void StoreHuffmanCode(Span<HuffmanTree> huffTree, HuffmanTreeToken[] tokens, HuffmanTreeCode huffmanCode)
{
int count = 0;
Span<int> symbols = this.scratch.Span.Slice(0, 2);
Span<int> symbols = this.scratch.Span[..2];
symbols.Clear();
const int maxBits = 8;
const int maxSymbol = 1 << maxBits;
@ -886,6 +885,7 @@ internal class Vp8LEncoder : IDisposable
private void StoreFullHuffmanCode(Span<HuffmanTree> huffTree, HuffmanTreeToken[] tokens, HuffmanTreeCode tree)
{
// TODO: Allocations.
int i;
byte[] codeLengthBitDepth = new byte[WebpConstants.CodeLengthCodes];
short[] codeLengthBitDepthSymbols = new short[WebpConstants.CodeLengthCodes];
@ -996,7 +996,12 @@ internal class Vp8LEncoder : IDisposable
}
}
private void StoreImageToBitMask(int width, int histoBits, Vp8LBackwardRefs backwardRefs, Span<ushort> histogramSymbols, HuffmanTreeCode[] huffmanCodes)
private void StoreImageToBitMask(
int width,
int histoBits,
Vp8LBackwardRefs backwardRefs,
Span<ushort> histogramSymbols,
HuffmanTreeCode[] huffmanCodes)
{
int histoXSize = histoBits > 0 ? LosslessUtils.SubSampleSize(width, histoBits) : 1;
int tileMask = histoBits == 0 ? 0 : -(1 << histoBits);
@ -1008,10 +1013,10 @@ internal class Vp8LEncoder : IDisposable
int tileY = y & tileMask;
int histogramIx = histogramSymbols[0];
Span<HuffmanTreeCode> codes = huffmanCodes.AsSpan(5 * histogramIx);
using List<PixOrCopy>.Enumerator c = backwardRefs.Refs.GetEnumerator();
while (c.MoveNext())
for (int i = 0; i < backwardRefs.Refs.Count; i++)
{
PixOrCopy v = c.Current;
PixOrCopy v = backwardRefs.Refs[i];
if (tileX != (x & tileMask) || tileY != (y & tileMask))
{
tileX = x & tileMask;
@ -1024,7 +1029,7 @@ internal class Vp8LEncoder : IDisposable
{
for (int k = 0; k < 4; k++)
{
int code = (int)v.Literal(Order[k]);
int code = v.Literal(Order[k]);
this.bitWriter.WriteHuffmanCode(codes[k], code);
}
}
@ -1379,10 +1384,8 @@ internal class Vp8LEncoder : IDisposable
useLut = false;
break;
}
else
{
buffer[ind] = (uint)j;
}
buffer[ind] = (uint)j;
}
if (useLut)
@ -1591,14 +1594,12 @@ internal class Vp8LEncoder : IDisposable
}
// Swap color(palette[bestIdx], palette[i]);
uint best = palette[bestIdx];
palette[bestIdx] = palette[i];
palette[i] = best;
(palette[i], palette[bestIdx]) = (palette[bestIdx], palette[i]);
predict = palette[i];
}
}
private static void GetHuffBitLengthsAndCodes(List<Vp8LHistogram> histogramImage, HuffmanTreeCode[] huffmanCodes)
private static void GetHuffBitLengthsAndCodes(Vp8LHistogramSet histogramImage, HuffmanTreeCode[] huffmanCodes)
{
int maxNumSymbols = 0;
@ -1609,9 +1610,20 @@ internal class Vp8LEncoder : IDisposable
int startIdx = 5 * i;
for (int k = 0; k < 5; k++)
{
int numSymbols =
k == 0 ? histo.NumCodes() :
k == 4 ? WebpConstants.NumDistanceCodes : 256;
int numSymbols;
if (k == 0)
{
numSymbols = histo.NumCodes();
}
else if (k == 4)
{
numSymbols = WebpConstants.NumDistanceCodes;
}
else
{
numSymbols = 256;
}
huffmanCodes[startIdx + k].NumSymbols = numSymbols;
}
}
@ -1629,6 +1641,7 @@ internal class Vp8LEncoder : IDisposable
}
// Create Huffman trees.
// TODO: Allocations. Size here has a max and can be sliced.
bool[] bufRle = new bool[maxNumSymbols];
Span<HuffmanTree> huffTree = stackalloc HuffmanTree[3 * maxNumSymbols];
@ -1682,8 +1695,18 @@ internal class Vp8LEncoder : IDisposable
histoBits++;
}
return histoBits < WebpConstants.MinHuffmanBits ? WebpConstants.MinHuffmanBits :
histoBits > WebpConstants.MaxHuffmanBits ? WebpConstants.MaxHuffmanBits : histoBits;
if (histoBits < WebpConstants.MinHuffmanBits)
{
return WebpConstants.MinHuffmanBits;
}
else if (histoBits > WebpConstants.MaxHuffmanBits)
{
return WebpConstants.MaxHuffmanBits;
}
else
{
return histoBits;
}
}
/// <summary>
@ -1720,11 +1743,7 @@ internal class Vp8LEncoder : IDisposable
[MethodImpl(InliningOptions.ShortMethod)]
private static void BitWriterSwap(ref Vp8LBitWriter src, ref Vp8LBitWriter dst)
{
Vp8LBitWriter tmp = src;
src = dst;
dst = tmp;
}
=> (dst, src) = (src, dst);
/// <summary>
/// Calculates the bits used for the transformation.
@ -1732,9 +1751,21 @@ internal class Vp8LEncoder : IDisposable
[MethodImpl(InliningOptions.ShortMethod)]
private static int GetTransformBits(WebpEncodingMethod method, int histoBits)
{
int maxTransformBits = (int)method < 4 ? 6 : method > WebpEncodingMethod.Level4 ? 4 : 5;
int res = histoBits > maxTransformBits ? maxTransformBits : histoBits;
return res;
int maxTransformBits;
if ((int)method < 4)
{
maxTransformBits = 6;
}
else if (method > WebpEncodingMethod.Level4)
{
maxTransformBits = 4;
}
else
{
maxTransformBits = 5;
}
return histoBits > maxTransformBits ? maxTransformBits : histoBits;
}
[MethodImpl(InliningOptions.ShortMethod)]

273
src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogram.cs

@ -1,63 +1,73 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Webp.Lossless;
internal sealed class Vp8LHistogram : IDeepCloneable
internal sealed class Vp8LHistogram : IDisposable
{
private const uint NonTrivialSym = 0xffffffff;
private readonly IMemoryOwner<uint> buffer;
private const int RedSize = WebpConstants.NumLiteralCodes;
private const int BlueSize = WebpConstants.NumLiteralCodes;
private const int AlphaSize = WebpConstants.NumLiteralCodes;
private const int DistanceSize = WebpConstants.NumDistanceCodes;
public const int LiteralSize = WebpConstants.NumLiteralCodes + WebpConstants.NumLengthCodes + (1 << WebpConstants.MaxColorCacheBits) + 1;
private const int UsedSize = 5; // 5 for literal, red, blue, alpha, distance
public const int BufferSize = RedSize + BlueSize + AlphaSize + DistanceSize + LiteralSize + UsedSize;
private readonly bool isSetMember;
/// <summary>
/// Initializes a new instance of the <see cref="Vp8LHistogram"/> class.
/// </summary>
/// <param name="other">The histogram to create an instance from.</param>
private Vp8LHistogram(Vp8LHistogram other)
: this(other.PaletteCodeBits)
/// <param name="memoryAllocator">The memory allocator.</param>
/// <param name="refs">The backward references to initialize the histogram with.</param>
/// <param name="paletteCodeBits">The palette code bits.</param>
public Vp8LHistogram(MemoryAllocator memoryAllocator, Vp8LBackwardRefs refs, int paletteCodeBits)
: this(memoryAllocator, paletteCodeBits) => this.StoreRefs(refs);
/// <summary>
/// Initializes a new instance of the <see cref="Vp8LHistogram"/> class.
/// </summary>
/// <param name="memoryAllocator">The memory allocator.</param>
/// <param name="paletteCodeBits">The palette code bits.</param>
public Vp8LHistogram(MemoryAllocator memoryAllocator, int paletteCodeBits)
{
other.Red.AsSpan().CopyTo(this.Red);
other.Blue.AsSpan().CopyTo(this.Blue);
other.Alpha.AsSpan().CopyTo(this.Alpha);
other.Literal.AsSpan().CopyTo(this.Literal);
other.Distance.AsSpan().CopyTo(this.Distance);
other.IsUsed.AsSpan().CopyTo(this.IsUsed);
this.LiteralCost = other.LiteralCost;
this.RedCost = other.RedCost;
this.BlueCost = other.BlueCost;
this.BitCost = other.BitCost;
this.TrivialSymbol = other.TrivialSymbol;
this.PaletteCodeBits = other.PaletteCodeBits;
this.buffer = memoryAllocator.Allocate<uint>(BufferSize, AllocationOptions.Clean);
this.PaletteCodeBits = paletteCodeBits;
}
/// <summary>
/// Initializes a new instance of the <see cref="Vp8LHistogram"/> class.
/// </summary>
/// <remarks>
/// This constructor should be used when the histogram is a member of a <see cref="Vp8LHistogramSet"/>.
/// </remarks>
/// <param name="buffer">The backing buffer.</param>
/// <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(paletteCodeBits) => this.StoreRefs(refs);
public Vp8LHistogram(IMemoryOwner<uint> buffer, Vp8LBackwardRefs refs, int paletteCodeBits)
: this(buffer, paletteCodeBits) => this.StoreRefs(refs);
/// <summary>
/// Initializes a new instance of the <see cref="Vp8LHistogram"/> class.
/// </summary>
/// <remarks>
/// This constructor should be used when the histogram is a member of a <see cref="Vp8LHistogramSet"/>.
/// </remarks>
/// <param name="buffer">The backing buffer.</param>
/// <param name="paletteCodeBits">The palette code bits.</param>
public Vp8LHistogram(int paletteCodeBits)
public Vp8LHistogram(IMemoryOwner<uint> buffer, int paletteCodeBits)
{
this.buffer = buffer;
this.PaletteCodeBits = paletteCodeBits;
this.Red = new uint[WebpConstants.NumLiteralCodes + 1];
this.Blue = new uint[WebpConstants.NumLiteralCodes + 1];
this.Alpha = new uint[WebpConstants.NumLiteralCodes + 1];
this.Distance = new uint[WebpConstants.NumDistanceCodes];
int literalSize = WebpConstants.NumLiteralCodes + WebpConstants.NumLengthCodes + (1 << WebpConstants.MaxColorCacheBits);
this.Literal = new uint[literalSize + 1];
// 5 for literal, red, blue, alpha, distance.
this.IsUsed = new bool[5];
this.isSetMember = true;
}
/// <summary>
@ -85,22 +95,59 @@ internal sealed class Vp8LHistogram : IDeepCloneable
/// </summary>
public double BlueCost { get; set; }
public uint[] Red { get; }
public Span<uint> Red => this.buffer.GetSpan()[..RedSize];
public uint[] Blue { get; }
public Span<uint> Blue => this.buffer.GetSpan().Slice(RedSize, BlueSize);
public uint[] Alpha { get; }
public Span<uint> Alpha => this.buffer.GetSpan().Slice(RedSize + BlueSize, AlphaSize);
public uint[] Literal { get; }
public Span<uint> Distance => this.buffer.GetSpan().Slice(RedSize + BlueSize + AlphaSize, DistanceSize);
public uint[] Distance { get; }
public Span<uint> Literal => this.buffer.GetSpan().Slice(RedSize + BlueSize + AlphaSize + DistanceSize, LiteralSize);
public uint TrivialSymbol { get; set; }
public bool[] IsUsed { get; }
private Span<uint> IsUsedSpan => this.buffer.GetSpan().Slice(RedSize + BlueSize + AlphaSize + DistanceSize + LiteralSize, UsedSize);
public bool IsDisposed { get; set; }
/// <inheritdoc/>
public IDeepCloneable DeepClone() => new Vp8LHistogram(this);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsUsed(int index) => this.IsUsedSpan[index] == 1u;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void IsUsed(int index, bool value) => this.IsUsedSpan[index] = value ? 1u : 0;
/// <summary>
/// Creates a copy of the given <see cref="Vp8LHistogram"/> class.
/// </summary>
/// <param name="other">The histogram to copy to.</param>
public void CopyTo(Vp8LHistogram other)
{
this.Red.CopyTo(other.Red);
this.Blue.CopyTo(other.Blue);
this.Alpha.CopyTo(other.Alpha);
this.Literal.CopyTo(other.Literal);
this.Distance.CopyTo(other.Distance);
this.IsUsedSpan.CopyTo(other.IsUsedSpan);
other.LiteralCost = this.LiteralCost;
other.RedCost = this.RedCost;
other.BlueCost = this.BlueCost;
other.BitCost = this.BitCost;
other.TrivialSymbol = this.TrivialSymbol;
other.PaletteCodeBits = this.PaletteCodeBits;
}
public void Clear()
{
this.buffer.Clear();
this.PaletteCodeBits = 0;
this.BitCost = 0;
this.LiteralCost = 0;
this.RedCost = 0;
this.BlueCost = 0;
this.TrivialSymbol = 0;
}
/// <summary>
/// Collect all the references into a histogram (without reset).
@ -108,10 +155,9 @@ internal sealed class Vp8LHistogram : IDeepCloneable
/// <param name="refs">The backward references.</param>
public void StoreRefs(Vp8LBackwardRefs refs)
{
using List<PixOrCopy>.Enumerator c = refs.Refs.GetEnumerator();
while (c.MoveNext())
for (int i = 0; i < refs.Refs.Count; i++)
{
this.AddSinglePixOrCopy(c.Current, false);
this.AddSinglePixOrCopy(refs.Refs[i], false);
}
}
@ -163,12 +209,12 @@ internal sealed class Vp8LHistogram : IDeepCloneable
{
uint notUsed = 0;
return
PopulationCost(this.Literal, this.NumCodes(), ref notUsed, ref this.IsUsed[0], stats, bitsEntropy)
+ PopulationCost(this.Red, WebpConstants.NumLiteralCodes, ref notUsed, ref this.IsUsed[1], stats, bitsEntropy)
+ PopulationCost(this.Blue, WebpConstants.NumLiteralCodes, ref notUsed, ref this.IsUsed[2], stats, bitsEntropy)
+ PopulationCost(this.Alpha, WebpConstants.NumLiteralCodes, ref notUsed, ref this.IsUsed[3], stats, bitsEntropy)
+ PopulationCost(this.Distance, WebpConstants.NumDistanceCodes, ref notUsed, ref this.IsUsed[4], stats, bitsEntropy)
+ ExtraCost(this.Literal.AsSpan(WebpConstants.NumLiteralCodes), WebpConstants.NumLengthCodes)
this.PopulationCost(this.Literal, this.NumCodes(), ref notUsed, 0, stats, bitsEntropy)
+ this.PopulationCost(this.Red, WebpConstants.NumLiteralCodes, ref notUsed, 1, stats, bitsEntropy)
+ this.PopulationCost(this.Blue, WebpConstants.NumLiteralCodes, ref notUsed, 2, stats, bitsEntropy)
+ this.PopulationCost(this.Alpha, WebpConstants.NumLiteralCodes, ref notUsed, 3, stats, bitsEntropy)
+ this.PopulationCost(this.Distance, WebpConstants.NumDistanceCodes, ref notUsed, 4, stats, bitsEntropy)
+ ExtraCost(this.Literal[WebpConstants.NumLiteralCodes..], WebpConstants.NumLengthCodes)
+ ExtraCost(this.Distance, WebpConstants.NumDistanceCodes);
}
@ -177,12 +223,12 @@ internal sealed class Vp8LHistogram : IDeepCloneable
uint alphaSym = 0, redSym = 0, blueSym = 0;
uint notUsed = 0;
double alphaCost = PopulationCost(this.Alpha, WebpConstants.NumLiteralCodes, ref alphaSym, ref this.IsUsed[3], stats, bitsEntropy);
double distanceCost = PopulationCost(this.Distance, WebpConstants.NumDistanceCodes, ref notUsed, ref this.IsUsed[4], stats, bitsEntropy) + ExtraCost(this.Distance, WebpConstants.NumDistanceCodes);
double alphaCost = this.PopulationCost(this.Alpha, WebpConstants.NumLiteralCodes, ref alphaSym, 3, stats, bitsEntropy);
double distanceCost = this.PopulationCost(this.Distance, WebpConstants.NumDistanceCodes, ref notUsed, 4, stats, bitsEntropy) + ExtraCost(this.Distance, WebpConstants.NumDistanceCodes);
int numCodes = this.NumCodes();
this.LiteralCost = PopulationCost(this.Literal, numCodes, ref notUsed, ref this.IsUsed[0], stats, bitsEntropy) + ExtraCost(this.Literal.AsSpan(WebpConstants.NumLiteralCodes), WebpConstants.NumLengthCodes);
this.RedCost = PopulationCost(this.Red, WebpConstants.NumLiteralCodes, ref redSym, ref this.IsUsed[1], stats, bitsEntropy);
this.BlueCost = PopulationCost(this.Blue, WebpConstants.NumLiteralCodes, ref blueSym, ref this.IsUsed[2], stats, bitsEntropy);
this.LiteralCost = this.PopulationCost(this.Literal, numCodes, ref notUsed, 0, stats, bitsEntropy) + ExtraCost(this.Literal[WebpConstants.NumLiteralCodes..], WebpConstants.NumLengthCodes);
this.RedCost = this.PopulationCost(this.Red, WebpConstants.NumLiteralCodes, ref redSym, 1, stats, bitsEntropy);
this.BlueCost = this.PopulationCost(this.Blue, WebpConstants.NumLiteralCodes, ref blueSym, 2, stats, bitsEntropy);
this.BitCost = this.LiteralCost + this.RedCost + this.BlueCost + alphaCost + distanceCost;
if ((alphaSym | redSym | blueSym) == NonTrivialSym)
{
@ -234,7 +280,7 @@ internal sealed class Vp8LHistogram : IDeepCloneable
for (int i = 0; i < 5; i++)
{
output.IsUsed[i] = this.IsUsed[i] | b.IsUsed[i];
output.IsUsed(i, this.IsUsed(i) | b.IsUsed(i));
}
output.TrivialSymbol = this.TrivialSymbol == b.TrivialSymbol
@ -247,9 +293,9 @@ internal sealed class Vp8LHistogram : IDeepCloneable
bool trivialAtEnd = false;
cost = costInitial;
cost += GetCombinedEntropy(this.Literal, b.Literal, this.NumCodes(), this.IsUsed[0], b.IsUsed[0], false, stats, bitEntropy);
cost += GetCombinedEntropy(this.Literal, b.Literal, this.NumCodes(), this.IsUsed(0), b.IsUsed(0), false, stats, bitEntropy);
cost += ExtraCostCombined(this.Literal.AsSpan(WebpConstants.NumLiteralCodes), b.Literal.AsSpan(WebpConstants.NumLiteralCodes), WebpConstants.NumLengthCodes);
cost += ExtraCostCombined(this.Literal[WebpConstants.NumLiteralCodes..], b.Literal[WebpConstants.NumLiteralCodes..], WebpConstants.NumLengthCodes);
if (cost > costThreshold)
{
@ -270,155 +316,158 @@ internal sealed class Vp8LHistogram : IDeepCloneable
}
}
cost += GetCombinedEntropy(this.Red, b.Red, WebpConstants.NumLiteralCodes, this.IsUsed[1], b.IsUsed[1], trivialAtEnd, stats, bitEntropy);
cost += GetCombinedEntropy(this.Red, b.Red, WebpConstants.NumLiteralCodes, this.IsUsed(1), b.IsUsed(1), trivialAtEnd, stats, bitEntropy);
if (cost > costThreshold)
{
return false;
}
cost += GetCombinedEntropy(this.Blue, b.Blue, WebpConstants.NumLiteralCodes, this.IsUsed[2], b.IsUsed[2], trivialAtEnd, stats, bitEntropy);
cost += GetCombinedEntropy(this.Blue, b.Blue, WebpConstants.NumLiteralCodes, this.IsUsed(2), b.IsUsed(2), trivialAtEnd, stats, bitEntropy);
if (cost > costThreshold)
{
return false;
}
cost += GetCombinedEntropy(this.Alpha, b.Alpha, WebpConstants.NumLiteralCodes, this.IsUsed[3], b.IsUsed[3], trivialAtEnd, stats, bitEntropy);
cost += GetCombinedEntropy(this.Alpha, b.Alpha, WebpConstants.NumLiteralCodes, this.IsUsed(3), b.IsUsed(3), trivialAtEnd, stats, bitEntropy);
if (cost > costThreshold)
{
return false;
}
cost += GetCombinedEntropy(this.Distance, b.Distance, WebpConstants.NumDistanceCodes, this.IsUsed[4], b.IsUsed[4], false, stats, bitEntropy);
cost += GetCombinedEntropy(this.Distance, b.Distance, WebpConstants.NumDistanceCodes, this.IsUsed(4), b.IsUsed(4), false, stats, bitEntropy);
if (cost > costThreshold)
{
return false;
}
cost += ExtraCostCombined(this.Distance, b.Distance, WebpConstants.NumDistanceCodes);
if (cost > costThreshold)
{
return false;
}
return true;
return cost <= costThreshold;
}
private void AddLiteral(Vp8LHistogram b, Vp8LHistogram output, int literalSize)
{
if (this.IsUsed[0])
if (this.IsUsed(0))
{
if (b.IsUsed[0])
if (b.IsUsed(0))
{
AddVector(this.Literal, b.Literal, output.Literal, literalSize);
}
else
{
this.Literal.AsSpan(0, literalSize).CopyTo(output.Literal);
this.Literal[..literalSize].CopyTo(output.Literal);
}
}
else if (b.IsUsed[0])
else if (b.IsUsed(0))
{
b.Literal.AsSpan(0, literalSize).CopyTo(output.Literal);
b.Literal[..literalSize].CopyTo(output.Literal);
}
else
{
output.Literal.AsSpan(0, literalSize).Clear();
output.Literal[..literalSize].Clear();
}
}
private void AddRed(Vp8LHistogram b, Vp8LHistogram output, int size)
{
if (this.IsUsed[1])
if (this.IsUsed(1))
{
if (b.IsUsed[1])
if (b.IsUsed(1))
{
AddVector(this.Red, b.Red, output.Red, size);
}
else
{
this.Red.AsSpan(0, size).CopyTo(output.Red);
this.Red[..size].CopyTo(output.Red);
}
}
else if (b.IsUsed[1])
else if (b.IsUsed(1))
{
b.Red.AsSpan(0, size).CopyTo(output.Red);
b.Red[..size].CopyTo(output.Red);
}
else
{
output.Red.AsSpan(0, size).Clear();
output.Red[..size].Clear();
}
}
private void AddBlue(Vp8LHistogram b, Vp8LHistogram output, int size)
{
if (this.IsUsed[2])
if (this.IsUsed(2))
{
if (b.IsUsed[2])
if (b.IsUsed(2))
{
AddVector(this.Blue, b.Blue, output.Blue, size);
}
else
{
this.Blue.AsSpan(0, size).CopyTo(output.Blue);
this.Blue[..size].CopyTo(output.Blue);
}
}
else if (b.IsUsed[2])
else if (b.IsUsed(2))
{
b.Blue.AsSpan(0, size).CopyTo(output.Blue);
b.Blue[..size].CopyTo(output.Blue);
}
else
{
output.Blue.AsSpan(0, size).Clear();
output.Blue[..size].Clear();
}
}
private void AddAlpha(Vp8LHistogram b, Vp8LHistogram output, int size)
{
if (this.IsUsed[3])
if (this.IsUsed(3))
{
if (b.IsUsed[3])
if (b.IsUsed(3))
{
AddVector(this.Alpha, b.Alpha, output.Alpha, size);
}
else
{
this.Alpha.AsSpan(0, size).CopyTo(output.Alpha);
this.Alpha[..size].CopyTo(output.Alpha);
}
}
else if (b.IsUsed[3])
else if (b.IsUsed(3))
{
b.Alpha.AsSpan(0, size).CopyTo(output.Alpha);
b.Alpha[..size].CopyTo(output.Alpha);
}
else
{
output.Alpha.AsSpan(0, size).Clear();
output.Alpha[..size].Clear();
}
}
private void AddDistance(Vp8LHistogram b, Vp8LHistogram output, int size)
{
if (this.IsUsed[4])
if (this.IsUsed(4))
{
if (b.IsUsed[4])
if (b.IsUsed(4))
{
AddVector(this.Distance, b.Distance, output.Distance, size);
}
else
{
this.Distance.AsSpan(0, size).CopyTo(output.Distance);
this.Distance[..size].CopyTo(output.Distance);
}
}
else if (b.IsUsed[4])
else if (b.IsUsed(4))
{
b.Distance.AsSpan(0, size).CopyTo(output.Distance);
b.Distance[..size].CopyTo(output.Distance);
}
else
{
output.Distance.AsSpan(0, size).Clear();
output.Distance[..size].Clear();
}
}
private static double GetCombinedEntropy(uint[] x, uint[] y, int length, bool isXUsed, bool isYUsed, bool trivialAtEnd, Vp8LStreaks stats, Vp8LBitEntropy bitEntropy)
private static double GetCombinedEntropy(
Span<uint> x,
Span<uint> y,
int length,
bool isXUsed,
bool isYUsed,
bool trivialAtEnd,
Vp8LStreaks stats,
Vp8LBitEntropy bitEntropy)
{
stats.Clear();
bitEntropy.Init();
@ -450,18 +499,15 @@ internal sealed class Vp8LHistogram : IDeepCloneable
bitEntropy.GetEntropyUnrefined(x, length, stats);
}
}
else if (isYUsed)
{
bitEntropy.GetEntropyUnrefined(y, 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();
}
stats.Counts[0] = 1;
stats.Streaks[0][length > 3 ? 1 : 0] = length;
bitEntropy.Init();
}
return bitEntropy.BitsEntropyRefine() + stats.FinalHuffmanCost();
@ -482,7 +528,7 @@ internal sealed class Vp8LHistogram : IDeepCloneable
/// <summary>
/// Get the symbol entropy for the distribution 'population'.
/// </summary>
private static double PopulationCost(uint[] population, int length, ref uint trivialSym, ref bool isUsed, Vp8LStreaks stats, Vp8LBitEntropy bitEntropy)
private double PopulationCost(Span<uint> population, int length, ref uint trivialSym, int isUsedIndex, Vp8LStreaks stats, Vp8LBitEntropy bitEntropy)
{
bitEntropy.Init();
stats.Clear();
@ -491,7 +537,7 @@ internal sealed class Vp8LHistogram : IDeepCloneable
trivialSym = (bitEntropy.NoneZeros == 1) ? bitEntropy.NoneZeroCode : NonTrivialSym;
// The histogram is used if there is at least one non-zero streak.
isUsed = stats.Streaks[1][0] != 0 || stats.Streaks[1][1] != 0;
this.IsUsed(isUsedIndex, stats.Streaks[1][0] != 0 || stats.Streaks[1][1] != 0);
return bitEntropy.BitsEntropyRefine() + stats.FinalHuffmanCost();
}
@ -556,4 +602,17 @@ internal sealed class Vp8LHistogram : IDeepCloneable
}
}
}
public void Dispose()
{
if (!this.IsDisposed)
{
if (!this.isSetMember)
{
this.buffer.Dispose();
}
this.IsDisposed = true;
}
}
}

122
src/ImageSharp/Formats/Webp/Lossless/Vp8LHistogramSet.cs

@ -0,0 +1,122 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
#nullable disable
using System.Buffers;
using System.Collections;
using System.Diagnostics;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Webp.Lossless;
internal sealed class Vp8LHistogramSet : IEnumerable<Vp8LHistogram>, IDisposable
{
private readonly IMemoryOwner<uint> buffer;
private readonly List<Vp8LHistogram> items;
private bool isDisposed;
public Vp8LHistogramSet(MemoryAllocator memoryAllocator, int capacity, int cacheBits)
{
this.buffer = memoryAllocator.Allocate<uint>(Vp8LHistogram.BufferSize * capacity, AllocationOptions.Clean);
this.items = new List<Vp8LHistogram>(capacity);
for (int i = 0; i < capacity; i++)
{
SetItemMemoryOwner owner = new(this.buffer.Memory.Slice(Vp8LHistogram.BufferSize * i, Vp8LHistogram.BufferSize));
this.items.Add(new Vp8LHistogram(owner, cacheBits));
}
}
public Vp8LHistogramSet(MemoryAllocator memoryAllocator, Vp8LBackwardRefs refs, int capacity, int cacheBits)
{
this.buffer = memoryAllocator.Allocate<uint>(Vp8LHistogram.BufferSize * capacity, AllocationOptions.Clean);
this.items = new List<Vp8LHistogram>(capacity);
for (int i = 0; i < capacity; i++)
{
SetItemMemoryOwner owner = new(this.buffer.Memory.Slice(Vp8LHistogram.BufferSize * i, Vp8LHistogram.BufferSize));
this.items.Add(new Vp8LHistogram(owner, refs, cacheBits));
}
}
public Vp8LHistogramSet(int capacity) => this.items = new(capacity);
public Vp8LHistogramSet() => this.items = new();
public int Count => this.items.Count;
public Vp8LHistogram this[int index]
{
get => this.items[index];
// TODO: Should we check and throw for null?
set => this.items[index] = value;
}
public void DisposeAt(int index)
{
this.CheckDisposed();
Vp8LHistogram item = this.items[index];
item?.Dispose();
this.items[index] = null;
}
public void RemoveAt(int index)
{
this.CheckDisposed();
Vp8LHistogram item = this.items[index];
item?.Dispose();
this.items.RemoveAt(index);
#pragma warning disable IDE0059 // Unnecessary assignment of a value
item = null;
#pragma warning restore IDE0059 // Unnecessary assignment of a value
}
public void Dispose()
{
if (this.isDisposed)
{
return;
}
this.buffer.Dispose();
foreach (Vp8LHistogram item in this.items)
{
item?.Dispose();
}
this.items.Clear();
this.isDisposed = true;
}
public IEnumerator<Vp8LHistogram> GetEnumerator() => ((IEnumerable<Vp8LHistogram>)this.items).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)this.items).GetEnumerator();
[Conditional("DEBUG")]
private void CheckDisposed()
{
if (this.isDisposed)
{
ThrowDisposed();
}
}
private static void ThrowDisposed() => throw new ObjectDisposedException(nameof(Vp8LHistogramSet));
private sealed class SetItemMemoryOwner : IMemoryOwner<uint>
{
public SetItemMemoryOwner(Memory<uint> memory) => this.Memory = memory;
public Memory<uint> Memory { get; }
public void Dispose()
{
// Do nothing, the underlying memory is owned by the parent set.
}
}
}

13
tests/ImageSharp.Benchmarks/Codecs/Webp/EncodeWebp.cs

@ -43,9 +43,9 @@ public class EncodeWebp
[Benchmark(Description = "Magick Webp Lossy")]
public void MagickWebpLossy()
{
using var memoryStream = new MemoryStream();
using MemoryStream memoryStream = new();
var defines = new WebPWriteDefines
WebPWriteDefines defines = new()
{
Lossless = false,
Method = 4,
@ -65,7 +65,7 @@ public class EncodeWebp
[Benchmark(Description = "ImageSharp Webp Lossy")]
public void ImageSharpWebpLossy()
{
using var memoryStream = new MemoryStream();
using MemoryStream memoryStream = new();
this.webp.Save(memoryStream, new WebpEncoder()
{
FileFormat = WebpFileFormatType.Lossy,
@ -80,8 +80,8 @@ public class EncodeWebp
[Benchmark(Baseline = true, Description = "Magick Webp Lossless")]
public void MagickWebpLossless()
{
using var memoryStream = new MemoryStream();
var defines = new WebPWriteDefines
using MemoryStream memoryStream = new();
WebPWriteDefines defines = new()
{
Lossless = true,
Method = 4,
@ -97,12 +97,13 @@ public class EncodeWebp
[Benchmark(Description = "ImageSharp Webp Lossless")]
public void ImageSharpWebpLossless()
{
using var memoryStream = new MemoryStream();
using MemoryStream memoryStream = new();
this.webp.Save(memoryStream, new WebpEncoder()
{
FileFormat = WebpFileFormatType.Lossless,
Method = WebpEncodingMethod.Level4,
NearLossless = false,
Quality = 75,
// This is equal to exact = false in libwebp, which is the default.
TransparentColorMode = WebpTransparentColorMode.Clear

10
tests/ImageSharp.Tests/Formats/WebP/DominantCostRangeTests.cs

@ -11,7 +11,7 @@ public class DominantCostRangeTests
[Fact]
public void DominantCost_Constructor()
{
var dominantCostRange = new DominantCostRange();
DominantCostRange dominantCostRange = new();
Assert.Equal(0, dominantCostRange.LiteralMax);
Assert.Equal(double.MaxValue, dominantCostRange.LiteralMin);
Assert.Equal(0, dominantCostRange.RedMax);
@ -24,8 +24,8 @@ public class DominantCostRangeTests
public void UpdateDominantCostRange_Works()
{
// arrange
var dominantCostRange = new DominantCostRange();
var histogram = new Vp8LHistogram(10)
DominantCostRange dominantCostRange = new();
using Vp8LHistogram histogram = new(Configuration.Default.MemoryAllocator, 10)
{
LiteralCost = 1.0d,
RedCost = 2.0d,
@ -50,7 +50,7 @@ public class DominantCostRangeTests
public void GetHistoBinIndex_Works(int partitions, int expectedIndex)
{
// arrange
var dominantCostRange = new DominantCostRange()
DominantCostRange dominantCostRange = new()
{
BlueMax = 253.4625,
BlueMin = 109.0,
@ -59,7 +59,7 @@ public class DominantCostRangeTests
RedMax = 191.0,
RedMin = 109.0
};
var histogram = new Vp8LHistogram(6)
using Vp8LHistogram histogram = new(Configuration.Default.MemoryAllocator, 6)
{
LiteralCost = 247.0d,
RedCost = 112.0d,

14
tests/ImageSharp.Tests/Formats/WebP/Vp8LHistogramTests.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Webp.Lossless;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Tests.TestUtilities;
namespace SixLabors.ImageSharp.Tests.Formats.Webp;
@ -65,7 +66,7 @@ public class Vp8LHistogramTests
// All remaining values are expected to be zero.
literals.AsSpan().CopyTo(expectedLiterals);
var backwardRefs = new Vp8LBackwardRefs(pixelData.Length);
Vp8LBackwardRefs backwardRefs = new(pixelData.Length);
for (int i = 0; i < pixelData.Length; i++)
{
backwardRefs.Add(new PixOrCopy()
@ -76,15 +77,16 @@ public class Vp8LHistogramTests
});
}
var histogram0 = new Vp8LHistogram(backwardRefs, 3);
var histogram1 = new Vp8LHistogram(backwardRefs, 3);
MemoryAllocator memoryAllocator = Configuration.Default.MemoryAllocator;
using Vp8LHistogram histogram0 = new(memoryAllocator, backwardRefs, 3);
using Vp8LHistogram histogram1 = new(memoryAllocator, backwardRefs, 3);
for (int i = 0; i < 5; i++)
{
histogram0.IsUsed[i] = true;
histogram1.IsUsed[i] = true;
histogram0.IsUsed(i, true);
histogram1.IsUsed(i, true);
}
var output = new Vp8LHistogram(3);
using Vp8LHistogram output = new(memoryAllocator, 3);
// act
histogram0.Add(histogram1, output);

Loading…
Cancel
Save