Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Webp improvements #1846

Merged
merged 13 commits into from
Nov 23, 2021
Merged
2 changes: 1 addition & 1 deletion src/ImageSharp/Formats/Webp/EntropyIx.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Webp
/// <summary>
/// These five modes are evaluated and their respective entropy is computed.
/// </summary>
internal enum EntropyIx
internal enum EntropyIx : byte
{
Direct = 0,

Expand Down
2 changes: 1 addition & 1 deletion src/ImageSharp/Formats/Webp/HistoIx.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace SixLabors.ImageSharp.Formats.Webp
{
internal enum HistoIx
internal enum HistoIx : byte
{
HistoAlpha = 0,

Expand Down
49 changes: 30 additions & 19 deletions src/ImageSharp/Formats/Webp/Lossless/BackwardReferenceEncoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
// Licensed under the Apache License, Version 2.0.

using System;
using System.Buffers;
using System.Collections.Generic;
using SixLabors.ImageSharp.Memory;

namespace SixLabors.ImageSharp.Formats.Webp.Lossless
{
internal class BackwardReferenceEncoder
internal static class BackwardReferenceEncoder
{
/// <summary>
/// Maximum bit length.
Expand Down Expand Up @@ -41,6 +43,7 @@ public static Vp8LBackwardRefs GetBackwardReferences(
int quality,
int lz77TypesToTry,
ref int cacheBits,
MemoryAllocator memoryAllocator,
Vp8LHashChain hashChain,
Vp8LBackwardRefs best,
Vp8LBackwardRefs worst)
Expand Down Expand Up @@ -69,7 +72,7 @@ public static Vp8LBackwardRefs GetBackwardReferences(
BackwardReferencesLz77(width, height, bgra, 0, hashChain, worst);
break;
case Vp8LLz77Type.Lz77Box:
hashChainBox = new Vp8LHashChain(width * height);
hashChainBox = new Vp8LHashChain(memoryAllocator, width * height);
BackwardReferencesLz77Box(width, height, bgra, 0, hashChain, hashChainBox, worst);
break;
}
Expand Down Expand Up @@ -100,7 +103,7 @@ public static Vp8LBackwardRefs GetBackwardReferences(
if ((lz77TypeBest == (int)Vp8LLz77Type.Lz77Standard || lz77TypeBest == (int)Vp8LLz77Type.Lz77Box) && quality >= 25)
{
Vp8LHashChain hashChainTmp = lz77TypeBest == (int)Vp8LLz77Type.Lz77Standard ? hashChain : hashChainBox;
BackwardReferencesTraceBackwards(width, height, bgra, cacheBits, hashChainTmp, best, worst);
BackwardReferencesTraceBackwards(width, height, memoryAllocator, bgra, cacheBits, hashChainTmp, best, worst);
var histo = new Vp8LHistogram(worst, cacheBits);
double bitCostTrace = histo.EstimateBits(stats, bitsEntropy);
if (bitCostTrace < bitCostBest)
Expand All @@ -111,6 +114,8 @@ public static Vp8LBackwardRefs GetBackwardReferences(

BackwardReferences2DLocality(width, best);

hashChainBox?.Dispose();

return best;
}

Expand Down Expand Up @@ -234,29 +239,32 @@ private static int CalculateBestCacheSize(ReadOnlySpan<uint> bgra, int quality,
private static void BackwardReferencesTraceBackwards(
int xSize,
int ySize,
MemoryAllocator memoryAllocator,
ReadOnlySpan<uint> bgra,
int cacheBits,
Vp8LHashChain hashChain,
Vp8LBackwardRefs refsSrc,
Vp8LBackwardRefs refsDst)
{
int distArraySize = xSize * ySize;
ushort[] distArray = new ushort[distArraySize];
using IMemoryOwner<ushort> distArrayBuffer = memoryAllocator.Allocate<ushort>(distArraySize);
Span<ushort> distArray = distArrayBuffer.GetSpan();

BackwardReferencesHashChainDistanceOnly(xSize, ySize, bgra, cacheBits, hashChain, refsSrc, distArray);
BackwardReferencesHashChainDistanceOnly(xSize, ySize, memoryAllocator, bgra, cacheBits, hashChain, refsSrc, distArrayBuffer);
int chosenPathSize = TraceBackwards(distArray, distArraySize);
Span<ushort> chosenPath = distArray.AsSpan(distArraySize - chosenPathSize);
Span<ushort> chosenPath = distArray.Slice(distArraySize - chosenPathSize);
BackwardReferencesHashChainFollowChosenPath(bgra, cacheBits, chosenPath, chosenPathSize, hashChain, refsDst);
}

private static void BackwardReferencesHashChainDistanceOnly(
int xSize,
int ySize,
MemoryAllocator memoryAllocator,
ReadOnlySpan<uint> bgra,
int cacheBits,
Vp8LHashChain hashChain,
Vp8LBackwardRefs refs,
ushort[] distArray)
IMemoryOwner<ushort> distArrayBuffer)
{
int pixCount = xSize * ySize;
bool useColorCache = cacheBits > 0;
Expand All @@ -275,22 +283,24 @@ private static void BackwardReferencesHashChainDistanceOnly(
}

costModel.Build(xSize, cacheBits, refs);
var costManager = new CostManager(distArray, pixCount, costModel);
using var costManager = new CostManager(memoryAllocator, distArrayBuffer, pixCount, costModel);
Span<float> costManagerCosts = costManager.Costs.GetSpan();
Span<ushort> distArray = distArrayBuffer.GetSpan();

// We loop one pixel at a time, but store all currently best points to non-processed locations from this point.
distArray[0] = 0;

// Add first pixel as literal.
AddSingleLiteralWithCostModel(bgra, colorCache, costModel, 0, useColorCache, 0.0f, costManager.Costs, distArray);
AddSingleLiteralWithCostModel(bgra, colorCache, costModel, 0, useColorCache, 0.0f, costManagerCosts, distArray);

for (int i = 1; i < pixCount; i++)
{
float prevCost = costManager.Costs[i - 1];
float prevCost = costManagerCosts[i - 1];
int offset = hashChain.FindOffset(i);
int len = hashChain.FindLength(i);

// Try adding the pixel as a literal.
AddSingleLiteralWithCostModel(bgra, colorCache, costModel, i, useColorCache, prevCost, costManager.Costs, distArray);
AddSingleLiteralWithCostModel(bgra, colorCache, costModel, i, useColorCache, prevCost, costManagerCosts, distArray);

// If we are dealing with a non-literal.
if (len >= 2)
Expand Down Expand Up @@ -334,7 +344,7 @@ private static void BackwardReferencesHashChainDistanceOnly(
costManager.UpdateCostAtIndex(j - 1, false);
costManager.UpdateCostAtIndex(j, false);

costManager.PushInterval(costManager.Costs[j - 1] + offsetCost, j, lenJ);
costManager.PushInterval(costManagerCosts[j - 1] + offsetCost, j, lenJ);
reach = j + lenJ - 1;
}
}
Expand All @@ -346,7 +356,7 @@ private static void BackwardReferencesHashChainDistanceOnly(
}
}

private static int TraceBackwards(ushort[] distArray, int distArraySize)
private static int TraceBackwards(Span<ushort> distArray, int distArraySize)
{
int chosenPathSize = 0;
int pathPos = distArraySize;
Expand Down Expand Up @@ -426,8 +436,8 @@ private static void AddSingleLiteralWithCostModel(
int idx,
bool useColorCache,
float prevCost,
float[] cost,
ushort[] distArray)
Span<float> cost,
Span<ushort> distArray)
{
double costVal = prevCost;
uint color = bgra[idx];
Expand Down Expand Up @@ -617,7 +627,8 @@ private static void BackwardReferencesLz77Box(int xSize, int ySize, ReadOnlySpan
}
}

hashChain.OffsetLength[0] = 0;
Span<uint> hashChainOffsetLength = hashChain.OffsetLength.GetSpan();
hashChainOffsetLength[0] = 0;
for (i = 1; i < pixelCount; i++)
{
int ind;
Expand Down Expand Up @@ -695,19 +706,19 @@ private static void BackwardReferencesLz77Box(int xSize, int ySize, ReadOnlySpan

if (bestLength <= MinLength)
{
hashChain.OffsetLength[i] = 0;
hashChainOffsetLength[i] = 0;
bestOffsetPrev = 0;
bestLengthPrev = 0;
}
else
{
hashChain.OffsetLength[i] = (uint)((bestOffset << MaxLengthBits) | bestLength);
hashChainOffsetLength[i] = (uint)((bestOffset << MaxLengthBits) | bestLength);
bestOffsetPrev = bestOffset;
bestLengthPrev = bestLength;
}
}

hashChain.OffsetLength[0] = 0;
hashChainOffsetLength[0] = 0;
BackwardReferencesLz77(xSize, ySize, bgra, cacheBits, hashChain, refs);
}

Expand Down
74 changes: 49 additions & 25 deletions src/ImageSharp/Formats/Webp/Lossless/CostManager.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.

using System;
using System.Buffers;
using System.Collections.Generic;
using SixLabors.ImageSharp.Memory;

namespace SixLabors.ImageSharp.Formats.Webp.Lossless
{
Expand All @@ -10,20 +13,29 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless
/// It caches the different CostCacheInterval, caches the different
/// GetLengthCost(costModel, k) in costCache and the CostInterval's.
/// </summary>
internal class CostManager
internal sealed class CostManager : IDisposable
{
private CostInterval head;

public CostManager(ushort[] distArray, int pixCount, CostModel costModel)
private const int FreeIntervalsStartCount = 25;

private readonly Stack<CostInterval> freeIntervals = new(FreeIntervalsStartCount);

public CostManager(MemoryAllocator memoryAllocator, IMemoryOwner<ushort> distArray, int pixCount, CostModel costModel)
{
int costCacheSize = pixCount > BackwardReferenceEncoder.MaxLength ? BackwardReferenceEncoder.MaxLength : pixCount;

this.CacheIntervals = new List<CostCacheInterval>();
this.CostCache = new List<double>();
this.Costs = new float[pixCount];
this.Costs = memoryAllocator.Allocate<float>(pixCount);
this.DistArray = distArray;
this.Count = 0;

for (int i = 0; i < FreeIntervalsStartCount; i++)
{
this.freeIntervals.Push(new CostInterval());
}

// Fill in the cost cache.
this.CacheIntervalsSize++;
this.CostCache.Add(costModel.GetLengthCost(0));
Expand Down Expand Up @@ -64,10 +76,7 @@ public CostManager(ushort[] distArray, int pixCount, CostModel costModel)
}

// Set the initial costs high for every pixel as we will keep the minimum.
for (int i = 0; i < pixCount; i++)
{
this.Costs[i] = 1e38f;
}
this.Costs.GetSpan().Fill(1e38f);
}

/// <summary>
Expand All @@ -82,9 +91,9 @@ public CostManager(ushort[] distArray, int pixCount, CostModel costModel)

public int CacheIntervalsSize { get; }

public float[] Costs { get; }
public IMemoryOwner<float> Costs { get; }

public ushort[] DistArray { get; }
public IMemoryOwner<ushort> DistArray { get; }

public List<CostCacheInterval> CacheIntervals { get; }

Expand Down Expand Up @@ -128,17 +137,19 @@ public void PushInterval(double distanceCost, int position, int len)
// interval logic, just serialize it right away. This constant is empirical.
int skipDistance = 10;

Span<float> costs = this.Costs.GetSpan();
Span<ushort> distArray = this.DistArray.GetSpan();
if (len < skipDistance)
{
for (int j = position; j < position + len; j++)
{
int k = j - position;
float costTmp = (float)(distanceCost + this.CostCache[k]);

if (this.Costs[j] > costTmp)
if (costs[j] > costTmp)
{
this.Costs[j] = costTmp;
this.DistArray[j] = (ushort)(k + 1);
costs[j] = costTmp;
distArray[j] = (ushort)(k + 1);
}
}

Expand Down Expand Up @@ -201,10 +212,8 @@ public void PushInterval(double distanceCost, int position, int len)
this.InsertInterval(interval, interval.Cost, interval.Index, end, endOriginal);
break;
}
else
{
interval.End = start;
}

interval.End = start;
}
}

Expand All @@ -226,6 +235,10 @@ private void PopInterval(CostInterval interval)

this.ConnectIntervals(interval.Previous, interval.Next);
this.Count--;

interval.Next = null;
interval.Previous = null;
this.freeIntervals.Push(interval);
}

private void InsertInterval(CostInterval intervalIn, float cost, int position, int start, int end)
Expand All @@ -236,13 +249,19 @@ private void InsertInterval(CostInterval intervalIn, float cost, int position, i
}

// TODO: should we use COST_CACHE_INTERVAL_SIZE_MAX?
var intervalNew = new CostInterval()
CostInterval intervalNew;
if (this.freeIntervals.Count > 0)
{
Cost = cost,
Start = start,
End = end,
Index = position
};
intervalNew = this.freeIntervals.Pop();
intervalNew.Cost = cost;
intervalNew.Start = start;
intervalNew.End = end;
intervalNew.Index = position;
}
else
{
intervalNew = new CostInterval() { Cost = cost, Start = start, End = end, Index = position };
}

this.PositionOrphanInterval(intervalNew, intervalIn);
this.Count++;
Expand Down Expand Up @@ -297,12 +316,17 @@ private void ConnectIntervals(CostInterval prev, CostInterval next)
/// </summary>
private void UpdateCost(int i, int position, float cost)
{
Span<float> costs = this.Costs.GetSpan();
Span<ushort> distArray = this.DistArray.GetSpan();
int k = i - position;
if (this.Costs[i] > cost)
if (costs[i] > cost)
{
this.Costs[i] = cost;
this.DistArray[i] = (ushort)(k + 1);
costs[i] = cost;
distArray[i] = (ushort)(k + 1);
}
}

/// <inheritdoc />
public void Dispose() => this.Costs.Dispose();
}
}
10 changes: 5 additions & 5 deletions src/ImageSharp/Formats/Webp/Lossless/HTreeGroup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,16 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless
/// - UsePackedTable: few enough literal symbols, so all the bit codes can fit into a small look-up table PackedTable[]
/// The common literal base, if applicable, is stored in 'LiteralArb'.
/// </summary>
internal class HTreeGroup
internal struct HTreeGroup
{
public HTreeGroup(uint packedTableSize)
{
this.HTrees = new List<HuffmanCode[]>(WebpConstants.HuffmanCodesPerMetaCode);
this.PackedTable = new HuffmanCode[packedTableSize];
for (int i = 0; i < packedTableSize; i++)
{
this.PackedTable[i] = new HuffmanCode();
}
this.IsTrivialCode = false;
this.IsTrivialLiteral = false;
this.LiteralArb = 0;
this.UsePackedTable = false;
}

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion src/ImageSharp/Formats/Webp/Lossless/HuffmanCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless
/// A classic way to do entropy coding where a smaller number of bits are used for more frequent codes.
/// </summary>
[DebuggerDisplay("BitsUsed: {BitsUsed}, Value: {Value}")]
internal class HuffmanCode
internal struct HuffmanCode
{
/// <summary>
/// Gets or sets the number of bits used for this symbol.
Expand Down
Loading