Browse Source

Analyze and create palette

pull/1552/head
Brian Popow 6 years ago
parent
commit
c72dd42406
  1. 34
      src/ImageSharp/Formats/WebP/BitWriter/Vp8BitWriter.cs
  2. 88
      src/ImageSharp/Formats/WebP/BitWriter/Vp8LBitWriter.cs
  3. 23
      src/ImageSharp/Formats/WebP/IWebPEncoderOptions.cs
  4. 22
      src/ImageSharp/Formats/WebP/Lossless/LosslessUtils.cs
  5. 36
      src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs
  6. 15
      src/ImageSharp/Formats/WebP/WebPConstants.cs
  7. 2
      src/ImageSharp/Formats/WebP/WebPDecoderCore.cs
  8. 12
      src/ImageSharp/Formats/WebP/WebPEncoder.cs
  9. 221
      src/ImageSharp/Formats/WebP/WebPEncoderCore.cs

34
src/ImageSharp/Formats/WebP/BitWriter/Vp8BitWriter.cs

@ -8,5 +8,39 @@ namespace SixLabors.ImageSharp.Formats.WebP.BitWriter
/// </summary>
internal class Vp8BitWriter
{
private uint range;
private uint value;
/// <summary>
/// Number of outstanding bits.
/// </summary>
private int run;
/// <summary>
/// Number of pending bits.
/// </summary>
private int nbBits;
private byte[] buffer;
private int pos;
private int maxPos;
private bool error;
public Vp8BitWriter(int expectedSize)
{
this.range = 255 - 1;
this.value = 0;
this.run = 0;
this.nbBits = -8;
this.pos = 0;
this.maxPos = 0;
this.error = false;
//BitWriterResize(expected_size);
}
}
}

88
src/ImageSharp/Formats/WebP/BitWriter/Vp8LBitWriter.cs

@ -1,6 +1,8 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the GNU Affero General Public License, Version 3.
using System;
namespace SixLabors.ImageSharp.Formats.WebP.BitWriter
{
/// <summary>
@ -8,5 +10,91 @@ namespace SixLabors.ImageSharp.Formats.WebP.BitWriter
/// </summary>
internal class Vp8LBitWriter
{
/// <summary>
/// This is the minimum amount of size the memory buffer is guaranteed to grow when extra space is needed.
/// </summary>
private const int MinExtraSize = 32768;
private const int WriterBytes = 4;
private const int WriterBits = 32;
private const int WriterMaxBits = 64;
/// <summary>
/// Bit accumulator.
/// </summary>
private ulong bits;
/// <summary>
/// Number of bits used in accumulator.
/// </summary>
private int used;
/// <summary>
/// Buffer to write to.
/// </summary>
private byte[] buffer;
/// <summary>
/// Current write position.
/// </summary>
private int cur;
private int end;
private bool error;
public Vp8LBitWriter(int expectedSize)
{
this.buffer = new byte[expectedSize];
}
/// <summary>
/// This function writes bits into bytes in increasing addresses (little endian),
/// and within a byte least-significant-bit first.
/// This function can write up to 32 bits in one go, but VP8LBitReader can only
/// read 24 bits max (VP8L_MAX_NUM_BIT_READ).
/// </summary>
public void PutBits(uint bits, int nBits)
{
if (nBits > 0)
{
if (this.used >= 32)
{
this.PutBitsFlushBits();
}
this.bits |= bits << this.used;
this.used += nBits;
}
}
/// <summary>
/// Internal function for PutBits flushing 32 bits from the written state.
/// </summary>
private void PutBitsFlushBits()
{
// If needed, make some room by flushing some bits out.
if (this.cur + WriterBytes > this.end)
{
var extraSize = (this.end - this.cur) + MinExtraSize;
if (!BitWriterResize(extraSize))
{
this.error = true;
return;
}
}
//*(vp8l_wtype_t*)bw->cur_ = (vp8l_wtype_t)WSWAP((vp8l_wtype_t)bw->bits_);
this.cur += WriterBytes;
this.bits >>= WriterBits;
this.used -= WriterBits;
}
private bool BitWriterResize(int extraSize)
{
return true;
}
}
}

23
src/ImageSharp/Formats/WebP/IWebPEncoderOptions.cs

@ -8,5 +8,28 @@ namespace SixLabors.ImageSharp.Formats.WebP
/// </summary>
internal interface IWebPEncoderOptions
{
/// <summary>
/// Gets a value indicating whether lossless compression should be used.
/// If false, lossy compression will be used.
/// </summary>
bool Lossless { get; }
/// <summary>
/// Gets the compression quality. Between 0 and 100.
/// For lossy, 0 gives the smallest size and 100 the largest. For lossless,
/// this parameter is the amount of effort put into the compression: 0 is the fastest but gives larger
/// files compared to the slowest, but best, 100.
/// </summary>
float Quality { get; }
/// <summary>
/// Gets a value indicating whether the alpha plane should be compressed with WebP lossless format.
/// </summary>
bool AlphaCompression { get; }
/// <summary>
/// Gets the number of entropy-analysis passes (in [1..10]).
/// </summary>
int EntropyPasses { get; }
}
}

22
src/ImageSharp/Formats/WebP/Lossless/LosslessUtils.cs

@ -294,6 +294,17 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
}
}
/// <summary>
/// Difference of each component, mod 256.
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
public static uint SubPixels(uint a, uint b)
{
uint alphaAndGreen = 0x00ff00ffu + (a & 0xff00ff00u) - (b & 0xff00ff00u);
uint redAndBlue = 0xff00ff00u + (a & 0x00ff00ffu) - (b & 0x00ff00ffu);
return (alphaAndGreen & 0xff00ff00u) | (redAndBlue & 0x00ff00ffu);
}
private static void PredictorAdd0(Span<uint> input, int startIdx, int numberOfPixels, Span<uint> output)
{
int endIdx = startIdx + numberOfPixels;
@ -629,17 +640,6 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
return (alphaAndGreen & 0xff00ff00u) | (redAndBlue & 0x00ff00ffu);
}
/// <summary>
/// Difference of each component, mod 256.
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
private static uint SubPixels(uint a, uint b)
{
uint alphaAndGreen = 0x00ff00ffu + (a & 0xff00ff00u) - (b & 0xff00ff00u);
uint redAndBlue = 0xff00ff00u + (a & 0x00ff00ffu) - (b & 0x00ff00ffu);
return (alphaAndGreen & 0xff00ff00u) | (redAndBlue & 0x00ff00ffu);
}
[MethodImpl(InliningOptions.ShortMethod)]
private static uint GetArgbIndex(uint idx)
{

36
src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs

@ -0,0 +1,36 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the GNU Affero General Public License, Version 3.
namespace SixLabors.ImageSharp.Formats.WebP.Lossless
{
/// <summary>
/// Encoder for lossless webp images.
/// </summary>
internal class Vp8LEncoder
{
/// <summary>
/// Gets a value indicating whether to use the cross color transform.
/// </summary>
public bool UseCrossColorTransform { get; }
/// <summary>
/// Gets a value indicating whether to use the substract green transform.
/// </summary>
public bool UseSubtractGreenTransform { get; }
/// <summary>
/// Gets a value indicating whether to use the predictor transform.
/// </summary>
public bool UsePredictorTransform { get; }
/// <summary>
/// Gets a value indicating whether to use color indexing transform.
/// </summary>
public bool UsePalette { get; }
/// <summary>
/// Gets the palette size.
/// </summary>
public int PaletteSize { get; }
}
}

15
src/ImageSharp/Formats/WebP/WebPConstants.cs

@ -67,6 +67,16 @@ namespace SixLabors.ImageSharp.Formats.WebP
/// </summary>
public const int Vp8LImageSizeBits = 14;
/// <summary>
/// The Vp8L version 0.
/// </summary>
public const int Vp8LVersion = 0;
/// <summary>
/// The maximum number of colors for a paletted images.
/// </summary>
public const int MaxPaletteSize = 256;
/// <summary>
/// Maximum number of color cache bits.
/// </summary>
@ -77,6 +87,11 @@ namespace SixLabors.ImageSharp.Formats.WebP
/// </summary>
public const int MaxNumberOfTransforms = 4;
/// <summary>
/// The maximum allowed width or height of a webp image.
/// </summary>
public const int MaxDimension = 16383;
public const int MaxAllowedCodeLength = 15;
public const int DefaultCodeLength = 8;

2
src/ImageSharp/Formats/WebP/WebPDecoderCore.cs

@ -78,7 +78,7 @@ namespace SixLabors.ImageSharp.Formats.WebP
this.Metadata = new ImageMetadata();
this.currentStream = stream;
uint fileSize = this.ReadImageHeader();
this.ReadImageHeader();
using WebPImageInfo imageInfo = this.ReadVp8Info();
if (imageInfo.Features != null && imageInfo.Features.Animation)
{

12
src/ImageSharp/Formats/WebP/WebPEncoder.cs

@ -12,6 +12,18 @@ namespace SixLabors.ImageSharp.Formats.WebP
/// </summary>
public sealed class WebPEncoder : IImageEncoder, IWebPEncoderOptions
{
/// <inheritdoc/>
public bool Lossless { get; set; }
/// <inheritdoc/>
public float Quality { get; set; }
/// <inheritdoc/>
public bool AlphaCompression { get; set; }
/// <inheritdoc/>
public int EntropyPasses { get; set; }
/// <inheritdoc/>
public void Encode<TPixel>(Image<TPixel> image, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>

221
src/ImageSharp/Formats/WebP/WebPEncoderCore.cs

@ -1,8 +1,12 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the GNU Affero General Public License, Version 3.
using System;
using System.Collections.Generic;
using System.IO;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Formats.WebP.BitWriter;
using SixLabors.ImageSharp.Formats.WebP.Lossless;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
@ -24,6 +28,11 @@ namespace SixLabors.ImageSharp.Formats.WebP
/// </summary>
private Configuration configuration;
/// <summary>
/// A bit writer for writing lossless webp streams.
/// </summary>
private Vp8LBitWriter bitWriter;
/// <summary>
/// Initializes a new instance of the <see cref="WebPEncoderCore"/> class.
/// </summary>
@ -48,6 +57,218 @@ namespace SixLabors.ImageSharp.Formats.WebP
this.configuration = image.GetConfiguration();
ImageMetadata metadata = image.Metadata;
int width = image.Width;
int height = image.Height;
int initialSize = width * height;
this.bitWriter = new Vp8LBitWriter(initialSize);
// Write image size.
this.WriteImageSize(width, height);
// Write the non-trivial Alpha flag and lossless version.
bool hasAlpha = false; // TODO: for the start, this will be always false.
this.WriteRealAlphaAndVersion(hasAlpha);
// Encode the main image stream.
this.EncodeStream(image);
}
private void WriteImageSize(int inputImgWidth, int inputImgHeight)
{
Guard.MustBeLessThan(inputImgWidth, WebPConstants.MaxDimension, nameof(inputImgWidth));
Guard.MustBeLessThan(inputImgHeight, WebPConstants.MaxDimension, nameof(inputImgHeight));
uint width = (uint)inputImgWidth - 1;
uint height = (uint)inputImgHeight - 1;
this.bitWriter.PutBits(width, WebPConstants.Vp8LImageSizeBits);
this.bitWriter.PutBits(height, WebPConstants.Vp8LImageSizeBits);
}
private void WriteRealAlphaAndVersion(bool hasAlpha)
{
this.bitWriter.PutBits(hasAlpha ? 1U : 0, 1);
this.bitWriter.PutBits(WebPConstants.Vp8LVersion, WebPConstants.Vp8LVersionBits);
}
private void EncodeStream<TPixel>(Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
var encoder = new Vp8LEncoder();
// Analyze image (entropy, num_palettes etc).
this.EncoderAnalyze(image);
}
/// <summary>
/// Analyzes the image and decides what transforms should be used.
/// </summary>
private void EncoderAnalyze<TPixel>(Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
// TODO: low effort is always false for now.
bool lowEffort = false;
// Check if we only deal with a small number of colors and should use a palette.
var usePalette = this.AnalyzeAndCreatePalette(image, lowEffort);
}
/// <summary>
/// If number of colors in the image is less than or equal to MAX_PALETTE_SIZE,
/// creates a palette and returns true, else returns false.
/// </summary>
/// <returns>true, if a palette should be used.</returns>
private bool AnalyzeAndCreatePalette<TPixel>(Image<TPixel> image, bool lowEffort)
where TPixel : unmanaged, IPixel<TPixel>
{
int numColors = this.GetColorPalette(image, out uint[] palette);
if (numColors > WebPConstants.MaxPaletteSize)
{
return false;
}
// TODO: figure out how the palette needs to be sorted.
Array.Sort(palette);
if (!lowEffort && PaletteHasNonMonotonousDeltas(palette, numColors))
{
GreedyMinimizeDeltas(palette, numColors);
}
return true;
}
private int GetColorPalette<TPixel>(Image<TPixel> image, out uint[] palette)
where TPixel : unmanaged, IPixel<TPixel>
{
Rgba32 color = default;
palette = null;
var colors = new HashSet<TPixel>();
for (int y = 0; y < image.Height; y++)
{
System.Span<TPixel> rowSpan = image.GetPixelRowSpan(y);
for (int x = 0; x < rowSpan.Length; x++)
{
colors.Add(rowSpan[x]);
if (colors.Count > WebPConstants.MaxPaletteSize)
{
// Exact count is not needed, because a palette will not be used then anyway.
return WebPConstants.MaxPaletteSize + 1;
}
}
}
// Fill the colors into the palette.
palette = new uint[colors.Count];
using HashSet<TPixel>.Enumerator colorEnumerator = colors.GetEnumerator();
int idx = 0;
while (colorEnumerator.MoveNext())
{
colorEnumerator.Current.ToRgba32(ref color);
var bgra = new Bgra32(color.R, color.G, color.B, color.A);
palette[idx++] = bgra.PackedValue;
}
return colors.Count;
}
/// <summary>
/// The palette has been sorted by alpha. This function checks if the other components of the palette
/// have a monotonic development with regards to position in the palette.
/// If all have monotonic development, there is no benefit to re-organize them greedily. A monotonic development
/// would be spotted in green-only situations (like lossy alpha) or gray-scale images.
/// </summary>
/// <param name="palette">The palette.</param>
/// <param name="numColors">Number of colors in the palette.</param>
/// <returns>True, if the palette has no monotonous deltas.</returns>
private static bool PaletteHasNonMonotonousDeltas(uint[] palette, int numColors)
{
uint predict = 0x000000;
byte signFound = 0x00;
for (int i = 0; i < numColors; ++i)
{
uint diff = LosslessUtils.SubPixels(palette[i], predict);
byte rd = (byte)((diff >> 16) & 0xff);
byte gd = (byte)((diff >> 8) & 0xff);
byte bd = (byte)((diff >> 0) & 0xff);
if (rd != 0x00)
{
signFound |= (byte)((rd < 0x80) ? 1 : 2);
}
if (gd != 0x00)
{
signFound |= (byte)((gd < 0x80) ? 8 : 16);
}
if (bd != 0x00)
{
signFound |= (byte)((bd < 0x80) ? 64 : 128);
}
}
return (signFound & (signFound << 1)) != 0; // two consequent signs.
}
/// <summary>
/// Find greedily always the closest color of the predicted color to minimize
/// deltas in the palette. This reduces storage needs since the palette is stored with delta encoding.
/// </summary>
/// <param name="palette">The palette.</param>
/// <param name="numColors">The number of colors in the palette.</param>
private static void GreedyMinimizeDeltas(uint[] palette, int numColors)
{
uint predict = 0x00000000;
for (int i = 0; i < numColors; ++i)
{
int bestIdx = i;
uint bestScore = ~0U;
for (int k = i; k < numColors; ++k)
{
uint curScore = PaletteColorDistance(palette[k], predict);
if (bestScore > curScore)
{
bestScore = curScore;
bestIdx = k;
}
}
// swap color(palette[bestIdx], palette[i]);
uint best = palette[bestIdx];
palette[bestIdx] = palette[i];
palette[i] = best;
predict = palette[i];
}
}
/// <summary>
/// Computes a value that is related to the entropy created by the
/// palette entry diff.
///
/// Note that the last & 0xff is a no-operation in the next statement, but
/// removed by most compilers and is here only for regularity of the code.
/// </summary>
/// <param name="col1">First color.</param>
/// <param name="col2">Second color.</param>
/// <returns>The color distance.</returns>
private static uint PaletteColorDistance(uint col1, uint col2)
{
uint diff = LosslessUtils.SubPixels(col1, col2);
int moreWeightForRGBThanForAlpha = 9;
uint score = PaletteComponentDistance((diff >> 0) & 0xff);
score += PaletteComponentDistance((diff >> 8) & 0xff);
score += PaletteComponentDistance((diff >> 16) & 0xff);
score *= moreWeightForRGBThanForAlpha;
score += PaletteComponentDistance((diff >> 24) & 0xff);
return score;
}
private static uint PaletteComponentDistance(uint v)
{
return (v <= 128) ? v : (256 - v);
}
}
}

Loading…
Cancel
Save