From dd7032c69355d895cd8ba9a83c4c545044a07b80 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 10 Nov 2020 19:59:40 +0100 Subject: [PATCH] CorrectDCValues --- .../Formats/WebP/BitWriter/Vp8BitWriter.cs | 2 +- .../Formats/WebP/Lossy/Vp8EncIterator.cs | 28 +++++-- .../Formats/WebP/Lossy/Vp8Encoder.cs | 84 ++++++++++++++++--- tests/ImageSharp.Tests/TestImages.cs | 3 + tests/Images/Input/WebP/peak.png | 3 + 5 files changed, 103 insertions(+), 17 deletions(-) create mode 100644 tests/Images/Input/WebP/peak.png diff --git a/src/ImageSharp/Formats/WebP/BitWriter/Vp8BitWriter.cs b/src/ImageSharp/Formats/WebP/BitWriter/Vp8BitWriter.cs index 598a23e552..813fbb7bca 100644 --- a/src/ImageSharp/Formats/WebP/BitWriter/Vp8BitWriter.cs +++ b/src/ImageSharp/Formats/WebP/BitWriter/Vp8BitWriter.cs @@ -564,7 +564,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.BitWriter // Writes the partition #0 modes (that is: all intra modes) private void CodeIntraModes(Vp8BitWriter bitWriter) { - var it = new Vp8EncIterator(this.enc.YTop, this.enc.UvTop, this.enc.Nz, this.enc.MbInfo, this.enc.Preds, this.enc.Mbw, this.enc.Mbh); + var it = new Vp8EncIterator(this.enc.YTop, this.enc.UvTop, this.enc.Nz, this.enc.MbInfo, this.enc.Preds, this.enc.TopDerr, this.enc.Mbw, this.enc.Mbh); int predsWidth = this.enc.PredsWidth; do diff --git a/src/ImageSharp/Formats/WebP/Lossy/Vp8EncIterator.cs b/src/ImageSharp/Formats/WebP/Lossy/Vp8EncIterator.cs index 649b1705a0..7ddc2edd08 100644 --- a/src/ImageSharp/Formats/WebP/Lossy/Vp8EncIterator.cs +++ b/src/ImageSharp/Formats/WebP/Lossy/Vp8EncIterator.cs @@ -97,19 +97,21 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy private int uvTopIdx; - public Vp8EncIterator(byte[] yTop, byte[] uvTop, uint[] nz, Vp8MacroBlockInfo[] mb, byte[] preds, int mbw, int mbh) + public Vp8EncIterator(byte[] yTop, byte[] uvTop, uint[] nz, Vp8MacroBlockInfo[] mb, byte[] preds, sbyte[] topDerr, int mbw, int mbh) { + this.YTop = yTop; + this.UvTop = uvTop; + this.Nz = nz; + this.Mb = mb; + this.Preds = preds; + this.TopDerr = topDerr; + this.LeftDerr = new sbyte[2 * 2]; this.mbw = mbw; this.mbh = mbh; - this.Mb = mb; this.currentMbIdx = 0; this.nzIdx = 1; this.yTopIdx = 0; this.uvTopIdx = 0; - this.YTop = yTop; - this.UvTop = uvTop; - this.Nz = nz; - this.Preds = preds; this.predsWidth = (4 * mbw) + 1; this.predIdx = this.predsWidth; this.YuvIn = new byte[WebPConstants.Bps * 16]; @@ -180,6 +182,11 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy /// public byte[] UvLeft { get; } + /// + /// Gets the left error diffusion (u/v). + /// + public sbyte[] LeftDerr { get; } + /// /// Gets the top luma samples at position 'X'. /// @@ -211,6 +218,11 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy /// public uint[] Nz { get; } + /// + /// Gets the diffusion error. + /// + public sbyte[] TopDerr { get; } + /// /// Gets 32+5 boundary samples needed by intra4x4. /// @@ -1284,6 +1296,8 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy vLeft.Slice(1, 8).Fill(129); this.LeftNz[8] = 0; + + this.LeftDerr.AsSpan().Fill(0); } private void InitTop() @@ -1297,6 +1311,8 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy int predsH = (4 * this.mbh) + 1; int predsSize = predsW * predsH; this.Preds.AsSpan(predsSize + this.predsWidth, this.mbw).Fill(0); + + this.TopDerr.AsSpan().Fill(0); } private int Bit(uint nz, int n) diff --git a/src/ImageSharp/Formats/WebP/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/WebP/Lossy/Vp8Encoder.cs index e92b2fb0aa..3853e56e46 100644 --- a/src/ImageSharp/Formats/WebP/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/WebP/Lossy/Vp8Encoder.cs @@ -4,7 +4,6 @@ using System; using System.Buffers; using System.IO; -using System.Linq; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.WebP.BitWriter; @@ -142,6 +141,13 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy // TODO: filterStrength is hardcoded, should be configurable. private const int FilterStrength = 60; + // Diffusion weights. We under-correct a bit (15/16th of the error is actually + // diffused) to avoid 'rainbow' chessboard pattern of blocks at q~=0. + private const int C1 = 7; // fraction of error sent to the 4x4 block below + private const int C2 = 8; // fraction of error sent to the 4x4 block on the right + private const int DSHIFT = 4; + private const int DSCALE = 1; // storage descaling, needed to make the error fit byte + /// /// Initializes a new instance of the class. /// @@ -175,7 +181,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy this.UvTop = new byte[this.mbw * 16 * 2]; this.Nz = new uint[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.TopDerr = new sbyte[this.mbw * 4]; // TODO: make partition_limit configurable? int limit = 100; // original code: limit = 100 - config->partition_limit; @@ -196,6 +202,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy } this.filterHeader = new Vp8FilterHeader(); + int predSize = (((4 * this.mbw) + 1) * ((4 * this.mbh) + 1)) + this.predsWidth + 1; this.proba = new Vp8EncProba(); this.Preds = new byte[predSize * 2]; // TODO: figure out how much mem we need here. This is too much. this.predsWidth = (4 * this.mbw) + 1; @@ -340,6 +347,11 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy /// public byte[] Preds { get; } + /// + /// Gets the diffusion error. + /// + public sbyte[] TopDerr { get; } + /// /// Gets a rough limit for header bits per MB. /// @@ -364,7 +376,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy int yStride = width; int uvStride = (yStride + 1) >> 1; - var it = new Vp8EncIterator(this.YTop, this.UvTop, this.Nz, this.mbInfo, this.Preds, this.mbw, this.mbh); + var it = new Vp8EncIterator(this.YTop, this.UvTop, this.Nz, this.mbInfo, this.Preds, this.TopDerr, this.mbw, this.mbh); var alphas = new int[WebPConstants.MaxAlpha + 1]; this.alpha = this.MacroBlockAnalysis(width, height, it, y, u, v, yStride, uvStride, alphas, out this.uvAlpha); @@ -487,7 +499,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy this.proba.FinalizeTokenProbas(); } - this.proba.CalculateLevelCosts(); // finalize costs + this.proba.CalculateLevelCosts(); // Finalize costs. } private long OneStatPass(int width, int height, int yStride, int uvStride, Vp8RdLevel rdOpt, int nbMbs, PassStats stats) @@ -495,7 +507,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy Span y = this.Y.GetSpan(); Span u = this.U.GetSpan(); Span v = this.V.GetSpan(); - var it = new Vp8EncIterator(this.YTop, this.UvTop, this.Nz, this.mbInfo, this.Preds, this.Mbw, this.Mbh); + var it = new Vp8EncIterator(this.YTop, this.UvTop, this.Nz, this.mbInfo, this.Preds, this.TopDerr, this.Mbw, this.Mbh); long size = 0; long sizeP0 = 0; long distortion = 0; @@ -1277,11 +1289,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy tmp.AsSpan((n + 1) * 16, 16)); } - /* TODO: - if (it->top_derr_ != NULL) - { - CorrectDCValues(it, &dqm->uv_, tmp, rd); - }*/ + this.CorrectDCValues(it, dqm.Uv, tmp, rd); for (n = 0; n < 8; n += 2) { @@ -1340,6 +1348,62 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy } } + private void CorrectDCValues(Vp8EncIterator it, Vp8Matrix mtx, short[] tmp, Vp8ModeScore rd) + { +#pragma warning disable SA1005 // Single line comments should begin with single space + // | top[0] | top[1] + // --------+--------+--------- + // left[0] | tmp[0] tmp[1] <-> err0 err1 + // left[1] | tmp[2] tmp[3] err2 err3 + // + // Final errors {err1,err2,err3} are preserved and later restored + // as top[]/left[] on the next block. +#pragma warning restore SA1005 // Single line comments should begin with single space + for (int ch = 0; ch <= 1; ++ch) + { + Span top = it.TopDerr.AsSpan((it.X * 4) + ch, 2); + Span left = it.LeftDerr.AsSpan(ch, 2); + int err0, err1, err2, err3; + Span c = tmp.AsSpan(ch * 4 * 16, 4 * 16); + c[0] += (short)(((C1 * top[0]) + (C2 * left[0])) >> (DSHIFT - DSCALE)); + err0 = QuantizeSingle(c, mtx); + c[1 * 16] += (short)(((C1 * top[1]) + (C2 * err0)) >> (DSHIFT - DSCALE)); + err1 = QuantizeSingle(c.Slice(1 * 16), mtx); + c[2 * 16] += (short)(((C1 * err0) + (C2 * left[1])) >> (DSHIFT - DSCALE)); + err2 = QuantizeSingle(c.Slice(2 * 16), mtx); + c[3 * 16] += (short)(((C1 * err1) + (C2 * err2)) >> (DSHIFT - DSCALE)); + err3 = QuantizeSingle(c.Slice(3 * 16), mtx); + + // TODO: set errors in rd + // rd->derr[ch][0] = (int8_t)err1; + // rd->derr[ch][1] = (int8_t)err2; + // rd->derr[ch][2] = (int8_t)err3; + } + } + + // Quantize as usual, but also compute and return the quantization error. + // Error is already divided by DSHIFT. + private static int QuantizeSingle(Span v, Vp8Matrix mtx) + { + int v0 = v[0]; + bool sign = v0 < 0; + if (sign) + { + v0 = -v0; + } + + if (v0 > (int)mtx.ZThresh[0]) + { + int qV = QuantDiv((uint)v0, mtx.IQ[0], mtx.Bias[0]) * mtx.Q[0]; + int err = v0 - qV; + v[0] = (short)(sign ? -qV : qV); + return (sign ? -err : err) >> DSCALE; + } + + v[0] = 0; + return (sign ? -v0 : v0) >> DSCALE; + } + private void FTransformWht(Span input, Span output) { var tmp = new int[16]; diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 3f54d206c4..67ef6cef35 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -497,6 +497,9 @@ namespace SixLabors.ImageSharp.Tests public static class WebP { + // Reference image as png + public const string Peak = "WebP/Peak.png"; + public static class Animated { public const string Animated1 = "WebP/animated-webp.webp"; diff --git a/tests/Images/Input/WebP/peak.png b/tests/Images/Input/WebP/peak.png new file mode 100644 index 0000000000..5a417b9c0a --- /dev/null +++ b/tests/Images/Input/WebP/peak.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b9b56ed5c1278664222c77f9a452b824b4f9215c819502b3f6b0e0d44270e7e7 +size 26456