diff --git a/src/ImageSharp/Formats/WebP/BitWriter/Vp8BitWriter.cs b/src/ImageSharp/Formats/WebP/BitWriter/Vp8BitWriter.cs index d55716159..4e6260aa2 100644 --- a/src/ImageSharp/Formats/WebP/BitWriter/Vp8BitWriter.cs +++ b/src/ImageSharp/Formats/WebP/BitWriter/Vp8BitWriter.cs @@ -54,7 +54,6 @@ namespace SixLabors.ImageSharp.Formats.WebP.BitWriter public int PutCoeffs(int ctx, Vp8Residual residual) { - int tabIdx = 0; int n = residual.First; Vp8ProbaArray p = residual.Prob[n][ctx]; if (!this.PutBit(residual.Last >= 0, p.Probabilities[0])) @@ -102,6 +101,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.BitWriter { int mask; byte[] tab; + var tabIdx = 0; if (v < 3 + (8 << 1)) { // VP8Cat3 (3b) @@ -163,6 +163,37 @@ namespace SixLabors.ImageSharp.Formats.WebP.BitWriter return 1; } + /// + /// Resizes the buffer to write to. + /// + /// The extra size in bytes needed. + public override void BitWriterResize(int extraSize) + { + // TODO: review again if this works as intended. Probably needs a unit test ... + var neededSize = this.pos + extraSize; + if (neededSize <= this.maxPos) + { + return; + } + + this.ResizeBuffer(this.maxPos, (int)neededSize); + } + + public void Finish() + { + this.PutBits(0, 9 - this.nbBits); + this.nbBits = 0; // pad with zeroes. + this.Flush(); + } + + private void PutBits(uint value, int nbBits) + { + for (uint mask = 1u << (nbBits - 1); mask != 0; mask >>= 1) + { + this.PutBitUniform((int)(value & mask)); + } + } + private bool PutBit(bool bit, int prob) { return this.PutBit(bit ? 1 : 0, prob); @@ -261,21 +292,5 @@ namespace SixLabors.ImageSharp.Formats.WebP.BitWriter this.run++; // Delay writing of bytes 0xff, pending eventual carry. } } - - /// - /// Resizes the buffer to write to. - /// - /// The extra size in bytes needed. - public override void BitWriterResize(int extraSize) - { - // TODO: review again if this works as intended. Probably needs a unit test ... - var neededSize = this.pos + extraSize; - if (neededSize <= this.maxPos) - { - return; - } - - this.ResizeBuffer(this.maxPos, (int)neededSize); - } } } diff --git a/src/ImageSharp/Formats/WebP/Lossy/LossyUtils.cs b/src/ImageSharp/Formats/WebP/Lossy/LossyUtils.cs index 8c73c094c..a5971b0be 100644 --- a/src/ImageSharp/Formats/WebP/Lossy/LossyUtils.cs +++ b/src/ImageSharp/Formats/WebP/Lossy/LossyUtils.cs @@ -458,7 +458,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy /// /// Paragraph 14.3: Implementation of the Walsh-Hadamard transform inversion. /// - public static void TransformWht(short[] input, short[] output) + public static void TransformWht(Span input, Span output) { var tmp = new int[16]; for (int i = 0; i < 4; ++i) diff --git a/src/ImageSharp/Formats/WebP/Lossy/Vp8EncIterator.cs b/src/ImageSharp/Formats/WebP/Lossy/Vp8EncIterator.cs index 818b8ece7..5ec73669d 100644 --- a/src/ImageSharp/Formats/WebP/Lossy/Vp8EncIterator.cs +++ b/src/ImageSharp/Formats/WebP/Lossy/Vp8EncIterator.cs @@ -133,6 +133,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy this.YuvP.AsSpan().Fill(defaultInitVal); this.YLeft.AsSpan().Fill(defaultInitVal); this.UvLeft.AsSpan().Fill(defaultInitVal); + this.Preds.GetSpan().Fill(defaultInitVal); for (int i = -255; i <= 255 + 255; ++i) { @@ -197,6 +198,17 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy /// public IMemoryOwner Preds { get; } + /// + /// Gets the current start index of the intra mode predictors. + /// + public int PredIdx + { + get + { + return this.predIdx; + } + } + /// /// Gets the non-zero pattern. /// @@ -277,7 +289,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy for (i = 0; i < 17; ++i) { // left - this.I4Boundary[i] = this.YLeft[15 - i]; + this.I4Boundary[i] = this.YLeft[15 - i + 1]; } Span yTop = this.YTop.GetSpan(); @@ -287,7 +299,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy this.I4Boundary[17 + i] = yTop[i]; } - // top-right samples have a special case on the far right of the picture + // top-right samples have a special case on the far right of the picture. if (this.X < this.mbw - 1) { for (i = 16; i < 16 + 4; ++i) @@ -487,10 +499,11 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy public short[] GetCostModeI4(byte[] modes) { int predsWidth = this.predsWidth; + int predIdx = this.predIdx; int x = this.I4 & 3; int y = this.I4 >> 2; - int left = (int)((x == 0) ? this.Preds.GetSpan()[(y * predsWidth) - 1] : modes[this.I4 - 1]); - int top = (int)((y == 0) ? this.Preds.GetSpan()[-predsWidth + x] : modes[this.I4 - 4]); + int left = (x == 0) ? this.Preds.Slice(predIdx)[(y * predsWidth) - 1] : modes[this.I4 - 1]; + int top = (y == 0) ? this.Preds.Slice(predIdx)[-predsWidth + x] : modes[this.I4 - 4]; return WebPLookupTables.Vp8FixedCostsI4[top, left]; } @@ -660,8 +673,9 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy public void NzToBytes() { Span nz = this.Nz.GetSpan(); + + uint lnz = 0; // TODO: -1? uint tnz = nz[0]; - uint lnz = nz[-1]; // TODO: -1? Span topNz = this.TopNz; Span leftNz = this.LeftNz; @@ -1245,7 +1259,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy this.nzIdx = 0; this.yTopIdx = 0; this.uvTopIdx = 0; - this.predIdx = y * 4 * this.predsWidth; + this.predIdx = this.predsWidth + (y * 4 * this.predsWidth); this.InitLeft(); } diff --git a/src/ImageSharp/Formats/WebP/Lossy/Vp8EncSegmentHeader.cs b/src/ImageSharp/Formats/WebP/Lossy/Vp8EncSegmentHeader.cs new file mode 100644 index 000000000..77a5ceecf --- /dev/null +++ b/src/ImageSharp/Formats/WebP/Lossy/Vp8EncSegmentHeader.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.WebP.Lossy +{ + internal class Vp8EncSegmentHeader + { + /// + /// Initializes a new instance of the class. + /// + /// Number of segments. + public Vp8EncSegmentHeader(int numSegments) + { + this.NumSegments = numSegments; + this.UpdateMap = this.NumSegments > 1; + this.Size = 0; + } + + /// + /// Gets the actual number of segments. 1 segment only = unused. + /// + public int NumSegments { get; } + + /// + /// Gets a value indicating whether to update the segment map or not. Must be false if there's only 1 segment. + /// + public bool UpdateMap { get; } + + /// + /// Gets the bit-cost for transmitting the segment map. + /// + public int Size { get; } + } +} diff --git a/src/ImageSharp/Formats/WebP/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/WebP/Lossy/Vp8Encoder.cs index e580421de..fd3f541c7 100644 --- a/src/ImageSharp/Formats/WebP/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/WebP/Lossy/Vp8Encoder.cs @@ -37,6 +37,45 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy /// private readonly int method; + /// + /// Stride of the prediction plane (=4*mb_w + 1) + /// + private int predsWidth; + + /// + /// Macroblock width. + /// + private int mbw; + + /// + /// Macroblock height. + /// + private int mbh; + + /// + /// The segment features. + /// + private Vp8EncSegmentHeader segmentHeader; + + /// + /// Contextual macroblock infos. + /// + private Vp8MacroBlockInfo[] mbInfo; + + private int dqUvDc; + + private int dqUvAc; + + /// + /// Global susceptibility. + /// + private int alpha; + + /// + /// U/V quantization susceptibility. + /// + private int uvAlpha; + /// /// Fixed-point precision for RGB->YUV. /// @@ -50,12 +89,14 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy private const int MaxLevel = 2047; - private const int QFix = 17; - private readonly byte[] zigzag = { 0, 1, 4, 8, 5, 2, 3, 6, 9, 12, 13, 10, 7, 11, 14, 15 }; private readonly byte[] averageBytesPerMb = { 50, 24, 16, 9, 7, 5, 3, 2 }; + private const int NumMbSegments = 4; + + private const int MaxItersKMeans = 6; + /// /// Initializes a new instance of the class. /// @@ -71,22 +112,34 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy this.method = method.Clamp(0, 6); var pixelCount = width * height; - int mbw = (width + 15) >> 4; - int mbh = (height + 15) >> 4; + this.mbw = (width + 15) >> 4; + this.mbh = (height + 15) >> 4; var uvSize = ((width + 1) >> 1) * ((height + 1) >> 1); this.Y = this.memoryAllocator.Allocate(pixelCount); this.U = this.memoryAllocator.Allocate(uvSize); this.V = this.memoryAllocator.Allocate(uvSize); - this.YTop = this.memoryAllocator.Allocate(mbw * 16); - this.UvTop = this.memoryAllocator.Allocate(mbw * 16 * 2); - this.Preds = this.memoryAllocator.Allocate(((4 * mbw) + 1) * ((4 * mbh) + 1)); - this.Nz = this.memoryAllocator.Allocate(mbw + 1); - this.MbHeaderLimit = 256 * 510 * 8 * 1024 / (mbw * mbh); + this.YTop = this.memoryAllocator.Allocate(this.mbw * 16); + this.UvTop = this.memoryAllocator.Allocate(this.mbw * 16 * 2); + this.Nz = this.memoryAllocator.Allocate(this.mbw + 1); + this.MbHeaderLimit = 256 * 510 * 8 * 1024 / (this.mbw * this.mbh); + int predSize = (((4 * this.mbw) + 1) * ((4 * this.mbh) + 1)) + this.predsWidth + 1; + + this.mbInfo = new Vp8MacroBlockInfo[this.mbw * this.mbh]; + for (int i = 0; i < this.mbInfo.Length; i++) + { + this.mbInfo[i] = new Vp8MacroBlockInfo(); + } + + // this.Preds = this.memoryAllocator.Allocate(predSize); + this.Preds = this.memoryAllocator.Allocate(predSize * 2); // TODO: figure out how much mem we need here. This is too much. + this.predsWidth = (4 * this.mbw) + 1; + + this.ResetBoundaryPredictions(); // Initialize the bitwriter. var baseQuant = 36; // TODO: hardCoded for now. int averageBytesPerMacroBlock = this.averageBytesPerMb[baseQuant >> 4]; - int expectedSize = mbw * mbh * averageBytesPerMacroBlock; + int expectedSize = this.mbw * this.mbh * averageBytesPerMacroBlock; this.bitWriter = new Vp8BitWriter(expectedSize); } @@ -151,32 +204,25 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy Span u = this.U.GetSpan(); Span v = this.V.GetSpan(); - int mbw = (width + 15) >> 4; - int mbh = (height + 15) >> 4; int yStride = width; int uvStride = (yStride + 1) >> 1; - var mb = new Vp8MacroBlockInfo[mbw * mbh]; - for (int i = 0; i < mb.Length; i++) - { - mb[i] = new Vp8MacroBlockInfo(); - } - var segmentInfos = new Vp8SegmentInfo[4]; for (int i = 0; i < 4; i++) { segmentInfos[i] = new Vp8SegmentInfo(); } - var it = new Vp8EncIterator(this.YTop, this.UvTop, this.Preds, this.Nz, mb, mbw, mbh); + var it = new Vp8EncIterator(this.YTop, this.UvTop, this.Preds, this.Nz, this.mbInfo, this.mbw, this.mbh); var alphas = new int[WebPConstants.MaxAlpha + 1]; - int alpha = this.MacroBlockAnalysis(width, height, it, y, u, v, yStride, uvStride, alphas, out int uvAlpha); + this.alpha = this.MacroBlockAnalysis(width, height, it, y, u, v, yStride, uvStride, alphas, out this.uvAlpha); // Analysis is done, proceed to actual coding. // TODO: EncodeAlpha(); - // Compute segment probabilities. + this.segmentHeader = new Vp8EncSegmentHeader(4); + this.AssignSegments(segmentInfos, alphas); + this.SetSegmentParams(segmentInfos); this.SetSegmentProbas(segmentInfos); - this.SetupMatrices(segmentInfos); it.Init(); it.InitFilter(); do @@ -196,7 +242,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy } while (it.Next()); - throw new NotImplementedException(); + this.bitWriter.Finish(); } /// @@ -210,6 +256,183 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy this.Preds.Dispose(); } + private void ResetBoundaryPredictions() + { + Span top = this.Preds.GetSpan(); + Span left = this.Preds.Slice(this.predsWidth - 1); + for (int i = 0; i < 4 * this.mbw; ++i) + { + top[i] = (int)IntraPredictionMode.DcPrediction; + } + + for (int i = 0; i < 4 * this.mbh; ++i) + { + left[i * this.predsWidth] = (int)IntraPredictionMode.DcPrediction; + } + + // TODO: enc->nz_[-1] = 0; // constant + } + + // Simplified k-Means, to assign Nb segments based on alpha-histogram. + private void AssignSegments(Vp8SegmentInfo[] dqm, int[] alphas) + { + int nb = (this.segmentHeader.NumSegments < NumMbSegments) ? this.segmentHeader.NumSegments : NumMbSegments; + var centers = new int[NumMbSegments]; + int weightedAverage = 0; + var map = new int[WebPConstants.MaxAlpha + 1]; + int a, n, k; + int minA; + int maxA; + int rangeA; + var accum = new int[NumMbSegments]; + var distAccum = new int[NumMbSegments]; + + // Bracket the input. + for (n = 0; n <= WebPConstants.MaxAlpha && alphas[n] == 0; ++n) { } + + minA = n; + for (n = WebPConstants.MaxAlpha; n > minA && alphas[n] == 0; --n) { } + + maxA = n; + rangeA = maxA - minA; + + // Spread initial centers evenly. + for (k = 0, n = 1; k < nb; ++k, n += 2) + { + centers[k] = minA + (n * rangeA / (2 * nb)); + } + + for (k = 0; k < MaxItersKMeans; ++k) + { + // Reset stats. + for (n = 0; n < nb; ++n) + { + accum[n] = 0; + distAccum[n] = 0; + } + + // Assign nearest center for each 'a' + n = 0; // track the nearest center for current 'a' + for (a = minA; a <= maxA; ++a) + { + if (alphas[a] != 0) + { + while (n + 1 < nb && Math.Abs(a - centers[n + 1]) < Math.Abs(a - centers[n])) + { + n++; + } + + map[a] = n; + + // Accumulate contribution into best centroid. + distAccum[n] += a * alphas[a]; + accum[n] += alphas[a]; + } + } + + // All point are classified. Move the centroids to the center of their respective cloud. + var displaced = 0; + weightedAverage = 0; + var totalWeight = 0; + for (n = 0; n < nb; ++n) + { + if (accum[n] != 0) + { + int newCenter = (distAccum[n] + (accum[n] / 2)) / accum[n]; + displaced += Math.Abs(centers[n] - newCenter); + centers[n] = newCenter; + weightedAverage += newCenter * accum[n]; + totalWeight += accum[n]; + } + } + + weightedAverage = (weightedAverage + (totalWeight / 2)) / totalWeight; + if (displaced < 5) + { + break; // no need to keep on looping... + } + } + + // Map each original value to the closest centroid + for (n = 0; n < this.mbw * this.mbh; ++n) + { + Vp8MacroBlockInfo mb = this.mbInfo[n]; + int alpha = mb.Alpha; + mb.Segment = map[alpha]; + mb.Alpha = centers[map[alpha]]; // for the record. + } + + // TODO: add possibility for SmoothSegmentMap + this.SetSegmentAlphas(dqm, centers, weightedAverage); + } + + private void SetSegmentAlphas(Vp8SegmentInfo[] dqm, int[] centers, int mid) + { + int nb = this.segmentHeader.NumSegments; + int min = centers[0], max = centers[0]; + int n; + + if (nb > 1) + { + for (n = 0; n < nb; ++n) + { + if (min > centers[n]) + { + min = centers[n]; + } + + if (max < centers[n]) + { + max = centers[n]; + } + } + } + + if (max == min) + { + max = min + 1; + } + + for (n = 0; n < nb; ++n) + { + int alpha = 255 * (centers[n] - mid) / (max - min); + int beta = 255 * (centers[n] - min) / (max - min); + dqm[n].Alpha = this.Clip(alpha, -127, 127); + dqm[n].Beta = this.Clip(beta, 0, 255); + } + } + + private void SetSegmentParams(Vp8SegmentInfo[] dqm) + { + int nb = this.segmentHeader.NumSegments; + int snsStrength = 50; // TODO: Spatial Noise Shaping, hardcoded for now. + double amp = WebPConstants.SnsToDq * snsStrength / 100.0d / 128.0d; + double Q = this.quality / 100.0d; + double cBase = this.QualityToCompression(Q); + for (int i = 0; i < nb; ++i) + { + // We modulate the base coefficient to accommodate for the quantization + // susceptibility and allow denser segments to be quantized more. + double expn = 1.0d - (amp * dqm[i].Alpha); + double c = Math.Pow(cBase, expn); + int q = (int)(127.0d * (1.0d - c)); + dqm[i].Quant = this.Clip(q, 0, 127); + } + + // uvAlpha is normally spread around ~60. The useful range is + // typically ~30 (quite bad) to ~100 (ok to decimate UV more). + // We map it to the safe maximal range of MAX/MIN_DQ_UV for dq_uv. + this.dqUvAc = (this.uvAlpha - WebPConstants.QuantEncMidAlpha) * (WebPConstants.QuantEncMaxDqUv - WebPConstants.QuantEncMinDqUv) / (WebPConstants.QuantEncMaxAlpha - WebPConstants.QuantEncMinAlpha); + + // We rescale by the user-defined strength of adaptation. + this.dqUvAc = this.dqUvAc * snsStrength / 100; + + // and make it safe. + this.dqUvAc = this.Clip(this.dqUvAc, WebPConstants.QuantEncMinDqUv, WebPConstants.QuantEncMaxDqUv); + + this.SetupMatrices(dqm); + } + private void SetSegmentProbas(Vp8SegmentInfo[] dqm) { // var p = new int[4]; @@ -220,7 +443,28 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy private void SetupMatrices(Vp8SegmentInfo[] dqm) { - // TODO: SetupMatrices + for (int i = 0; i < dqm.Length; ++i) + { + Vp8SegmentInfo m = dqm[i]; + int q = m.Quant; + + m.Y1 = new Vp8Matrix(); + m.Y2 = new Vp8Matrix(); + m.Uv = new Vp8Matrix(); + + m.Y1.Q[0] = WebPLookupTables.DcTable[this.Clip(q, 0, 127)]; + m.Y1.Q[1] = WebPLookupTables.AcTable[this.Clip(q, 0, 127)]; + + m.Y2.Q[0] = (ushort)(WebPLookupTables.DcTable[this.Clip(q, 0, 127)] * 2); + m.Y2.Q[1] = WebPLookupTables.AcTable2[this.Clip(q, 0, 127)]; + + m.Uv.Q[0] = WebPLookupTables.DcTable[this.Clip(q + this.dqUvDc, 0, 117)]; + m.Uv.Q[1] = WebPLookupTables.AcTable[this.Clip(q + this.dqUvAc, 0, 127)]; + + var qi4 = m.Y1.Expand(0); + + m.I4Penalty = 1000 * qi4 * qi4; + } } private int MacroBlockAnalysis(int width, int height, Vp8EncIterator it, Span y, Span u, Span v, int yStride, int uvStride, int[] alphas, out int uvAlpha) @@ -304,7 +548,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy int lambdaDi16 = 106; int lambdaDi4 = 11; int lambdaDuv = 120; - long scoreI4 = 676000; // TODO: hardcoded for now: long scoreI4 = dqm->i4_penalty_; + long scoreI4 = dqm.I4Penalty; long i4BitSum = 0; long bitLimit = tryBothModes ? this.MbHeaderLimit @@ -387,7 +631,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy { // Reconstruct partial block inside yuv_out2 buffer Span tmpDst = it.YuvOut2.AsSpan(Vp8EncIterator.YOffEnc + WebPLookupTables.Vp8Scan[it.I4]); - nz |= this.ReconstructIntra4(it, dqm, rd.YAcLevels[it.I4], src, tmpDst, bestI4Mode) << it.I4; + nz |= this.ReconstructIntra4(it, dqm, rd.YAcLevels.AsSpan(it.I4, 16), src, tmpDst, bestI4Mode) << it.I4; } } while (it.RotateI4(it.YuvOut2.AsSpan(Vp8EncIterator.YOffEnc))); @@ -402,7 +646,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy } else { - nz = this.ReconstructIntra16(it, dqm, rd, it.YuvOut.AsSpan(Vp8EncIterator.YOffEnc), it.Preds.GetSpan()[0]); + nz = this.ReconstructIntra16(it, dqm, rd, it.YuvOut.AsSpan(Vp8EncIterator.YOffEnc), it.Preds.Slice(it.PredIdx)[0]); } // ... and UV! @@ -460,7 +704,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy for (x = 0; x < 4; ++x) { int ctx = it.TopNz[x] + it.LeftNz[y]; - residual.SetCoeffs(rd.YAcLevels[x + (y * 4)]); + residual.SetCoeffs(rd.YAcLevels.AsSpan(x + (y * 4), 16)); int res = this.bitWriter.PutCoeffs(ctx, residual); it.TopNz[x] = it.LeftNz[y] = res; } @@ -477,7 +721,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy for (x = 0; x < 2; ++x) { int ctx = it.TopNz[4 + ch + x] + it.LeftNz[4 + ch + y]; - residual.SetCoeffs(rd.UvLevels[(ch * 2) + x + (y * 2)]); + residual.SetCoeffs(rd.UvLevels.AsSpan((ch * 2) + x + (y * 2), 16)); var res = this.bitWriter.PutCoeffs(ctx, residual); it.TopNz[4 + ch + x] = it.LeftNz[4 + ch + y] = res; } @@ -499,39 +743,36 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy int nz = 0; int n; var dcTmp = new short[16]; - var tmp = new short[16][]; - for (int i = 0; i < 16; i++) - { - tmp[i] = new short[16]; - } + var tmp = new short[16 * 16]; + Span tmpSpan = tmp.AsSpan(); for (n = 0; n < 16; n += 2) { - this.FTransform2(src.Slice(WebPLookupTables.Vp8Scan[n]), reference.Slice(WebPLookupTables.Vp8Scan[n]), tmp[n]); + this.FTransform2(src.Slice(WebPLookupTables.Vp8Scan[n]), reference.Slice(WebPLookupTables.Vp8Scan[n]), tmpSpan.Slice(n * 16, 16), tmpSpan.Slice((n + 1) * 16, 16)); } - this.FTransformWht(tmp[0], dcTmp); + this.FTransformWht(tmp.AsSpan(0), dcTmp); nz |= this.QuantizeBlock(dcTmp, rd.YDcLevels, dqm.Y2) << 24; for (n = 0; n < 16; n += 2) { // Zero-out the first coeff, so that: a) nz is correct below, and // b) finding 'last' non-zero coeffs in SetResidualCoeffs() is simplified. - tmp[n][0] = tmp[n + 1][0] = 0; - nz |= this.Quantize2Blocks(tmp[n], rd.YAcLevels[n], dqm.Y1) << n; + tmp[n * 16] = tmp[(n + 1) * 16] = 0; + nz |= this.Quantize2Blocks(tmpSpan.Slice(n * 16), rd.YAcLevels.AsSpan(n, 32), dqm.Y1) << n; } // Transform back. - LossyUtils.TransformWht(dcTmp, tmp[0]); + LossyUtils.TransformWht(dcTmp, tmpSpan); for (n = 0; n < 16; n += 2) { - this.ITransform(reference.Slice(WebPLookupTables.Vp8Scan[n]), tmp[n], yuvOut.Slice(WebPLookupTables.Vp8Scan[n]), true); + this.ITransform(reference.Slice(WebPLookupTables.Vp8Scan[n]), tmpSpan.Slice(n * 16, 32), yuvOut.Slice(WebPLookupTables.Vp8Scan[n]), true); } return nz; } - private int ReconstructIntra4(Vp8EncIterator it, Vp8SegmentInfo dqm, short[] levels, Span src, Span yuvOut, int mode) + private int ReconstructIntra4(Vp8EncIterator it, Vp8SegmentInfo dqm, Span levels, Span src, Span yuvOut, int mode) { Span reference = it.YuvP.AsSpan(Vp8EncIterator.Vp8I4ModeOffsets[mode]); var tmp = new short[16]; @@ -548,15 +789,14 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy Span src = it.YuvIn.AsSpan(Vp8EncIterator.YOffEnc); int nz = 0; int n; - var tmp = new short[8][]; - for (int i = 0; i < 8; i++) - { - tmp[i] = new short[16]; - } + var tmp = new short[8* 16]; for (n = 0; n < 8; n += 2) { - this.FTransform2(src.Slice(WebPLookupTables.Vp8ScanUv[n]), reference.Slice(WebPLookupTables.Vp8ScanUv[n]), tmp[n]); + this.FTransform2(src.Slice(WebPLookupTables.Vp8ScanUv[n]), + reference.Slice(WebPLookupTables.Vp8ScanUv[n]), + tmp.AsSpan(n * 16, 16), + tmp.AsSpan((n + 1) * 16, 16)); } /* TODO: @@ -567,33 +807,35 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy for (n = 0; n < 8; n += 2) { - nz |= this.Quantize2Blocks(tmp[n], rd.UvLevels[n], dqm.Uv) << n; + nz |= this.Quantize2Blocks(tmp.AsSpan(n * 16, 32), rd.UvLevels.AsSpan(n, 32), dqm.Uv) << n; } for (n = 0; n < 8; n += 2) { - this.ITransform(reference.Slice(WebPLookupTables.Vp8ScanUv[n]), tmp[n], yuvOut.Slice(WebPLookupTables.Vp8ScanUv[n]), true); + this.ITransform(reference.Slice(WebPLookupTables.Vp8ScanUv[n]), tmp.AsSpan(n * 16, 32), yuvOut.Slice(WebPLookupTables.Vp8ScanUv[n]), true); } return nz << 16; } - private void FTransform2(Span src, Span reference, short[] output) + private void FTransform2(Span src, Span reference, Span output, Span output2) { this.FTransform(src, reference, output); - this.FTransform(src.Slice(4), reference.Slice(4), output.AsSpan(16)); + this.FTransform(src.Slice(4), reference.Slice(4), output2); } private void FTransform(Span src, Span reference, Span output) { int i; var tmp = new int[16]; + int srcIdx = 0; + int refIdx = 0; for (i = 0; i < 4; ++i) { - int d0 = src[0] - reference[0]; // 9bit dynamic range ([-255,255]) - int d1 = src[1] - reference[1]; - int d2 = src[2] - reference[2]; - int d3 = src[3] - reference[3]; + int d0 = src[srcIdx] - reference[refIdx]; // 9bit dynamic range ([-255,255]) + int d1 = src[srcIdx + 1] - reference[refIdx + 1]; + int d2 = src[srcIdx + 2] - reference[refIdx + 2]; + int d3 = src[srcIdx + 3] - reference[refIdx + 3]; int a0 = d0 + d3; // 10b [-510,510] int a1 = d1 + d2; int a2 = d1 - d2; @@ -603,8 +845,8 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy tmp[2 + (i * 4)] = (a0 - a1) * 8; tmp[3 + (i * 4)] = ((a3 * 2217) - (a2 * 5352) + 937) >> 9; - src = src.Slice(WebPConstants.Bps); - reference = reference.Slice(WebPConstants.Bps); + srcIdx += WebPConstants.Bps; + refIdx += WebPConstants.Bps; } for (i = 0; i < 4; ++i) @@ -624,18 +866,19 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy { var tmp = new int[16]; int i; + int inputIdx = 0; for (i = 0; i < 4; ++i) { - int a0 = input[0 * 16] + input[2 * 16]; // 13b - int a1 = input[1 * 16] + input[3 * 16]; - int a2 = input[1 * 16] - input[3 * 16]; - int a3 = input[0 * 16] - input[2 * 16]; + int a0 = input[inputIdx + (0 * 16)] + input[inputIdx + (2 * 16)]; // 13b + int a1 = input[inputIdx + (1 * 16)] + input[inputIdx + (3 * 16)]; + int a2 = input[inputIdx + (1 * 16)] - input[inputIdx + (3 * 16)]; + int a3 = input[inputIdx + (0 * 16)] - input[inputIdx + (2 * 16)]; tmp[0 + (i * 4)] = a0 + a1; // 14b tmp[1 + (i * 4)] = a3 + a2; tmp[2 + (i * 4)] = a3 - a2; tmp[3 + (i * 4)] = a0 - a1; - input = input.Slice(64); + inputIdx += 64; } for (i = 0; i < 4; ++i) @@ -705,12 +948,12 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy return (last >= 0) ? 1 : 0; } - private void ITransform(Span reference, short[] input, Span dst, bool doTwo) + private void ITransform(Span reference, Span input, Span dst, bool doTwo) { this.ITransformOne(reference, input, dst); if (doTwo) { - this.ITransformOne(reference.Slice(4), input.AsSpan(16), dst.Slice(4)); + this.ITransformOne(reference.Slice(4), input.Slice(16), dst.Slice(4)); } } @@ -1096,8 +1339,8 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy Span vSpan = BitConverter.GetBytes(v).AsSpan(); for (int i = 0; i < 16; ++i) { - if (src.Slice(0, 4).SequenceEqual(vSpan) || src.Slice(4, 4).SequenceEqual(vSpan) || - src.Slice(0, 8).SequenceEqual(vSpan) || src.Slice(12, 4).SequenceEqual(vSpan)) + if (!src.Slice(0, 4).SequenceEqual(vSpan) || !src.Slice(4, 4).SequenceEqual(vSpan) || + !src.Slice(8, 4).SequenceEqual(vSpan) || !src.Slice(12, 4).SequenceEqual(vSpan)) { return false; } @@ -1108,10 +1351,30 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy return true; } + /// + /// We want to emulate jpeg-like behaviour where the expected "good" quality + /// is around q=75. Internally, our "good" middle is around c=50. So we + /// map accordingly using linear piece-wise function + /// + private double QualityToCompression(double c) + { + double linearC = (c < 0.75) ? c * (2.0d / 3.0d) : (2.0d * c) - 1.0d; + + // The file size roughly scales as pow(quantizer, 3.). Actually, the + // exponent is somewhere between 2.8 and 3.2, but we're mostly interested + // in the mid-quant range. So we scale the compressibility inversely to + // this power-law: quant ~= compression ^ 1/3. This law holds well for + // low quant. Finer modeling for high-quant would make use of AcTable[] + // more explicitly. + double v = Math.Pow(linearC, 1 / 3.0d); + + return v; + } + [MethodImpl(InliningOptions.ShortMethod)] private int QuantDiv(uint n, uint iQ, uint b) { - return (int)(((n * iQ) + b) >> QFix); + return (int)(((n * iQ) + b) >> WebPConstants.QFix); } [MethodImpl(InliningOptions.ShortMethod)] diff --git a/src/ImageSharp/Formats/WebP/Lossy/Vp8Matrix.cs b/src/ImageSharp/Formats/WebP/Lossy/Vp8Matrix.cs index 2ee9671ed..9c7711352 100644 --- a/src/ImageSharp/Formats/WebP/Lossy/Vp8Matrix.cs +++ b/src/ImageSharp/Formats/WebP/Lossy/Vp8Matrix.cs @@ -5,13 +5,30 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy { internal class Vp8Matrix { + private static readonly int[][] BiasMatrices = + { + // [luma-ac,luma-dc,chroma][dc,ac] + new[] { 96, 110 }, + new[] { 96, 108 }, + new[] { 110, 115 } + }; + + // Sharpening by (slightly) raising the hi-frequency coeffs. + // Hack-ish but helpful for mid-bitrate range. Use with care. + private static readonly byte[] FreqSharpening = { 0, 30, 60, 90, 30, 60, 90, 90, 60, 90, 90, 90, 90, 90, 90, 90 }; + + /// + /// Number of descaling bits for sharpening bias. + /// + private const int SharpenBits = 11; + /// /// Initializes a new instance of the class. /// public Vp8Matrix() { - this.Q = new short[16]; - this.IQ = new short[16]; + this.Q = new ushort[16]; + this.IQ = new ushort[16]; this.Bias = new uint[16]; this.ZThresh = new uint[16]; this.Sharpen = new short[16]; @@ -20,12 +37,12 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy /// /// Gets the quantizer steps. /// - public short[] Q { get; } + public ushort[] Q { get; } /// /// Gets the reciprocals, fixed point. /// - public short[] IQ { get; } + public ushort[] IQ { get; } /// /// Gets the rounding bias. @@ -41,5 +58,57 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy /// Gets the frequency boosters for slight sharpening. /// public short[] Sharpen { get; } + + /// + /// Returns the average quantizer. + /// + /// The average quantizer. + public int Expand(int type) + { + int sum; + int i; + for (i = 0; i < 2; ++i) + { + int isAcCoeff = (i > 0) ? 1 : 0; + int bias = BiasMatrices[type][isAcCoeff]; + this.IQ[i] = (ushort)((1 << WebPConstants.QFix) / this.Q[i]); + this.Bias[i] = (uint)this.BIAS(bias); + + // zthresh_ is the exact value such that QUANTDIV(coeff, iQ, B) is: + // * zero if coeff <= zthresh + // * non-zero if coeff > zthresh + this.ZThresh[i] = (uint)(((1 << WebPConstants.QFix) - 1 - this.Bias[i]) / this.IQ[i]); + } + + for (i = 2; i < 16; ++i) + { + this.Q[i] = this.Q[1]; + this.IQ[i] = this.IQ[1]; + this.Bias[i] = this.Bias[1]; + this.ZThresh[i] = this.ZThresh[1]; + } + + for (sum = 0, i = 0; i < 16; ++i) + { + if (type == 0) + { + // We only use sharpening for AC luma coeffs. + this.Sharpen[i] = (short)((FreqSharpening[i] * this.Q[i]) >> SharpenBits); + } + else + { + this.Sharpen[i] = 0; + } + + sum += this.Q[i]; + } + + return (sum + 8) >> 4; + } + + private int BIAS(int b) + { + return b << (WebPConstants.QFix - 8); + } } } diff --git a/src/ImageSharp/Formats/WebP/Lossy/Vp8ModeScore.cs b/src/ImageSharp/Formats/WebP/Lossy/Vp8ModeScore.cs index 7ec02fac6..f9316e40b 100644 --- a/src/ImageSharp/Formats/WebP/Lossy/Vp8ModeScore.cs +++ b/src/ImageSharp/Formats/WebP/Lossy/Vp8ModeScore.cs @@ -16,17 +16,8 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy public Vp8ModeScore() { this.YDcLevels = new short[16]; - this.YAcLevels = new short[16][]; - for (int i = 0; i < 16; i++) - { - this.YAcLevels[i] = new short[16]; - } - - this.UvLevels = new short[4 + 4][]; - for (int i = 0; i < 8; i++) - { - this.UvLevels[i] = new short[16]; - } + this.YAcLevels = new short[16 * 16]; + this.UvLevels = new short[4 + (4 * 16)]; this.ModesI4 = new byte[16]; } @@ -64,12 +55,12 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy /// /// Gets the quantized levels for luma-AC. /// - public short[][] YAcLevels { get; } + public short[] YAcLevels { get; } /// /// Gets the quantized levels for chroma. /// - public short[][] UvLevels { get; } + public short[] UvLevels { get; } /// /// Gets or sets the mode number for intra16 prediction. diff --git a/src/ImageSharp/Formats/WebP/Lossy/Vp8Residual.cs b/src/ImageSharp/Formats/WebP/Lossy/Vp8Residual.cs index 58e60a76c..ccc4471ee 100644 --- a/src/ImageSharp/Formats/WebP/Lossy/Vp8Residual.cs +++ b/src/ImageSharp/Formats/WebP/Lossy/Vp8Residual.cs @@ -43,7 +43,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy // res->costs = enc->proba_.remapped_costs_[coeff_type]; } - public void SetCoeffs(short[] coeffs) + public void SetCoeffs(Span coeffs) { int n; this.Last = -1; @@ -56,7 +56,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy } } - this.Coeffs = coeffs; + this.Coeffs = coeffs.ToArray(); } } } diff --git a/src/ImageSharp/Formats/WebP/WebPConstants.cs b/src/ImageSharp/Formats/WebP/WebPConstants.cs index 9a2b3ee8b..6721b2da1 100644 --- a/src/ImageSharp/Formats/WebP/WebPConstants.cs +++ b/src/ImageSharp/Formats/WebP/WebPConstants.cs @@ -205,10 +205,42 @@ namespace SixLabors.ImageSharp.Formats.WebP public const int AlphaFix = 19; + /// + /// 8b of precision for susceptibilities. + /// public const int MaxAlpha = 255; + /// + /// Scaling factor for alpha. + /// public const int AlphaScale = 2 * MaxAlpha; + /// + /// Neutral value for susceptibility. + /// + public const int QuantEncMidAlpha = 64; + + /// + /// Lowest usable value for susceptibility. + /// + public const int QuantEncMinAlpha = 30; + + /// + /// Higher meaningful value for susceptibility. + /// + public const int QuantEncMaxAlpha = 100; + + /// + /// Scaling constant between the sns (Spatial Noise Shaping) value and the QP power-law modulation. Must be strictly less than 1. + /// + public const double SnsToDq = 0.9; + + public const int QuantEncMaxDqUv = 6; + + public const int QuantEncMinDqUv = -4; + + public const int QFix = 17; + public static readonly short[] Vp8FixedCostsUv = { 302, 984, 439, 642 }; public static readonly short[] Vp8FixedCostsI16 = { 663, 919, 872, 919 }; diff --git a/src/ImageSharp/Formats/WebP/WebPLookupTables.cs b/src/ImageSharp/Formats/WebP/WebPLookupTables.cs index b23cdd323..6474d812d 100644 --- a/src/ImageSharp/Formats/WebP/WebPLookupTables.cs +++ b/src/ImageSharp/Formats/WebP/WebPLookupTables.cs @@ -334,7 +334,7 @@ namespace SixLabors.ImageSharp.Formats.WebP }; // Paragraph 14.1 - public static readonly int[] DcTable = + public static readonly byte[] DcTable = { 4, 5, 6, 7, 8, 9, 10, 10, 11, 12, 13, 14, 15, 16, 17, 17, @@ -355,7 +355,7 @@ namespace SixLabors.ImageSharp.Formats.WebP }; // Paragraph 14.1 - public static readonly int[] AcTable = + public static readonly ushort[] AcTable = { 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, @@ -375,6 +375,26 @@ namespace SixLabors.ImageSharp.Formats.WebP 249, 254, 259, 264, 269, 274, 279, 284 }; + public static readonly ushort[] AcTable2 = + { + 8, 8, 9, 10, 12, 13, 15, 17, + 18, 20, 21, 23, 24, 26, 27, 29, + 31, 32, 34, 35, 37, 38, 40, 41, + 43, 44, 46, 48, 49, 51, 52, 54, + 55, 57, 58, 60, 62, 63, 65, 66, + 68, 69, 71, 72, 74, 75, 77, 79, + 80, 82, 83, 85, 86, 88, 89, 93, + 96, 99, 102, 105, 108, 111, 114, 117, + 120, 124, 127, 130, 133, 136, 139, 142, + 145, 148, 151, 155, 158, 161, 164, 167, + 170, 173, 176, 179, 184, 189, 193, 198, + 203, 207, 212, 217, 221, 226, 230, 235, + 240, 244, 249, 254, 258, 263, 268, 274, + 280, 286, 292, 299, 305, 311, 317, 323, + 330, 336, 342, 348, 354, 362, 370, 379, + 385, 393, 401, 409, 416, 424, 432, 440 + }; + // Paragraph 13 public static readonly byte[,,,] CoeffsUpdateProba = {