diff --git a/src/ImageSharp/Formats/WebP/Lossy/LossyUtils.cs b/src/ImageSharp/Formats/WebP/Lossy/LossyUtils.cs index 350f6d00dc..a03d2c5c55 100644 --- a/src/ImageSharp/Formats/WebP/Lossy/LossyUtils.cs +++ b/src/ImageSharp/Formats/WebP/Lossy/LossyUtils.cs @@ -10,6 +10,76 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy { internal static class LossyUtils { + [MethodImpl(InliningOptions.ShortMethod)] + public static int Vp8Sse16X16(Span a, Span b) => GetSse(a, b, 16, 16); + + [MethodImpl(InliningOptions.ShortMethod)] + public static int Vp8Sse16X8(Span a, Span b) => GetSse(a, b, 16, 8); + + [MethodImpl(InliningOptions.ShortMethod)] + public static int Vp8Sse4X4(Span a, Span b) => GetSse(a, b, 4, 4); + + [MethodImpl(InliningOptions.ShortMethod)] + public static int GetSse(Span a, Span b, int w, int h) + { + int count = 0; + int aOffset = 0; + int bOffset = 0; + for (int y = 0; y < h; ++y) + { + for (int x = 0; x < w; ++x) + { + int diff = a[aOffset + x] - b[bOffset + x]; + count += diff * diff; + } + + aOffset += WebpConstants.Bps; + bOffset += WebpConstants.Bps; + } + + return count; + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static void Vp8Copy4X4(Span src, Span dst) => Copy(src, dst, 4, 4); + + [MethodImpl(InliningOptions.ShortMethod)] + public static void Vp8Copy16X8(Span src, Span dst) => Copy(src, dst, 16, 8); + + [MethodImpl(InliningOptions.ShortMethod)] + public static void Copy(Span src, Span dst, int w, int h) + { + for (int y = 0; y < h; ++y) + { + src.Slice(0, w).CopyTo(dst); + src = src.Slice(WebpConstants.Bps); + dst = dst.Slice(WebpConstants.Bps); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static int Vp8Disto16X16(Span a, Span b, Span w) + { + int d = 0; + for (int y = 0; y < 16 * WebpConstants.Bps; y += 4 * WebpConstants.Bps) + { + for (int x = 0; x < 16; x += 4) + { + d += Vp8Disto4X4(a.Slice(x + y), b.Slice(x + y), w); + } + } + + return d; + } + + [MethodImpl(InliningOptions.ShortMethod)] + public static int Vp8Disto4X4(Span a, Span b, Span w) + { + int sum1 = TTransform(a, w); + int sum2 = TTransform(b, w); + return Math.Abs(sum2 - sum1) >> 5; + } + public static void DC16(Span dst, Span yuv, int offset) { int offsetMinus1 = offset - 1; diff --git a/src/ImageSharp/Formats/WebP/Lossy/QuantEnc.cs b/src/ImageSharp/Formats/WebP/Lossy/QuantEnc.cs index 5763e79a93..fda44f7e0e 100644 --- a/src/ImageSharp/Formats/WebP/Lossy/QuantEnc.cs +++ b/src/ImageSharp/Formats/WebP/Lossy/QuantEnc.cs @@ -13,6 +13,8 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy { private static readonly byte[] Zigzag = { 0, 1, 4, 8, 5, 2, 3, 6, 9, 12, 13, 10, 7, 11, 14, 15 }; + private static readonly ushort[] WeightY = { 38, 32, 20, 9, 32, 28, 17, 7, 20, 17, 10, 4, 9, 7, 4, 2 }; + private const int MaxLevel = 2047; // Diffusion weights. We under-correct a bit (15/16th of the error is actually @@ -22,10 +24,460 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy private const int DSHIFT = 4; private const int DSCALE = 1; // storage descaling, needed to make the error fit byte + public static void PickBestIntra16(Vp8EncIterator it, ref Vp8ModeScore rd, Vp8SegmentInfo[] segmentInfos, Vp8EncProba proba) + { + const int numBlocks = 16; + Vp8SegmentInfo dqm = segmentInfos[it.CurrentMacroBlockInfo.Segment]; + int lambda = dqm.LambdaI16; + int tlambda = dqm.TLambda; + Span src = it.YuvIn.AsSpan(Vp8EncIterator.YOffEnc); + var rdTmp = new Vp8ModeScore(); + Vp8ModeScore rdCur = rdTmp; + Vp8ModeScore rdBest = rd; + int mode; + bool isFlat = IsFlatSource16(src); + rd.ModeI16 = -1; + for (mode = 0; mode < WebpConstants.NumPredModes; ++mode) + { + // scratch buffer. + Span tmpDst = it.YuvOut2.AsSpan(Vp8EncIterator.YOffEnc); + rdCur.ModeI16 = mode; + + // Reconstruct. + rdCur.Nz = (uint)ReconstructIntra16(it, dqm, rdCur, tmpDst, mode); + + // Measure RD-score. + rdCur.D = LossyUtils.Vp8Sse16X16(src, tmpDst); + rdCur.SD = tlambda != 0 ? Mult8B(tlambda, LossyUtils.Vp8Disto16X16(src, tmpDst, WeightY)) : 0; + rdCur.H = WebpConstants.Vp8FixedCostsI16[mode]; + rdCur.R = it.GetCostLuma16(rdCur, proba); + + if (isFlat) + { + // Refine the first impression (which was in pixel space). + isFlat = IsFlat(rdCur.YAcLevels, numBlocks, WebpConstants.FlatnessLimitI16); + if (isFlat) + { + // Block is very flat. We put emphasis on the distortion being very low! + rdCur.D *= 2; + rdCur.SD *= 2; + } + } + + // Since we always examine Intra16 first, we can overwrite *rd directly. + rdCur.SetRdScore(lambda); + + if (mode == 0 || rdCur.Score < rdBest.Score) + { + Vp8ModeScore tmp = rdCur; + rdCur = rdBest; + rdBest = tmp; + it.SwapOut(); + } + } + + if (rdBest != rd) + { + rd = rdBest; + } + + // Finalize score for mode decision. + rd.SetRdScore(dqm.LambdaMode); + it.SetIntra16Mode(rd.ModeI16); + + // We have a blocky macroblock (only DCs are non-zero) with fairly high + // distortion, record max delta so we can later adjust the minimal filtering + // strength needed to smooth these blocks out. + if ((rd.Nz & 0x100ffff) == 0x1000000 && rd.D > dqm.MinDisto) + { + dqm.StoreMaxDelta(rd.YDcLevels); + } + } + + public static bool PickBestIntra4(Vp8EncIterator it, ref Vp8ModeScore rd, Vp8SegmentInfo[] segmentInfos, Vp8EncProba proba, int maxI4HeaderBits) + { + Vp8SegmentInfo dqm = segmentInfos[it.CurrentMacroBlockInfo.Segment]; + int lambda = dqm.LambdaI4; + int tlambda = dqm.TLambda; + Span src0 = it.YuvIn.AsSpan(Vp8EncIterator.YOffEnc); + Span bestBlocks = it.YuvOut2.AsSpan(Vp8EncIterator.YOffEnc); + int totalHeaderBits = 0; + var rdBest = new Vp8ModeScore(); + + if (maxI4HeaderBits == 0) + { + return false; + } + + rdBest.InitScore(); + rdBest.H = 211; // '211' is the value of VP8BitCost(0, 145) + rdBest.SetRdScore(dqm.LambdaMode); + it.StartI4(); + do + { + int numBlocks = 1; + var rdi4 = new Vp8ModeScore(); + int mode; + int bestMode = -1; + Span src = src0.Slice(WebpLookupTables.Vp8Scan[it.I4]); + short[] modeCosts = it.GetCostModeI4(rd.ModesI4); + Span bestBlock = bestBlocks.Slice(WebpLookupTables.Vp8Scan[it.I4]); + Span tmpDst = it.Scratch.AsSpan(); + tmpDst.Fill(0); + + rdi4.InitScore(); + it.MakeIntra4Preds(); + for (mode = 0; mode < WebpConstants.NumBModes; ++mode) + { + var rdTmp = new Vp8ModeScore(); + short[] tmpLevels = new short[16]; + + // Reconstruct. + rdTmp.Nz = (uint)ReconstructIntra4(it, dqm, tmpLevels, src, tmpDst, mode); + + // Compute RD-score. + rdTmp.D = LossyUtils.Vp8Sse4X4(src, tmpDst); + rdTmp.SD = tlambda != 0 ? Mult8B(tlambda, LossyUtils.Vp8Disto4X4(src, tmpDst, WeightY)) : 0; + rdTmp.H = modeCosts[mode]; + + // Add flatness penalty, to avoid flat area to be mispredicted by a complex mode. + if (mode > 0 && IsFlat(tmpLevels, numBlocks, WebpConstants.FlatnessLimitI4)) + { + rdTmp.R = WebpConstants.FlatnessPenality * numBlocks; + } + else + { + rdTmp.R = 0; + } + + // early-out check. + rdTmp.SetRdScore(lambda); + if (bestMode >= 0 && rdTmp.Score >= rdi4.Score) + { + continue; + } + + // finish computing score. + rdTmp.R += it.GetCostLuma4(tmpLevels, proba); + rdTmp.SetRdScore(lambda); + + if (bestMode < 0 || rdTmp.Score < rdi4.Score) + { + rdi4.CopyScore(rdTmp); + bestMode = mode; + Span tmp = tmpDst; + tmpDst = bestBlock; + bestBlock = tmp; + tmpLevels.AsSpan().CopyTo(rdBest.YAcLevels); + } + } + + rdi4.SetRdScore(dqm.LambdaMode); + rdBest.AddScore(rdi4); + if (rdBest.Score >= rd.Score) + { + return false; + } + + totalHeaderBits += (int)rdi4.H; // <- equal to modeCosts[bestMode]; + if (totalHeaderBits > maxI4HeaderBits) + { + return false; + } + + // Copy selected samples to the right place. + LossyUtils.Vp8Copy4X4(bestBlock, bestBlocks.Slice(WebpLookupTables.Vp8Scan[it.I4])); + + rd.ModesI4[it.I4] = (byte)bestMode; + it.TopNz[it.I4 & 3] = it.LeftNz[it.I4 >> 2] = rdi4.Nz != 0 ? 1 : 0; + } + while (it.RotateI4(bestBlocks)); + + // Finalize state. + rd.CopyScore(rdBest); + it.SetIntra4Mode(rd.ModesI4); + it.SwapOut(); + rdBest.YAcLevels.AsSpan().CopyTo(rd.YAcLevels); + + // Select intra4x4 over intra16x16. + return true; + } + + public static void PickBestUv(Vp8EncIterator it, ref Vp8ModeScore rd, Vp8SegmentInfo[] segmentInfos, Vp8EncProba proba) + { + const int numBlocks = 8; + Vp8SegmentInfo dqm = segmentInfos[it.CurrentMacroBlockInfo.Segment]; + int lambda = dqm.LambdaUv; + Span src = it.YuvIn.AsSpan(Vp8EncIterator.UOffEnc); + Span tmpDst = it.YuvOut2.AsSpan(Vp8EncIterator.UOffEnc); + Span dst0 = it.YuvOut.AsSpan(Vp8EncIterator.UOffEnc); + Span dst = dst0; + var rdBest = new Vp8ModeScore(); + int mode; + + rd.ModeUv = -1; + rdBest.InitScore(); + for (mode = 0; mode < WebpConstants.NumPredModes; ++mode) + { + var rdUv = new Vp8ModeScore(); + + // Reconstruct + rdUv.Nz = (uint)ReconstructUv(it, dqm, rdUv, tmpDst, mode); + + // Compute RD-score + rdUv.D = LossyUtils.Vp8Sse16X8(src, tmpDst); + rdUv.SD = 0; // not calling TDisto here: it tends to flatten areas. + rdUv.H = WebpConstants.Vp8FixedCostsUv[mode]; + rdUv.R = it.GetCostUv(rdUv, proba); + if (mode > 0 && IsFlat(rdUv.UvLevels, numBlocks, WebpConstants.FlatnessLimitIUv)) + { + rdUv.R += WebpConstants.FlatnessPenality * numBlocks; + } + + rdUv.SetRdScore(lambda); + if (mode == 0 || rdUv.Score < rdBest.Score) + { + rdBest.CopyScore(rdUv); + rd.ModeUv = mode; + rdUv.UvLevels.CopyTo(rd.UvLevels.AsSpan()); + Span tmp = dst; + dst = tmpDst; + tmpDst = tmp; + } + } + + it.SetIntraUvMode(rd.ModeUv); + rd.AddScore(rdBest); + if (dst != dst0) + { + // copy 16x8 block if needed. + LossyUtils.Vp8Copy16X8(dst, dst0); + } + } + + public static int ReconstructIntra16(Vp8EncIterator it, Vp8SegmentInfo dqm, Vp8ModeScore rd, Span yuvOut, int mode) + { + Span reference = it.YuvP.AsSpan(Vp8Encoding.Vp8I16ModeOffsets[mode]); + Span src = it.YuvIn.AsSpan(Vp8EncIterator.YOffEnc); + int nz = 0; + int n; + short[] dcTmp = new short[16]; + short[] tmp = new short[16 * 16]; + Span tmpSpan = tmp.AsSpan(); + + for (n = 0; n < 16; n += 2) + { + Vp8Encoding.FTransform2(src.Slice(WebpLookupTables.Vp8Scan[n]), reference.Slice(WebpLookupTables.Vp8Scan[n]), tmpSpan.Slice(n * 16, 16), tmpSpan.Slice((n + 1) * 16, 16)); + } + + Vp8Encoding.FTransformWht(tmp, dcTmp); + nz |= 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 * 16] = tmp[(n + 1) * 16] = 0; + nz |= Quantize2Blocks(tmpSpan.Slice(n * 16, 32), rd.YAcLevels.AsSpan(n * 16, 32), dqm.Y1) << n; + } + + // Transform back. + LossyUtils.TransformWht(dcTmp, tmpSpan); + for (n = 0; n < 16; n += 2) + { + Vp8Encoding.ITransform(reference.Slice(WebpLookupTables.Vp8Scan[n]), tmpSpan.Slice(n * 16, 32), yuvOut.Slice(WebpLookupTables.Vp8Scan[n]), true); + } + + return nz; + } + + public static int ReconstructIntra4(Vp8EncIterator it, Vp8SegmentInfo dqm, Span levels, Span src, Span yuvOut, int mode) + { + Span reference = it.YuvP.AsSpan(Vp8Encoding.Vp8I4ModeOffsets[mode]); + short[] tmp = new short[16]; + Vp8Encoding.FTransform(src, reference, tmp); + int nz = QuantizeBlock(tmp, levels, dqm.Y1); + Vp8Encoding.ITransform(reference, tmp, yuvOut, false); + + return nz; + } + + public static int ReconstructUv(Vp8EncIterator it, Vp8SegmentInfo dqm, Vp8ModeScore rd, Span yuvOut, int mode) + { + Span reference = it.YuvP.AsSpan(Vp8Encoding.Vp8UvModeOffsets[mode]); + Span src = it.YuvIn.AsSpan(Vp8EncIterator.UOffEnc); + int nz = 0; + int n; + short[] tmp = new short[8 * 16]; + + for (n = 0; n < 8; n += 2) + { + Vp8Encoding.FTransform2( + src.Slice(WebpLookupTables.Vp8ScanUv[n]), + reference.Slice(WebpLookupTables.Vp8ScanUv[n]), + tmp.AsSpan(n * 16, 16), + tmp.AsSpan((n + 1) * 16, 16)); + } + + CorrectDcValues(it, dqm.Uv, tmp, rd); + + for (n = 0; n < 8; n += 2) + { + nz |= Quantize2Blocks(tmp.AsSpan(n * 16, 32), rd.UvLevels.AsSpan(n * 16, 32), dqm.Uv) << n; + } + + for (n = 0; n < 8; n += 2) + { + Vp8Encoding.ITransform(reference.Slice(WebpLookupTables.Vp8ScanUv[n]), tmp.AsSpan(n * 16, 32), yuvOut.Slice(WebpLookupTables.Vp8ScanUv[n]), true); + } + + return nz << 16; + } + + // Refine intra16/intra4 sub-modes based on distortion only (not rate). + public static void RefineUsingDistortion(Vp8EncIterator it, Vp8SegmentInfo[] segmentInfos, Vp8ModeScore rd, bool tryBothModes, bool refineUvMode, int mbHeaderLimit) + { + long bestScore = Vp8ModeScore.MaxCost; + int nz = 0; + int mode; + bool isI16 = tryBothModes || (it.CurrentMacroBlockInfo.MacroBlockType == Vp8MacroBlockType.I16X16); + Vp8SegmentInfo dqm = segmentInfos[it.CurrentMacroBlockInfo.Segment]; + + // Some empiric constants, of approximate order of magnitude. + const int lambdaDi16 = 106; + const int lambdaDi4 = 11; + const int lambdaDuv = 120; + long scoreI4 = dqm.I4Penalty; + long i4BitSum = 0; + long bitLimit = tryBothModes + ? mbHeaderLimit + : Vp8ModeScore.MaxCost; // no early-out allowed. + + if (isI16) + { + int bestMode = -1; + Span src = it.YuvIn.AsSpan(Vp8EncIterator.YOffEnc); + for (mode = 0; mode < WebpConstants.NumPredModes; ++mode) + { + Span reference = it.YuvP.AsSpan(Vp8Encoding.Vp8I16ModeOffsets[mode]); + long score = (LossyUtils.Vp8Sse16X16(src, reference) * WebpConstants.RdDistoMult) + (WebpConstants.Vp8FixedCostsI16[mode] * lambdaDi16); + + if (mode > 0 && WebpConstants.Vp8FixedCostsI16[mode] > bitLimit) + { + continue; + } + + if (score < bestScore) + { + bestMode = mode; + bestScore = score; + } + } + + if (it.X == 0 || it.Y == 0) + { + // Avoid starting a checkerboard resonance from the border. See bug #432 of libwebp. + if (IsFlatSource16(src)) + { + bestMode = (it.X == 0) ? 0 : 2; + tryBothModes = false; // Stick to i16. + } + } + + it.SetIntra16Mode(bestMode); + + // We'll reconstruct later, if i16 mode actually gets selected. + } + + // Next, evaluate Intra4. + if (tryBothModes || !isI16) + { + // We don't evaluate the rate here, but just account for it through a + // constant penalty (i4 mode usually needs more bits compared to i16). + isI16 = false; + it.StartI4(); + do + { + int bestI4Mode = -1; + long bestI4Score = Vp8ModeScore.MaxCost; + Span src = it.YuvIn.AsSpan(Vp8EncIterator.YOffEnc + WebpLookupTables.Vp8Scan[it.I4]); + short[] modeCosts = it.GetCostModeI4(rd.ModesI4); + + it.MakeIntra4Preds(); + for (mode = 0; mode < WebpConstants.NumBModes; ++mode) + { + Span reference = it.YuvP.AsSpan(Vp8Encoding.Vp8I4ModeOffsets[mode]); + long score = (LossyUtils.Vp8Sse4X4(src, reference) * WebpConstants.RdDistoMult) + (modeCosts[mode] * lambdaDi4); + if (score < bestI4Score) + { + bestI4Mode = mode; + bestI4Score = score; + } + } + + i4BitSum += modeCosts[bestI4Mode]; + rd.ModesI4[it.I4] = (byte)bestI4Mode; + scoreI4 += bestI4Score; + if (scoreI4 >= bestScore || i4BitSum > bitLimit) + { + // Intra4 won't be better than Intra16. Bail out and pick Intra16. + isI16 = true; + break; + } + else + { + // Reconstruct partial block inside YuvOut2 buffer + Span tmpDst = it.YuvOut2.AsSpan(Vp8EncIterator.YOffEnc + WebpLookupTables.Vp8Scan[it.I4]); + nz |= ReconstructIntra4(it, dqm, rd.YAcLevels.AsSpan(it.I4 * 16, 16), src, tmpDst, bestI4Mode) << it.I4; + } + } + while (it.RotateI4(it.YuvOut2.AsSpan(Vp8EncIterator.YOffEnc))); + } + + // Final reconstruction, depending on which mode is selected. + if (!isI16) + { + it.SetIntra4Mode(rd.ModesI4); + it.SwapOut(); + bestScore = scoreI4; + } + else + { + int intra16Mode = it.Preds[it.PredIdx]; + nz = ReconstructIntra16(it, dqm, rd, it.YuvOut.AsSpan(Vp8EncIterator.YOffEnc), intra16Mode); + } + + // ... and UV! + if (refineUvMode) + { + int bestMode = -1; + long bestUvScore = Vp8ModeScore.MaxCost; + Span src = it.YuvIn.AsSpan(Vp8EncIterator.UOffEnc); + for (mode = 0; mode < WebpConstants.NumPredModes; ++mode) + { + Span reference = it.YuvP.AsSpan(Vp8Encoding.Vp8UvModeOffsets[mode]); + long score = (LossyUtils.Vp8Sse16X8(src, reference) * WebpConstants.RdDistoMult) + (WebpConstants.Vp8FixedCostsUv[mode] * lambdaDuv); + if (score < bestUvScore) + { + bestMode = mode; + bestUvScore = score; + } + } + + it.SetIntraUvMode(bestMode); + } + + nz |= ReconstructUv(it, dqm, rd, it.YuvOut.AsSpan(Vp8EncIterator.UOffEnc), it.CurrentMacroBlockInfo.UvMode); + + rd.Nz = (uint)nz; + rd.Score = bestScore; + } + + [MethodImpl(InliningOptions.ShortMethod)] public static int Quantize2Blocks(Span input, Span output, Vp8Matrix mtx) { - var nz = QuantEnc.QuantizeBlock(input, output, mtx) << 0; - nz |= QuantEnc.QuantizeBlock(input.Slice(1 * 16), output.Slice(1 * 16), mtx) << 1; + int nz = QuantizeBlock(input, output, mtx) << 0; + nz |= QuantizeBlock(input.Slice(1 * 16), output.Slice(1 * 16), mtx) << 1; return nz; } @@ -111,13 +563,13 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy Span left = it.LeftDerr.AsSpan(ch, 2); Span c = tmp.AsSpan(ch * 4 * 16, 4 * 16); c[0] += (short)(((C1 * top[0]) + (C2 * left[0])) >> (DSHIFT - DSCALE)); - var err0 = QuantEnc.QuantizeSingle(c, mtx); + int err0 = QuantizeSingle(c, mtx); c[1 * 16] += (short)(((C1 * top[1]) + (C2 * err0)) >> (DSHIFT - DSCALE)); - var err1 = QuantEnc.QuantizeSingle(c.Slice(1 * 16), mtx); + int err1 = QuantizeSingle(c.Slice(1 * 16), mtx); c[2 * 16] += (short)(((C1 * err0) + (C2 * left[1])) >> (DSHIFT - DSCALE)); - var err2 = QuantEnc.QuantizeSingle(c.Slice(2 * 16), mtx); + int err2 = QuantizeSingle(c.Slice(2 * 16), mtx); c[3 * 16] += (short)(((C1 * err1) + (C2 * err2)) >> (DSHIFT - DSCALE)); - var err3 = QuantEnc.QuantizeSingle(c.Slice(3 * 16), mtx); + int err3 = QuantizeSingle(c.Slice(3 * 16), mtx); // TODO: set errors in rd // rd->derr[ch][0] = (int8_t)err1; @@ -127,9 +579,50 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy } [MethodImpl(InliningOptions.ShortMethod)] - private static int QuantDiv(uint n, uint iQ, uint b) + private static bool IsFlatSource16(Span src) { - return (int)(((n * iQ) + b) >> WebpConstants.QFix); + uint v = src[0] * 0x01010101u; + 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(8, 4).SequenceEqual(vSpan) || !src.Slice(12, 4).SequenceEqual(vSpan)) + { + return false; + } + + src = src.Slice(WebpConstants.Bps); + } + + return true; } + + [MethodImpl(InliningOptions.ShortMethod)] + private static bool IsFlat(Span levels, int numBlocks, int thresh) + { + int score = 0; + while (numBlocks-- > 0) + { + for (int i = 1; i < 16; ++i) + { + // omit DC, we're only interested in AC + score += (levels[i] != 0) ? 1 : 0; + if (score > thresh) + { + return false; + } + } + + levels = levels.Slice(16); + } + + return true; + } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int Mult8B(int a, int b) => ((a * b) + 128) >> 8; + + [MethodImpl(InliningOptions.ShortMethod)] + private static int QuantDiv(uint n, uint iQ, uint b) => (int)(((n * iQ) + b) >> WebpConstants.QFix); } } diff --git a/src/ImageSharp/Formats/WebP/Lossy/Vp8EncIterator.cs b/src/ImageSharp/Formats/WebP/Lossy/Vp8EncIterator.cs index ff8f192e10..f1fb6c13ad 100644 --- a/src/ImageSharp/Formats/WebP/Lossy/Vp8EncIterator.cs +++ b/src/ImageSharp/Formats/WebP/Lossy/Vp8EncIterator.cs @@ -450,6 +450,35 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy this.CurrentMacroBlockInfo.MacroBlockType = Vp8MacroBlockType.I4X4; } + public int GetCostLuma16(Vp8ModeScore rd, Vp8EncProba proba) + { + var res = new Vp8Residual(); + int r = 0; + + // re-import the non-zero context. + this.NzToBytes(); + + // DC + res.Init(0, 1, proba); + res.SetCoeffs(rd.YDcLevels); + r += res.GetResidualCost(this.TopNz[8] + this.LeftNz[8]); + + // AC + res.Init(1, 0, proba); + for (int y = 0; y < 4; ++y) + { + for (int x = 0; x < 4; ++x) + { + int ctx = this.TopNz[x] + this.LeftNz[y]; + res.SetCoeffs(rd.YAcLevels.AsSpan(x + (y * 4))); + r += res.GetResidualCost(ctx); + this.TopNz[x] = this.LeftNz[y] = (res.Last >= 0) ? 1 : 0; + } + } + + return r; + } + public short[] GetCostModeI4(byte[] modes) { int predsWidth = this.predsWidth; diff --git a/src/ImageSharp/Formats/WebP/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/WebP/Lossy/Vp8Encoder.cs index 757babdc0a..86f0144249 100644 --- a/src/ImageSharp/Formats/WebP/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/WebP/Lossy/Vp8Encoder.cs @@ -63,17 +63,8 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy private readonly byte[] averageBytesPerMb = { 50, 24, 16, 9, 7, 5, 3, 2 }; - private readonly ushort[] weightY = { 38, 32, 20, 9, 32, 28, 17, 7, 20, 17, 10, 4, 9, 7, 4, 2 }; - private const int NumMbSegments = 4; - private const int NumBModes = 10; - - /// - /// The number of prediction modes. - /// - private const int NumPredModes = 4; - private const int MaxItersKMeans = 6; // Convergence is considered reached if dq < DqLimit @@ -281,10 +272,10 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy { int width = image.Width; int height = image.Height; - this.ConvertRgbToYuv(image); Span y = this.Y.GetSpan(); Span u = this.U.GetSpan(); Span v = this.V.GetSpan(); + YuvConversion.ConvertRgbToYuv(image, this.configuration, this.memoryAllocator, y, u, v); int yStride = width; int uvStride = (yStride + 1) >> 1; @@ -657,8 +648,8 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy { int alpha = 255 * (centers[n] - mid) / (max - min); int beta = 255 * (centers[n] - min) / (max - min); - dqm[n].Alpha = Clip(alpha, -127, 127); - dqm[n].Beta = Clip(beta, 0, 255); + dqm[n].Alpha = Numerics.Clamp(alpha, -127, 127); + dqm[n].Beta = Numerics.Clamp(beta, 0, 255); } } @@ -676,7 +667,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy 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 = Clip(q, 0, 127); + dqm[i].Quant = Numerics.Clamp(q, 0, 127); } // Purely indicative in the bitstream (except for the 1-segment case). @@ -691,13 +682,13 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy this.DqUvAc = this.DqUvAc * snsStrength / 100; // and make it safe. - this.DqUvAc = Clip(this.DqUvAc, WebpConstants.QuantEncMinDqUv, WebpConstants.QuantEncMaxDqUv); + this.DqUvAc = Numerics.Clamp(this.DqUvAc, WebpConstants.QuantEncMinDqUv, WebpConstants.QuantEncMaxDqUv); // We also boost the dc-uv-quant a little, based on sns-strength, since // U/V channels are quite more reactive to high quants (flat DC-blocks // tend to appear, and are unpleasant). this.DqUvDc = -4 * snsStrength / 100; - this.DqUvDc = Clip(this.DqUvDc, -15, 15); // 4bit-signed max allowed + this.DqUvDc = Numerics.Clamp(this.DqUvDc, -15, 15); // 4bit-signed max allowed this.DqY1Dc = 0; this.DqY2Dc = 0; @@ -721,7 +712,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy Vp8SegmentInfo m = this.SegmentInfos[i]; // We focus on the quantization of AC coeffs. - int qstep = WebpLookupTables.AcTable[Clip(m.Quant, 0, 127)] >> 2; + int qstep = WebpLookupTables.AcTable[Numerics.Clamp(m.Quant, 0, 127)] >> 2; int baseStrength = this.FilterStrengthFromDelta(this.FilterHeader.Sharpness, qstep); // Segments with lower complexity ('beta') will be less filtered. @@ -799,14 +790,14 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy m.Y2 = new Vp8Matrix(); m.Uv = new Vp8Matrix(); - m.Y1.Q[0] = WebpLookupTables.DcTable[Clip(q, 0, 127)]; - m.Y1.Q[1] = WebpLookupTables.AcTable[Clip(q, 0, 127)]; + m.Y1.Q[0] = WebpLookupTables.DcTable[Numerics.Clamp(q, 0, 127)]; + m.Y1.Q[1] = WebpLookupTables.AcTable[Numerics.Clamp(q, 0, 127)]; - m.Y2.Q[0] = (ushort)(WebpLookupTables.DcTable[Clip(q, 0, 127)] * 2); - m.Y2.Q[1] = WebpLookupTables.AcTable2[Clip(q, 0, 127)]; + m.Y2.Q[0] = (ushort)(WebpLookupTables.DcTable[Numerics.Clamp(q, 0, 127)] * 2); + m.Y2.Q[1] = WebpLookupTables.AcTable2[Numerics.Clamp(q, 0, 127)]; - m.Uv.Q[0] = WebpLookupTables.DcTable[Clip(q + this.DqUvDc, 0, 117)]; - m.Uv.Q[1] = WebpLookupTables.AcTable[Clip(q + this.DqUvAc, 0, 127)]; + m.Uv.Q[0] = WebpLookupTables.DcTable[Numerics.Clamp(q + this.DqUvDc, 0, 117)]; + m.Uv.Q[1] = WebpLookupTables.AcTable[Numerics.Clamp(q + this.DqUvAc, 0, 127)]; int qi4 = m.Y1.Expand(0); int qi16 = m.Y2.Expand(1); @@ -884,16 +875,15 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy it.MakeChroma8Preds(); // TODO: disabled picking best mode because its still bugged. - // if (rdOpt > Vp8RdLevel.RdOptNone) - if (false) + /* if (rdOpt > Vp8RdLevel.RdOptNone) { - this.PickBestIntra16(it, ref rd); + QuantEnc.PickBestIntra16(it, ref rd, this.SegmentInfos, this.Proba); if (this.method >= 2) { - this.PickBestIntra4(it, ref rd); + QuantEnc.PickBestIntra4(it, ref rd, this.SegmentInfos, this.Proba, this.maxI4HeaderBits); } - this.PickBestUv(it, ref rd); + QuantEnc.PickBestUv(it, ref rd, this.SegmentInfos, this.Proba); } else { @@ -901,8 +891,11 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy // For method >= 2, pick the best intra4/intra16 based on SSE (~tad slower). // For method <= 1, we don't re-examine the decision but just go ahead with // quantization/reconstruction. - this.RefineUsingDistortion(it, rd, this.method >= 2, this.method >= 1); + QuantEnc.RefineUsingDistortion(it, this.SegmentInfos, rd, this.method >= 2, this.method >= 1, this.MbHeaderLimit); } + */ + + QuantEnc.RefineUsingDistortion(it, this.SegmentInfos, rd, this.method >= 2, this.method >= 1, this.MbHeaderLimit); bool isSkipped = rd.Nz == 0; it.SetSkip(isSkipped); @@ -910,406 +903,6 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy return isSkipped; } - private void PickBestIntra16(Vp8EncIterator it, ref Vp8ModeScore rd) - { - const int numBlocks = 16; - Vp8SegmentInfo dqm = this.SegmentInfos[it.CurrentMacroBlockInfo.Segment]; - int lambda = dqm.LambdaI16; - int tlambda = dqm.TLambda; - Span src = it.YuvIn.AsSpan(Vp8EncIterator.YOffEnc); - var rdTmp = new Vp8ModeScore(); - Vp8ModeScore rdCur = rdTmp; - Vp8ModeScore rdBest = rd; - int mode; - bool isFlat = IsFlatSource16(src); - rd.ModeI16 = -1; - for (mode = 0; mode < NumPredModes; ++mode) - { - // scratch buffer. - Span tmpDst = it.YuvOut2.AsSpan(Vp8EncIterator.YOffEnc); - rdCur.ModeI16 = mode; - - // Reconstruct. - rdCur.Nz = (uint)this.ReconstructIntra16(it, dqm, rdCur, tmpDst, mode); - - // Measure RD-score. - rdCur.D = Vp8Sse16X16(src, tmpDst); - rdCur.SD = tlambda != 0 ? Mult8B(tlambda, Vp8Disto16X16(src, tmpDst, this.weightY)) : 0; - rdCur.H = WebpConstants.Vp8FixedCostsI16[mode]; - rdCur.R = this.GetCostLuma16(it, rdCur); - - if (isFlat) - { - // Refine the first impression (which was in pixel space). - isFlat = IsFlat(rdCur.YAcLevels, numBlocks, WebpConstants.FlatnessLimitI16); - if (isFlat) - { - // Block is very flat. We put emphasis on the distortion being very low! - rdCur.D *= 2; - rdCur.SD *= 2; - } - } - - // Since we always examine Intra16 first, we can overwrite *rd directly. - rdCur.SetRdScore(lambda); - - if (mode == 0 || rdCur.Score < rdBest.Score) - { - Vp8ModeScore tmp = rdCur; - rdCur = rdBest; - rdBest = tmp; - it.SwapOut(); - } - } - - if (rdBest != rd) - { - rd = rdBest; - } - - // Finalize score for mode decision. - rd.SetRdScore(dqm.LambdaMode); - it.SetIntra16Mode(rd.ModeI16); - - // We have a blocky macroblock (only DCs are non-zero) with fairly high - // distortion, record max delta so we can later adjust the minimal filtering - // strength needed to smooth these blocks out. - if ((rd.Nz & 0x100ffff) == 0x1000000 && rd.D > dqm.MinDisto) - { - dqm.StoreMaxDelta(rd.YDcLevels); - } - } - - private bool PickBestIntra4(Vp8EncIterator it, ref Vp8ModeScore rd) - { - Vp8SegmentInfo dqm = this.SegmentInfos[it.CurrentMacroBlockInfo.Segment]; - int lambda = dqm.LambdaI4; - int tlambda = dqm.TLambda; - Span src0 = it.YuvIn.AsSpan(Vp8EncIterator.YOffEnc); - Span bestBlocks = it.YuvOut2.AsSpan(Vp8EncIterator.YOffEnc); - int totalHeaderBits = 0; - var rdBest = new Vp8ModeScore(); - - if (this.maxI4HeaderBits == 0) - { - return false; - } - - rdBest.InitScore(); - rdBest.H = 211; // '211' is the value of VP8BitCost(0, 145) - rdBest.SetRdScore(dqm.LambdaMode); - it.StartI4(); - do - { - int numBlocks = 1; - var rdi4 = new Vp8ModeScore(); - int mode; - int bestMode = -1; - Span src = src0.Slice(WebpLookupTables.Vp8Scan[it.I4]); - short[] modeCosts = it.GetCostModeI4(rd.ModesI4); - Span bestBlock = bestBlocks.Slice(WebpLookupTables.Vp8Scan[it.I4]); - Span tmpDst = it.Scratch.AsSpan(); - tmpDst.Fill(0); - - rdi4.InitScore(); - it.MakeIntra4Preds(); - for (mode = 0; mode < NumBModes; ++mode) - { - var rdTmp = new Vp8ModeScore(); - short[] tmpLevels = new short[16]; - - // Reconstruct. - rdTmp.Nz = (uint)this.ReconstructIntra4(it, dqm, tmpLevels, src, tmpDst, mode); - - // Compute RD-score. - rdTmp.D = Vp8Sse4X4(src, tmpDst); - rdTmp.SD = tlambda != 0 ? Mult8B(tlambda, Vp8Disto4X4(src, tmpDst, this.weightY)) : 0; - rdTmp.H = modeCosts[mode]; - - // Add flatness penalty, to avoid flat area to be mispredicted by a complex mode. - if (mode > 0 && IsFlat(tmpLevels, numBlocks, WebpConstants.FlatnessLimitI4)) - { - rdTmp.R = WebpConstants.FlatnessPenality * numBlocks; - } - else - { - rdTmp.R = 0; - } - - // early-out check. - rdTmp.SetRdScore(lambda); - if (bestMode >= 0 && rdTmp.Score >= rdi4.Score) - { - continue; - } - - // finish computing score. - rdTmp.R += it.GetCostLuma4(tmpLevels, this.Proba); - rdTmp.SetRdScore(lambda); - - if (bestMode < 0 || rdTmp.Score < rdi4.Score) - { - rdi4.CopyScore(rdTmp); - bestMode = mode; - Span tmp = tmpDst; - tmpDst = bestBlock; - bestBlock = tmp; - tmpLevels.AsSpan().CopyTo(rdBest.YAcLevels); - } - } - - rdi4.SetRdScore(dqm.LambdaMode); - rdBest.AddScore(rdi4); - if (rdBest.Score >= rd.Score) - { - return false; - } - - totalHeaderBits += (int)rdi4.H; // <- equal to modeCosts[bestMode]; - if (totalHeaderBits > this.maxI4HeaderBits) - { - return false; - } - - // Copy selected samples to the right place. - Vp8Copy4X4(bestBlock, bestBlocks.Slice(WebpLookupTables.Vp8Scan[it.I4])); - - rd.ModesI4[it.I4] = (byte)bestMode; - it.TopNz[it.I4 & 3] = it.LeftNz[it.I4 >> 2] = rdi4.Nz != 0 ? 1 : 0; - } - while (it.RotateI4(bestBlocks)); - - // Finalize state. - rd.CopyScore(rdBest); - it.SetIntra4Mode(rd.ModesI4); - it.SwapOut(); - rdBest.YAcLevels.AsSpan().CopyTo(rd.YAcLevels); - - // Select intra4x4 over intra16x16. - return true; - } - - private void PickBestUv(Vp8EncIterator it, ref Vp8ModeScore rd) - { - const int numBlocks = 8; - Vp8SegmentInfo dqm = this.SegmentInfos[it.CurrentMacroBlockInfo.Segment]; - int lambda = dqm.LambdaUv; - Span src = it.YuvIn.AsSpan(Vp8EncIterator.UOffEnc); - Span tmpDst = it.YuvOut2.AsSpan(Vp8EncIterator.UOffEnc); - Span dst0 = it.YuvOut.AsSpan(Vp8EncIterator.UOffEnc); - Span dst = dst0; - var rdBest = new Vp8ModeScore(); - int mode; - - rd.ModeUv = -1; - rdBest.InitScore(); - for (mode = 0; mode < NumPredModes; ++mode) - { - var rdUv = new Vp8ModeScore(); - - // Reconstruct - rdUv.Nz = (uint)this.ReconstructUv(it, dqm, rdUv, tmpDst, mode); - - // Compute RD-score - rdUv.D = Vp8Sse16X8(src, tmpDst); - rdUv.SD = 0; // not calling TDisto here: it tends to flatten areas. - rdUv.H = WebpConstants.Vp8FixedCostsUv[mode]; - rdUv.R = it.GetCostUv(rdUv, this.Proba); - if (mode > 0 && IsFlat(rdUv.UvLevels, numBlocks, WebpConstants.FlatnessLimitIUv)) - { - rdUv.R += WebpConstants.FlatnessPenality * numBlocks; - } - - rdUv.SetRdScore(lambda); - if (mode == 0 || rdUv.Score < rdBest.Score) - { - rdBest.CopyScore(rdUv); - rd.ModeUv = mode; - rdUv.UvLevels.CopyTo(rd.UvLevels.AsSpan()); - Span tmp = dst; - dst = tmpDst; - tmpDst = tmp; - } - } - - it.SetIntraUvMode(rd.ModeUv); - rd.AddScore(rdBest); - if (dst != dst0) - { - // copy 16x8 block if needed. - Vp8Copy16X8(dst, dst0); - } - } - - // TODO: move to Vp8EncIterator - private int GetCostLuma16(Vp8EncIterator it, Vp8ModeScore rd) - { - var res = new Vp8Residual(); - int r = 0; - - // re-import the non-zero context. - it.NzToBytes(); - - // DC - res.Init(0, 1, this.Proba); - res.SetCoeffs(rd.YDcLevels); - r += res.GetResidualCost(it.TopNz[8] + it.LeftNz[8]); - - // AC - res.Init(1, 0, this.Proba); - for (int y = 0; y < 4; ++y) - { - for (int x = 0; x < 4; ++x) - { - int ctx = it.TopNz[x] + it.LeftNz[y]; - res.SetCoeffs(rd.YAcLevels.AsSpan(x + (y * 4))); - r += res.GetResidualCost(ctx); - it.TopNz[x] = it.LeftNz[y] = (res.Last >= 0) ? 1 : 0; - } - } - - return r; - } - - // Refine intra16/intra4 sub-modes based on distortion only (not rate). - private void RefineUsingDistortion(Vp8EncIterator it, Vp8ModeScore rd, bool tryBothModes, bool refineUvMode) - { - long bestScore = Vp8ModeScore.MaxCost; - int nz = 0; - int mode; - bool isI16 = tryBothModes || (it.CurrentMacroBlockInfo.MacroBlockType == Vp8MacroBlockType.I16X16); - Vp8SegmentInfo dqm = this.SegmentInfos[it.CurrentMacroBlockInfo.Segment]; - - // Some empiric constants, of approximate order of magnitude. - const int lambdaDi16 = 106; - const int lambdaDi4 = 11; - const int lambdaDuv = 120; - long scoreI4 = dqm.I4Penalty; - long i4BitSum = 0; - long bitLimit = tryBothModes - ? this.MbHeaderLimit - : Vp8ModeScore.MaxCost; // no early-out allowed. - - if (isI16) - { - int bestMode = -1; - Span src = it.YuvIn.AsSpan(Vp8EncIterator.YOffEnc); - for (mode = 0; mode < NumPredModes; ++mode) - { - Span reference = it.YuvP.AsSpan(Vp8Encoding.Vp8I16ModeOffsets[mode]); - long score = (Vp8Sse16X16(src, reference) * WebpConstants.RdDistoMult) + (WebpConstants.Vp8FixedCostsI16[mode] * lambdaDi16); - - if (mode > 0 && WebpConstants.Vp8FixedCostsI16[mode] > bitLimit) - { - continue; - } - - if (score < bestScore) - { - bestMode = mode; - bestScore = score; - } - } - - if (it.X == 0 || it.Y == 0) - { - // Avoid starting a checkerboard resonance from the border. See bug #432 of libwebp. - if (IsFlatSource16(src)) - { - bestMode = (it.X == 0) ? 0 : 2; - tryBothModes = false; // Stick to i16. - } - } - - it.SetIntra16Mode(bestMode); - - // We'll reconstruct later, if i16 mode actually gets selected. - } - - // Next, evaluate Intra4. - if (tryBothModes || !isI16) - { - // We don't evaluate the rate here, but just account for it through a - // constant penalty (i4 mode usually needs more bits compared to i16). - isI16 = false; - it.StartI4(); - do - { - int bestI4Mode = -1; - long bestI4Score = Vp8ModeScore.MaxCost; - Span src = it.YuvIn.AsSpan(Vp8EncIterator.YOffEnc + WebpLookupTables.Vp8Scan[it.I4]); - short[] modeCosts = it.GetCostModeI4(rd.ModesI4); - - it.MakeIntra4Preds(); - for (mode = 0; mode < NumBModes; ++mode) - { - Span reference = it.YuvP.AsSpan(Vp8Encoding.Vp8I4ModeOffsets[mode]); - long score = (Vp8Sse4X4(src, reference) * WebpConstants.RdDistoMult) + (modeCosts[mode] * lambdaDi4); - if (score < bestI4Score) - { - bestI4Mode = mode; - bestI4Score = score; - } - } - - i4BitSum += modeCosts[bestI4Mode]; - rd.ModesI4[it.I4] = (byte)bestI4Mode; - scoreI4 += bestI4Score; - if (scoreI4 >= bestScore || i4BitSum > bitLimit) - { - // Intra4 won't be better than Intra16. Bail out and pick Intra16. - isI16 = true; - break; - } - else - { - // Reconstruct partial block inside YuvOut2 buffer - Span tmpDst = it.YuvOut2.AsSpan(Vp8EncIterator.YOffEnc + WebpLookupTables.Vp8Scan[it.I4]); - nz |= this.ReconstructIntra4(it, dqm, rd.YAcLevels.AsSpan(it.I4 * 16, 16), src, tmpDst, bestI4Mode) << it.I4; - } - } - while (it.RotateI4(it.YuvOut2.AsSpan(Vp8EncIterator.YOffEnc))); - } - - // Final reconstruction, depending on which mode is selected. - if (!isI16) - { - it.SetIntra4Mode(rd.ModesI4); - it.SwapOut(); - bestScore = scoreI4; - } - else - { - int intra16Mode = it.Preds[it.PredIdx]; - nz = this.ReconstructIntra16(it, dqm, rd, it.YuvOut.AsSpan(Vp8EncIterator.YOffEnc), intra16Mode); - } - - // ... and UV! - if (refineUvMode) - { - int bestMode = -1; - long bestUvScore = Vp8ModeScore.MaxCost; - Span src = it.YuvIn.AsSpan(Vp8EncIterator.UOffEnc); - for (mode = 0; mode < NumPredModes; ++mode) - { - Span reference = it.YuvP.AsSpan(Vp8Encoding.Vp8UvModeOffsets[mode]); - long score = (Vp8Sse16X8(src, reference) * WebpConstants.RdDistoMult) + (WebpConstants.Vp8FixedCostsUv[mode] * lambdaDuv); - if (score < bestUvScore) - { - bestMode = mode; - bestUvScore = score; - } - } - - it.SetIntraUvMode(bestMode); - } - - nz |= this.ReconstructUv(it, dqm, rd, it.YuvOut.AsSpan(Vp8EncIterator.UOffEnc), it.CurrentMacroBlockInfo.UvMode); - - rd.Nz = (uint)nz; - rd.Score = bestScore; - } - private void CodeResiduals(Vp8EncIterator it, Vp8ModeScore rd) { int x, y, ch; @@ -1433,268 +1026,11 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy it.BytesToNz(); } - private int ReconstructIntra16(Vp8EncIterator it, Vp8SegmentInfo dqm, Vp8ModeScore rd, Span yuvOut, int mode) - { - Span reference = it.YuvP.AsSpan(Vp8Encoding.Vp8I16ModeOffsets[mode]); - Span src = it.YuvIn.AsSpan(Vp8EncIterator.YOffEnc); - int nz = 0; - int n; - short[] dcTmp = new short[16]; - short[] tmp = new short[16 * 16]; - Span tmpSpan = tmp.AsSpan(); - - for (n = 0; n < 16; n += 2) - { - Vp8Encoding.FTransform2(src.Slice(WebpLookupTables.Vp8Scan[n]), reference.Slice(WebpLookupTables.Vp8Scan[n]), tmpSpan.Slice(n * 16, 16), tmpSpan.Slice((n + 1) * 16, 16)); - } - - Vp8Encoding.FTransformWht(tmp, dcTmp); - nz |= QuantEnc.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 * 16] = tmp[(n + 1) * 16] = 0; - nz |= QuantEnc.Quantize2Blocks(tmpSpan.Slice(n * 16, 32), rd.YAcLevels.AsSpan(n * 16, 32), dqm.Y1) << n; - } - - // Transform back. - LossyUtils.TransformWht(dcTmp, tmpSpan); - for (n = 0; n < 16; n += 2) - { - Vp8Encoding.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, Span levels, Span src, Span yuvOut, int mode) - { - Span reference = it.YuvP.AsSpan(Vp8Encoding.Vp8I4ModeOffsets[mode]); - short[] tmp = new short[16]; - Vp8Encoding.FTransform(src, reference, tmp); - int nz = QuantEnc.QuantizeBlock(tmp, levels, dqm.Y1); - Vp8Encoding.ITransform(reference, tmp, yuvOut, false); - - return nz; - } - - private int ReconstructUv(Vp8EncIterator it, Vp8SegmentInfo dqm, Vp8ModeScore rd, Span yuvOut, int mode) - { - Span reference = it.YuvP.AsSpan(Vp8Encoding.Vp8UvModeOffsets[mode]); - Span src = it.YuvIn.AsSpan(Vp8EncIterator.UOffEnc); - int nz = 0; - int n; - short[] tmp = new short[8 * 16]; - - for (n = 0; n < 8; n += 2) - { - Vp8Encoding.FTransform2( - src.Slice(WebpLookupTables.Vp8ScanUv[n]), - reference.Slice(WebpLookupTables.Vp8ScanUv[n]), - tmp.AsSpan(n * 16, 16), - tmp.AsSpan((n + 1) * 16, 16)); - } - - QuantEnc.CorrectDcValues(it, dqm.Uv, tmp, rd); - - for (n = 0; n < 8; n += 2) - { - nz |= QuantEnc.Quantize2Blocks(tmp.AsSpan(n * 16, 32), rd.UvLevels.AsSpan(n * 16, 32), dqm.Uv) << n; - } - - for (n = 0; n < 8; n += 2) - { - Vp8Encoding.ITransform(reference.Slice(WebpLookupTables.Vp8ScanUv[n]), tmp.AsSpan(n * 16, 32), yuvOut.Slice(WebpLookupTables.Vp8ScanUv[n]), true); - } - - return nz << 16; - } - - /// - /// Converts the RGB values of the image to YUV. - /// - /// The pixel type of the image. - /// The image to convert. - private void ConvertRgbToYuv(Image image) - where TPixel : unmanaged, IPixel - { - int width = image.Width; - int height = image.Height; - int uvWidth = (width + 1) >> 1; - - // Temporary storage for accumulated R/G/B values during conversion to U/V. - using IMemoryOwner tmpRgb = this.memoryAllocator.Allocate(4 * uvWidth); - using IMemoryOwner rgbaRow0Buffer = this.memoryAllocator.Allocate(width); - using IMemoryOwner rgbaRow1Buffer = this.memoryAllocator.Allocate(width); - Span tmpRgbSpan = tmpRgb.GetSpan(); - Span rgbaRow0 = rgbaRow0Buffer.GetSpan(); - Span rgbaRow1 = rgbaRow1Buffer.GetSpan(); - int uvRowIndex = 0; - int rowIndex; - bool rowsHaveAlpha = false; - for (rowIndex = 0; rowIndex < height - 1; rowIndex += 2) - { - Span rowSpan = image.GetPixelRowSpan(rowIndex); - Span nextRowSpan = image.GetPixelRowSpan(rowIndex + 1); - PixelOperations.Instance.ToRgba32(this.configuration, rowSpan, rgbaRow0); - PixelOperations.Instance.ToRgba32(this.configuration, nextRowSpan, rgbaRow1); - - rowsHaveAlpha = YuvConversion.CheckNonOpaque(rgbaRow0) && YuvConversion.CheckNonOpaque(rgbaRow1); - - // Downsample U/V planes, two rows at a time. - if (!rowsHaveAlpha) - { - YuvConversion.AccumulateRgb(rgbaRow0, rgbaRow1, tmpRgbSpan, width); - } - else - { - YuvConversion.AccumulateRgba(rgbaRow0, rgbaRow1, tmpRgbSpan, width); - } - - YuvConversion.ConvertRgbaToUv(tmpRgbSpan, this.U.Slice(uvRowIndex * uvWidth), this.V.Slice(uvRowIndex * uvWidth), uvWidth); - uvRowIndex++; - - YuvConversion.ConvertRgbaToY(rgbaRow0, this.Y.Slice(rowIndex * width), width); - YuvConversion.ConvertRgbaToY(rgbaRow1, this.Y.Slice((rowIndex + 1) * width), width); - } - - // Extra last row. - if ((height & 1) != 0) - { - if (!rowsHaveAlpha) - { - YuvConversion.AccumulateRgb(rgbaRow0, rgbaRow0, tmpRgbSpan, width); - } - else - { - YuvConversion.AccumulateRgba(rgbaRow0, rgbaRow0, tmpRgbSpan, width); - } - - YuvConversion.ConvertRgbaToY(rgbaRow0, this.Y.Slice(rowIndex * width), width); - } - } - [MethodImpl(InliningOptions.ShortMethod)] private static int FinalAlphaValue(int alpha) { alpha = WebpConstants.MaxAlpha - alpha; - return Clip(alpha, 0, WebpConstants.MaxAlpha); - } - - [MethodImpl(InliningOptions.ShortMethod)] - private static int Clip(int v, int min, int max) => (v < min) ? min : (v > max) ? max : v; - - [MethodImpl(InliningOptions.ShortMethod)] - private static int Vp8Sse16X16(Span a, Span b) => GetSse(a, b, 16, 16); - - [MethodImpl(InliningOptions.ShortMethod)] - private static int Vp8Sse16X8(Span a, Span b) => GetSse(a, b, 16, 8); - - [MethodImpl(InliningOptions.ShortMethod)] - private static int Vp8Sse4X4(Span a, Span b) => GetSse(a, b, 4, 4); - - [MethodImpl(InliningOptions.ShortMethod)] - private static int GetSse(Span a, Span b, int w, int h) - { - int count = 0; - int aOffset = 0; - int bOffset = 0; - for (int y = 0; y < h; ++y) - { - for (int x = 0; x < w; ++x) - { - int diff = a[aOffset + x] - b[bOffset + x]; - count += diff * diff; - } - - aOffset += WebpConstants.Bps; - bOffset += WebpConstants.Bps; - } - - return count; - } - - [MethodImpl(InliningOptions.ShortMethod)] - private static void Vp8Copy4X4(Span src, Span dst) => Copy(src, dst, 4, 4); - - [MethodImpl(InliningOptions.ShortMethod)] - private static void Vp8Copy16X8(Span src, Span dst) => Copy(src, dst, 16, 8); - - [MethodImpl(InliningOptions.ShortMethod)] - private static void Copy(Span src, Span dst, int w, int h) - { - for (int y = 0; y < h; ++y) - { - src.Slice(0, w).CopyTo(dst); - src = src.Slice(WebpConstants.Bps); - dst = dst.Slice(WebpConstants.Bps); - } - } - - [MethodImpl(InliningOptions.ShortMethod)] - private static int Vp8Disto16X16(Span a, Span b, Span w) - { - int d = 0; - for (int y = 0; y < 16 * WebpConstants.Bps; y += 4 * WebpConstants.Bps) - { - for (int x = 0; x < 16; x += 4) - { - d += Vp8Disto4X4(a.Slice(x + y), b.Slice(x + y), w); - } - } - - return d; - } - - [MethodImpl(InliningOptions.ShortMethod)] - private static int Vp8Disto4X4(Span a, Span b, Span w) - { - int sum1 = LossyUtils.TTransform(a, w); - int sum2 = LossyUtils.TTransform(b, w); - return Math.Abs(sum2 - sum1) >> 5; - } - - [MethodImpl(InliningOptions.ShortMethod)] - private static bool IsFlat(Span levels, int numBlocks, int thresh) - { - int score = 0; - while (numBlocks-- > 0) - { - for (int i = 1; i < 16; ++i) - { - // omit DC, we're only interested in AC - score += (levels[i] != 0) ? 1 : 0; - if (score > thresh) - { - return false; - } - } - - levels = levels.Slice(16); - } - - return true; - } - - [MethodImpl(InliningOptions.ShortMethod)] - private static bool IsFlatSource16(Span src) - { - uint v = src[0] * 0x01010101u; - 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(8, 4).SequenceEqual(vSpan) || !src.Slice(12, 4).SequenceEqual(vSpan)) - { - return false; - } - - src = src.Slice(WebpConstants.Bps); - } - - return true; + return Numerics.Clamp(alpha, 0, WebpConstants.MaxAlpha); } /// @@ -1725,9 +1061,6 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy return WebpLookupTables.LevelsFromDelta[sharpness, pos]; } - [MethodImpl(InliningOptions.ShortMethod)] - private static int Mult8B(int a, int b) => ((a * b) + 128) >> 8; - [MethodImpl(InliningOptions.ShortMethod)] private static double GetPsnr(long mse, long size) => (mse > 0 && size > 0) ? 10.0f * Math.Log10(255.0f * 255.0f * size / mse) : 99; diff --git a/src/ImageSharp/Formats/WebP/Lossy/YuvConversion.cs b/src/ImageSharp/Formats/WebP/Lossy/YuvConversion.cs index fbc2996fcd..68ef9416b1 100644 --- a/src/ImageSharp/Formats/WebP/Lossy/YuvConversion.cs +++ b/src/ImageSharp/Formats/WebP/Lossy/YuvConversion.cs @@ -2,7 +2,9 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Buffers; using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Webp.Lossy @@ -16,6 +18,75 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy private const int YuvHalf = 1 << (YuvFix - 1); + /// + /// Converts the RGB values of the image to YUV. + /// + /// The pixel type of the image. + /// The image to convert. + /// The global configuration. + /// The memory allocator. + /// Span to store the luma component of the image. + /// Span to store the u component of the image. + /// Span to store the v component of the image. + public static void ConvertRgbToYuv(Image image, Configuration configuration, MemoryAllocator memoryAllocator, Span y, Span u, Span v) + where TPixel : unmanaged, IPixel + { + int width = image.Width; + int height = image.Height; + int uvWidth = (width + 1) >> 1; + + // Temporary storage for accumulated R/G/B values during conversion to U/V. + using IMemoryOwner tmpRgb = memoryAllocator.Allocate(4 * uvWidth); + using IMemoryOwner rgbaRow0Buffer = memoryAllocator.Allocate(width); + using IMemoryOwner rgbaRow1Buffer = memoryAllocator.Allocate(width); + Span tmpRgbSpan = tmpRgb.GetSpan(); + Span rgbaRow0 = rgbaRow0Buffer.GetSpan(); + Span rgbaRow1 = rgbaRow1Buffer.GetSpan(); + int uvRowIndex = 0; + int rowIndex; + bool rowsHaveAlpha = false; + for (rowIndex = 0; rowIndex < height - 1; rowIndex += 2) + { + Span rowSpan = image.GetPixelRowSpan(rowIndex); + Span nextRowSpan = image.GetPixelRowSpan(rowIndex + 1); + PixelOperations.Instance.ToRgba32(configuration, rowSpan, rgbaRow0); + PixelOperations.Instance.ToRgba32(configuration, nextRowSpan, rgbaRow1); + + rowsHaveAlpha = YuvConversion.CheckNonOpaque(rgbaRow0) && YuvConversion.CheckNonOpaque(rgbaRow1); + + // Downsample U/V planes, two rows at a time. + if (!rowsHaveAlpha) + { + YuvConversion.AccumulateRgb(rgbaRow0, rgbaRow1, tmpRgbSpan, width); + } + else + { + YuvConversion.AccumulateRgba(rgbaRow0, rgbaRow1, tmpRgbSpan, width); + } + + YuvConversion.ConvertRgbaToUv(tmpRgbSpan, u.Slice(uvRowIndex * uvWidth), v.Slice(uvRowIndex * uvWidth), uvWidth); + uvRowIndex++; + + YuvConversion.ConvertRgbaToY(rgbaRow0, y.Slice(rowIndex * width), width); + YuvConversion.ConvertRgbaToY(rgbaRow1, y.Slice((rowIndex + 1) * width), width); + } + + // Extra last row. + if ((height & 1) != 0) + { + if (!rowsHaveAlpha) + { + YuvConversion.AccumulateRgb(rgbaRow0, rgbaRow0, tmpRgbSpan, width); + } + else + { + YuvConversion.AccumulateRgba(rgbaRow0, rgbaRow0, tmpRgbSpan, width); + } + + YuvConversion.ConvertRgbaToY(rgbaRow0, y.Slice(rowIndex * width), width); + } + } + /// /// Checks if the pixel row is not opaque. /// diff --git a/src/ImageSharp/Formats/WebP/WebpConstants.cs b/src/ImageSharp/Formats/WebP/WebpConstants.cs index 88aa7728af..85b65e0a19 100644 --- a/src/ImageSharp/Formats/WebP/WebpConstants.cs +++ b/src/ImageSharp/Formats/WebP/WebpConstants.cs @@ -220,6 +220,10 @@ namespace SixLabors.ImageSharp.Formats.Webp public const int NumProbas = 11; + public const int NumPredModes = 4; + + public const int NumBModes = 10; + public const int NumCtx = 3; public const int MaxVariableLevel = 67;