diff --git a/src/ImageSharp/Formats/WebP/IWebpEncoderOptions.cs b/src/ImageSharp/Formats/WebP/IWebpEncoderOptions.cs index 6c8449772b..2cf2cd3111 100644 --- a/src/ImageSharp/Formats/WebP/IWebpEncoderOptions.cs +++ b/src/ImageSharp/Formats/WebP/IWebpEncoderOptions.cs @@ -39,6 +39,14 @@ namespace SixLabors.ImageSharp.Formats.Webp /// int EntropyPasses { get; } + /// + /// Gets the amplitude of the spatial noise shaping. Spatial noise shaping (or sns for short) refers to a general collection of built-in algorithms + /// used to decide which area of the picture should use relatively less bits, and where else to better transfer these bits. + /// The possible range goes from 0 (algorithm is off) to 100 (the maximal effect). + /// Defaults to 50. + /// + int SpatialNoiseShaping { get; } + /// /// Gets the strength of the deblocking filter, between 0 (no filtering) and 100 (maximum filtering). /// A value of 0 will turn off any filtering. Higher value will increase the strength of the filtering process applied after decoding the picture. diff --git a/src/ImageSharp/Formats/WebP/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/WebP/Lossy/Vp8Encoder.cs index ee3f1a8a5b..9da7217b8a 100644 --- a/src/ImageSharp/Formats/WebP/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/WebP/Lossy/Vp8Encoder.cs @@ -47,6 +47,11 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy /// private readonly int filterStrength; + /// + /// The spatial noise shaping. 0=off, 100=maximum. + /// + private readonly int spatialNoiseShaping; + /// /// A bit writer for writing lossy webp streams. /// @@ -94,7 +99,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy /// Quality/speed trade-off (0=fast, 6=slower-better). /// Number of entropy-analysis passes (in [1..10]). /// The filter the strength of the deblocking filter, between 0 (no filtering) and 100 (maximum filtering). - public Vp8Encoder(MemoryAllocator memoryAllocator, Configuration configuration, int width, int height, int quality, int method, int entropyPasses, int filterStrength) + public Vp8Encoder(MemoryAllocator memoryAllocator, Configuration configuration, int width, int height, int quality, int method, int entropyPasses, int filterStrength, int spatialNoiseShaping) { this.memoryAllocator = memoryAllocator; this.configuration = configuration; @@ -104,6 +109,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy this.method = Numerics.Clamp(method, 0, 6); this.entropyPasses = Numerics.Clamp(entropyPasses, 1, 10); this.filterStrength = Numerics.Clamp(filterStrength, 0, 100); + this.spatialNoiseShaping = Numerics.Clamp(spatialNoiseShaping, 0, 100); this.rdOptLevel = (method >= 6) ? Vp8RdLevel.RdOptTrellisAll : (method >= 5) ? Vp8RdLevel.RdOptTrellis : (method >= 3) ? Vp8RdLevel.RdOptBasic @@ -353,7 +359,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy { int targetSize = 0; // TODO: target size is hardcoded. float targetPsnr = 0.0f; // TODO: targetPsnr is hardcoded. - bool doSearch = false; // TODO: doSearch hardcoded for now. + bool doSearch = targetSize > 0 || targetPsnr > 0; bool fastProbe = (this.method == 0 || this.method == 3) && !doSearch; int numPassLeft = this.entropyPasses; Vp8RdLevel rdOpt = (this.method >= 3 || doSearch) ? Vp8RdLevel.RdOptBasic : Vp8RdLevel.RdOptNone; @@ -663,8 +669,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy { int nb = this.SegmentHeader.NumSegments; Vp8SegmentInfo[] dqm = this.SegmentInfos; - int snsStrength = 50; // TODO: Spatial Noise Shaping, hardcoded for now. - double amp = WebpConstants.SnsToDq * snsStrength / 100.0d / 128.0d; + double amp = WebpConstants.SnsToDq * this.spatialNoiseShaping / 100.0d / 128.0d; double cBase = QualityToCompression(quality / 100.0d); for (int i = 0; i < nb; ++i) { @@ -685,25 +690,24 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy 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; + this.DqUvAc = this.DqUvAc * this.spatialNoiseShaping / 100; // and make it safe. 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 = Numerics.Clamp(this.DqUvDc, -15, 15); // 4bit-signed max allowed + // U/V channels are quite more reactive to high quants (flat DC-blocks tend to appear, and are unpleasant). + this.DqUvDc = -4 * this.spatialNoiseShaping / 100; + this.DqUvDc = Numerics.Clamp(this.DqUvDc, -15, 15); // 4bit-signed max allowed. this.DqY1Dc = 0; this.DqY2Dc = 0; this.DqY2Ac = 0; - // Initialize segments' filtering + // Initialize segments' filtering. this.SetupFilterStrength(); - this.SetupMatrices(dqm, snsStrength); + this.SetupMatrices(dqm); } private void SetupFilterStrength() @@ -784,9 +788,9 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy proba.NbSkip = 0; } - private void SetupMatrices(Vp8SegmentInfo[] dqm, int snsStrength) + private void SetupMatrices(Vp8SegmentInfo[] dqm) { - int tlambdaScale = (this.method >= 4) ? snsStrength : 0; + int tlambdaScale = (this.method >= 4) ? this.spatialNoiseShaping : 0; for (int i = 0; i < dqm.Length; ++i) { Vp8SegmentInfo m = dqm[i]; diff --git a/src/ImageSharp/Formats/WebP/WebpEncoder.cs b/src/ImageSharp/Formats/WebP/WebpEncoder.cs index eb7148386f..e9b9fbbe69 100644 --- a/src/ImageSharp/Formats/WebP/WebpEncoder.cs +++ b/src/ImageSharp/Formats/WebP/WebpEncoder.cs @@ -29,6 +29,9 @@ namespace SixLabors.ImageSharp.Formats.Webp /// public int EntropyPasses { get; set; } + /// + public int SpatialNoiseShaping { get; set; } = 50; + /// public int FilterStrength { get; set; } = 60; diff --git a/src/ImageSharp/Formats/WebP/WebpEncoderCore.cs b/src/ImageSharp/Formats/WebP/WebpEncoderCore.cs index b515bd48ba..ae7bce72fa 100644 --- a/src/ImageSharp/Formats/WebP/WebpEncoderCore.cs +++ b/src/ImageSharp/Formats/WebP/WebpEncoderCore.cs @@ -47,6 +47,11 @@ namespace SixLabors.ImageSharp.Formats.Webp /// private readonly int entropyPasses; + /// + /// Spatial Noise Shaping. 0=off, 100=maximum. + /// + private readonly int spatialNoiseShaping; + /// /// The filter the strength of the deblocking filter, between 0 (no filtering) and 100 (maximum filtering). /// @@ -86,6 +91,7 @@ namespace SixLabors.ImageSharp.Formats.Webp this.quality = options.Quality; this.method = options.Method; this.entropyPasses = options.EntropyPasses; + this.spatialNoiseShaping = options.SpatialNoiseShaping; this.filterStrength = options.FilterStrength; this.exact = options.Exact; this.nearLossless = options.NearLossless; @@ -117,7 +123,8 @@ namespace SixLabors.ImageSharp.Formats.Webp this.quality, this.method, this.entropyPasses, - this.filterStrength); + this.filterStrength, + this.spatialNoiseShaping); enc.Encode(image, stream); } else diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs index 8831e6054b..8374342e85 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs @@ -188,6 +188,26 @@ namespace SixLabors.ImageSharp.Tests.Formats.Webp image.VerifyEncoder(provider, "webp", testOutputDetails, encoder, customComparer: GetComparer(75)); } + [Theory] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 100)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 80)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 50)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 30)] + [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 10)] + public void Encode_Lossy_WithDifferentSpatialNoiseShapingStrength_Works(TestImageProvider provider, int snsStrength) + where TPixel : unmanaged, IPixel + { + var encoder = new WebpEncoder() + { + Lossy = true, + SpatialNoiseShaping = snsStrength + }; + + using Image image = provider.GetImage(); + string testOutputDetails = string.Concat("lossy", "_sns", snsStrength); + image.VerifyEncoder(provider, "webp", testOutputDetails, encoder, customComparer: GetComparer(75)); + } + [Theory] [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 0, 75)] [WithFile(Lossy.NoFilter06, PixelTypes.Rgba32, 1, 75)]