diff --git a/src/ImageSharp/Formats/WebP/Lossless/Vp8LHistogram.cs b/src/ImageSharp/Formats/WebP/Lossless/Vp8LHistogram.cs index dd5c9ebda..a97dfc3de 100644 --- a/src/ImageSharp/Formats/WebP/Lossless/Vp8LHistogram.cs +++ b/src/ImageSharp/Formats/WebP/Lossless/Vp8LHistogram.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.Formats.WebP.Lossless { @@ -10,6 +11,15 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless { private const uint NonTrivialSym = 0xffffffff; + /// + /// Size of histogram used by CollectHistogram. + /// + private const int MaxCoeffThresh = 31; + + private int maxValue; + + private int lastNonZero; + /// /// Initializes a new instance of the class. /// @@ -313,6 +323,101 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless return true; } + public void CollectHistogram(Span reference, Span pred, int startBlock, int endBlock) + { + int j; + var distribution = new int[MaxCoeffThresh + 1]; + for (j = startBlock; j < endBlock; ++j) + { + var output = new short[16]; + + this.Vp8FTransform(reference.Slice(WebPLookupTables.Vp8DspScan[j]), pred.Slice(WebPLookupTables.Vp8DspScan[j]), output); + + // Convert coefficients to bin. + for (int k = 0; k < 16; ++k) + { + int v = Math.Abs(output[k]) >> 3; + int clippedValue = ClipMax(v, MaxCoeffThresh); + ++distribution[clippedValue]; + } + } + + this.SetHistogramData(distribution); + } + + public int GetAlpha() + { + // 'alpha' will later be clipped to [0..MAX_ALPHA] range, clamping outer + // values which happen to be mostly noise. This leaves the maximum precision + // for handling the useful small values which contribute most. + int maxValue = this.maxValue; + int lastNonZero = this.lastNonZero; + int alpha = (maxValue > 1) ? WebPConstants.AlphaScale * lastNonZero / maxValue : 0; + return alpha; + } + + private void SetHistogramData(int[] distribution) + { + int maxValue = 0; + int lastNonZero = 1; + for (int k = 0; k <= MaxCoeffThresh; ++k) + { + int value = distribution[k]; + if (value > 0) + { + if (value > maxValue) + { + maxValue = value; + } + + lastNonZero = k; + } + } + + this.maxValue = maxValue; + this.lastNonZero = lastNonZero; + } + + private void Vp8FTransform(Span src, Span reference, Span output) + { + int i; + var tmp = new int[16]; + for (i = 0; i < 4; ++i) + { + int d0 = src[0] - reference[0]; // 9bit dynamic range ([-255,255]) + int d1 = src[1] - reference[1]; + int d2 = src[2] - reference[2]; + int d3 = src[3] - reference[3]; + int a0 = d0 + d3; // 10b [-510,510] + int a1 = d1 + d2; + int a2 = d1 - d2; + int a3 = d0 - d3; + tmp[0 + (i * 4)] = (a0 + a1) * 8; // 14b [-8160,8160] + tmp[1 + (i * 4)] = ((a2 * 2217) + (a3 * 5352) + 1812) >> 9; // [-7536,7542] + tmp[2 + (i * 4)] = (a0 - a1) * 8; + tmp[3 + (i * 4)] = ((a3 * 2217) - (a2 * 5352) + 937) >> 9; + + // Do not change the span in the last iteration. + if (i < 3) + { + src = src.Slice(WebPConstants.Bps); + reference = reference.Slice(WebPConstants.Bps); + } + } + + for (i = 0; i < 4; ++i) + { + int a0 = tmp[0 + i] + tmp[12 + i]; // 15b + int a1 = tmp[4 + i] + tmp[8 + i]; + int a2 = tmp[4 + i] - tmp[8 + i]; + int a3 = tmp[0 + i] - tmp[12 + i]; + output[0 + i] = (short)((a0 + a1 + 7) >> 4); // 12b + output[4 + i] = (short)((((a2 * 2217) + (a3 * 5352) + 12000) >> 16) + ((a3 != 0) ? 1 : 0)); + output[8 + i] = (short)((a0 - a1 + 7) >> 4); + output[12 + i] = (short)(((a3 * 2217) - (a2 * 5352) + 51000) >> 16); + } + } + private void AddLiteral(Vp8LHistogram b, Vp8LHistogram output, int literalSize) { if (this.IsUsed[0]) @@ -524,5 +629,11 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossless output[i] = a[i] + b[i]; } } + + [MethodImpl(InliningOptions.ShortMethod)] + private static int ClipMax(int v, int max) + { + return (v > max) ? max : v; + } } } diff --git a/src/ImageSharp/Formats/WebP/Lossy/Vp8EncIterator.cs b/src/ImageSharp/Formats/WebP/Lossy/Vp8EncIterator.cs index da026b199..fe7d4e095 100644 --- a/src/ImageSharp/Formats/WebP/Lossy/Vp8EncIterator.cs +++ b/src/ImageSharp/Formats/WebP/Lossy/Vp8EncIterator.cs @@ -3,6 +3,7 @@ using System; using System.Buffers; +using SixLabors.ImageSharp.Formats.WebP.Lossless; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.WebP.Lossy @@ -19,24 +20,92 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy private const int VOffEnc = 16 + 8; + private const int MaxUvMode = 2; + + private const int MaxIntra16Mode = 2; + + private const int MaxIntra4Mode = 2; + private readonly int mbw; private readonly int mbh; - public Vp8EncIterator(MemoryAllocator memoryAllocator, IMemoryOwner yTop, IMemoryOwner uvTop, int mbw, int mbh) + /// + /// Stride of the prediction plane(=4*mbw + 1). + /// + private readonly int predsWidth; + + private const int I16DC16 = 0 * 16 * WebPConstants.Bps; + + private const int I16TM16 = I16DC16 + 16; + + private const int I16VE16 = 1 * 16 * WebPConstants.Bps; + + private const int I16HE16 = I16VE16 + 16; + + private const int C8DC8 = 2 * 16 * WebPConstants.Bps; + + private const int C8TM8 = C8DC8 + (1 * 16); + + private const int C8VE8 = (2 * 16 * WebPConstants.Bps) + (8 * WebPConstants.Bps); + + private const int C8HE8 = C8VE8 + (1 * 16); + + private readonly int[] vp8I16ModeOffsets = { I16DC16, I16TM16, I16VE16, I16HE16 }; + + private readonly int[] vp8UvModeOffsets = { C8DC8, C8TM8, C8VE8, C8HE8 }; + + private readonly byte[] clip1 = new byte[255 + 510 + 1]; // clips [-255,510] to [0,255] + + private int currentMbIdx; + + private int nzIdx; + + private int predIdx; + + private int yTopIdx; + + private int uvTopIdx; + + public Vp8EncIterator(IMemoryOwner yTop, IMemoryOwner uvTop, IMemoryOwner preds, IMemoryOwner nz, Vp8MacroBlockInfo[] mb, int mbw, int mbh) { this.mbw = mbw; this.mbh = mbh; + this.Mb = mb; + this.currentMbIdx = 0; + this.nzIdx = 0; + this.predIdx = 0; + this.yTopIdx = 0; + this.uvTopIdx = 0; this.YTop = yTop; this.UvTop = uvTop; - this.YuvIn = memoryAllocator.Allocate(WebPConstants.Bps * 16); - this.YuvOut = memoryAllocator.Allocate(WebPConstants.Bps * 16); - this.YuvOut2 = memoryAllocator.Allocate(WebPConstants.Bps * 16); - this.YLeft = memoryAllocator.Allocate(WebPConstants.Bps + 1); - this.ULeft = memoryAllocator.Allocate(16); - this.VLeft = memoryAllocator.Allocate(16); - this.TopNz = memoryAllocator.Allocate(9); - this.LeftNz = memoryAllocator.Allocate(9); + this.Preds = preds; + this.Nz = nz; + this.predsWidth = (4 * mbw) + 1; + this.YuvIn = new byte[WebPConstants.Bps * 16]; + this.YuvOut = new byte[WebPConstants.Bps * 16]; + this.YuvOut2 = new byte[WebPConstants.Bps * 16]; + this.YuvP = new byte[(32 * WebPConstants.Bps) + (16 * WebPConstants.Bps) + (8 * WebPConstants.Bps)]; // I16+Chroma+I4 preds + this.YLeft = new byte[32]; + this.ULeft = new byte[32]; + this.VLeft = new byte[32]; + this.TopNz = new int[9]; + this.LeftNz = new int[9]; + + // To match the C++ initial values, initialize all with 204. + byte defaultInitVal = 204; + this.YuvIn.AsSpan().Fill(defaultInitVal); + this.YuvOut.AsSpan().Fill(defaultInitVal); + this.YuvOut2.AsSpan().Fill(defaultInitVal); + this.YuvP.AsSpan().Fill(defaultInitVal); + this.YLeft.AsSpan().Fill(defaultInitVal); + this.ULeft.AsSpan().Fill(defaultInitVal); + this.VLeft.AsSpan().Fill(defaultInitVal); + + for (int i = -255; i <= 255 + 255; ++i) + { + this.clip1[255 + i] = this.Clip8b(i); + } this.Reset(); } @@ -54,29 +123,37 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy /// /// Gets or sets the input samples. /// - public IMemoryOwner YuvIn { get; set; } + public byte[] YuvIn { get; set; } /// /// Gets or sets the output samples. /// - public IMemoryOwner YuvOut { get; set; } + public byte[] YuvOut { get; set; } + + /// + /// Gets or sets the secondary buffer swapped with YuvOut. + /// + public byte[] YuvOut2 { get; set; } - public IMemoryOwner YuvOut2 { get; set; } + /// + /// Gets or sets the scratch buffer for prediction. + /// + public byte[] YuvP { get; set; } /// /// Gets or sets the left luma samples. /// - public IMemoryOwner YLeft { get; set; } + public byte[] YLeft { get; set; } /// /// Gets or sets the left u samples. /// - public IMemoryOwner ULeft { get; set; } + public byte[] ULeft { get; set; } /// /// Gets or sets the left v samples. /// - public IMemoryOwner VLeft { get; set; } + public byte[] VLeft { get; set; } /// /// Gets or sets the top luma samples at position 'X'. @@ -93,21 +170,37 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy /// public IMemoryOwner Nz { get; set; } + /// + /// Gets or sets the intra mode predictors (4x4 blocks). + /// + public IMemoryOwner Preds { get; set; } + /// /// Gets or sets the top-non-zero context. /// - public IMemoryOwner TopNz { get; set; } + public int[] TopNz { get; set; } /// /// Gets or sets the left-non-zero. leftNz[8] is independent. /// - public IMemoryOwner LeftNz { get; set; } + public int[] LeftNz { get; set; } /// /// Gets or sets the number of mb still to be processed. /// public int CountDown { get; set; } + public Vp8MacroBlockInfo CurrentMacroBlockInfo + { + get + { + return this.Mb[this.currentMbIdx]; + } + } + + private Vp8MacroBlockInfo[] Mb { get; } + + // Import uncompressed samples from source. public void Import(Span y, Span u, Span v, int yStride, int uvStride, int width, int height) { int yStartIdx = ((this.Y * yStride) + this.X) * 16; @@ -120,9 +213,9 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy int uvw = (w + 1) >> 1; int uvh = (h + 1) >> 1; - Span yuvIn = this.YuvIn.Slice(YOffEnc); - Span uIn = this.YuvIn.Slice(UOffEnc); - Span vIn = this.YuvIn.Slice(VOffEnc); + Span yuvIn = this.YuvIn.AsSpan(YOffEnc); + Span uIn = this.YuvIn.AsSpan(UOffEnc); + Span vIn = this.YuvIn.AsSpan(VOffEnc); this.ImportBlock(ySrc, yStride, yuvIn, w, h, 16); this.ImportBlock(uSrc, uvStride, uIn, uvw, uvh, 8); this.ImportBlock(vSrc, uvStride, vIn, uvw, uvh, 8); @@ -134,9 +227,9 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy } else { - Span yLeft = this.YLeft.GetSpan(); - Span uLeft = this.ULeft.GetSpan(); - Span vLeft = this.VLeft.GetSpan(); + Span yLeft = this.YLeft.AsSpan(); + Span uLeft = this.ULeft.AsSpan(); + Span vLeft = this.VLeft.AsSpan(); if (this.Y == 0) { yLeft[0] = 127; @@ -145,9 +238,9 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy } else { - yLeft[0] = ySrc[-1 - yStride]; - uLeft[0] = uSrc[-1 - uvStride]; - vLeft[0] = vSrc[-1 - uvStride]; + yLeft[0] = y[yStartIdx - 1 - yStride]; + uLeft[0] = u[uvStartIdx - 1 - uvStride]; + vLeft[0] = v[uvStartIdx - 1 - uvStride]; } this.ImportLine(y.Slice(yStartIdx - 1), yStride, yLeft.Slice(1), h, 16); @@ -155,19 +248,155 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy this.ImportLine(v.Slice(uvStartIdx - 1), uvStride, vLeft.Slice(1), uvh, 8); } + Span yTop = this.YTop.Slice(this.yTopIdx); if (this.Y == 0) { - this.YTop.GetSpan().Fill(127); + yTop.Fill(127); this.UvTop.GetSpan().Fill(127); } else { - this.ImportLine(y.Slice(yStartIdx - yStride), 1, this.YTop.GetSpan(), w, 16); + this.ImportLine(y.Slice(yStartIdx - yStride), 1, yTop, w, 16); this.ImportLine(u.Slice(uvStartIdx - uvStride), 1, this.UvTop.GetSpan(), uvw, 8); this.ImportLine(v.Slice(uvStartIdx - uvStride), 1, this.UvTop.GetSpan().Slice(8), uvw, 8); } } + public int FastMbAnalyze(int quality) + { + // Empirical cut-off value, should be around 16 (~=block size). We use the + // [8-17] range and favor intra4 at high quality, intra16 for low quality. + int q = quality; + int kThreshold = 8 + ((17 - 8) * q / 100); + int k; + var dc = new uint[16]; + uint m; + uint m2; + for (k = 0; k < 16; k += 4) + { + this.Mean16x4(this.YuvIn.AsSpan(YOffEnc + (k * WebPConstants.Bps)), dc.AsSpan(k)); + } + + for (m = 0, m2 = 0, k = 0; k < 16; ++k) + { + m += dc[k]; + m2 += dc[k] * dc[k]; + } + + if (kThreshold * m2 < m * m) + { + this.SetIntra16Mode(0); // DC16 + } + else + { + var modes = new byte[16]; // DC4 + this.SetIntra4Mode(modes); + } + + return 0; + } + + public int MbAnalyzeBestIntra16Mode() + { + int maxMode = MaxIntra16Mode; + int mode; + int bestAlpha = -1; + int bestMode = 0; + + this.MakeLuma16Preds(); + for (mode = 0; mode < maxMode; ++mode) + { + var histo = new Vp8LHistogram(); + histo.CollectHistogram(this.YuvIn.AsSpan(YOffEnc), this.YuvP.AsSpan(this.vp8I16ModeOffsets[mode]), 0, 16); + int alpha = histo.GetAlpha(); + if (alpha > bestAlpha) + { + bestAlpha = alpha; + bestMode = mode; + } + } + + this.SetIntra16Mode(bestMode); + return bestAlpha; + } + + public int MbAnalyzeBestUvMode() + { + int bestAlpha = -1; + int smallestAlpha = 0; + int bestMode = 0; + int maxMode = MaxUvMode; + int mode; + + this.MakeChroma8Preds(); + for (mode = 0; mode < maxMode; ++mode) + { + var histo = new Vp8LHistogram(); + histo.CollectHistogram(this.YuvIn.AsSpan(UOffEnc), this.YuvP.AsSpan(this.vp8UvModeOffsets[mode]), 16, 16 + 4 + 4); + int alpha = histo.GetAlpha(); + if (alpha > bestAlpha) + { + bestAlpha = alpha; + } + + // The best prediction mode tends to be the one with the smallest alpha. + if (mode == 0 || alpha < smallestAlpha) + { + smallestAlpha = alpha; + bestMode = mode; + } + } + + this.SetIntraUvMode(bestMode); + return bestAlpha; + } + + public void SetIntra16Mode(int mode) + { + Span preds = this.Preds.Slice(this.predIdx); + for (int y = 0; y < 4; ++y) + { + preds.Slice(0, 4).Fill((byte)mode); + preds = preds.Slice(this.predsWidth); + } + + this.CurrentMacroBlockInfo.MacroBlockType = Vp8MacroBlockType.I16X16; + } + + private void SetIntra4Mode(byte[] modes) + { + int modesIdx = 0; + Span preds = this.Preds.Slice(this.predIdx); + for (int y = 4; y > 0; --y) + { + // TODO: + // memcpy(preds, modes, 4 * sizeof(*modes)); + preds = preds.Slice(this.predsWidth); + modesIdx += 4; + } + + this.CurrentMacroBlockInfo.MacroBlockType = Vp8MacroBlockType.I4X4; + } + + private void SetIntraUvMode(int mode) + { + this.CurrentMacroBlockInfo.UvMode = mode; + } + + public void SetSkip(bool skip) + { + this.CurrentMacroBlockInfo.Skip = skip; + } + + public void SetSegment(int segment) + { + this.CurrentMacroBlockInfo.Segment = segment; + } + + /// + /// Returns true if iteration is finished. + /// + /// True if iterator is finished. public bool IsDone() { return this.CountDown <= 0; @@ -176,36 +405,226 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy /// public void Dispose() { - this.YuvIn.Dispose(); - this.YuvOut.Dispose(); - this.YuvOut2.Dispose(); - this.YLeft.Dispose(); - this.ULeft.Dispose(); - this.VLeft.Dispose(); - this.Nz.Dispose(); - this.LeftNz.Dispose(); - this.TopNz.Dispose(); } - public bool Next(int mbw) + /// + /// Go to next macroblock. + /// + /// Returns false if not finished. + public bool Next() { - if (++this.X == mbw) + if (++this.X == this.mbw) { this.SetRow(++this.Y); } else { - // TODO: - /* it->preds_ += 4; - it->mb_ += 1; - it->nz_ += 1; - it->y_top_ += 16; - it->uv_top_ += 16;*/ + this.currentMbIdx++; + this.nzIdx++; + this.predIdx += 4; + this.yTopIdx += 16; + this.uvTopIdx += 16; } return --this.CountDown > 0; } + private void Mean16x4(Span input, Span dc) + { + int x; + for (int k = 0; k < 4; ++k) + { + uint avg = 0; + for (int y = 0; y < 4; ++y) + { + for (x = 0; x < 4; ++x) + { + avg += input[x + (y * WebPConstants.Bps)]; + } + } + + dc[k] = avg; + input = input.Slice(4); // go to next 4x4 block. + } + } + + private void MakeLuma16Preds() + { + Span left = this.X != 0 ? this.YLeft.AsSpan() : null; + Span top = this.Y != 0 ? this.YTop.Slice(this.yTopIdx) : null; + this.EncPredLuma16(this.YuvP, left, top); + } + + private void MakeChroma8Preds() + { + Span left = this.X != 0 ? this.ULeft.AsSpan() : null; + Span top = this.Y != 0 ? this.UvTop.Slice(this.uvTopIdx) : null; + this.EncPredChroma8(this.YuvP, left, top); + } + + // luma 16x16 prediction (paragraph 12.3) + private void EncPredLuma16(Span dst, Span left, Span top) + { + this.DcMode(dst.Slice(I16DC16), left, top, 16, 16, 5); + this.VerticalPred(dst.Slice(I16VE16), top, 16); + this.HorizontalPred(dst.Slice(I16HE16), left, 16); + this.TrueMotion(dst.Slice(I16TM16), left, top, 16); + } + + // Chroma 8x8 prediction (paragraph 12.2) + private void EncPredChroma8(Span dst, Span left, Span top) + { + // U block + this.DcMode(dst.Slice(C8DC8), left, top, 8, 8, 4); + this.VerticalPred(dst.Slice(C8VE8), top, 8); + this.HorizontalPred(dst.Slice(C8HE8), left, 8); + this.TrueMotion(dst.Slice(C8TM8), left, top, 8); + + // V block + dst = dst.Slice(8); + if (top != null) + { + top = top.Slice(8); + } + + if (left != null) + { + left = left.Slice(16); + } + + this.DcMode(dst.Slice(C8DC8), left, top, 8, 8, 4); + this.VerticalPred(dst.Slice(C8VE8), top, 8); + this.HorizontalPred(dst.Slice(C8HE8), left, 8); + this.TrueMotion(dst.Slice(C8TM8), left, top, 8); + } + + private void DcMode(Span dst, Span left, Span top, int size, int round, int shift) + { + int dc = 0; + int j; + if (top != null) + { + for (j = 0; j < size; ++j) + { + dc += top[j]; + } + + if (left != null) + { + // top and left present. + left = left.Slice(1); // in the reference implementation, left starts at -1. + for (j = 0; j < size; ++j) + { + dc += left[j]; + } + } + else + { + // top, but no left. + dc += dc; + } + + dc = (dc + round) >> shift; + } + else if (left != null) + { + // left but no top. + for (j = 0; j < size; ++j) + { + dc += left[j]; + } + + dc += dc; + dc = (dc + round) >> shift; + } + else + { + // no top, no left, nothing. + dc = 0x80; + } + + this.Fill(dst, dc, size); + } + + private void VerticalPred(Span dst, Span top, int size) + { + if (top != null) + { + for (int j = 0; j < size; ++j) + { + top.Slice(0, size).CopyTo(dst.Slice(j * WebPConstants.Bps)); + } + } + else + { + this.Fill(dst, 127, size); + } + } + + private void HorizontalPred(Span dst, Span left, int size) + { + if (left != null) + { + left = left.Slice(1); // in the reference implementation, left starts at - 1. + for (int j = 0; j < size; ++j) + { + dst.Slice(j * WebPConstants.Bps, size).Fill(left[j]); + } + } + else + { + this.Fill(dst, 129, size); + } + } + + private void TrueMotion(Span dst, Span left, Span top, int size) + { + if (left != null) + { + if (top != null) + { + Span clip = this.clip1.AsSpan(255 - left[0]); // left [0] instead of left[-1], original left starts at -1 + for (int y = 0; y < size; ++y) + { + Span clipTable = clip.Slice(left[y + 1]); // left[y] + for (int x = 0; x < size; ++x) + { + dst[x] = clipTable[top[x]]; + } + + dst = dst.Slice(WebPConstants.Bps); + } + } + else + { + this.HorizontalPred(dst, left, size); + } + } + else + { + // true motion without left samples (hence: with default 129 value) + // is equivalent to VE prediction where you just copy the top samples. + // Note that if top samples are not available, the default value is + // then 129, and not 127 as in the VerticalPred case. + if (top != null) + { + this.VerticalPred(dst, top, size); + } + else + { + this.Fill(dst, 129, size); + } + } + } + + private void Fill(Span dst, int value, int size) + { + for (int j = 0; j < size; ++j) + { + dst.Slice(j * WebPConstants.Bps, size).Fill((byte)value); + } + } + private void ImportBlock(Span src, int srcStride, Span dst, int w, int h, int size) { int dstIdx = 0; @@ -243,52 +662,77 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy } } + /// + /// Restart a scan. + /// private void Reset() { this.SetRow(0); this.SetCountDown(this.mbw * this.mbh); this.InitTop(); + // TODO: memset(it->bit_count_, 0, sizeof(it->bit_count_)); } + /// + /// Reset iterator position to row 'y'. + /// + /// The y position. private void SetRow(int y) { this.X = 0; this.Y = y; + this.currentMbIdx = y * this.mbw; + this.nzIdx = 0; + this.yTopIdx = 0; + this.uvTopIdx = 0; + + this.InitLeft(); // TODO: // it->preds_ = enc->preds_ + y * 4 * enc->preds_w_; - // it->nz_ = enc->nz_; - // it->mb_ = enc->mb_info_ + y * enc->mb_w_; - // it->y_top_ = enc->y_top_; - // it->uv_top_ = enc->uv_top_; } private void InitLeft() { - Span yLeft = this.YLeft.GetSpan(); - Span uLeft = this.ULeft.GetSpan(); - Span vLeft = this.VLeft.GetSpan(); + Span yLeft = this.YLeft.AsSpan(); + Span uLeft = this.ULeft.AsSpan(); + Span vLeft = this.VLeft.AsSpan(); byte val = (byte)((this.Y > 0) ? 129 : 127); yLeft[0] = val; uLeft[0] = val; vLeft[0] = val; - this.YLeft.Slice(1).Fill(129); - this.ULeft.Slice(1).Fill(129); - this.VLeft.Slice(1).Fill(129); - this.LeftNz.GetSpan()[8] = 0; + uLeft[16] = val; + vLeft[16] = val; + + yLeft.Slice(1, 16).Fill(129); + uLeft.Slice(1, 8).Fill(129); + vLeft.Slice(1, 8).Fill(129); + uLeft.Slice(1 + 16, 8).Fill(129); + vLeft.Slice(1 + 16, 8).Fill(129); + + this.LeftNz[8] = 0; } private void InitTop() { int topSize = this.mbw * 16; this.YTop.Slice(0, topSize).Fill(127); - // TODO: memset(enc->nz_, 0, enc->mb_w_ * sizeof(*enc->nz_)); + this.Nz.GetSpan().Fill(0); } + /// + /// Set count down. + /// + /// Number of iterations to go. private void SetCountDown(int countDown) { this.CountDown = countDown; } + + private byte Clip8b(int v) + { + return ((v & ~0xff) == 0) ? (byte)v : (v < 0) ? (byte)0 : (byte)255; + } } } diff --git a/src/ImageSharp/Formats/WebP/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/WebP/Lossy/Vp8Encoder.cs index 1fddabaa0..7aec1870d 100644 --- a/src/ImageSharp/Formats/WebP/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/WebP/Lossy/Vp8Encoder.cs @@ -46,17 +46,25 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy var pixelCount = width * height; int mbw = (width + 15) >> 4; + int mbh = (height + 15) >> 4; var uvSize = ((width + 1) >> 1) * ((height + 1) >> 1); this.Y = this.memoryAllocator.Allocate(pixelCount); this.U = this.memoryAllocator.Allocate(uvSize); this.V = this.memoryAllocator.Allocate(uvSize); this.YTop = this.memoryAllocator.Allocate(mbw * 16); this.UvTop = this.memoryAllocator.Allocate(mbw * 16 * 2); + this.Preds = this.memoryAllocator.Allocate(((4 * mbw) + 1) * ((4 * mbh) + 1)); + this.Nz = this.memoryAllocator.Allocate(mbw + 1); // TODO: properly initialize the bitwriter this.bitWriter = new Vp8BitWriter(); } + /// + /// Gets or sets the global susceptibility. + /// + public int Alpha { get; set; } + private IMemoryOwner Y { get; } private IMemoryOwner U { get; } @@ -73,6 +81,16 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy /// private IMemoryOwner UvTop { get; } + /// + /// Gets the prediction modes: (4*mbw+1) * (4*mbh+1). + /// + private IMemoryOwner Preds { get; } + + /// + /// Gets the non-zero pattern. + /// + private IMemoryOwner Nz { get; } + public void Encode(Image image, Stream stream) where TPixel : unmanaged, IPixel { @@ -85,15 +103,30 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy int mbh = (image.Height + 15) >> 4; int yStride = image.Width; int uvStride = (yStride + 1) >> 1; - var it = new Vp8EncIterator(this.memoryAllocator, this.YTop, this.UvTop, mbw, mbh); + var mb = new Vp8MacroBlockInfo[mbw * mbh]; + for (int i = 0; i < mb.Length; i++) + { + mb[i] = new Vp8MacroBlockInfo(); + } + + var it = new Vp8EncIterator(this.YTop, this.UvTop, this.Preds, this.Nz, mb, mbw, mbh); + int method = 4; // TODO: hardcoded for now + int quality = 100; // TODO: hardcoded for now + var alphas = new int[WebPConstants.MaxAlpha + 1]; + int uvAlpha = 0; + int alpha = 0; if (!it.IsDone()) { do { it.Import(y, u, v, yStride, uvStride, image.Width, image.Height); - // TODO: MBAnalyze + int bestAlpha = this.MbAnalyze(it, method, quality, alphas, out var bestUvAlpha); + + // Accumulate for later complexity analysis. + alpha += bestAlpha; + uvAlpha += bestUvAlpha; } - while (it.Next(mbw)); + while (it.Next()); } throw new NotImplementedException(); @@ -107,6 +140,34 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy this.V.Dispose(); this.YTop.Dispose(); this.UvTop.Dispose(); + this.Preds.Dispose(); + } + + private int MbAnalyze(Vp8EncIterator it, int method, int quality, int[] alphas, out int bestUvAlpha) + { + it.SetIntra16Mode(0); // default: Intra16, DC_PRED + it.SetSkip(false); // not skipped. + it.SetSegment(0); // default segment, spec-wise. + + int bestAlpha; + if (method <= 1) + { + bestAlpha = it.FastMbAnalyze(quality); + } + else + { + bestAlpha = it.MbAnalyzeBestIntra16Mode(); + } + + bestUvAlpha = it.MbAnalyzeBestUvMode(); + + // Final susceptibility mix. + bestAlpha = ((3 * bestAlpha) + bestUvAlpha + 2) >> 2; + bestAlpha = this.FinalAlphaValue(bestAlpha); + alphas[bestAlpha]++; + it.CurrentMacroBlockInfo.Alpha = bestAlpha; // For later remapping. + + return bestAlpha; // Mixed susceptibility (not just luma) } private void ConvertRgbToYuv(Image image) @@ -395,5 +456,18 @@ namespace SixLabors.ImageSharp.Formats.WebP.Lossy uv = (uv + rounding + (128 << (YuvFix + 2))) >> (YuvFix + 2); return ((uv & ~0xff) == 0) ? uv : (uv < 0) ? 0 : 255; } + + [MethodImpl(InliningOptions.ShortMethod)] + private int FinalAlphaValue(int alpha) + { + alpha = WebPConstants.MaxAlpha - alpha; + return this.Clip(alpha, 0, WebPConstants.MaxAlpha); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private int Clip(int v, int min, int max) + { + return (v < min) ? min : (v > max) ? max : v; + } } } diff --git a/src/ImageSharp/Formats/WebP/Lossy/Vp8MacroBlockInfo.cs b/src/ImageSharp/Formats/WebP/Lossy/Vp8MacroBlockInfo.cs new file mode 100644 index 000000000..294e548e8 --- /dev/null +++ b/src/ImageSharp/Formats/WebP/Lossy/Vp8MacroBlockInfo.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.WebP.Lossy +{ + internal class Vp8MacroBlockInfo + { + public Vp8MacroBlockType MacroBlockType { get; set; } + + public int UvMode { get; set; } + + public bool Skip { get; set; } + + public int Segment { get; set; } + + public int Alpha { get; set; } + } +} diff --git a/src/ImageSharp/Formats/WebP/Lossy/Vp8MacroBlockType.cs b/src/ImageSharp/Formats/WebP/Lossy/Vp8MacroBlockType.cs new file mode 100644 index 000000000..1178851ca --- /dev/null +++ b/src/ImageSharp/Formats/WebP/Lossy/Vp8MacroBlockType.cs @@ -0,0 +1,12 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.WebP.Lossy +{ + internal enum Vp8MacroBlockType + { + I4X4 = 0, + + I16X16 = 1 + } +} diff --git a/src/ImageSharp/Formats/WebP/WebPConstants.cs b/src/ImageSharp/Formats/WebP/WebPConstants.cs index 4293018ef..cb751e216 100644 --- a/src/ImageSharp/Formats/WebP/WebPConstants.cs +++ b/src/ImageSharp/Formats/WebP/WebPConstants.cs @@ -205,6 +205,10 @@ namespace SixLabors.ImageSharp.Formats.WebP public const int AlphaFix = 19; + public const int MaxAlpha = 255; + + public const int AlphaScale = 2 * MaxAlpha; + /// /// How many extra lines are needed on the MB boundary for caching, given a filtering level. /// Simple filter(1): up to 2 luma samples are read and 1 is written. diff --git a/src/ImageSharp/Formats/WebP/WebPLookupTables.cs b/src/ImageSharp/Formats/WebP/WebPLookupTables.cs index 448a4a3c2..1a38912ea 100644 --- a/src/ImageSharp/Formats/WebP/WebPLookupTables.cs +++ b/src/ImageSharp/Formats/WebP/WebPLookupTables.cs @@ -22,6 +22,20 @@ namespace SixLabors.ImageSharp.Formats.WebP public static readonly int[] LinearToGammaTab = new int[WebPConstants.GammaTabSize + 1]; + // Compute susceptibility based on DCT-coeff histograms: + // the higher, the "easier" the macroblock is to compress. + public static readonly int[] Vp8DspScan = + { + // Luma + 0 + (0 * WebPConstants.Bps), 4 + (0 * WebPConstants.Bps), 8 + (0 * WebPConstants.Bps), 12 + (0 * WebPConstants.Bps), + 0 + (4 * WebPConstants.Bps), 4 + (4 * WebPConstants.Bps), 8 + (4 * WebPConstants.Bps), 12 + (4 * WebPConstants.Bps), + 0 + (8 * WebPConstants.Bps), 4 + (8 * WebPConstants.Bps), 8 + (8 * WebPConstants.Bps), 12 + (8 * WebPConstants.Bps), + 0 + (12 * WebPConstants.Bps), 4 + (12 * WebPConstants.Bps), 8 + (12 * WebPConstants.Bps), 12 + (12 * WebPConstants.Bps), + + 0 + (0 * WebPConstants.Bps), 4 + (0 * WebPConstants.Bps), 0 + (4 * WebPConstants.Bps), 4 + (4 * WebPConstants.Bps), // U + 8 + (0 * WebPConstants.Bps), 12 + (0 * WebPConstants.Bps), 8 + (4 * WebPConstants.Bps), 12 + (4 * WebPConstants.Bps) // V + }; + /// /// Lookup table for small values of log2(int). ///