From 1c3110699e1de1bf12a38c20900edd910be25dec Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 11 Jul 2021 12:37:24 +0200 Subject: [PATCH] Add low effort path for method == 0 --- .../Formats/WebP/Lossless/PredictorEncoder.cs | 137 ++++++++++-------- .../Formats/WebP/Lossless/Vp8LEncoder.cs | 46 +++--- .../Formats/WebP/Lossless/Vp8LHashChain.cs | 42 +++--- .../Formats/WebP/WebpEncoderTests.cs | 1 + tests/ImageSharp.Tests/TestImages.cs | 1 + tests/Images/Input/WebP/rgb_pattern.png | 4 +- tests/Images/Input/WebP/rgb_pattern_63x63.png | 3 + 7 files changed, 137 insertions(+), 97 deletions(-) create mode 100644 tests/Images/Input/WebP/rgb_pattern_63x63.png diff --git a/src/ImageSharp/Formats/WebP/Lossless/PredictorEncoder.cs b/src/ImageSharp/Formats/WebP/Lossless/PredictorEncoder.cs index e060bbc10..c705a6b2d 100644 --- a/src/ImageSharp/Formats/WebP/Lossless/PredictorEncoder.cs +++ b/src/ImageSharp/Formats/WebP/Lossless/PredictorEncoder.cs @@ -27,6 +27,8 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless private const float SpatialPredictorBias = 15.0f; + private const int PredLowEffort = 11; + /// /// Finds the best predictor for each tile, and converts the image to residuals /// with respect to predictions. If nearLosslessQuality < 100, applies @@ -42,7 +44,8 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless bool nearLossless, int nearLosslessQuality, bool exact, - bool usedSubtractGreen) + bool usedSubtractGreen, + bool lowEffort) { int tilesPerRow = LosslessUtils.SubSampleSize(width, bits); int tilesPerCol = LosslessUtils.SubSampleSize(height, bits); @@ -55,27 +58,36 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless histo[i] = new int[256]; } - // TODO: Low Effort - for (int tileY = 0; tileY < tilesPerCol; ++tileY) + if (lowEffort) { - for (int tileX = 0; tileX < tilesPerRow; ++tileX) + for (int i = 0; i < tilesPerRow * tilesPerCol; ++i) { - int pred = GetBestPredictorForTile( - width, - height, - tileX, - tileY, - bits, - histo, - bgraScratch, - bgra, - maxQuantization, - exact, - usedSubtractGreen, - nearLossless, - image); - - image[(tileY * tilesPerRow) + tileX] = (uint)(WebpConstants.ArgbBlack | (pred << 8)); + image[i] = WebpConstants.ArgbBlack | (PredLowEffort << 8); + } + } + else + { + for (int tileY = 0; tileY < tilesPerCol; ++tileY) + { + for (int tileX = 0; tileX < tilesPerRow; ++tileX) + { + int pred = GetBestPredictorForTile( + width, + height, + tileX, + tileY, + bits, + histo, + bgraScratch, + bgra, + maxQuantization, + exact, + usedSubtractGreen, + nearLossless, + image); + + image[(tileY * tilesPerRow) + tileX] = (uint)(WebpConstants.ArgbBlack | (pred << 8)); + } } } @@ -89,7 +101,8 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless maxQuantization, exact, usedSubtractGreen, - nearLossless); + nearLossless, + lowEffort); } public static void ColorSpaceTransform(int width, int height, int bits, int quality, Span bgra, Span image) @@ -558,18 +571,18 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless int maxQuantization, bool exact, bool usedSubtractGreen, - bool nearLossless) + bool nearLossless, + bool lowEffort) { int tilesPerRow = LosslessUtils.SubSampleSize(width, bits); - // The width of upper_row and current_row is one pixel larger than image width + // The width of upperRow and currentRow is one pixel larger than image width // to allow the top right pixel to point to the leftmost pixel of the next row // when at the right edge. Span upperRow = argbScratch; Span currentRow = upperRow.Slice(width + 1); Span currentMaxDiffs = MemoryMarshal.Cast(currentRow.Slice(width + 1)); - // TODO: This should be wrapped in a condition? Span lowerMaxDiffs = currentMaxDiffs.Slice(width); for (int y = 0; y < height; ++y) { @@ -579,47 +592,53 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless Span src = argb.Slice(y * width, width + ((y + 1) < height ? 1 : 0)); src.CopyTo(currentRow); - // TODO: Near lossless conditional? - if (maxQuantization > 1) + if (lowEffort) { - // Compute max_diffs for the lower row now, because that needs the - // contents of bgra for the current row, which we will overwrite with - // residuals before proceeding with the next row. - Span tmp8 = currentMaxDiffs; - currentMaxDiffs = lowerMaxDiffs; - lowerMaxDiffs = tmp8; - if (y + 2 < height) - { - MaxDiffsForRow(width, width, argb, (y + 1) * width, lowerMaxDiffs, usedSubtractGreen); - } + PredictBatch(PredLowEffort, 0, y, width, currentRow, upperRow, argb.Slice(y * width)); } - - for (int x = 0; x < width;) + else { - int mode = (int)((modes[((y >> bits) * tilesPerRow) + (x >> bits)] >> 8) & 0xff); - int xEnd = x + (1 << bits); - if (xEnd > width) + if (nearLossless && maxQuantization > 1) { - xEnd = width; + // Compute maxDiffs for the lower row now, because that needs the + // contents of bgra for the current row, which we will overwrite with + // residuals before proceeding with the next row. + Span tmp8 = currentMaxDiffs; + currentMaxDiffs = lowerMaxDiffs; + lowerMaxDiffs = tmp8; + if (y + 2 < height) + { + MaxDiffsForRow(width, width, argb, (y + 1) * width, lowerMaxDiffs, usedSubtractGreen); + } } - GetResidual( - width, - height, - upperRow, - currentRow, - currentMaxDiffs, - mode, - x, - xEnd, - y, - maxQuantization, - exact, - usedSubtractGreen, - nearLossless, - argb.Slice((y * width) + x)); - - x = xEnd; + for (int x = 0; x < width;) + { + int mode = (int)((modes[((y >> bits) * tilesPerRow) + (x >> bits)] >> 8) & 0xff); + int xEnd = x + (1 << bits); + if (xEnd > width) + { + xEnd = width; + } + + GetResidual( + width, + height, + upperRow, + currentRow, + currentMaxDiffs, + mode, + x, + xEnd, + y, + maxQuantization, + exact, + usedSubtractGreen, + nearLossless, + argb.Slice((y * width) + x)); + + x = xEnd; + } } } } diff --git a/src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs index 678eb696a..e337881cf 100644 --- a/src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs @@ -268,6 +268,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless Span bgra = this.Bgra.GetSpan(); Span encodedData = this.EncodedData.GetSpan(); + bool lowEffort = this.method == 0; // Analyze image (entropy, numPalettes etc). CrunchConfig[] crunchConfigs = this.EncoderAnalyze(bgra, width, height, out bool redAndBlueAlwaysZero); @@ -286,7 +287,15 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless (crunchConfig.EntropyIdx == EntropyIx.SpatialSubGreen); this.UsePredictorTransform = (crunchConfig.EntropyIdx == EntropyIx.Spatial) || (crunchConfig.EntropyIdx == EntropyIx.SpatialSubGreen); - this.UseCrossColorTransform = !redAndBlueAlwaysZero && this.UsePredictorTransform; + if (lowEffort) + { + this.UseCrossColorTransform = false; + } + else + { + this.UseCrossColorTransform = !redAndBlueAlwaysZero && this.UsePredictorTransform; + } + this.AllocateTransformBuffer(width, height); // Reset any parameter in the encoder that is set in the previous iteration. @@ -307,7 +316,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless // Encode palette. if (this.UsePalette) { - this.EncodePalette(); + this.EncodePalette(lowEffort); this.MapImageFromPalette(width, height); // If using a color cache, do not have it bigger than the number of colors. @@ -325,12 +334,12 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless if (this.UsePredictorTransform) { - this.ApplyPredictFilter(this.CurrentWidth, height); + this.ApplyPredictFilter(this.CurrentWidth, height, lowEffort); } if (this.UseCrossColorTransform) { - this.ApplyCrossColorFilter(this.CurrentWidth, height); + this.ApplyCrossColorFilter(this.CurrentWidth, height, lowEffort); } this.bitWriter.PutBits(0, 1); // No more transforms. @@ -341,7 +350,8 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless height, useCache, crunchConfig, - this.CacheBits); + this.CacheBits, + lowEffort); // If we are better than what we already have. if (isFirstConfig || this.bitWriter.NumBytes() < bestSize) @@ -462,7 +472,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless return crunchConfigs.ToArray(); } - private void EncodeImage(int width, int height, bool useCache, CrunchConfig config, int cacheBits) + private void EncodeImage(int width, int height, bool useCache, CrunchConfig config, int cacheBits, bool lowEffort) { // bgra data with transformations applied. Span bgra = this.EncodedData.GetSpan(); @@ -487,7 +497,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless } // Calculate backward references from BGRA image. - this.HashChain.Fill(this.memoryAllocator, bgra, this.quality, width, height); + this.HashChain.Fill(this.memoryAllocator, bgra, this.quality, width, height, lowEffort); Vp8LBitWriter bitWriterBest = config.SubConfigs.Count > 1 ? this.bitWriter.Clone() : this.bitWriter; Vp8LBitWriter bwInit = this.bitWriter; @@ -568,7 +578,8 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless this.Refs[2], LosslessUtils.SubSampleSize(width, this.HistoBits), LosslessUtils.SubSampleSize(height, this.HistoBits), - this.quality); + this.quality, + lowEffort); } // Store Huffman codes. @@ -615,7 +626,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless /// /// Save the palette to the bitstream. /// - private void EncodePalette() + private void EncodePalette(bool lowEffort) { Span tmpPalette = new uint[WebpConstants.MaxPaletteSize]; int paletteSize = this.PaletteSize; @@ -629,7 +640,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless } tmpPalette[0] = palette[0]; - this.EncodeImageNoHuffman(tmpPalette, this.HashChain, this.Refs[0], this.Refs[1], width: paletteSize, height: 1, quality: 20); + this.EncodeImageNoHuffman(tmpPalette, this.HashChain, this.Refs[0], this.Refs[1], width: paletteSize, height: 1, quality: 20, lowEffort); } /// @@ -642,7 +653,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless LosslessUtils.SubtractGreenFromBlueAndRed(this.EncodedData.GetSpan()); } - private void ApplyPredictFilter(int width, int height) + private void ApplyPredictFilter(int width, int height, bool lowEffort) { // We disable near-lossless quantization if palette is used. int nearLosslessStrength = this.UsePalette ? 100 : this.nearLosslessQuality; @@ -660,16 +671,17 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless this.nearLossless, nearLosslessStrength, this.exact, - this.UseSubtractGreenTransform); + this.UseSubtractGreenTransform, + lowEffort); this.bitWriter.PutBits(WebpConstants.TransformPresent, 1); this.bitWriter.PutBits((uint)Vp8LTransformType.PredictorTransform, 2); this.bitWriter.PutBits((uint)(predBits - 2), 3); - this.EncodeImageNoHuffman(this.TransformData.GetSpan(), this.HashChain, this.Refs[0], this.Refs[1], transformWidth, transformHeight, this.quality); + this.EncodeImageNoHuffman(this.TransformData.GetSpan(), this.HashChain, this.Refs[0], this.Refs[1], transformWidth, transformHeight, this.quality, lowEffort); } - private void ApplyCrossColorFilter(int width, int height) + private void ApplyCrossColorFilter(int width, int height, bool lowEffort) { int colorTransformBits = this.TransformBits; int transformWidth = LosslessUtils.SubSampleSize(width, colorTransformBits); @@ -681,10 +693,10 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless this.bitWriter.PutBits((uint)Vp8LTransformType.CrossColorTransform, 2); this.bitWriter.PutBits((uint)(colorTransformBits - 2), 3); - this.EncodeImageNoHuffman(this.TransformData.GetSpan(), this.HashChain, this.Refs[0], this.Refs[1], transformWidth, transformHeight, this.quality); + this.EncodeImageNoHuffman(this.TransformData.GetSpan(), this.HashChain, this.Refs[0], this.Refs[1], transformWidth, transformHeight, this.quality, lowEffort); } - private void EncodeImageNoHuffman(Span bgra, Vp8LHashChain hashChain, Vp8LBackwardRefs refsTmp1, Vp8LBackwardRefs refsTmp2, int width, int height, int quality) + private void EncodeImageNoHuffman(Span bgra, Vp8LHashChain hashChain, Vp8LBackwardRefs refsTmp1, Vp8LBackwardRefs refsTmp2, int width, int height, int quality, bool lowEffort) { int cacheBits = 0; ushort[] histogramSymbols = new ushort[1]; // Only one tree, one symbol. @@ -702,7 +714,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless } // Calculate backward references from the image pixels. - hashChain.Fill(this.memoryAllocator, bgra, quality, width, height); + hashChain.Fill(this.memoryAllocator, bgra, quality, width, height, lowEffort); Vp8LBackwardRefs refs = BackwardReferenceEncoder.GetBackwardReferences( width, diff --git a/src/ImageSharp/Formats/WebP/Lossless/Vp8LHashChain.cs b/src/ImageSharp/Formats/WebP/Lossless/Vp8LHashChain.cs index 0dafe8e9e..14b62b11a 100644 --- a/src/ImageSharp/Formats/WebP/Lossless/Vp8LHashChain.cs +++ b/src/ImageSharp/Formats/WebP/Lossless/Vp8LHashChain.cs @@ -54,7 +54,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless /// public int Size { get; } - public void Fill(MemoryAllocator memoryAllocator, ReadOnlySpan bgra, int quality, int xSize, int ySize) + public void Fill(MemoryAllocator memoryAllocator, ReadOnlySpan bgra, int quality, int xSize, int ySize, bool lowEffort) { int size = xSize * ySize; int iterMax = GetMaxItersForQuality(quality); @@ -147,32 +147,36 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless pos = chain[basePosition]; int currLength; - // Heuristic: use the comparison with the above line as an initialization. - if (basePosition >= (uint)xSize) + if (!lowEffort) { - currLength = LosslessUtils.FindMatchLength(bgra.Slice(bgraStart - xSize), bgra.Slice(bgraStart), bestLength, maxLen); + // Heuristic: use the comparison with the above line as an initialization. + if (basePosition >= (uint)xSize) + { + currLength = LosslessUtils.FindMatchLength(bgra.Slice(bgraStart - xSize), bgra.Slice(bgraStart), bestLength, maxLen); + if (currLength > bestLength) + { + bestLength = currLength; + bestDistance = (uint)xSize; + } + + iter--; + } + + // Heuristic: compare to the previous pixel. + currLength = LosslessUtils.FindMatchLength(bgra.Slice(bgraStart - 1), bgra.Slice(bgraStart), bestLength, maxLen); if (currLength > bestLength) { bestLength = currLength; - bestDistance = (uint)xSize; + bestDistance = 1; } iter--; - } - // Heuristic: compare to the previous pixel. - currLength = LosslessUtils.FindMatchLength(bgra.Slice(bgraStart - 1), bgra.Slice(bgraStart), bestLength, maxLen); - if (currLength > bestLength) - { - bestLength = currLength; - bestDistance = 1; - } - - iter--; - - if (bestLength == BackwardReferenceEncoder.MaxLength) - { - pos = minPos - 1; + // Skip the for loop if we already have the maximum. + if (bestLength == BackwardReferenceEncoder.MaxLength) + { + pos = minPos - 1; + } } uint bestBgra = bgra.Slice(bgraStart)[bestLength]; diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs index 6173ebfc3..2dee02a18 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs @@ -86,6 +86,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Webp [WithFile(RgbTestPattern, PixelTypes.Rgba32, 40)] [WithFile(RgbTestPattern, PixelTypes.Rgba32, 20)] [WithFile(RgbTestPattern, PixelTypes.Rgba32, 10)] + [WithFile(RgbTestPattern63x63, PixelTypes.Rgba32, 40)] public void Encode_Lossless_WithNearLosslessFlag_Works(TestImageProvider provider, int nearLosslessQuality) where TPixel : unmanaged, IPixel { diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 524ea9849..406f65289 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -509,6 +509,7 @@ namespace SixLabors.ImageSharp.Tests public const string TestPatternOpaque = "WebP/testpattern_opaque.png"; public const string TestPatternOpaqueSmall = "WebP/testpattern_opaque_small.png"; public const string RgbTestPattern = "WebP/rgb_pattern.png"; + public const string RgbTestPattern63x63 = "WebP/rgb_pattern_63x63.png"; // Test image for encoding image with a palette. public const string Flag = "WebP/flag_of_germany.png"; diff --git a/tests/Images/Input/WebP/rgb_pattern.png b/tests/Images/Input/WebP/rgb_pattern.png index d3c59cf88..554afd50c 100644 --- a/tests/Images/Input/WebP/rgb_pattern.png +++ b/tests/Images/Input/WebP/rgb_pattern.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f1ffed99a3cc701fff7f63cdca32c437a3e03d2a8a178380744190636decb0f8 -size 12453 +oid sha256:5150fccc821b2196678771d46567e01af4702c53e4031aee24e2611cecfdf48e +size 12841 diff --git a/tests/Images/Input/WebP/rgb_pattern_63x63.png b/tests/Images/Input/WebP/rgb_pattern_63x63.png new file mode 100644 index 000000000..37a6e8812 --- /dev/null +++ b/tests/Images/Input/WebP/rgb_pattern_63x63.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a7826312b4dabc2d8a89bf84e501ddb0bcc09932c54d2dedb0c96909da94da8 +size 12071