Browse Source

Write Vp8 partition 0 and frame header

pull/1552/head
Brian Popow 6 years ago
parent
commit
92c51d8af1
  1. 52
      src/ImageSharp/Formats/WebP/BitWriter/BitWriterBase.cs
  2. 369
      src/ImageSharp/Formats/WebP/BitWriter/Vp8BitWriter.cs
  3. 28
      src/ImageSharp/Formats/WebP/BitWriter/Vp8LBitWriter.cs
  4. 2
      src/ImageSharp/Formats/WebP/Lossless/Vp8LEncoder.cs
  5. 270
      src/ImageSharp/Formats/WebP/Lossy/Vp8Encoder.cs
  6. 10
      src/ImageSharp/Formats/WebP/Lossy/Vp8FilterHeader.cs
  7. 2
      src/ImageSharp/Formats/WebP/Lossy/Vp8Residual.cs
  8. 3
      src/ImageSharp/Formats/WebP/Lossy/Vp8SegmentHeader.cs
  9. 25
      src/ImageSharp/Formats/WebP/WebPConstants.cs
  10. 10
      src/ImageSharp/Formats/WebP/WebPDecoderCore.cs
  11. 56
      src/ImageSharp/Formats/WebP/WebPLookupTables.cs

52
src/ImageSharp/Formats/WebP/BitWriter/BitWriterBase.cs

@ -61,6 +61,12 @@ namespace SixLabors.ImageSharp.Formats.WebP.BitWriter
/// </summary>
public abstract void Finish();
/// <summary>
/// Writes the encoded image to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
public abstract void WriteEncodedImageToStream(Stream stream);
protected bool ResizeBuffer(int maxBytes, int sizeRequired)
{
if (maxBytes > 0 && sizeRequired < maxBytes)
@ -83,59 +89,19 @@ namespace SixLabors.ImageSharp.Formats.WebP.BitWriter
return false;
}
/// <summary>
/// Writes the encoded image to the stream.
/// </summary>
/// <param name="lossy">If true, lossy tag will be written, otherwise a lossless tag.</param>
/// <param name="stream">The stream to write to.</param>
public void WriteEncodedImageToStream(bool lossy, Stream stream)
{
this.Finish();
var numBytes = this.NumBytes();
var size = numBytes;
if (!lossy)
{
size++; // One byte extra for the VP8L signature.
}
var pad = size & 1;
var riffSize = WebPConstants.TagSize + WebPConstants.ChunkHeaderSize + size + pad;
this.WriteRiffHeader(riffSize, size, lossy, stream);
this.WriteToStream(stream);
if (pad == 1)
{
stream.WriteByte(0);
}
}
/// <summary>
/// Writes the RIFF header to the stream.
/// </summary>
/// <param name="riffSize">The block length.</param>
/// <param name="size">The size in bytes of the encoded image.</param>
/// <param name="lossy">If true, lossy tag will be written, otherwise a lossless tag.</param>
/// <param name="stream">The stream to write to.</param>
private void WriteRiffHeader(int riffSize, int size, bool lossy, Stream stream)
/// <param name="riffSize">The block length.</param>
protected void WriteRiffHeader(Stream stream, uint riffSize)
{
Span<byte> buffer = stackalloc byte[4];
stream.Write(WebPConstants.RiffFourCc);
BinaryPrimitives.WriteUInt32LittleEndian(buffer, (uint)riffSize);
BinaryPrimitives.WriteUInt32LittleEndian(buffer, riffSize);
stream.Write(buffer);
stream.Write(WebPConstants.WebPHeader);
if (lossy)
{
stream.Write(WebPConstants.Vp8MagicBytes);
}
else
{
stream.Write(WebPConstants.Vp8LMagicBytes);
}
BinaryPrimitives.WriteUInt32LittleEndian(buffer, (uint)size);
stream.Write(buffer);
stream.WriteByte(WebPConstants.Vp8LMagicByte);
}
}
}

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

@ -1,6 +1,9 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers.Binary;
using System.IO;
using SixLabors.ImageSharp.Formats.WebP.Lossy;
namespace SixLabors.ImageSharp.Formats.WebP.BitWriter
@ -10,6 +13,27 @@ namespace SixLabors.ImageSharp.Formats.WebP.BitWriter
/// </summary>
internal class Vp8BitWriter : BitWriterBase
{
#pragma warning disable SA1310 // Field names should not contain underscore
private const int DC_PRED = 0;
private const int TM_PRED = 1;
private const int V_PRED = 2;
private const int H_PRED = 3;
// 4x4 modes
private const int B_DC_PRED = 0;
private const int B_TM_PRED = 1;
private const int B_VE_PRED = 2;
private const int B_HE_PRED = 3;
private const int B_RD_PRED = 4;
private const int B_VR_PRED = 5;
private const int B_LD_PRED = 6;
private const int B_VL_PRED = 7;
private const int B_HD_PRED = 8;
private const int B_HU_PRED = 9;
#pragma warning restore SA1310 // Field names should not contain underscore
private readonly Vp8Encoder enc;
private int range;
private int value;
@ -43,6 +67,17 @@ namespace SixLabors.ImageSharp.Formats.WebP.BitWriter
this.maxPos = 0;
}
/// <summary>
/// Initializes a new instance of the <see cref="Vp8BitWriter"/> class.
/// </summary>
/// <param name="expectedSize">The expected size in bytes.</param>
/// <param name="enc">The Vp8Encoder.</param>
public Vp8BitWriter(int expectedSize, Vp8Encoder enc)
: this(expectedSize)
{
this.enc = enc;
}
/// <inheritdoc/>
public override int NumBytes()
{
@ -180,6 +215,74 @@ namespace SixLabors.ImageSharp.Formats.WebP.BitWriter
this.Flush();
}
public void PutSegment(int s, Span<byte> p)
{
if (this.PutBit(s >= 2, p[0]))
{
p = p.Slice(1);
}
this.PutBit(s & 1, p[1]);
}
public void PutI16Mode(int mode)
{
if (this.PutBit(mode == TM_PRED || mode == H_PRED, 156))
{
this.PutBit(mode == TM_PRED, 128); // TM or HE
}
else
{
this.PutBit(mode == V_PRED, 163); // VE or DC
}
}
public int PutI4Mode(int mode, Span<byte> prob)
{
if (this.PutBit(mode != B_DC_PRED, prob[0]))
{
if (this.PutBit(mode != B_TM_PRED, prob[1]))
{
if (this.PutBit(mode != B_VE_PRED, prob[2]))
{
if (!this.PutBit(mode >= B_LD_PRED, prob[3]))
{
if (this.PutBit(mode != B_HE_PRED, prob[4]))
{
this.PutBit(mode != B_RD_PRED, prob[5]);
}
}
else
{
if (this.PutBit(mode != B_LD_PRED, prob[6]))
{
if (this.PutBit(mode != B_VL_PRED, prob[7]))
{
this.PutBit(mode != B_HD_PRED, prob[8]);
}
}
}
}
}
}
return mode;
}
public void PutUvMode(int uvMode)
{
// DC_PRED
if (this.PutBit(uvMode != DC_PRED, 142))
{
// V_PRED
if (this.PutBit(uvMode != V_PRED, 114))
{
// H_PRED
this.PutBit(uvMode != H_PRED, 183);
}
}
}
private void PutBits(uint value, int nbBits)
{
for (uint mask = 1u << (nbBits - 1); mask != 0; mask >>= 1)
@ -249,6 +352,24 @@ namespace SixLabors.ImageSharp.Formats.WebP.BitWriter
return bit;
}
private void PutSignedBits(int value, int nbBits)
{
if (this.PutBitUniform(value != 0 ? 1 : 0) == 0)
{
return;
}
if (value < 0)
{
var valueToWrite = ((-value) << 1) | 1;
this.PutBits((uint)valueToWrite, nbBits + 1);
}
else
{
this.PutBits((uint)(value << 1), nbBits + 1);
}
}
private void Flush()
{
int s = 8 + this.nbBits;
@ -286,5 +407,253 @@ namespace SixLabors.ImageSharp.Formats.WebP.BitWriter
this.run++; // Delay writing of bytes 0xff, pending eventual carry.
}
}
/// <inheritdoc/>
public override void WriteEncodedImageToStream(Stream stream)
{
this.Finish();
uint numBytes = (uint)this.NumBytes();
int mbSize = this.enc.Mbw * this.enc.Mbh;
int expectedSize = mbSize * 7 / 8;
var bitWriterPartZero = new Vp8BitWriter(expectedSize);
// Partition #0 with header and partition sizes
uint size0 = this.GeneratePartition0(bitWriterPartZero);
uint vp8Size = WebPConstants.Vp8FrameHeaderSize + size0;
vp8Size += numBytes;
uint pad = vp8Size & 1;
vp8Size += pad;
// Compute RIFF size
// At the minimum it is: "WEBPVP8 nnnn" + VP8 data size.
var riffSize = WebPConstants.TagSize + WebPConstants.ChunkHeaderSize + vp8Size;
// Emit headers and partition #0
this.WriteWebPHeaders(stream, size0, vp8Size, riffSize);
bitWriterPartZero.WriteToStream(stream);
// Write the encoded image to the stream.
this.WriteToStream(stream);
if (pad == 1)
{
stream.WriteByte(0);
}
}
private uint GeneratePartition0(Vp8BitWriter bitWriter)
{
bitWriter.PutBitUniform(0); // colorspace
bitWriter.PutBitUniform(0); // clamp type
this.WriteSegmentHeader(bitWriter);
this.WriteFilterHeader(bitWriter);
bitWriter.PutBits(0, 2);
this.WriteQuant(bitWriter);
bitWriter.PutBitUniform(0);
this.WriteProbas(bitWriter);
this.CodeIntraModes(bitWriter);
bitWriter.Finish();
return (uint)bitWriter.NumBytes();
}
private void WriteSegmentHeader(Vp8BitWriter bitWriter)
{
Vp8EncSegmentHeader hdr = this.enc.SegmentHeader;
Vp8EncProba proba = this.enc.Proba;
if (bitWriter.PutBitUniform(hdr.NumSegments > 1 ? 1 : 0) != 0)
{
// We always 'update' the quant and filter strength values.
int updateData = 1;
bitWriter.PutBitUniform(hdr.UpdateMap ? 1 : 0);
if (bitWriter.PutBitUniform(updateData) != 0)
{
// We always use absolute values, not relative ones.
bitWriter.PutBitUniform(1); // (segment_feature_mode = 1. Paragraph 9.3.)
for (int s = 0; s < WebPConstants.NumMbSegments; ++s)
{
bitWriter.PutSignedBits(this.enc.SegmentInfos[s].Quant, 7);
}
for (int s = 0; s < WebPConstants.NumMbSegments; ++s)
{
bitWriter.PutSignedBits(this.enc.SegmentInfos[s].FStrength, 6);
}
}
if (hdr.UpdateMap)
{
for (int s = 0; s < 3; ++s)
{
if (bitWriter.PutBitUniform((proba.Segments[s] != 255) ? 1 : 0) != 0)
{
bitWriter.PutBits(proba.Segments[s], 8);
}
}
}
}
}
private void WriteFilterHeader(Vp8BitWriter bitWriter)
{
Vp8FilterHeader hdr = this.enc.FilterHeader;
var useLfDelta = hdr.I4x4LfDelta != 0;
bitWriter.PutBitUniform(hdr.Simple ? 1 : 0);
bitWriter.PutBits((uint)hdr.FilterLevel, 6);
bitWriter.PutBits((uint)hdr.Sharpness, 3);
if (bitWriter.PutBitUniform(useLfDelta ? 1 : 0) != 0)
{
// '0' is the default value for i4x4LfDelta at frame #0.
bool needUpdate = hdr.I4x4LfDelta != 0;
if (bitWriter.PutBitUniform(needUpdate ? 1 : 0) != 0)
{
// we don't use refLfDelta => emit four 0 bits.
bitWriter.PutBits(0, 4);
// we use modeLfDelta for i4x4
bitWriter.PutSignedBits(hdr.I4x4LfDelta, 6);
bitWriter.PutBits(0, 3); // all others unused.
}
}
}
// Nominal quantization parameters
private void WriteQuant(Vp8BitWriter bitWriter)
{
bitWriter.PutBits((uint)this.enc.BaseQuant, 7);
bitWriter.PutSignedBits(this.enc.DqY1Dc, 4);
bitWriter.PutSignedBits(this.enc.DqY2Dc, 4);
bitWriter.PutSignedBits(this.enc.DqY2Ac, 4);
bitWriter.PutSignedBits(this.enc.DqUvDc, 4);
bitWriter.PutSignedBits(this.enc.DqUvAc, 4);
}
private void WriteProbas(Vp8BitWriter bitWriter)
{
Vp8EncProba probas = this.enc.Proba;
for (int t = 0; t < WebPConstants.NumTypes; ++t)
{
for (int b = 0; b < WebPConstants.NumBands; ++b)
{
for (int c = 0; c < WebPConstants.NumCtx; ++c)
{
for (int p = 0; p < WebPConstants.NumProbas; ++p)
{
byte p0 = probas.Coeffs[t][b].Probabilities[c].Probabilities[p];
bool update = p0 != WebPLookupTables.DefaultCoeffsProba[t, b, c, p];
if (bitWriter.PutBit(update, WebPLookupTables.CoeffsUpdateProba[t, b, c, p]))
{
bitWriter.PutBits(p0, 8);
}
}
}
}
}
if (bitWriter.PutBitUniform(probas.UseSkipProba ? 1 : 0) != 0)
{
bitWriter.PutBits(probas.SkipProba, 8);
}
}
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);
int predsWidth = this.enc.PredsWidth;
do
{
Vp8MacroBlockInfo mb = it.CurrentMacroBlockInfo;
Span<byte> preds = it.Preds.AsSpan(it.PredIdx);
if (this.enc.SegmentHeader.UpdateMap)
{
bitWriter.PutSegment(mb.Segment, this.enc.Proba.Segments);
}
if (this.enc.Proba.UseSkipProba)
{
bitWriter.PutBit(mb.Skip, this.enc.Proba.SkipProba);
}
if (bitWriter.PutBit(mb.MacroBlockType != 0, 145))
{
// i16x16
bitWriter.PutI16Mode(preds[0]);
}
else
{
Span<byte> topPred = it.Preds.AsSpan(it.PredIdx);
int x, y;
for (y = 0; y < 4; ++y)
{
int left = preds[it.PredIdx - 1];
for (x = 0; x < 4; ++x)
{
byte[] probas = WebPLookupTables.ModesProba[topPred[x], left];
left = bitWriter.PutI4Mode(preds[x], probas);
}
topPred = preds;
preds = preds.Slice(predsWidth);
}
}
bitWriter.PutUvMode(mb.UvMode);
}
while (it.Next());
}
private void WriteWebPHeaders(Stream stream, uint size0, uint vp8Size, uint riffSize)
{
this.WriteRiffHeader(stream, riffSize);
this.WriteVp8Header(stream, vp8Size);
this.WriteFrameHeader(stream, size0);
}
private void WriteVp8Header(Stream stream, uint size)
{
Span<byte> vp8ChunkHeader = stackalloc byte[WebPConstants.ChunkHeaderSize];
WebPConstants.Vp8MagicBytes.AsSpan().CopyTo(vp8ChunkHeader);
BinaryPrimitives.WriteUInt32LittleEndian(vp8ChunkHeader.Slice(4), size);
stream.Write(vp8ChunkHeader);
}
private void WriteFrameHeader(Stream stream, uint size0)
{
uint profile = 0;
int width = this.enc.Width;
int height = this.enc.Height;
var vp8FrameHeader = new byte[WebPConstants.Vp8FrameHeaderSize];
// Paragraph 9.1.
uint bits = 0 // keyframe (1b)
| (profile << 1) // profile (3b)
| (1 << 4) // visible (1b)
| (size0 << 5); // partition length (19b)
vp8FrameHeader[0] = (byte)((bits >> 0) & 0xff);
vp8FrameHeader[1] = (byte)((bits >> 8) & 0xff);
vp8FrameHeader[2] = (byte)((bits >> 16) & 0xff);
// signature
vp8FrameHeader[3] = WebPConstants.Vp8HeaderMagicBytes[0];
vp8FrameHeader[4] = WebPConstants.Vp8HeaderMagicBytes[1];
vp8FrameHeader[5] = WebPConstants.Vp8HeaderMagicBytes[2];
// dimensions
vp8FrameHeader[6] = (byte)(width & 0xff);
vp8FrameHeader[7] = (byte)(width >> 8);
vp8FrameHeader[8] = (byte)(height & 0xff);
vp8FrameHeader[9] = (byte)(height >> 8);
stream.Write(vp8FrameHeader);
}
}
}

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

@ -3,6 +3,7 @@
using System;
using System.Buffers.Binary;
using System.IO;
using SixLabors.ImageSharp.Formats.WebP.Lossless;
namespace SixLabors.ImageSharp.Formats.WebP.BitWriter
@ -126,6 +127,33 @@ namespace SixLabors.ImageSharp.Formats.WebP.BitWriter
this.used = 0;
}
/// <inheritdoc/>
public override void WriteEncodedImageToStream(Stream stream)
{
Span<byte> buffer = stackalloc byte[4];
this.Finish();
uint size = (uint)this.NumBytes();
size++; // One byte extra for the VP8L signature.
// Write RIFF header.
uint pad = size & 1;
uint riffSize = WebPConstants.TagSize + WebPConstants.ChunkHeaderSize + size + pad;
this.WriteRiffHeader(stream, riffSize);
stream.Write(WebPConstants.Vp8LMagicBytes);
// Write Vp8 Header.
BinaryPrimitives.WriteUInt32LittleEndian(buffer, size);
stream.Write(buffer);
stream.WriteByte(WebPConstants.Vp8LHeaderMagicByte);
this.WriteToStream(stream);
if (pad == 1)
{
stream.WriteByte(0);
}
}
/// <summary>
/// Internal function for PutBits flushing 32 bits from the written state.
/// </summary>

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

@ -186,7 +186,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless
this.EncodeStream(image);
// Write bytes from the bitwriter buffer to the stream.
this.bitWriter.WriteEncodedImageToStream(lossy: false, stream);
this.bitWriter.WriteEncodedImageToStream(stream);
}
/// <summary>

270
src/ImageSharp/Formats/WebP/Lossy/Vp8Encoder.cs

@ -62,6 +62,16 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
/// </summary>
private Vp8EncSegmentHeader segmentHeader;
/// <summary>
/// The filter header info's.
/// </summary>
private Vp8FilterHeader filterHeader;
/// <summary>
/// The segment infos.
/// </summary>
private Vp8SegmentInfo[] segmentInfos;
/// <summary>
/// Contextual macroblock infos.
/// </summary>
@ -74,6 +84,12 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
private readonly Vp8RdLevel rdOptLevel;
private int dqY1Dc;
private int dqY2Dc;
private int dqY2Ac;
private int dqUvDc;
private int dqUvAc;
@ -122,6 +138,9 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
private const int QMax = 100;
// TODO: filterStrength is hardcoded, should be configurable.
private const int FilterStrength = 60;
/// <summary>
/// Initializes a new instance of the <see cref="Vp8Encoder"/> class.
/// </summary>
@ -133,6 +152,8 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
/// <param name="entropyPasses">Number of entropy-analysis passes (in [1..10]).</param>
public Vp8Encoder(MemoryAllocator memoryAllocator, int width, int height, int quality, int method, int entropyPasses)
{
this.Width = width;
this.Height = height;
this.memoryAllocator = memoryAllocator;
this.quality = quality.Clamp(0, 100);
this.method = method.Clamp(0, 6);
@ -167,8 +188,14 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
this.mbInfo[i] = new Vp8MacroBlockInfo();
}
this.proba = new Vp8EncProba();
this.segmentInfos = new Vp8SegmentInfo[4];
for (int i = 0; i < 4; i++)
{
this.segmentInfos[i] = new Vp8SegmentInfo();
}
this.filterHeader = new Vp8FilterHeader();
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;
@ -180,10 +207,52 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
this.ResetBoundaryPredictions();
// Initialize the bitwriter.
var baseQuant = 36; // TODO: hardCoded for now.
int averageBytesPerMacroBlock = this.averageBytesPerMb[baseQuant >> 4];
this.BaseQuant = 36; // TODO: hardCoded for now.
int averageBytesPerMacroBlock = this.averageBytesPerMb[this.BaseQuant >> 4];
int expectedSize = this.mbw * this.mbh * averageBytesPerMacroBlock;
this.bitWriter = new Vp8BitWriter(expectedSize);
this.bitWriter = new Vp8BitWriter(expectedSize, this);
}
public int BaseQuant { get; }
/// <summary>
/// Gets the probabilities.
/// </summary>
public Vp8EncProba Proba
{
get => this.proba;
}
/// <summary>
/// Gets the segment features.
/// </summary>
public Vp8EncSegmentHeader SegmentHeader
{
get => this.segmentHeader;
}
/// <summary>
/// Gets the segment infos.
/// </summary>
public Vp8SegmentInfo[] SegmentInfos
{
get => this.segmentInfos;
}
/// <summary>
/// Gets the macro block info's.
/// </summary>
public Vp8MacroBlockInfo[] MbInfo
{
get => this.mbInfo;
}
/// <summary>
/// Gets the filter header.
/// </summary>
public Vp8FilterHeader FilterHeader
{
get => this.filterHeader;
}
/// <summary>
@ -191,6 +260,56 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
/// </summary>
public int Alpha { get; set; }
/// <summary>
/// Gets the width of the image.
/// </summary>
public int Width { get; }
/// <summary>
/// Gets the height of the image.
/// </summary>
public int Height { get; }
public int PredsWidth
{
get => this.predsWidth;
}
public int Mbw
{
get => this.mbw;
}
public int Mbh
{
get => this.mbh;
}
public int DqY1Dc
{
get => this.dqY1Dc;
}
public int DqY2Ac
{
get => this.dqY2Ac;
}
public int DqY2Dc
{
get => this.dqY2Dc;
}
public int DqUvAc
{
get => this.dqUvAc;
}
public int DqUvDc
{
get => this.dqUvDc;
}
/// <summary>
/// Gets the luma component.
/// </summary>
@ -209,22 +328,22 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
/// <summary>
/// Gets the top luma samples.
/// </summary>
private byte[] YTop { get; }
public byte[] YTop { get; }
/// <summary>
/// Gets the top u/v samples. U and V are packed into 16 bytes (8 U + 8 V).
/// </summary>
private byte[] UvTop { get; }
public byte[] UvTop { get; }
/// <summary>
/// Gets the non-zero pattern.
/// </summary>
private uint[] Nz { get; }
public uint[] Nz { get; }
/// <summary>
/// Gets the prediction modes: (4*mbw+1) * (4*mbh+1).
/// </summary>
private byte[] Preds { get; }
public byte[] Preds { get; }
/// <summary>
/// Gets a rough limit for header bits per MB.
@ -249,11 +368,6 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
int yStride = width;
int uvStride = (yStride + 1) >> 1;
var segmentInfos = new Vp8SegmentInfo[4];
for (int i = 0; i < 4; i++)
{
segmentInfos[i] = new Vp8SegmentInfo();
}
var it = new Vp8EncIterator(this.YTop, this.UvTop, this.Nz, this.mbInfo, this.Preds, this.mbw, this.mbh);
var alphas = new int[WebPConstants.MaxAlpha + 1];
@ -261,19 +375,19 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
// Analysis is done, proceed to actual encoding.
this.segmentHeader = new Vp8EncSegmentHeader(4);
this.AssignSegments(segmentInfos, alphas);
this.SetLoopParams(segmentInfos, this.quality);
this.AssignSegments(alphas);
this.SetLoopParams(this.quality);
// TODO: EncodeAlpha();
// Stats-collection loop.
this.StatLoop(width, height, yStride, uvStride, segmentInfos);
this.StatLoop(width, height, yStride, uvStride);
it.Init();
it.InitFilter();
do
{
var info = new Vp8ModeScore();
it.Import(y, u, v, yStride, uvStride, width, height, false);
if (!this.Decimate(it, segmentInfos, info, this.rdOptLevel))
if (!this.Decimate(it, info, this.rdOptLevel))
{
this.CodeResiduals(it, info);
}
@ -286,8 +400,11 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
}
while (it.Next());
// Store filter stats.
this.AdjustFilterStrength();
// Write bytes from the bitwriter buffer to the stream.
this.bitWriter.WriteEncodedImageToStream(lossy: true, stream);
this.bitWriter.WriteEncodedImageToStream(stream);
}
/// <inheritdoc/>
@ -303,7 +420,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
/// This is used for deciding optimal probabilities. It also modifies the
/// quantizer value if some target (size, PSNR) was specified.
/// </summary>
private void StatLoop(int width, int height, int yStride, int uvStride, Vp8SegmentInfo[] segmentInfos)
private void StatLoop(int width, int height, int yStride, int uvStride)
{
int targetSize = 0; // TODO: target size is hardcoded.
float targetPsnr = 0.0f; // TODO: targetPsnr is hardcoded.
@ -334,7 +451,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
while (numPassLeft-- > 0)
{
bool isLastPass = (MathF.Abs(stats.Dq) <= DqLimit) || (numPassLeft == 0) || (this.maxI4HeaderBits == 0);
var sizeP0 = this.OneStatPass(width, height, yStride, uvStride, rdOpt, nbMbs, stats, segmentInfos);
var sizeP0 = this.OneStatPass(width, height, yStride, uvStride, rdOpt, nbMbs, stats);
if (sizeP0 == 0)
{
return;
@ -373,23 +490,23 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
this.proba.CalculateLevelCosts(); // finalize costs
}
private long OneStatPass(int width, int height, int yStride, int uvStride, Vp8RdLevel rdOpt, int nbMbs, PassStats stats, Vp8SegmentInfo[] segmentInfos)
private long OneStatPass(int width, int height, int yStride, int uvStride, Vp8RdLevel rdOpt, int nbMbs, PassStats stats)
{
Span<byte> y = this.Y.GetSpan();
Span<byte> u = this.U.GetSpan();
Span<byte> 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.Mbw, this.Mbh);
long size = 0;
long sizeP0 = 0;
long distortion = 0;
long pixelCount = nbMbs * 384;
this.SetLoopParams(segmentInfos, stats.Q);
this.SetLoopParams(stats.Q);
do
{
var info = new Vp8ModeScore();
it.Import(y, u, v, yStride, uvStride, width, height, false);
if (this.Decimate(it, segmentInfos, info, rdOpt))
if (this.Decimate(it, info, rdOpt))
{
// Just record the number of skips and act like skipProba is not used.
++this.proba.NbSkip;
@ -420,10 +537,10 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
return sizeP0;
}
private void SetLoopParams(Vp8SegmentInfo[] dqm, float q)
private void SetLoopParams(float q)
{
// Setup segment quantizations and filters.
this.SetSegmentParams(dqm, q);
this.SetSegmentParams(q);
// Compute segment probabilities.
this.SetSegmentProbas();
@ -431,6 +548,33 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
this.ResetStats();
}
private void AdjustFilterStrength()
{
if (FilterStrength > 0)
{
int maxLevel = 0;
for (int s = 0; s < WebPConstants.NumMbSegments; s++)
{
Vp8SegmentInfo dqm = this.SegmentInfos[s];
// this '>> 3' accounts for some inverse WHT scaling
int delta = (dqm.MaxEdge * dqm.Y2.Q[1]) >> 3;
int level = this.FilterStrengthFromDelta(this.filterHeader.Sharpness, delta);
if (level > dqm.FStrength)
{
dqm.FStrength = level;
}
if (maxLevel < dqm.FStrength)
{
maxLevel = dqm.FStrength;
}
}
this.filterHeader.FilterLevel = maxLevel;
}
}
private void ResetBoundaryPredictions()
{
Span<byte> top = this.Preds.AsSpan(); // original source top starts at: enc->preds_ - enc->preds_w_
@ -449,16 +593,13 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
}
// Simplified k-Means, to assign Nb segments based on alpha-histogram.
private void AssignSegments(Vp8SegmentInfo[] dqm, int[] alphas)
private void AssignSegments(int[] alphas)
{
int nb = (this.segmentHeader.NumSegments < NumMbSegments) ? this.segmentHeader.NumSegments : NumMbSegments;
var centers = new int[NumMbSegments];
int weightedAverage = 0;
var map = new int[WebPConstants.MaxAlpha + 1];
int a, n, k;
int minA;
int maxA;
int rangeA;
var accum = new int[NumMbSegments];
var distAccum = new int[NumMbSegments];
@ -467,13 +608,13 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
{
}
minA = n;
var minA = n;
for (n = WebPConstants.MaxAlpha; n > minA && alphas[n] == 0; --n)
{
}
maxA = n;
rangeA = maxA - minA;
var maxA = n;
var rangeA = maxA - minA;
// Spread initial centers evenly.
for (k = 0, n = 1; k < nb; ++k, n += 2)
@ -542,12 +683,13 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
}
// TODO: add possibility for SmoothSegmentMap
this.SetSegmentAlphas(dqm, centers, weightedAverage);
this.SetSegmentAlphas(centers, weightedAverage);
}
private void SetSegmentAlphas(Vp8SegmentInfo[] dqm, int[] centers, int mid)
private void SetSegmentAlphas(int[] centers, int mid)
{
int nb = this.segmentHeader.NumSegments;
Vp8SegmentInfo[] dqm = this.segmentInfos;
int min = centers[0], max = centers[0];
int n;
@ -581,9 +723,10 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
}
}
private void SetSegmentParams(Vp8SegmentInfo[] dqm, float quality)
private void SetSegmentParams(float quality)
{
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 cBase = QualityToCompression(quality / 100.0d);
@ -614,9 +757,42 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
this.dqUvDc = -4 * snsStrength / 100;
this.dqUvDc = Clip(this.dqUvDc, -15, 15); // 4bit-signed max allowed
this.dqY1Dc = 0;
this.dqY2Dc = 0;
this.dqY2Ac = 0;
// Initialize segments' filtering
this.SetupFilterStrength();
this.SetupMatrices(dqm);
}
private void SetupFilterStrength()
{
var filterSharpness = 0; // TODO: filterSharpness is hardcoded
var filterType = 1; // TODO: filterType is hardcoded
// level0 is in [0..500]. Using '-f 50' as filter_strength is mid-filtering.
int level0 = 5 * FilterStrength;
for (int i = 0; i < WebPConstants.NumMbSegments; ++i)
{
Vp8SegmentInfo m = this.SegmentInfos[i];
// We focus on the quantization of AC coeffs.
int qstep = WebPLookupTables.AcTable[Clip(m.Quant, 0, 127)] >> 2;
int baseStrength = this.FilterStrengthFromDelta(this.filterHeader.Sharpness, qstep);
// Segments with lower complexity ('beta') will be less filtered.
int f = baseStrength * level0 / (256 + m.Beta);
m.FStrength = (f < WebPConstants.FilterStrengthCutoff) ? 0 : (f > 63) ? 63 : f;
}
// We record the initial strength (mainly for the case of 1-segment only).
this.filterHeader.FilterLevel = this.SegmentInfos[0].FStrength;
this.filterHeader.Simple = filterType == 0;
this.filterHeader.Sharpness = filterSharpness;
}
private void SetSegmentProbas()
{
var p = new int[NumMbSegments];
@ -745,7 +921,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
return bestAlpha; // Mixed susceptibility (not just luma).
}
private bool Decimate(Vp8EncIterator it, Vp8SegmentInfo[] segmentInfos, Vp8ModeScore rd, Vp8RdLevel rdOpt)
private bool Decimate(Vp8EncIterator it, Vp8ModeScore rd, Vp8RdLevel rdOpt)
{
rd.InitScore();
@ -757,7 +933,7 @@ 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, segmentInfos, rd, this.method >= 2, this.method >= 1);
this.RefineUsingDistortion(it, rd, this.method >= 2, this.method >= 1);
bool isSkipped = rd.Nz == 0;
it.SetSkip(isSkipped);
@ -766,13 +942,13 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
}
// Refine intra16/intra4 sub-modes based on distortion only (not rate).
private void RefineUsingDistortion(Vp8EncIterator it, Vp8SegmentInfo[] segmentInfos, Vp8ModeScore rd, bool tryBothModes, bool refineUvMode)
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 = segmentInfos[it.CurrentMacroBlockInfo.Segment];
Vp8SegmentInfo dqm = this.segmentInfos[it.CurrentMacroBlockInfo.Segment];
// Some empiric constants, of approximate order of magnitude.
int lambdaDi16 = 106;
@ -1280,10 +1456,10 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
int b = dc - tmp[8];
int c = Mul(tmp[4], KC2) - Mul(tmp[12], KC1);
int d = Mul(tmp[4], KC1) + Mul(tmp[12], KC2);
Store(dst, reference, 0, i, (a + d));
Store(dst, reference, 1, i, (b + c));
Store(dst, reference, 2, i, (b - c));
Store(dst, reference, 3, i, (a - d));
Store(dst, reference, 0, i, a + d);
Store(dst, reference, 1, i, b + c);
Store(dst, reference, 2, i, b - c);
Store(dst, reference, 3, i, a - d);
tmp = tmp.Slice(1);
}
}
@ -1663,6 +1839,12 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
return v;
}
private int FilterStrengthFromDelta(int sharpness, int delta)
{
int pos = (delta < WebPConstants.MaxDelzaSize) ? delta : WebPConstants.MaxDelzaSize - 1;
return WebPLookupTables.LevelsFromDelta[sharpness, pos];
}
[MethodImpl(InliningOptions.ShortMethod)]
private static double GetPsnr(long mse, long size)
{

10
src/ImageSharp/Formats/WebP/Lossy/Vp8FilterHeader.cs

@ -53,6 +53,16 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
}
}
/// <summary>
/// Gets or sets a value indicating whether the filtering type is: 0=complex, 1=simple.
/// </summary>
public bool Simple { get; set; }
/// <summary>
/// Gets or sets delta filter level for i4x4 relative to i16x16.
/// </summary>
public int I4x4LfDelta { get; set; }
public bool UseLfDelta { get; set; }
public int[] RefLfDelta { get; }

2
src/ImageSharp/Formats/WebP/Lossy/Vp8Residual.cs

@ -74,7 +74,7 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
}
this.RecordStats(1, s, 1);
var bit = 2u < (uint)(v + 1);
var bit = (uint)(v + 1) > 2u;
if (this.RecordStats(bit ? 1 : 0, s, 2) == 0)
{
// v = -1 or 1

3
src/ImageSharp/Formats/WebP/Lossy/Vp8SegmentHeader.cs

@ -10,6 +10,9 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy
{
private const int NumMbSegments = 4;
/// <summary>
/// Initializes a new instance of the <see cref="Vp8SegmentHeader"/> class.
/// </summary>
public Vp8SegmentHeader()
{
this.Quantizer = new byte[NumMbSegments];

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

@ -23,7 +23,7 @@ namespace SixLabors.ImageSharp.Formats.WebP
/// <summary>
/// Signature which identifies a VP8 header.
/// </summary>
public static readonly byte[] Vp8MagicBytes =
public static readonly byte[] Vp8HeaderMagicBytes =
{
0x9D,
0x01,
@ -33,10 +33,21 @@ namespace SixLabors.ImageSharp.Formats.WebP
/// <summary>
/// Signature byte which identifies a VP8L header.
/// </summary>
public const byte Vp8LMagicByte = 0x2F;
public const byte Vp8LHeaderMagicByte = 0x2F;
/// <summary>
/// Signature bytes identifying a lossy image.
/// </summary>
public static readonly byte[] Vp8MagicBytes =
{
0x56, // V
0x50, // P
0x38, // 8
0x20 // ' '
};
/// <summary>
/// Header bytes identifying a lossless image.
/// Signature bytes identifying a lossless image.
/// </summary>
public static readonly byte[] Vp8LMagicBytes =
{
@ -251,6 +262,14 @@ namespace SixLabors.ImageSharp.Formats.WebP
public const int QFix = 17;
public const int MaxDelzaSize = 64;
/// <summary>
/// Very small filter-strength values have close to no visual effect. So we can
/// save a little decoding-CPU by turning filtering off for these.
/// </summary>
public const int FilterStrengthCutoff = 2;
/// <summary>
/// Max size of mode partition.
/// </summary>

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

@ -307,7 +307,7 @@ namespace SixLabors.ImageSharp.Formats.WebP
// Check for VP8 magic bytes.
this.currentStream.Read(this.buffer, 0, 3);
if (!this.buffer.AsSpan().Slice(0, 3).SequenceEqual(WebPConstants.Vp8MagicBytes))
if (!this.buffer.AsSpan().Slice(0, 3).SequenceEqual(WebPConstants.Vp8HeaderMagicBytes))
{
WebPThrowHelper.ThrowImageFormatException("VP8 magic bytes not found");
}
@ -341,8 +341,10 @@ namespace SixLabors.ImageSharp.Formats.WebP
this.currentStream,
remaining,
this.memoryAllocator,
partitionLength);
bitReader.Remaining = remaining;
partitionLength)
{
Remaining = remaining
};
return new WebPImageInfo()
{
@ -375,7 +377,7 @@ namespace SixLabors.ImageSharp.Formats.WebP
// One byte signature, should be 0x2f.
uint signature = bitReader.ReadValue(8);
if (signature != WebPConstants.Vp8LMagicByte)
if (signature != WebPConstants.Vp8LHeaderMagicByte)
{
WebPThrowHelper.ThrowImageFormatException("Invalid VP8L signature");
}

56
src/ImageSharp/Formats/WebP/WebPLookupTables.cs

@ -22,6 +22,8 @@ namespace SixLabors.ImageSharp.Formats.WebP
public static readonly int[] LinearToGammaTab = new int[WebPConstants.GammaTabSize + 1];
public static readonly short[,][] Vp8FixedCostsI4 = new short[10, 10][];
// Compute susceptibility based on DCT-coeff histograms:
// the higher, the "easier" the macroblock is to compress.
public static readonly int[] Vp8DspScan =
@ -51,7 +53,59 @@ namespace SixLabors.ImageSharp.Formats.WebP
8 + (0 * WebPConstants.Bps), 12 + (0 * WebPConstants.Bps), 8 + (4 * WebPConstants.Bps), 12 + (4 * WebPConstants.Bps) // V
};
public static readonly short[,][] Vp8FixedCostsI4 = new short[10, 10][];
// This table gives, for a given sharpness, the filtering strength to be
// used (at least) in order to filter a given edge step delta.
public static readonly byte[,] LevelsFromDelta =
{
{
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31,
32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47,
48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63
},
{
0, 1, 2, 3, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15, 17, 18,
20, 21, 23, 24, 26, 27, 29, 30, 32, 33, 35, 36, 38, 39, 41, 42,
44, 45, 47, 48, 50, 51, 53, 54, 56, 57, 59, 60, 62, 63, 63, 63,
63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63
},
{
0, 1, 2, 3, 5, 6, 7, 8, 9, 11, 12, 13, 14, 16, 17, 19,
20, 22, 23, 25, 26, 28, 29, 31, 32, 34, 35, 37, 38, 40, 41, 43,
44, 46, 47, 49, 50, 52, 53, 55, 56, 58, 59, 61, 62, 63, 63, 63,
63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63
},
{
0, 1, 2, 3, 5, 6, 7, 8, 9, 11, 12, 13, 15, 16, 18, 19,
21, 22, 24, 25, 27, 28, 30, 31, 33, 34, 36, 37, 39, 40, 42, 43,
45, 46, 48, 49, 51, 52, 54, 55, 57, 58, 60, 61, 63, 63, 63, 63,
63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63
},
{
0, 1, 2, 3, 5, 6, 7, 8, 9, 11, 12, 14, 15, 17, 18, 20,
21, 23, 24, 26, 27, 29, 30, 32, 33, 35, 36, 38, 39, 41, 42, 44,
45, 47, 48, 50, 51, 53, 54, 56, 57, 59, 60, 62, 63, 63, 63, 63,
63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63
},
{
0, 1, 2, 4, 5, 7, 8, 9, 11, 12, 13, 15, 16, 17, 19, 20,
22, 23, 25, 26, 28, 29, 31, 32, 34, 35, 37, 38, 40, 41, 43, 44,
46, 47, 49, 50, 52, 53, 55, 56, 58, 59, 61, 62, 63, 63, 63, 63,
63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63
},
{
0, 1, 2, 4, 5, 7, 8, 9, 11, 12, 13, 15, 16, 18, 19, 21,
22, 24, 25, 27, 28, 30, 31, 33, 34, 36, 37, 39, 40, 42, 43, 45,
46, 48, 49, 51, 52, 54, 55, 57, 58, 60, 61, 63, 63, 63, 63, 63,
63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63
},
{
0, 1, 2, 4, 5, 7, 8, 9, 11, 12, 14, 15, 17, 18, 20, 21,
23, 24, 26, 27, 29, 30, 32, 33, 35, 36, 38, 39, 41, 42, 44, 45,
47, 48, 50, 51, 53, 54, 56, 57, 59, 60, 62, 63, 63, 63, 63, 63,
63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63
}
};
public static readonly byte[] Norm =
{

Loading…
Cancel
Save