From a22a56af7ac6bec45bd9e84f38ae8445034e0a13 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 5 Apr 2019 00:12:13 +1100 Subject: [PATCH 1/5] Use more accuracy when calculating variance. Fix #866 --- .../Quantization/WuFrameQuantizer{TPixel}.cs | 366 ++++++++++-------- 1 file changed, 203 insertions(+), 163 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs index 44df226cfd..77e1e00889 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs @@ -39,17 +39,21 @@ internal sealed class WuFrameQuantizer : FrameQuantizerBase // TODO: The WuFrameQuantizer code is rising several questions: // - Do we really need to ALWAYS allocate the whole table of size TableLength? (~ 2471625 * sizeof(long) * 5 bytes ) JS. I'm afraid so. // - Isn't an AOS ("array of structures") layout more efficient & more readable than SOA ("structure of arrays") for this particular use case? - // (T, R, G, B, A, M2) could be grouped together! + // (T, R, G, B, A, M2) could be grouped together! Investigate a ColorMoment struct. // - It's a frequently used class, we need tests! (So we can optimize safely.) There are tests in the original!!! We should just adopt them! // https://github.com/JeremyAnsel/JeremyAnsel.ColorQuant/blob/master/JeremyAnsel.ColorQuant/JeremyAnsel.ColorQuant.Tests/WuColorQuantizerTests.cs + // The following two variables determine the amount of bits to preserve when calculating the histogram. + // Reducing the value of these numbers the granularity of the color maps produced, making it much faster + // and using much less memory but potentially less accurate. Current results are very good though! + /// - /// The index bits. + /// The index bits. 6 in original code. /// private const int IndexBits = 5; /// - /// The index alpha bits. Keep separate for now to allow easy adjustment. + /// The index alpha bits. 3 in original code. /// private const int IndexAlphaBits = 5; @@ -64,7 +68,7 @@ internal sealed class WuFrameQuantizer : FrameQuantizerBase private const int IndexAlphaCount = (1 << IndexAlphaBits) + 1; /// - /// The table length. Now 1185921. + /// The table length. Now 1185921. originally 2471625. /// private const int TableLength = IndexCount * IndexCount * IndexCount * IndexAlphaCount; @@ -96,7 +100,7 @@ internal sealed class WuFrameQuantizer : FrameQuantizerBase /// /// Moment of c^2*P(c). /// - private IMemoryOwner m2; + private IMemoryOwner m2; /// /// Color space tag. @@ -156,7 +160,7 @@ public override QuantizedFrame QuantizeFrame(ImageFrame image) this.vmg = memoryAllocator.Allocate(TableLength, AllocationOptions.Clean); this.vmb = memoryAllocator.Allocate(TableLength, AllocationOptions.Clean); this.vma = memoryAllocator.Allocate(TableLength, AllocationOptions.Clean); - this.m2 = memoryAllocator.Allocate(TableLength, AllocationOptions.Clean); + this.m2 = memoryAllocator.Allocate(TableLength, AllocationOptions.Clean); this.tag = memoryAllocator.Allocate(TableLength, AllocationOptions.Clean); return base.QuantizeFrame(image); @@ -275,9 +279,14 @@ protected override void SecondPass(ImageFrame source, Span output, [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int GetPaletteIndex(int r, int g, int b, int a) { - return (r << ((IndexBits * 2) + IndexAlphaBits)) + (r << (IndexBits + IndexAlphaBits + 1)) - + (g << (IndexBits + IndexAlphaBits)) + (r << (IndexBits * 2)) + (r << (IndexBits + 1)) - + (g << IndexBits) + ((r + g + b) << IndexAlphaBits) + r + g + b + a; + return (r << ((IndexBits * 2) + IndexAlphaBits)) + + (r << (IndexBits + IndexAlphaBits + 1)) + + (g << (IndexBits + IndexAlphaBits)) + + (r << (IndexBits * 2)) + + (r << (IndexBits + 1)) + + (g << IndexBits) + + ((r + g + b) << IndexAlphaBits) + + r + g + b + a; } /// @@ -288,22 +297,22 @@ private static int GetPaletteIndex(int r, int g, int b, int a) /// The result. private static float Volume(ref Box cube, Span moment) { - return moment[GetPaletteIndex(cube.R1, cube.G1, cube.B1, cube.A1)] - - moment[GetPaletteIndex(cube.R1, cube.G1, cube.B1, cube.A0)] - - moment[GetPaletteIndex(cube.R1, cube.G1, cube.B0, cube.A1)] - + moment[GetPaletteIndex(cube.R1, cube.G1, cube.B0, cube.A0)] - - moment[GetPaletteIndex(cube.R1, cube.G0, cube.B1, cube.A1)] - + moment[GetPaletteIndex(cube.R1, cube.G0, cube.B1, cube.A0)] - + moment[GetPaletteIndex(cube.R1, cube.G0, cube.B0, cube.A1)] - - moment[GetPaletteIndex(cube.R1, cube.G0, cube.B0, cube.A0)] - - moment[GetPaletteIndex(cube.R0, cube.G1, cube.B1, cube.A1)] - + moment[GetPaletteIndex(cube.R0, cube.G1, cube.B1, cube.A0)] - + moment[GetPaletteIndex(cube.R0, cube.G1, cube.B0, cube.A1)] - - moment[GetPaletteIndex(cube.R0, cube.G1, cube.B0, cube.A0)] - + moment[GetPaletteIndex(cube.R0, cube.G0, cube.B1, cube.A1)] - - moment[GetPaletteIndex(cube.R0, cube.G0, cube.B1, cube.A0)] - - moment[GetPaletteIndex(cube.R0, cube.G0, cube.B0, cube.A1)] - + moment[GetPaletteIndex(cube.R0, cube.G0, cube.B0, cube.A0)]; + return moment[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMax, cube.AMax)] + - moment[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMax, cube.AMin)] + - moment[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMin, cube.AMax)] + + moment[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMin, cube.AMin)] + - moment[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMax, cube.AMax)] + + moment[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMax, cube.AMin)] + + moment[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMax)] + - moment[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMin)] + - moment[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMax, cube.AMax)] + + moment[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMax, cube.AMin)] + + moment[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMax)] + - moment[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMin)] + + moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMax)] + - moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMin)] + - moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMax)] + + moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMin)]; } /// @@ -319,47 +328,47 @@ private static long Bottom(ref Box cube, int direction, Span moment) { // Red case 3: - return -moment[GetPaletteIndex(cube.R0, cube.G1, cube.B1, cube.A1)] - + moment[GetPaletteIndex(cube.R0, cube.G1, cube.B1, cube.A0)] - + moment[GetPaletteIndex(cube.R0, cube.G1, cube.B0, cube.A1)] - - moment[GetPaletteIndex(cube.R0, cube.G1, cube.B0, cube.A0)] - + moment[GetPaletteIndex(cube.R0, cube.G0, cube.B1, cube.A1)] - - moment[GetPaletteIndex(cube.R0, cube.G0, cube.B1, cube.A0)] - - moment[GetPaletteIndex(cube.R0, cube.G0, cube.B0, cube.A1)] - + moment[GetPaletteIndex(cube.R0, cube.G0, cube.B0, cube.A0)]; + return -moment[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMax, cube.AMax)] + + moment[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMax, cube.AMin)] + + moment[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMax)] + - moment[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMin)] + + moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMax)] + - moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMin)] + - moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMax)] + + moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMin)]; // Green case 2: - return -moment[GetPaletteIndex(cube.R1, cube.G0, cube.B1, cube.A1)] - + moment[GetPaletteIndex(cube.R1, cube.G0, cube.B1, cube.A0)] - + moment[GetPaletteIndex(cube.R1, cube.G0, cube.B0, cube.A1)] - - moment[GetPaletteIndex(cube.R1, cube.G0, cube.B0, cube.A0)] - + moment[GetPaletteIndex(cube.R0, cube.G0, cube.B1, cube.A1)] - - moment[GetPaletteIndex(cube.R0, cube.G0, cube.B1, cube.A0)] - - moment[GetPaletteIndex(cube.R0, cube.G0, cube.B0, cube.A1)] - + moment[GetPaletteIndex(cube.R0, cube.G0, cube.B0, cube.A0)]; + return -moment[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMax, cube.AMax)] + + moment[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMax, cube.AMin)] + + moment[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMax)] + - moment[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMin)] + + moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMax)] + - moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMin)] + - moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMax)] + + moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMin)]; // Blue case 1: - return -moment[GetPaletteIndex(cube.R1, cube.G1, cube.B0, cube.A1)] - + moment[GetPaletteIndex(cube.R1, cube.G1, cube.B0, cube.A0)] - + moment[GetPaletteIndex(cube.R1, cube.G0, cube.B0, cube.A1)] - - moment[GetPaletteIndex(cube.R1, cube.G0, cube.B0, cube.A0)] - + moment[GetPaletteIndex(cube.R0, cube.G1, cube.B0, cube.A1)] - - moment[GetPaletteIndex(cube.R0, cube.G1, cube.B0, cube.A0)] - - moment[GetPaletteIndex(cube.R0, cube.G0, cube.B0, cube.A1)] - + moment[GetPaletteIndex(cube.R0, cube.G0, cube.B0, cube.A0)]; + return -moment[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMin, cube.AMax)] + + moment[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMin, cube.AMin)] + + moment[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMax)] + - moment[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMin)] + + moment[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMax)] + - moment[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMin)] + - moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMax)] + + moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMin)]; // Alpha case 0: - return -moment[GetPaletteIndex(cube.R1, cube.G1, cube.B1, cube.A0)] - + moment[GetPaletteIndex(cube.R1, cube.G1, cube.B0, cube.A0)] - + moment[GetPaletteIndex(cube.R1, cube.G0, cube.B1, cube.A0)] - - moment[GetPaletteIndex(cube.R1, cube.G0, cube.B0, cube.A0)] - + moment[GetPaletteIndex(cube.R0, cube.G1, cube.B1, cube.A0)] - - moment[GetPaletteIndex(cube.R0, cube.G1, cube.B0, cube.A0)] - - moment[GetPaletteIndex(cube.R0, cube.G0, cube.B1, cube.A0)] - + moment[GetPaletteIndex(cube.R0, cube.G0, cube.B0, cube.A0)]; + return -moment[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMax, cube.AMin)] + + moment[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMin, cube.AMin)] + + moment[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMax, cube.AMin)] + - moment[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMin)] + + moment[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMax, cube.AMin)] + - moment[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMin)] + - moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMin)] + + moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMin)]; default: throw new ArgumentOutOfRangeException(nameof(direction)); @@ -380,47 +389,47 @@ private static long Top(ref Box cube, int direction, int position, Span mo { // Red case 3: - return moment[GetPaletteIndex(position, cube.G1, cube.B1, cube.A1)] - - moment[GetPaletteIndex(position, cube.G1, cube.B1, cube.A0)] - - moment[GetPaletteIndex(position, cube.G1, cube.B0, cube.A1)] - + moment[GetPaletteIndex(position, cube.G1, cube.B0, cube.A0)] - - moment[GetPaletteIndex(position, cube.G0, cube.B1, cube.A1)] - + moment[GetPaletteIndex(position, cube.G0, cube.B1, cube.A0)] - + moment[GetPaletteIndex(position, cube.G0, cube.B0, cube.A1)] - - moment[GetPaletteIndex(position, cube.G0, cube.B0, cube.A0)]; + return moment[GetPaletteIndex(position, cube.GMax, cube.BMax, cube.AMax)] + - moment[GetPaletteIndex(position, cube.GMax, cube.BMax, cube.AMin)] + - moment[GetPaletteIndex(position, cube.GMax, cube.BMin, cube.AMax)] + + moment[GetPaletteIndex(position, cube.GMax, cube.BMin, cube.AMin)] + - moment[GetPaletteIndex(position, cube.GMin, cube.BMax, cube.AMax)] + + moment[GetPaletteIndex(position, cube.GMin, cube.BMax, cube.AMin)] + + moment[GetPaletteIndex(position, cube.GMin, cube.BMin, cube.AMax)] + - moment[GetPaletteIndex(position, cube.GMin, cube.BMin, cube.AMin)]; // Green case 2: - return moment[GetPaletteIndex(cube.R1, position, cube.B1, cube.A1)] - - moment[GetPaletteIndex(cube.R1, position, cube.B1, cube.A0)] - - moment[GetPaletteIndex(cube.R1, position, cube.B0, cube.A1)] - + moment[GetPaletteIndex(cube.R1, position, cube.B0, cube.A0)] - - moment[GetPaletteIndex(cube.R0, position, cube.B1, cube.A1)] - + moment[GetPaletteIndex(cube.R0, position, cube.B1, cube.A0)] - + moment[GetPaletteIndex(cube.R0, position, cube.B0, cube.A1)] - - moment[GetPaletteIndex(cube.R0, position, cube.B0, cube.A0)]; + return moment[GetPaletteIndex(cube.RMax, position, cube.BMax, cube.AMax)] + - moment[GetPaletteIndex(cube.RMax, position, cube.BMax, cube.AMin)] + - moment[GetPaletteIndex(cube.RMax, position, cube.BMin, cube.AMax)] + + moment[GetPaletteIndex(cube.RMax, position, cube.BMin, cube.AMin)] + - moment[GetPaletteIndex(cube.RMin, position, cube.BMax, cube.AMax)] + + moment[GetPaletteIndex(cube.RMin, position, cube.BMax, cube.AMin)] + + moment[GetPaletteIndex(cube.RMin, position, cube.BMin, cube.AMax)] + - moment[GetPaletteIndex(cube.RMin, position, cube.BMin, cube.AMin)]; // Blue case 1: - return moment[GetPaletteIndex(cube.R1, cube.G1, position, cube.A1)] - - moment[GetPaletteIndex(cube.R1, cube.G1, position, cube.A0)] - - moment[GetPaletteIndex(cube.R1, cube.G0, position, cube.A1)] - + moment[GetPaletteIndex(cube.R1, cube.G0, position, cube.A0)] - - moment[GetPaletteIndex(cube.R0, cube.G1, position, cube.A1)] - + moment[GetPaletteIndex(cube.R0, cube.G1, position, cube.A0)] - + moment[GetPaletteIndex(cube.R0, cube.G0, position, cube.A1)] - - moment[GetPaletteIndex(cube.R0, cube.G0, position, cube.A0)]; + return moment[GetPaletteIndex(cube.RMax, cube.GMax, position, cube.AMax)] + - moment[GetPaletteIndex(cube.RMax, cube.GMax, position, cube.AMin)] + - moment[GetPaletteIndex(cube.RMax, cube.GMin, position, cube.AMax)] + + moment[GetPaletteIndex(cube.RMax, cube.GMin, position, cube.AMin)] + - moment[GetPaletteIndex(cube.RMin, cube.GMax, position, cube.AMax)] + + moment[GetPaletteIndex(cube.RMin, cube.GMax, position, cube.AMin)] + + moment[GetPaletteIndex(cube.RMin, cube.GMin, position, cube.AMax)] + - moment[GetPaletteIndex(cube.RMin, cube.GMin, position, cube.AMin)]; // Alpha case 0: - return moment[GetPaletteIndex(cube.R1, cube.G1, cube.B1, position)] - - moment[GetPaletteIndex(cube.R1, cube.G1, cube.B0, position)] - - moment[GetPaletteIndex(cube.R1, cube.G0, cube.B1, position)] - + moment[GetPaletteIndex(cube.R1, cube.G0, cube.B0, position)] - - moment[GetPaletteIndex(cube.R0, cube.G1, cube.B1, position)] - + moment[GetPaletteIndex(cube.R0, cube.G1, cube.B0, position)] - + moment[GetPaletteIndex(cube.R0, cube.G0, cube.B1, position)] - - moment[GetPaletteIndex(cube.R0, cube.G0, cube.B0, position)]; + return moment[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMax, position)] + - moment[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMin, position)] + - moment[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMax, position)] + + moment[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, position)] + - moment[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMax, position)] + + moment[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, position)] + + moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, position)] + - moment[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, position)]; default: throw new ArgumentOutOfRangeException(nameof(direction)); @@ -440,7 +449,7 @@ private void Build3DHistogram(ImageFrame source, int width, int height) Span vmgSpan = this.vmg.GetSpan(); Span vmbSpan = this.vmb.GetSpan(); Span vmaSpan = this.vma.GetSpan(); - Span m2Span = this.m2.GetSpan(); + Span m2Span = this.m2.GetSpan(); // Build up the 3-D color histogram // Loop through each row @@ -489,34 +498,34 @@ private void Get3DMoments(MemoryAllocator memoryAllocator) Span vmgSpan = this.vmg.GetSpan(); Span vmbSpan = this.vmb.GetSpan(); Span vmaSpan = this.vma.GetSpan(); - Span m2Span = this.m2.GetSpan(); + Span m2Span = this.m2.GetSpan(); using (IMemoryOwner volume = memoryAllocator.Allocate(IndexCount * IndexAlphaCount)) using (IMemoryOwner volumeR = memoryAllocator.Allocate(IndexCount * IndexAlphaCount)) using (IMemoryOwner volumeG = memoryAllocator.Allocate(IndexCount * IndexAlphaCount)) using (IMemoryOwner volumeB = memoryAllocator.Allocate(IndexCount * IndexAlphaCount)) using (IMemoryOwner volumeA = memoryAllocator.Allocate(IndexCount * IndexAlphaCount)) - using (IMemoryOwner volume2 = memoryAllocator.Allocate(IndexCount * IndexAlphaCount)) + using (IMemoryOwner volume2 = memoryAllocator.Allocate(IndexCount * IndexAlphaCount)) using (IMemoryOwner area = memoryAllocator.Allocate(IndexAlphaCount)) using (IMemoryOwner areaR = memoryAllocator.Allocate(IndexAlphaCount)) using (IMemoryOwner areaG = memoryAllocator.Allocate(IndexAlphaCount)) using (IMemoryOwner areaB = memoryAllocator.Allocate(IndexAlphaCount)) using (IMemoryOwner areaA = memoryAllocator.Allocate(IndexAlphaCount)) - using (IMemoryOwner area2 = memoryAllocator.Allocate(IndexAlphaCount)) + using (IMemoryOwner area2 = memoryAllocator.Allocate(IndexAlphaCount)) { Span volumeSpan = volume.GetSpan(); Span volumeRSpan = volumeR.GetSpan(); Span volumeGSpan = volumeG.GetSpan(); Span volumeBSpan = volumeB.GetSpan(); Span volumeASpan = volumeA.GetSpan(); - Span volume2Span = volume2.GetSpan(); + Span volume2Span = volume2.GetSpan(); Span areaSpan = area.GetSpan(); Span areaRSpan = areaR.GetSpan(); Span areaGSpan = areaG.GetSpan(); Span areaBSpan = areaB.GetSpan(); Span areaASpan = areaA.GetSpan(); - Span area2Span = area2.GetSpan(); + Span area2Span = area2.GetSpan(); for (int r = 1; r < IndexCount; r++) { @@ -543,7 +552,7 @@ private void Get3DMoments(MemoryAllocator memoryAllocator) long lineG = 0; long lineB = 0; long lineA = 0; - float line2 = 0; + double line2 = 0; for (int a = 1; a < IndexAlphaCount; a++) { @@ -592,35 +601,35 @@ private void Get3DMoments(MemoryAllocator memoryAllocator) /// /// The cube. /// The . - private float Variance(ref Box cube) + private double Variance(ref Box cube) { float dr = Volume(ref cube, this.vmr.GetSpan()); float dg = Volume(ref cube, this.vmg.GetSpan()); float db = Volume(ref cube, this.vmb.GetSpan()); float da = Volume(ref cube, this.vma.GetSpan()); - Span m2Span = this.m2.GetSpan(); - - float xx = - m2Span[GetPaletteIndex(cube.R1, cube.G1, cube.B1, cube.A1)] - - m2Span[GetPaletteIndex(cube.R1, cube.G1, cube.B1, cube.A0)] - - m2Span[GetPaletteIndex(cube.R1, cube.G1, cube.B0, cube.A1)] - + m2Span[GetPaletteIndex(cube.R1, cube.G1, cube.B0, cube.A0)] - - m2Span[GetPaletteIndex(cube.R1, cube.G0, cube.B1, cube.A1)] - + m2Span[GetPaletteIndex(cube.R1, cube.G0, cube.B1, cube.A0)] - + m2Span[GetPaletteIndex(cube.R1, cube.G0, cube.B0, cube.A1)] - - m2Span[GetPaletteIndex(cube.R1, cube.G0, cube.B0, cube.A0)] - - m2Span[GetPaletteIndex(cube.R0, cube.G1, cube.B1, cube.A1)] - + m2Span[GetPaletteIndex(cube.R0, cube.G1, cube.B1, cube.A0)] - + m2Span[GetPaletteIndex(cube.R0, cube.G1, cube.B0, cube.A1)] - - m2Span[GetPaletteIndex(cube.R0, cube.G1, cube.B0, cube.A0)] - + m2Span[GetPaletteIndex(cube.R0, cube.G0, cube.B1, cube.A1)] - - m2Span[GetPaletteIndex(cube.R0, cube.G0, cube.B1, cube.A0)] - - m2Span[GetPaletteIndex(cube.R0, cube.G0, cube.B0, cube.A1)] - + m2Span[GetPaletteIndex(cube.R0, cube.G0, cube.B0, cube.A0)]; + Span m2Span = this.m2.GetSpan(); + + double moment = + m2Span[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMax, cube.AMax)] + - m2Span[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMax, cube.AMin)] + - m2Span[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMin, cube.AMax)] + + m2Span[GetPaletteIndex(cube.RMax, cube.GMax, cube.BMin, cube.AMin)] + - m2Span[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMax, cube.AMax)] + + m2Span[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMax, cube.AMin)] + + m2Span[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMax)] + - m2Span[GetPaletteIndex(cube.RMax, cube.GMin, cube.BMin, cube.AMin)] + - m2Span[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMax, cube.AMax)] + + m2Span[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMax, cube.AMin)] + + m2Span[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMax)] + - m2Span[GetPaletteIndex(cube.RMin, cube.GMax, cube.BMin, cube.AMin)] + + m2Span[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMax)] + - m2Span[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMax, cube.AMin)] + - m2Span[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMax)] + + m2Span[GetPaletteIndex(cube.RMin, cube.GMin, cube.BMin, cube.AMin)]; var vector = new Vector4(dr, dg, db, da); - return xx - (Vector4.Dot(vector, vector) / Volume(ref cube, this.vwt.GetSpan())); + return moment - (Vector4.Dot(vector, vector) / Volume(ref cube, this.vwt.GetSpan())); } /// @@ -714,10 +723,10 @@ private bool Cut(ref Box set1, ref Box set2) float wholeA = Volume(ref set1, this.vma.GetSpan()); float wholeW = Volume(ref set1, this.vwt.GetSpan()); - float maxr = this.Maximize(ref set1, 3, set1.R0 + 1, set1.R1, out int cutr, wholeR, wholeG, wholeB, wholeA, wholeW); - float maxg = this.Maximize(ref set1, 2, set1.G0 + 1, set1.G1, out int cutg, wholeR, wholeG, wholeB, wholeA, wholeW); - float maxb = this.Maximize(ref set1, 1, set1.B0 + 1, set1.B1, out int cutb, wholeR, wholeG, wholeB, wholeA, wholeW); - float maxa = this.Maximize(ref set1, 0, set1.A0 + 1, set1.A1, out int cuta, wholeR, wholeG, wholeB, wholeA, wholeW); + float maxr = this.Maximize(ref set1, 3, set1.RMin + 1, set1.RMax, out int cutr, wholeR, wholeG, wholeB, wholeA, wholeW); + float maxg = this.Maximize(ref set1, 2, set1.GMin + 1, set1.GMax, out int cutg, wholeR, wholeG, wholeB, wholeA, wholeW); + float maxb = this.Maximize(ref set1, 1, set1.BMin + 1, set1.BMax, out int cutb, wholeR, wholeG, wholeB, wholeA, wholeW); + float maxa = this.Maximize(ref set1, 0, set1.AMin + 1, set1.AMax, out int cuta, wholeR, wholeG, wholeB, wholeA, wholeW); int dir; @@ -743,48 +752,48 @@ private bool Cut(ref Box set1, ref Box set2) dir = 0; } - set2.R1 = set1.R1; - set2.G1 = set1.G1; - set2.B1 = set1.B1; - set2.A1 = set1.A1; + set2.RMax = set1.RMax; + set2.GMax = set1.GMax; + set2.BMax = set1.BMax; + set2.AMax = set1.AMax; switch (dir) { // Red case 3: - set2.R0 = set1.R1 = cutr; - set2.G0 = set1.G0; - set2.B0 = set1.B0; - set2.A0 = set1.A0; + set2.RMin = set1.RMax = cutr; + set2.GMin = set1.GMin; + set2.BMin = set1.BMin; + set2.AMin = set1.AMin; break; // Green case 2: - set2.G0 = set1.G1 = cutg; - set2.R0 = set1.R0; - set2.B0 = set1.B0; - set2.A0 = set1.A0; + set2.GMin = set1.GMax = cutg; + set2.RMin = set1.RMin; + set2.BMin = set1.BMin; + set2.AMin = set1.AMin; break; // Blue case 1: - set2.B0 = set1.B1 = cutb; - set2.R0 = set1.R0; - set2.G0 = set1.G0; - set2.A0 = set1.A0; + set2.BMin = set1.BMax = cutb; + set2.RMin = set1.RMin; + set2.GMin = set1.GMin; + set2.AMin = set1.AMin; break; // Alpha case 0: - set2.A0 = set1.A1 = cuta; - set2.R0 = set1.R0; - set2.G0 = set1.G0; - set2.B0 = set1.B0; + set2.AMin = set1.AMax = cuta; + set2.RMin = set1.RMin; + set2.GMin = set1.GMin; + set2.BMin = set1.BMin; break; } - set1.Volume = (set1.R1 - set1.R0) * (set1.G1 - set1.G0) * (set1.B1 - set1.B0) * (set1.A1 - set1.A0); - set2.Volume = (set2.R1 - set2.R0) * (set2.G1 - set2.G0) * (set2.B1 - set2.B0) * (set2.A1 - set2.A0); + set1.Volume = (set1.RMax - set1.RMin) * (set1.GMax - set1.GMin) * (set1.BMax - set1.BMin) * (set1.AMax - set1.AMin); + set2.Volume = (set2.RMax - set2.RMin) * (set2.GMax - set2.GMin) * (set2.BMax - set2.BMin) * (set2.AMax - set2.AMin); return true; } @@ -798,13 +807,13 @@ private void Mark(ref Box cube, byte label) { Span tagSpan = this.tag.GetSpan(); - for (int r = cube.R0 + 1; r <= cube.R1; r++) + for (int r = cube.RMin + 1; r <= cube.RMax; r++) { - for (int g = cube.G0 + 1; g <= cube.G1; g++) + for (int g = cube.GMin + 1; g <= cube.GMax; g++) { - for (int b = cube.B0 + 1; b <= cube.B1; b++) + for (int b = cube.BMin + 1; b <= cube.BMax; b++) { - for (int a = cube.A0 + 1; a <= cube.A1; a++) + for (int a = cube.AMin + 1; a <= cube.AMax; a++) { tagSpan[GetPaletteIndex(r, g, b, a)] = label; } @@ -819,12 +828,12 @@ private void Mark(ref Box cube, byte label) private void BuildCube() { this.colorCube = new Box[this.colors]; - float[] vv = new float[this.colors]; + double[] vv = new double[this.colors]; ref Box cube = ref this.colorCube[0]; - cube.R0 = cube.G0 = cube.B0 = cube.A0 = 0; - cube.R1 = cube.G1 = cube.B1 = IndexCount - 1; - cube.A1 = IndexAlphaCount - 1; + cube.RMin = cube.GMin = cube.BMin = cube.AMin = 0; + cube.RMax = cube.GMax = cube.BMax = IndexCount - 1; + cube.AMax = IndexAlphaCount - 1; int next = 0; @@ -839,13 +848,13 @@ private void BuildCube() } else { - vv[next] = 0F; + vv[next] = 0D; i--; } next = 0; - float temp = vv[0]; + double temp = vv[0]; for (int k = 1; k <= i; k++) { if (vv[k] > temp) @@ -855,7 +864,7 @@ private void BuildCube() } } - if (temp <= 0F) + if (temp <= 0D) { this.colors = i + 1; break; @@ -897,52 +906,83 @@ private byte QuantizePixel(ref TPixel pixel) /// /// Represents a box color cube. /// - private struct Box + private struct Box : IEquatable { /// /// Gets or sets the min red value, exclusive. /// - public int R0; + public int RMin; /// /// Gets or sets the max red value, inclusive. /// - public int R1; + public int RMax; /// /// Gets or sets the min green value, exclusive. /// - public int G0; + public int GMin; /// /// Gets or sets the max green value, inclusive. /// - public int G1; + public int GMax; /// /// Gets or sets the min blue value, exclusive. /// - public int B0; + public int BMin; /// /// Gets or sets the max blue value, inclusive. /// - public int B1; + public int BMax; /// /// Gets or sets the min alpha value, exclusive. /// - public int A0; + public int AMin; /// /// Gets or sets the max alpha value, inclusive. /// - public int A1; + public int AMax; /// /// Gets or sets the volume. /// public int Volume; + + /// + public override bool Equals(object obj) => obj is Box box && this.Equals(box); + + /// + public bool Equals(Box other) => + this.RMin == other.RMin + && this.RMax == other.RMax + && this.GMin == other.GMin + && this.GMax == other.GMax + && this.BMin == other.BMin + && this.BMax == other.BMax + && this.AMin == other.AMin + && this.AMax == other.AMax + && this.Volume == other.Volume; + + /// + public override int GetHashCode() + { + HashCode hash = default; + hash.Add(this.RMin); + hash.Add(this.RMax); + hash.Add(this.GMin); + hash.Add(this.GMax); + hash.Add(this.BMin); + hash.Add(this.BMax); + hash.Add(this.AMin); + hash.Add(this.AMax); + hash.Add(this.Volume); + return hash.ToHashCode(); + } } } } \ No newline at end of file From 23290222b2a93656831ac9a32b5bbd647f5006a3 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 5 Apr 2019 20:56:58 +1100 Subject: [PATCH 2/5] Add unit tests --- .../Quantization/WuFrameQuantizer{TPixel}.cs | 13 +- .../Quantization/WuQuantizerTests.cs | 145 ++++++++++++++++++ 2 files changed, 149 insertions(+), 9 deletions(-) create mode 100644 tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs diff --git a/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs index 77e1e00889..e9283da303 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs @@ -11,6 +11,8 @@ using SixLabors.ImageSharp.PixelFormats; using SixLabors.Memory; +// TODO: Isn't an AOS ("array of structures") layout more efficient & more readable than SOA ("structure of arrays") for this particular use case? +// (T, R, G, B, A, M2) could be grouped together! Investigate a ColorMoment struct. namespace SixLabors.ImageSharp.Processing.Processors.Quantization { /// @@ -36,13 +38,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization internal sealed class WuFrameQuantizer : FrameQuantizerBase where TPixel : struct, IPixel { - // TODO: The WuFrameQuantizer code is rising several questions: - // - Do we really need to ALWAYS allocate the whole table of size TableLength? (~ 2471625 * sizeof(long) * 5 bytes ) JS. I'm afraid so. - // - Isn't an AOS ("array of structures") layout more efficient & more readable than SOA ("structure of arrays") for this particular use case? - // (T, R, G, B, A, M2) could be grouped together! Investigate a ColorMoment struct. - // - It's a frequently used class, we need tests! (So we can optimize safely.) There are tests in the original!!! We should just adopt them! - // https://github.com/JeremyAnsel/JeremyAnsel.ColorQuant/blob/master/JeremyAnsel.ColorQuant/JeremyAnsel.ColorQuant.Tests/WuColorQuantizerTests.cs - // The following two variables determine the amount of bits to preserve when calculating the histogram. // Reducing the value of these numbers the granularity of the color maps produced, making it much faster // and using much less memory but potentially less accurate. Current results are very good though! @@ -316,7 +311,7 @@ private static float Volume(ref Box cube, Span moment) } /// - /// Computes part of Volume(cube, moment) that doesn't depend on r1, g1, or b1 (depending on direction). + /// Computes part of Volume(cube, moment) that doesn't depend on RMax, GMax, BMax, or AMax (depending on direction). /// /// The cube. /// The direction. @@ -376,7 +371,7 @@ private static long Bottom(ref Box cube, int direction, Span moment) } /// - /// Computes remainder of Volume(cube, moment), substituting position for r1, g1, or b1 (depending on direction). + /// Computes remainder of Volume(cube, moment), substituting position for RMax, GMax, BMax, or AMax (depending on direction). /// /// The cube. /// The direction. diff --git a/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs b/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs new file mode 100644 index 0000000000..57b6060b2f --- /dev/null +++ b/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs @@ -0,0 +1,145 @@ +using System; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors.Quantization; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Quantization +{ + public class WuQuantizerTests + { + [Fact] + public void SinglePixelOpaque() + { + Configuration config = Configuration.Default; + var quantizer = new WuQuantizer(false); + + using (var image = new Image(config, 1, 1, Rgba32.Black)) + using (QuantizedFrame result = quantizer.CreateFrameQuantizer(config).QuantizeFrame(image.Frames[0])) + { + Assert.Equal(1, result.Palette.Length); + Assert.Equal(1, result.GetPixelSpan().Length); + + Assert.Equal(Rgba32.Black, result.Palette[0]); + Assert.Equal(0, result.GetPixelSpan()[0]); + } + } + + [Fact] + public void SinglePixelTransparent() + { + Configuration config = Configuration.Default; + var quantizer = new WuQuantizer(false); + + using (var image = new Image(config, 1, 1, default(Rgba32))) + using (QuantizedFrame result = quantizer.CreateFrameQuantizer(config).QuantizeFrame(image.Frames[0])) + { + Assert.Equal(1, result.Palette.Length); + Assert.Equal(1, result.GetPixelSpan().Length); + + Assert.Equal(default, result.Palette[0]); + Assert.Equal(0, result.GetPixelSpan()[0]); + } + } + + [Fact] + public void GrayScale() => TestScale(c => new Rgba32(c, c, c, 128)); + + [Fact] + public void RedScale() => TestScale(c => new Rgba32(c, 0, 0, 128)); + + [Fact] + public void GreenScale() => TestScale(c => new Rgba32(0, c, 0, 128)); + + [Fact] + public void BlueScale() => TestScale(c => new Rgba32(0, 0, c, 128)); + + [Fact] + public void AlphaScale() => TestScale(c => new Rgba32(0, 0, 0, c)); + + [Fact] + public void Palette256() + { + var image = new Image(1, 256); + + for (int i = 0; i < 256; i++) + { + byte r = (byte)((i % 4) * 85); + byte g = (byte)(((i / 4) % 4) * 85); + byte b = (byte)(((i / 16) % 4) * 85); + byte a = (byte)((i / 64) * 85); + + image[0, i] = new Rgba32(r, g, b, a); + } + + Configuration config = Configuration.Default; + var quantizer = new WuQuantizer(false); + QuantizedFrame result = quantizer.CreateFrameQuantizer(config).QuantizeFrame(image.Frames[0]); + + Assert.Equal(256, result.Palette.Length); + Assert.Equal(256, result.GetPixelSpan().Length); + + var actualImage = new Image(1, 256); + + int paletteCount = result.Palette.Length - 1; + for (int y = 0; y < actualImage.Height; y++) + { + Span row = actualImage.GetPixelRowSpan(y); + ReadOnlySpan quantizedPixelSpan = result.GetPixelSpan(); + int yy = y * actualImage.Width; + + for (int x = 0; x < actualImage.Width; x++) + { + int i = x + yy; + row[x] = result.Palette[Math.Min(paletteCount, quantizedPixelSpan[i])]; + } + } + + Assert.True(image.GetPixelSpan().SequenceEqual(actualImage.GetPixelSpan())); + } + + private static void TestScale(Func pixelBuilder) + { + using (var image = new Image(1, 256)) + using (var expectedImage = new Image(1, 256)) + using (var actualImage = new Image(1, 256)) + { + for (int i = 0; i < 256; i++) + { + byte c = (byte)i; + image[0, i] = pixelBuilder.Invoke(c); + } + + for (int i = 0; i < 256; i++) + { + byte c = (byte)((i & ~7) + 4); + expectedImage[0, i] = pixelBuilder.Invoke(c); + } + + Configuration config = Configuration.Default; + var quantizer = new WuQuantizer(false); + using (QuantizedFrame result = quantizer.CreateFrameQuantizer(config).QuantizeFrame(image.Frames[0])) + { + Assert.Equal(4 * 8, result.Palette.Length); + Assert.Equal(256, result.GetPixelSpan().Length); + + int paletteCount = result.Palette.Length - 1; + for (int y = 0; y < actualImage.Height; y++) + { + Span row = actualImage.GetPixelRowSpan(y); + ReadOnlySpan quantizedPixelSpan = result.GetPixelSpan(); + int yy = y * actualImage.Width; + + for (int x = 0; x < actualImage.Width; x++) + { + int i = x + yy; + row[x] = result.Palette[Math.Min(paletteCount, quantizedPixelSpan[i])]; + } + } + } + + Assert.True(expectedImage.GetPixelSpan().SequenceEqual(actualImage.GetPixelSpan())); + } + } + } +} \ No newline at end of file From d4130877e121128d908a95df25aab20498a13dfa Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 12 Apr 2019 23:35:35 +1000 Subject: [PATCH 3/5] Add test that fails with old image. --- .../Quantization/WuQuantizerTests.cs | 71 +++++++++++------- tests/ImageSharp.Tests/TestImages.cs | 1 + tests/Images/Input/Png/low-variance.png | Bin 0 -> 7844 bytes 3 files changed, 46 insertions(+), 26 deletions(-) create mode 100644 tests/Images/Input/Png/low-variance.png diff --git a/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs b/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs index 57b6060b2f..de7726d85a 100644 --- a/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs +++ b/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs @@ -60,42 +60,61 @@ public void SinglePixelTransparent() [Fact] public void Palette256() { - var image = new Image(1, 256); - - for (int i = 0; i < 256; i++) + using (var image = new Image(1, 256)) { - byte r = (byte)((i % 4) * 85); - byte g = (byte)(((i / 4) % 4) * 85); - byte b = (byte)(((i / 16) % 4) * 85); - byte a = (byte)((i / 64) * 85); + for (int i = 0; i < 256; i++) + { + byte r = (byte)((i % 4) * 85); + byte g = (byte)(((i / 4) % 4) * 85); + byte b = (byte)(((i / 16) % 4) * 85); + byte a = (byte)((i / 64) * 85); - image[0, i] = new Rgba32(r, g, b, a); - } + image[0, i] = new Rgba32(r, g, b, a); + } - Configuration config = Configuration.Default; - var quantizer = new WuQuantizer(false); - QuantizedFrame result = quantizer.CreateFrameQuantizer(config).QuantizeFrame(image.Frames[0]); + Configuration config = Configuration.Default; + var quantizer = new WuQuantizer(false); + using (QuantizedFrame result = quantizer.CreateFrameQuantizer(config).QuantizeFrame(image.Frames[0])) + { + Assert.Equal(256, result.Palette.Length); + Assert.Equal(256, result.GetPixelSpan().Length); - Assert.Equal(256, result.Palette.Length); - Assert.Equal(256, result.GetPixelSpan().Length); + var actualImage = new Image(1, 256); - var actualImage = new Image(1, 256); + int paletteCount = result.Palette.Length - 1; + for (int y = 0; y < actualImage.Height; y++) + { + Span row = actualImage.GetPixelRowSpan(y); + ReadOnlySpan quantizedPixelSpan = result.GetPixelSpan(); + int yy = y * actualImage.Width; - int paletteCount = result.Palette.Length - 1; - for (int y = 0; y < actualImage.Height; y++) - { - Span row = actualImage.GetPixelRowSpan(y); - ReadOnlySpan quantizedPixelSpan = result.GetPixelSpan(); - int yy = y * actualImage.Width; + for (int x = 0; x < actualImage.Width; x++) + { + int i = x + yy; + row[x] = result.Palette[Math.Min(paletteCount, quantizedPixelSpan[i])]; + } + } - for (int x = 0; x < actualImage.Width; x++) - { - int i = x + yy; - row[x] = result.Palette[Math.Min(paletteCount, quantizedPixelSpan[i])]; + Assert.True(image.GetPixelSpan().SequenceEqual(actualImage.GetPixelSpan())); } } + } - Assert.True(image.GetPixelSpan().SequenceEqual(actualImage.GetPixelSpan())); + [Theory] + [WithFile(TestImages.Png.LowColorVariance, PixelTypes.Rgba32)] + public void LowVariance(TestImageProvider provider) + where TPixel : struct, IPixel + { + // See https://github.com/SixLabors/ImageSharp/issues/866 + using (Image image = provider.GetImage()) + { + Configuration config = Configuration.Default; + var quantizer = new WuQuantizer(false); + using (QuantizedFrame result = quantizer.CreateFrameQuantizer(config).QuantizeFrame(image.Frames[0])) + { + Assert.Equal(48, result.Palette.Length); + } + } } private static void TestScale(Func pixelBuilder) diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 9099696247..1efb9e35fd 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -53,6 +53,7 @@ public static class Png public const string Gray2BitTrans = "Png/gray-2-tRNS.png"; public const string Gray4BitTrans = "Png/gray-4-tRNS.png"; public const string Gray8BitTrans = "Png/gray-8-tRNS.png"; + public const string LowColorVariance = "Png/low-variance.png"; // Filtered test images from http://www.schaik.com/pngsuite/pngsuite_fil_png.html public const string Filter0 = "Png/filter0.png"; diff --git a/tests/Images/Input/Png/low-variance.png b/tests/Images/Input/Png/low-variance.png new file mode 100644 index 0000000000000000000000000000000000000000..5b6c19bace8ef66d02b0fec96e2f06b9f2534701 GIT binary patch literal 7844 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 Date: Sat, 13 Apr 2019 00:06:27 +1000 Subject: [PATCH 4/5] Make IFrameQuantizer IDisposable --- src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 24 +++++++---- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 7 ++- .../FrameQuantizerBase{TPixel}.cs | 5 +++ .../Quantization/IFrameQuantizer{TPixel}.cs | 3 +- .../Quantization/QuantizeProcessor.cs | 2 +- .../Quantization/WuFrameQuantizer{TPixel}.cs | 43 +++++++++---------- .../Quantization/WuQuantizerTests.cs | 10 +++-- 7 files changed, 56 insertions(+), 38 deletions(-) diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index c8580144ad..85af08cbb5 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -95,8 +95,11 @@ public void Encode(Image image, Stream stream) bool useGlobalTable = this.colorTableMode == GifColorTableMode.Global; // Quantize the image returning a palette. - QuantizedFrame quantized = - this.quantizer.CreateFrameQuantizer(image.GetConfiguration()).QuantizeFrame(image.Frames.RootFrame); + QuantizedFrame quantized = null; + using (IFrameQuantizer frameQuantizer = this.quantizer.CreateFrameQuantizer(image.GetConfiguration())) + { + quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame); + } // Get the number of bits. this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length).Clamp(1, 8); @@ -133,7 +136,6 @@ public void Encode(Image image, Stream stream) // Clean up. quantized?.Dispose(); - quantized = null; // TODO: Write extension etc stream.WriteByte(GifConstants.EndIntroducer); @@ -158,7 +160,8 @@ private void EncodeGlobal(Image image, QuantizedFrame qu } else { - using (QuantizedFrame paletteQuantized = palleteQuantizer.CreateFrameQuantizer(image.GetConfiguration()).QuantizeFrame(frame)) + using (IFrameQuantizer palleteFrameQuantizer = palleteQuantizer.CreateFrameQuantizer(image.GetConfiguration())) + using (QuantizedFrame paletteQuantized = palleteFrameQuantizer.QuantizeFrame(frame)) { this.WriteImageData(paletteQuantized, stream); } @@ -181,14 +184,17 @@ private void EncodeLocal(Image image, QuantizedFrame qua if (previousFrame != null && previousMeta.ColorTableLength != frameMetaData.ColorTableLength && frameMetaData.ColorTableLength > 0) { - quantized = this.quantizer.CreateFrameQuantizer( - image.GetConfiguration(), - frameMetaData.ColorTableLength).QuantizeFrame(frame); + using (IFrameQuantizer frameQuantizer = this.quantizer.CreateFrameQuantizer(image.GetConfiguration(), frameMetaData.ColorTableLength)) + { + quantized = frameQuantizer.QuantizeFrame(frame); + } } else { - quantized = this.quantizer.CreateFrameQuantizer(image.GetConfiguration()) - .QuantizeFrame(frame); + using (IFrameQuantizer frameQuantizer = this.quantizer.CreateFrameQuantizer(image.GetConfiguration())) + { + quantized = frameQuantizer.QuantizeFrame(frame); + } } } diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 96e2dd946d..0992f239d7 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -243,8 +243,11 @@ public void Encode(Image image, Stream stream) } // Create quantized frame returning the palette and set the bit depth. - quantized = this.quantizer.CreateFrameQuantizer(image.GetConfiguration()) - .QuantizeFrame(image.Frames.RootFrame); + using (IFrameQuantizer frameQuantizer = this.quantizer.CreateFrameQuantizer(image.GetConfiguration())) + { + quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame); + } + byte quantizedBits = (byte)ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length).Clamp(1, 8); bits = Math.Max(bits, quantizedBits); diff --git a/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerBase{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerBase{TPixel}.cs index a70cfb6601..f23343f6d7 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerBase{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizerBase{TPixel}.cs @@ -98,6 +98,11 @@ public virtual QuantizedFrame QuantizeFrame(ImageFrame image) return quantizedFrame; } + /// + public virtual void Dispose() + { + } + /// /// Execute the first pass through the pixels in the image to create the palette. /// diff --git a/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs index 50fdb5b587..f0b68b3a08 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing.Processors.Dithering; @@ -10,7 +11,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// Provides methods to allow the execution of the quantization process on an image frame. /// /// The pixel format. - public interface IFrameQuantizer + public interface IFrameQuantizer : IDisposable where TPixel : struct, IPixel { /// diff --git a/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor.cs b/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor.cs index 8da89bf94a..e99f504b42 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor.cs @@ -33,7 +33,7 @@ public QuantizeProcessor(IQuantizer quantizer) /// protected override void OnFrameApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) { - IFrameQuantizer executor = this.Quantizer.CreateFrameQuantizer(configuration); + using (IFrameQuantizer executor = this.Quantizer.CreateFrameQuantizer(configuration)) using (QuantizedFrame quantized = executor.QuantizeFrame(source)) { int paletteCount = quantized.Palette.Length - 1; diff --git a/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs index e9283da303..1f1513adf1 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs @@ -148,28 +148,27 @@ public override QuantizedFrame QuantizeFrame(ImageFrame image) Guard.NotNull(image, nameof(image)); MemoryAllocator memoryAllocator = image.MemoryAllocator; - try - { - this.vwt = memoryAllocator.Allocate(TableLength, AllocationOptions.Clean); - this.vmr = memoryAllocator.Allocate(TableLength, AllocationOptions.Clean); - this.vmg = memoryAllocator.Allocate(TableLength, AllocationOptions.Clean); - this.vmb = memoryAllocator.Allocate(TableLength, AllocationOptions.Clean); - this.vma = memoryAllocator.Allocate(TableLength, AllocationOptions.Clean); - this.m2 = memoryAllocator.Allocate(TableLength, AllocationOptions.Clean); - this.tag = memoryAllocator.Allocate(TableLength, AllocationOptions.Clean); - - return base.QuantizeFrame(image); - } - finally - { - this.vwt?.Dispose(); - this.vmr?.Dispose(); - this.vmg?.Dispose(); - this.vmb?.Dispose(); - this.vma?.Dispose(); - this.m2?.Dispose(); - this.tag?.Dispose(); - } + this.vwt = memoryAllocator.Allocate(TableLength, AllocationOptions.Clean); + this.vmr = memoryAllocator.Allocate(TableLength, AllocationOptions.Clean); + this.vmg = memoryAllocator.Allocate(TableLength, AllocationOptions.Clean); + this.vmb = memoryAllocator.Allocate(TableLength, AllocationOptions.Clean); + this.vma = memoryAllocator.Allocate(TableLength, AllocationOptions.Clean); + this.m2 = memoryAllocator.Allocate(TableLength, AllocationOptions.Clean); + this.tag = memoryAllocator.Allocate(TableLength, AllocationOptions.Clean); + + return base.QuantizeFrame(image); + } + + /// + public override void Dispose() + { + this.vwt?.Dispose(); + this.vmr?.Dispose(); + this.vmg?.Dispose(); + this.vmb?.Dispose(); + this.vma?.Dispose(); + this.m2?.Dispose(); + this.tag?.Dispose(); } internal TPixel[] AotGetPalette() => this.GetPalette(); diff --git a/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs b/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs index de7726d85a..3eacd74ea1 100644 --- a/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs +++ b/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs @@ -74,7 +74,8 @@ public void Palette256() Configuration config = Configuration.Default; var quantizer = new WuQuantizer(false); - using (QuantizedFrame result = quantizer.CreateFrameQuantizer(config).QuantizeFrame(image.Frames[0])) + using (IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(config)) + using (QuantizedFrame result = frameQuantizer.QuantizeFrame(image.Frames[0])) { Assert.Equal(256, result.Palette.Length); Assert.Equal(256, result.GetPixelSpan().Length); @@ -110,7 +111,8 @@ public void LowVariance(TestImageProvider provider) { Configuration config = Configuration.Default; var quantizer = new WuQuantizer(false); - using (QuantizedFrame result = quantizer.CreateFrameQuantizer(config).QuantizeFrame(image.Frames[0])) + using (IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(config)) + using (QuantizedFrame result = frameQuantizer.QuantizeFrame(image.Frames[0])) { Assert.Equal(48, result.Palette.Length); } @@ -137,7 +139,9 @@ private static void TestScale(Func pixelBuilder) Configuration config = Configuration.Default; var quantizer = new WuQuantizer(false); - using (QuantizedFrame result = quantizer.CreateFrameQuantizer(config).QuantizeFrame(image.Frames[0])) + + using (IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(config)) + using (QuantizedFrame result = frameQuantizer.QuantizeFrame(image.Frames[0])) { Assert.Equal(4 * 8, result.Palette.Length); Assert.Equal(256, result.GetPixelSpan().Length); From 448651285019977d37a82e63df9df56269ab7c0c Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 13 Apr 2019 16:33:46 +1000 Subject: [PATCH 5/5] Update GifEncoderCore.cs --- src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index 9d442e2289..12a515cca7 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -184,7 +184,7 @@ private void EncodeLocal(Image image, QuantizedFrame qua if (previousFrame != null && previousMeta.ColorTableLength != frameMetadata.ColorTableLength && frameMetadata.ColorTableLength > 0) { - using (IFrameQuantizer frameQuantizer = this.quantizer.CreateFrameQuantizer(image.GetConfiguration(), frameMetaData.ColorTableLength)) + using (IFrameQuantizer frameQuantizer = this.quantizer.CreateFrameQuantizer(image.GetConfiguration(), frameMetadata.ColorTableLength)) { quantized = frameQuantizer.QuantizeFrame(frame); }