From 96400d78031bd40286e0082d57ca8cc1f2c905e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kaarlo=20R=C3=A4ih=C3=A4?= Date: Wed, 5 Feb 2025 21:16:04 +0200 Subject: [PATCH] Massive generic math refactor Breaks compatibility --- src/AtkinsonDithering.cs | 7 +- src/BurkesDithering.cs | 7 +- src/FakeDithering.cs | 6 +- src/FloydSteinbergDithering.cs | 29 ++-- src/JarvisJudiceNinkeDithering.cs | 7 +- src/LibDithering.csproj | 2 +- src/SierraDithering.cs | 7 +- src/SierraLiteDithering.cs | 7 +- src/SierraTwoRowDithering.cs | 7 +- src/StuckiDithering.cs | 7 +- src/TempByteImageFormat.cs | 11 +- src/TempDoubleImageFormat.cs | 56 ++++++- src/TempImageFormat.cs | 265 ++++++++++++++++++++++++++++++ tests/DitheringTests.cs | 133 +++++++++++---- tests/TempImageFormatTests.cs | 225 +++++++++++++++++++++++++ 15 files changed, 690 insertions(+), 86 deletions(-) create mode 100644 src/TempImageFormat.cs create mode 100644 tests/TempImageFormatTests.cs diff --git a/src/AtkinsonDithering.cs b/src/AtkinsonDithering.cs index dd4db60..a848acb 100644 --- a/src/AtkinsonDithering.cs +++ b/src/AtkinsonDithering.cs @@ -4,17 +4,18 @@ This file implements error pushing of dithering via Atkinson kernel. This is free and unencumbered software released into the public domain. */ using System; +using System.Numerics; /// -/// Atkinson dithering for RGB bytes +/// Atkinson dithering for RGB /// -public sealed class AtkinsonDitheringRGBByte : DitheringBase +public sealed class AtkinsonDitheringRGB : DitheringBase where T : INumber { /// /// Constructor for Atkinson dithering /// /// Color function - public AtkinsonDitheringRGBByte(ColorFunction colorfunc) : base(colorfunc, "Atkinson", "_ATK") + public AtkinsonDitheringRGB(ColorFunction colorfunc) : base(colorfunc, "Atkinson", "_ATK") { } diff --git a/src/BurkesDithering.cs b/src/BurkesDithering.cs index 1502dce..23a19ad 100644 --- a/src/BurkesDithering.cs +++ b/src/BurkesDithering.cs @@ -4,17 +4,18 @@ This file implements error pushing of dithering via (Daniel) Burkes kernel. This is free and unencumbered software released into the public domain. */ using System; +using System.Numerics; /// -/// Burkes dithering for RGB bytes +/// Burkes dithering for RGB /// -public sealed class BurkesDitheringRGBByte : DitheringBase +public sealed class BurkesDitheringRGB : DitheringBase where T : INumber { /// /// Constructor for Burkes dithering /// /// Color function - public BurkesDitheringRGBByte(ColorFunction colorfunc) : base(colorfunc, "Burkes", "_BUR") + public BurkesDitheringRGB(ColorFunction colorfunc) : base(colorfunc, "Burkes", "_BUR") { } diff --git a/src/FakeDithering.cs b/src/FakeDithering.cs index 8e81671..98df417 100644 --- a/src/FakeDithering.cs +++ b/src/FakeDithering.cs @@ -4,18 +4,19 @@ This is free and unencumbered software released into the public domain. */ using System; +using System.Numerics; /// /// Fake dithering doesn't do any dithering. It only does color reduction /// -public sealed class FakeDitheringRGBByte : DitheringBase +public sealed class FakeDitheringRGB : DitheringBase where T : INumber { /// /// Constructor for fake dithering (no dither, just color reduction) /// /// /// - public FakeDitheringRGBByte(ColorFunction colorfunc) : base(colorfunc, "No dithering", "_NONE") + public FakeDitheringRGB(ColorFunction colorfunc) : base(colorfunc, "No dithering", "_NONE") { } @@ -31,4 +32,3 @@ override protected void PushError(int x, int y, double[] quantError) // Don't do anything } } - diff --git a/src/FloydSteinbergDithering.cs b/src/FloydSteinbergDithering.cs index 656fe43..0bff510 100644 --- a/src/FloydSteinbergDithering.cs +++ b/src/FloydSteinbergDithering.cs @@ -4,20 +4,21 @@ This file implements error pushing of dithering via (Robert) Floyd and (Louis) S This is free and unencumbered software released into the public domain. */ using System; +using System.Numerics; /// -/// Floyd-Steinberg dithering for RGB bytes +/// Floyd-Steinberg dithering for RGB /// -public sealed class FloydSteinbergDitheringRGBByte : DitheringBase +public sealed class FloydSteinbergDitheringRGB : DitheringBase where T : INumber { /// /// Constructor for Floyd-Steinberg dithering /// /// Color function - public FloydSteinbergDitheringRGBByte(ColorFunction colorfunc) : base(colorfunc, "Floyd-Steinberg", "_FS") - { + public FloydSteinbergDitheringRGB(ColorFunction colorfunc) : base(colorfunc, "Floyd-Steinberg", "_FS") + { - } + } /// /// Push error method for Floyd-Steinberg dithering @@ -35,26 +36,26 @@ override protected void PushError(int x, int y, double[] quantError) int xPlusOne = x + 1; int yPlusOne = y + 1; - // Current row - if (this.IsValidCoordinate(xPlusOne, y)) + // Current row + if (this.IsValidCoordinate(xPlusOne, y)) { - this.ModifyImageWithErrorAndMultiplier(xPlusOne, y, quantError, 7.0 / 16.0); + this.ModifyImageWithErrorAndMultiplier(xPlusOne, y, quantError, 7.0 / 16.0); } - // Next row - if (this.IsValidCoordinate(xMinusOne, yPlusOne)) + // Next row + if (this.IsValidCoordinate(xMinusOne, yPlusOne)) { - this.ModifyImageWithErrorAndMultiplier(xMinusOne, yPlusOne, quantError, 3.0 / 16.0); + this.ModifyImageWithErrorAndMultiplier(xMinusOne, yPlusOne, quantError, 3.0 / 16.0); } if (this.IsValidCoordinate(x, yPlusOne)) { - this.ModifyImageWithErrorAndMultiplier(x, yPlusOne, quantError, 5.0 / 16.0); + this.ModifyImageWithErrorAndMultiplier(x, yPlusOne, quantError, 5.0 / 16.0); } if (this.IsValidCoordinate(xPlusOne, yPlusOne)) { - this.ModifyImageWithErrorAndMultiplier(xPlusOne, yPlusOne, quantError, 1.0 / 16.0); + this.ModifyImageWithErrorAndMultiplier(xPlusOne, yPlusOne, quantError, 1.0 / 16.0); } } -} +} \ No newline at end of file diff --git a/src/JarvisJudiceNinkeDithering.cs b/src/JarvisJudiceNinkeDithering.cs index 2325b48..febfc5d 100644 --- a/src/JarvisJudiceNinkeDithering.cs +++ b/src/JarvisJudiceNinkeDithering.cs @@ -4,17 +4,18 @@ This is free and unencumbered software released into the public domain. */ using System; +using System.Numerics; /// -/// Jarvis-Judice-Ninke dithering for RGB bytes +/// Jarvis-Judice-Ninke dithering for RGB /// -public sealed class JarvisJudiceNinkeDitheringRGBByte : DitheringBase +public sealed class JarvisJudiceNinkeDitheringRGB : DitheringBase where T : INumber { /// /// Constructor for Jarvis-Judice-Ninke dithering /// /// Color function - public JarvisJudiceNinkeDitheringRGBByte(ColorFunction colorfunc) : base(colorfunc, "Jarvis-Judice-Ninke", "_JJN") + public JarvisJudiceNinkeDitheringRGB(ColorFunction colorfunc) : base(colorfunc, "Jarvis-Judice-Ninke", "_JJN") { } diff --git a/src/LibDithering.csproj b/src/LibDithering.csproj index ebda221..311f3cd 100644 --- a/src/LibDithering.csproj +++ b/src/LibDithering.csproj @@ -1,7 +1,7 @@ - net8.0 + net8.0;net9.0 LibDithering 1.0.1 $(VersionSuffix) diff --git a/src/SierraDithering.cs b/src/SierraDithering.cs index ff687d6..df39be6 100644 --- a/src/SierraDithering.cs +++ b/src/SierraDithering.cs @@ -5,17 +5,18 @@ This is free and unencumbered software released into the public domain. */ using System; +using System.Numerics; /// -/// Sierra dithering for RGB bytes +/// Sierra dithering for RGB /// -public sealed class SierraDitheringRGBByte : DitheringBase +public sealed class SierraDitheringRGB : DitheringBase where T : INumber { /// /// Constructor for Sierra dithering /// /// Color function - public SierraDitheringRGBByte(ColorFunction colorfunc) : base(colorfunc, "Sierra", "_SIE") + public SierraDitheringRGB(ColorFunction colorfunc) : base(colorfunc, "Sierra", "_SIE") { } diff --git a/src/SierraLiteDithering.cs b/src/SierraLiteDithering.cs index 6da4003..baa372c 100644 --- a/src/SierraLiteDithering.cs +++ b/src/SierraLiteDithering.cs @@ -4,17 +4,18 @@ This file implements error pushing of dithering via (Frankie) Sierra Lite kernel This is free and unencumbered software released into the public domain. */ using System; +using System.Numerics; /// -/// Sierra lite dithering for RGB bytes +/// Sierra lite dithering for RGB /// -public sealed class SierraLiteDitheringRGBByte : DitheringBase +public sealed class SierraLiteDitheringRGB : DitheringBase where T : INumber { /// /// Constructor for Sierra lite dithering /// /// Color function - public SierraLiteDitheringRGBByte(ColorFunction colorfunc) : base(colorfunc, "SierraLite", "_SIEL") + public SierraLiteDitheringRGB(ColorFunction colorfunc) : base(colorfunc, "SierraLite", "_SIEL") { } diff --git a/src/SierraTwoRowDithering.cs b/src/SierraTwoRowDithering.cs index 9a7af1f..c012fc1 100644 --- a/src/SierraTwoRowDithering.cs +++ b/src/SierraTwoRowDithering.cs @@ -4,17 +4,18 @@ This file implements error pushing of dithering via (Frankie) Sierra Two Row ker This is free and unencumbered software released into the public domain. */ using System; +using System.Numerics; /// -/// Sierra two row dithering for RGB bytes +/// Sierra two row dithering for RGB /// -public sealed class SierraTwoRowDitheringRGBByte : DitheringBase +public sealed class SierraTwoRowDitheringRGB : DitheringBase where T : INumber { /// /// Constructor for Sierra two row dithering /// /// Color function - public SierraTwoRowDitheringRGBByte(ColorFunction colorfunc) : base(colorfunc, "SierraTwoRow", "_SIE2R") + public SierraTwoRowDitheringRGB(ColorFunction colorfunc) : base(colorfunc, "SierraTwoRow", "_SIE2R") { } diff --git a/src/StuckiDithering.cs b/src/StuckiDithering.cs index 086236d..eb7d44e 100644 --- a/src/StuckiDithering.cs +++ b/src/StuckiDithering.cs @@ -5,17 +5,18 @@ This is free and unencumbered software released into the public domain. */ using System; +using System.Numerics; /// -/// Stucki dithering for RGB bytes +/// Stucki dithering for RGB /// -public sealed class StuckiDitheringRGBByte : DitheringBase +public sealed class StuckiDitheringRGB : DitheringBase where T : INumber { /// /// Constructor for Stucki dithering /// /// Color function - public StuckiDitheringRGBByte(ColorFunction colorfunc) : base(colorfunc, "Stucki", "_STU") + public StuckiDitheringRGB(ColorFunction colorfunc) : base(colorfunc, "Stucki", "_STU") { } diff --git a/src/TempByteImageFormat.cs b/src/TempByteImageFormat.cs index 77ad8ae..52482de 100644 --- a/src/TempByteImageFormat.cs +++ b/src/TempByteImageFormat.cs @@ -270,15 +270,12 @@ public void ModifyPixelChannelsWithQuantError(ref byte[] modifyValues, double[] } } + private const double clampMin = (double)byte.MinValue; + private const double clampMax = (double)byte.MaxValue; + private static byte GetLimitedValue(byte original, double error) { double newValue = original + error; - return Clamp(newValue, byte.MinValue, byte.MaxValue); - } - - // C# doesn't have a Clamp method so we need this - private static byte Clamp(double value, double min, double max) - { - return (value < min) ? (byte)min : (value > max) ? (byte)max : (byte)value; + return (byte)Math.Clamp(newValue, clampMin, clampMax); } } \ No newline at end of file diff --git a/src/TempDoubleImageFormat.cs b/src/TempDoubleImageFormat.cs index 8c9290a..05b1b53 100644 --- a/src/TempDoubleImageFormat.cs +++ b/src/TempDoubleImageFormat.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.Intrinsics; /// /// Temp double based image format. 0.0 is zero color, 1.0 is max color @@ -262,23 +263,60 @@ public void GetQuantErrorsPerChannel(in double[] originalPixel, in double[] newP /// Values to modify /// Quantization errors /// Multiplier + #if NET9_0 public void ModifyPixelChannelsWithQuantError(ref double[] modifyValues, double[] quantErrors, double multiplier) { - for (int i = 0; i < this.channelsPerPixel; i++) + if (this.channelsPerPixel == 1) { - modifyValues[i] = GetLimitedValue(modifyValues[i], quantErrors[i] * multiplier); + modifyValues[0] = GetLimitedValue(modifyValues[0], quantErrors[0] * multiplier); } - } + else if (this.channelsPerPixel == 3) + { + Span temp = stackalloc double[4]; - private static double GetLimitedValue(double original, double error) + Vector256 originals = Vector256.Create(modifyValues[0], modifyValues[1], modifyValues[2], 0); + Vector256 errors = Vector256.Create(quantErrors[0], quantErrors[1], quantErrors[2], 0); + errors *= multiplier; + originals += errors; + + Vector256.Clamp(originals, Vector256.Zero, Vector256.One); + Vector256.CopyTo(originals, temp); + temp.Slice(0, 3).CopyTo(modifyValues); + } + else + { + for (int i = 0; i < this.channelsPerPixel; i++) + { + modifyValues[i] = GetLimitedValue(modifyValues[i], quantErrors[i] * multiplier); + } + } + } + #else + public void ModifyPixelChannelsWithQuantError(ref double[] modifyValues, double[] quantErrors, double multiplier) { - double newValue = original + error; - return Clamp(newValue, 0.0, 1.0); + if (this.channelsPerPixel == 1) + { + modifyValues[0] = GetLimitedValue(modifyValues[0], quantErrors[0] * multiplier); + } + else if (this.channelsPerPixel == 3) + { + modifyValues[0] = GetLimitedValue(modifyValues[0], quantErrors[0] * multiplier); + modifyValues[1] = GetLimitedValue(modifyValues[1], quantErrors[1] * multiplier); + modifyValues[2] = GetLimitedValue(modifyValues[2], quantErrors[2] * multiplier); + } + else + { + for (int i = 0; i < this.channelsPerPixel; i++) + { + modifyValues[i] = GetLimitedValue(modifyValues[i], quantErrors[i] * multiplier); + } + } } + #endif // NET9_0 - // C# doesn't have a Clamp method so we need this - private static double Clamp(double value, double min, double max) + private static double GetLimitedValue(double original, double error) { - return (value < min) ? 0.0 : (value > max) ? 1.0 : value; + double newValue = original + error; + return Math.Clamp(newValue, 0.0, 1.0); } } \ No newline at end of file diff --git a/src/TempImageFormat.cs b/src/TempImageFormat.cs new file mode 100644 index 0000000..36b06f0 --- /dev/null +++ b/src/TempImageFormat.cs @@ -0,0 +1,265 @@ +using System; +using System.Numerics; + +/// +/// Temp generic INumber based image format. T.MinValue is zero color, T.MaxValue is max color. Channels per color can be defined +/// +public sealed class TempImageFormat : IImageFormat where T : INumber, IMinMaxValue +{ + /// + /// Width of bitmap + /// + public readonly int width; + + /// + /// Height of bitmap + /// + public readonly int height; + + private readonly T[,,] content3d; + + private readonly T[] content1d; + + /// + /// How many color channels per pixel + /// + public readonly int channelsPerPixel; + + /// + /// Constructor for temp byte image format + /// + /// Input bitmap as three dimensional (width, height, channels per pixel) byte array + /// True if you want to create copy of data + public TempImageFormat(T[,,] input, bool createCopy = false) + { + if (createCopy) + { + this.content3d = (T[,,])input.Clone(); + } + else + { + this.content3d = input; + } + + this.content1d = null; + this.width = input.GetLength(0); + this.height = input.GetLength(1); + this.channelsPerPixel = input.GetLength(2); + } + + /// + /// Constructor for temp byte image format + /// + /// Input byte array + /// Width + /// Height + /// Image channels per pixel + /// True if you want to create copy of data + public TempImageFormat(T[] input, int imageWidth, int imageHeight, int imageChannelsPerPixel, bool createCopy = false) + { + this.content3d = null; + if (createCopy) + { + this.content1d = (T[])input.Clone(); + } + else + { + this.content1d = input; + } + this.width = imageWidth; + this.height = imageHeight; + this.channelsPerPixel = imageChannelsPerPixel; + } + + /// + /// Constructor for temp byte image format + /// + /// Existing TempByteImageFormat + public TempImageFormat(TempImageFormat input) + { + if (input.content1d != null) + { + this.content1d = input.content1d; + this.content3d = null; + } + else + { + this.content3d = input.content3d; + this.content1d = null; + } + + this.width = input.width; + this.height = input.height; + this.channelsPerPixel = input.channelsPerPixel; + } + + /// + /// Get width of bitmap + /// + /// Width in pixels + public int GetWidth() + { + return this.width; + } + + /// + /// Get height of bitmap + /// + /// Height in pixels + public int GetHeight() + { + return this.height; + } + + /// + /// Get channels per pixel + /// + /// Channels per pixel + public int GetChannelsPerPixel() + { + return this.channelsPerPixel; + } + + /// + /// Get raw content as byte array + /// + /// Byte array + public T[] GetRawContent() + { + if (this.content1d != null) + { + return this.content1d; + } + else + { + T[] returnArray = new T[this.width * this.height * this.channelsPerPixel]; + int currentIndex = 0; + for (int y = 0; y < this.height; y++) + { + for (int x = 0; x < this.width; x++) + { + for (int i = 0; i < this.channelsPerPixel; i++) + { + returnArray[currentIndex] = this.content3d[x, y, i]; + currentIndex++; + } + } + } + + return returnArray; + } + } + + /// + /// Set pixel channels of certain coordinate + /// + /// X coordinate + /// Y coordinate + /// New values as object array + public void SetPixelChannels(int x, int y, T[] newValues) + { + if (this.content1d != null) + { + int indexBase = y * this.width * this.channelsPerPixel + x * this.channelsPerPixel; + for (int i = 0; i < this.channelsPerPixel; i++) + { + this.content1d[indexBase + i] = newValues[i]; + } + } + else + { + for (int i = 0; i < this.channelsPerPixel; i++) + { + this.content3d[x, y, i] = newValues[i]; + } + } + } + + /// + /// Get pixel channels of certain coordinate + /// + /// X coordinate + /// Y coordinate + /// Values as byte array + public T[] GetPixelChannels(int x, int y) + { + T[] returnArray = new T[this.channelsPerPixel]; + + if (this.content1d != null) + { + int indexBase = y * this.width * this.channelsPerPixel + x * this.channelsPerPixel; + for (int i = 0; i < this.channelsPerPixel; i++) + { + returnArray[i] = this.content1d[indexBase + i]; + } + } + else + { + for (int i = 0; i < this.channelsPerPixel; i++) + { + returnArray[i] = this.content3d[x, y, i]; + } + } + + return returnArray; + } + + /// + /// Get pixel channels of certain coordinate + /// + /// X coordinate + /// Y coordinate + /// Array where pixel channels values will be written + public void GetPixelChannels(int x, int y, ref T[] pixelStorage) + { + if (this.content1d != null) + { + int indexBase = y * this.width * this.channelsPerPixel + x * this.channelsPerPixel; + for (int i = 0; i < this.channelsPerPixel; i++) + { + pixelStorage[i] = this.content1d[indexBase + i]; + } + } + else + { + for (int i = 0; i < this.channelsPerPixel; i++) + { + pixelStorage[i] = this.content3d[x, y, i]; + } + } + } + + /// + /// Get quantization errors per channel + /// + /// Original pixels + /// New pixels + /// Error values as double array + public void GetQuantErrorsPerChannel(in T[] originalPixel, in T[] newPixel, ref double[] errorValues) + { + for (int i = 0; i < this.channelsPerPixel; i++) + { + errorValues[i] = double.CreateChecked(originalPixel[i]) - double.CreateChecked(newPixel[i]); + } + } + + /// + /// Modify existing values with quantization errors + /// + /// Values to modify + /// Quantization errors + /// Multiplier + public void ModifyPixelChannelsWithQuantError(ref T[] modifyValues, double[] quantErrors, double multiplier) + { + for (int i = 0; i < this.channelsPerPixel; i++) + { + double newValue = double.CreateChecked(modifyValues[i]) + quantErrors[i] * multiplier; + double clamped = Math.Clamp(newValue, clampMin, clampMax); + modifyValues[i] = T.CreateChecked(clamped); + } + } + + private readonly double clampMin = double.CreateChecked(T.MinValue); + private readonly double clampMax = double.CreateChecked(T.MaxValue); + +} \ No newline at end of file diff --git a/tests/DitheringTests.cs b/tests/DitheringTests.cs index 6fe45a8..1267197 100644 --- a/tests/DitheringTests.cs +++ b/tests/DitheringTests.cs @@ -14,10 +14,11 @@ public void Setup() } [Test, Description("Test that AtkinsonDithering produces modified output")] - public void AtkinsonDitheringTest() + public void AtkinsonDitheringTests() { // Arrange - AtkinsonDitheringRGBByte atkinson = new AtkinsonDitheringRGBByte(TrueColorBytesToWebSafeColorBytes); + AtkinsonDitheringRGB atkinsonBytes = new AtkinsonDitheringRGB(TrueColorBytesToWebSafeColorBytes); + AtkinsonDitheringRGB atkinsonDoubles = new AtkinsonDitheringRGB(TrueColorBytesToWebSafeColorDoubles); // Act var stream = File.OpenRead("half.png"); @@ -25,18 +26,22 @@ public void AtkinsonDitheringTest() long originalImageChecksum = GetImageTotalPixelSum(image); int originalImageColorCount = CountTotalColors(image); - (long atkinsonImageChecksum, int atkinsonImageColorCount) = DoDitheringAndGetChecksumAndColorCount(atkinson, image); + (long atkinsonImageChecksumBytes, int atkinsonImageColorCountBytes) = DoDitheringAndGetChecksumAndColorCountBytes(atkinsonBytes, image); + (long atkinsonImageChecksumDoubles, int atkinsonImageColorCountDoubles) = DoDitheringAndGetChecksumAndColorCountDoubles(atkinsonDoubles, image); // Assert - Assert.AreNotEqual(originalImageChecksum, atkinsonImageChecksum); - Assert.Greater(originalImageColorCount, 10 * atkinsonImageColorCount); + Assert.AreNotEqual(originalImageChecksum, atkinsonImageChecksumBytes); + Assert.AreNotEqual(originalImageChecksum, atkinsonImageChecksumDoubles); + + Assert.Greater(originalImageColorCount, 10 * atkinsonImageColorCountBytes); + Assert.Greater(originalImageColorCount, atkinsonImageColorCountDoubles); } [Test, Description("Test that BurkesDithering produces modified output")] - public void BurkesDitheringTest() + public void BurkesDitheringTests() { // Arrange - BurkesDitheringRGBByte burkes = new BurkesDitheringRGBByte(TrueColorBytesToWebSafeColorBytes); + BurkesDitheringRGB burkes = new BurkesDitheringRGB(TrueColorBytesToWebSafeColorBytes); // Act var stream = File.OpenRead("half.png"); @@ -44,7 +49,7 @@ public void BurkesDitheringTest() long originalImageChecksum = GetImageTotalPixelSum(image); int originalImageColorCount = CountTotalColors(image); - (long burkesImageChecksum, int burkesImageColorCount) = DoDitheringAndGetChecksumAndColorCount(burkes, image); + (long burkesImageChecksum, int burkesImageColorCount) = DoDitheringAndGetChecksumAndColorCountBytes(burkes, image); // Assert Assert.AreNotEqual(originalImageChecksum, burkesImageChecksum); @@ -52,10 +57,10 @@ public void BurkesDitheringTest() } [Test, Description("Test that FloydSteinbergDithering produces modified output")] - public void FloydSteinbergDitheringTest() + public void FloydSteinbergDitheringTests() { // Arrange - FloydSteinbergDitheringRGBByte floydSteinberg = new FloydSteinbergDitheringRGBByte(TrueColorBytesToWebSafeColorBytes); + FloydSteinbergDitheringRGB floydSteinberg = new FloydSteinbergDitheringRGB(TrueColorBytesToWebSafeColorBytes); // Act var stream = File.OpenRead("half.png"); @@ -63,7 +68,7 @@ public void FloydSteinbergDitheringTest() long originalImageChecksum = GetImageTotalPixelSum(image); int originalImageColorCount = CountTotalColors(image); - (long floydSteinbergImageChecksum, int floydSteinbergColorCount) = DoDitheringAndGetChecksumAndColorCount(floydSteinberg, image); + (long floydSteinbergImageChecksum, int floydSteinbergColorCount) = DoDitheringAndGetChecksumAndColorCountBytes(floydSteinberg, image); // Assert Assert.AreNotEqual(originalImageChecksum, floydSteinbergImageChecksum); @@ -71,10 +76,10 @@ public void FloydSteinbergDitheringTest() } [Test, Description("Test that JarvisJudiceNinkeDithering produces modified output")] - public void JarvisJudiceNinkeDitheringTest() + public void JarvisJudiceNinkeDitheringTests() { // Arrange - JarvisJudiceNinkeDitheringRGBByte jarvisJudiceNinke = new JarvisJudiceNinkeDitheringRGBByte(TrueColorBytesToWebSafeColorBytes); + JarvisJudiceNinkeDitheringRGB jarvisJudiceNinke = new JarvisJudiceNinkeDitheringRGB(TrueColorBytesToWebSafeColorBytes); // Act var stream = File.OpenRead("half.png"); @@ -82,7 +87,7 @@ public void JarvisJudiceNinkeDitheringTest() long originalImageChecksum = GetImageTotalPixelSum(image); int originalImageColorCount = CountTotalColors(image); - (long jarvisJudiceNinkeImageChecksum, int jarvisJudiceNinkeColorCount) = DoDitheringAndGetChecksumAndColorCount(jarvisJudiceNinke, image); + (long jarvisJudiceNinkeImageChecksum, int jarvisJudiceNinkeColorCount) = DoDitheringAndGetChecksumAndColorCountBytes(jarvisJudiceNinke, image); // Assert Assert.AreNotEqual(originalImageChecksum, jarvisJudiceNinkeImageChecksum); @@ -90,10 +95,10 @@ public void JarvisJudiceNinkeDitheringTest() } [Test, Description("Test that SierraDithering produces modified output")] - public void SierraDitheringTest() + public void SierraDitheringTests() { // Arrange - SierraDitheringRGBByte sierra = new SierraDitheringRGBByte(TrueColorBytesToWebSafeColorBytes); + SierraDitheringRGB sierra = new SierraDitheringRGB(TrueColorBytesToWebSafeColorBytes); // Act var stream = File.OpenRead("half.png"); @@ -101,7 +106,7 @@ public void SierraDitheringTest() long originalImageChecksum = GetImageTotalPixelSum(image); int originalImageColorCount = CountTotalColors(image); - (long sierraImageChecksum, int sierraColorCount) = DoDitheringAndGetChecksumAndColorCount(sierra, image); + (long sierraImageChecksum, int sierraColorCount) = DoDitheringAndGetChecksumAndColorCountBytes(sierra, image); // Assert Assert.AreNotEqual(originalImageChecksum, sierraImageChecksum); @@ -109,10 +114,10 @@ public void SierraDitheringTest() } [Test, Description("Test that SierraLiteDithering produces modified output")] - public void SierraLiteDitheringTest() + public void SierraLiteDitheringTests() { // Arrange - SierraLiteDitheringRGBByte sierraLite = new SierraLiteDitheringRGBByte(TrueColorBytesToWebSafeColorBytes); + SierraLiteDitheringRGB sierraLite = new SierraLiteDitheringRGB(TrueColorBytesToWebSafeColorBytes); // Act var stream = File.OpenRead("half.png"); @@ -120,7 +125,7 @@ public void SierraLiteDitheringTest() long originalImageChecksum = GetImageTotalPixelSum(image); int originalImageColorCount = CountTotalColors(image); - (long sierraLiteImageChecksum, int sierraLiteColorCount) = DoDitheringAndGetChecksumAndColorCount(sierraLite, image); + (long sierraLiteImageChecksum, int sierraLiteColorCount) = DoDitheringAndGetChecksumAndColorCountBytes(sierraLite, image); // Assert Assert.AreNotEqual(originalImageChecksum, sierraLiteImageChecksum); @@ -128,10 +133,10 @@ public void SierraLiteDitheringTest() } [Test, Description("Test that SierraTwoRowDithering produces modified output")] - public void SierraTwoRowDitheringTest() + public void SierraTwoRowDitheringTests() { // Arrange - SierraTwoRowDitheringRGBByte sierraTwoRow = new SierraTwoRowDitheringRGBByte(TrueColorBytesToWebSafeColorBytes); + SierraTwoRowDitheringRGB sierraTwoRow = new SierraTwoRowDitheringRGB(TrueColorBytesToWebSafeColorBytes); // Act var stream = File.OpenRead("half.png"); @@ -139,7 +144,7 @@ public void SierraTwoRowDitheringTest() long originalImageChecksum = GetImageTotalPixelSum(image); int originalImageColorCount = CountTotalColors(image); - (long sierraTwoRowImageChecksum, int sierraTwoRowColorCount) = DoDitheringAndGetChecksumAndColorCount(sierraTwoRow, image); + (long sierraTwoRowImageChecksum, int sierraTwoRowColorCount) = DoDitheringAndGetChecksumAndColorCountBytes(sierraTwoRow, image); // Assert Assert.AreNotEqual(originalImageChecksum, sierraTwoRowImageChecksum); @@ -147,10 +152,10 @@ public void SierraTwoRowDitheringTest() } [Test, Description("Test that StuckiDithering produces modified output")] - public void StuckiDitheringTest() + public void StuckiDitheringTests() { // Arrange - StuckiDitheringRGBByte stucki = new StuckiDitheringRGBByte(TrueColorBytesToWebSafeColorBytes); + StuckiDitheringRGB stucki = new StuckiDitheringRGB(TrueColorBytesToWebSafeColorBytes); // Act var stream = File.OpenRead("half.png"); @@ -158,7 +163,7 @@ public void StuckiDitheringTest() long originalImageChecksum = GetImageTotalPixelSum(image); int originalImageColorCount = CountTotalColors(image); - (long stuckiImageChecksum, int stuckiColorCount) = DoDitheringAndGetChecksumAndColorCount(stucki, image); + (long stuckiImageChecksum, int stuckiColorCount) = DoDitheringAndGetChecksumAndColorCountBytes(stucki, image); // Assert Assert.AreNotEqual(originalImageChecksum, stuckiImageChecksum); @@ -166,10 +171,10 @@ public void StuckiDitheringTest() } [Test, Description("Test that FakeDithering produces modified output because of color func")] - public void FakeDitheringTest() + public void FakeDitheringTests() { // Arrange - FakeDitheringRGBByte fake = new FakeDitheringRGBByte(TrueColorBytesToWebSafeColorBytes); + FakeDitheringRGB fake = new FakeDitheringRGB(TrueColorBytesToWebSafeColorBytes); // Act var stream = File.OpenRead("half.png"); @@ -177,14 +182,14 @@ public void FakeDitheringTest() long originalImageChecksum = GetImageTotalPixelSum(image); int originalImageColorCount = CountTotalColors(image); - (long fakeImageChecksum, int fakeImageColorCount) = DoDitheringAndGetChecksumAndColorCount(fake, image); + (long fakeImageChecksum, int fakeImageColorCount) = DoDitheringAndGetChecksumAndColorCountBytes(fake, image); // Assert Assert.AreNotEqual(originalImageChecksum, fakeImageChecksum); Assert.Greater(originalImageColorCount, 10 * fakeImageColorCount); } - private static (long checksum, int colorCount) DoDitheringAndGetChecksumAndColorCount(DitheringBase dithering, Png image) + private static (long checksum, int colorCount) DoDitheringAndGetChecksumAndColorCountBytes(DitheringBase dithering, Png image) { byte[,,] bytes = ReadTo3DBytes(image); @@ -198,6 +203,23 @@ private static (long checksum, int colorCount) DoDitheringAndGetChecksumAndColor memoryStream.Position = 0; Png ditheredImage = Png.Open(memoryStream); + return (GetImageTotalPixelSum(ditheredImage), CountTotalColors(ditheredImage)); + } + + private static (long checksum, int colorCount) DoDitheringAndGetChecksumAndColorCountDoubles(DitheringBase dithering, Png image) + { + double[,,] doubles = ReadTo3DDoubles(image); + + TempDoubleImageFormat temp = new TempDoubleImageFormat(doubles); + dithering.DoDithering(temp); + + PngBuilder pngBuilder = PngBuilder.Create(image.Width, image.Height, hasAlphaChannel: false); + WriteToBitmap(pngBuilder, image.Width, image.Height, temp.GetPixelChannels); + var memoryStream = new MemoryStream(); + pngBuilder.Save(memoryStream); + memoryStream.Position = 0; + Png ditheredImage = Png.Open(memoryStream); + //image.Save("temp123.png"); return (GetImageTotalPixelSum(ditheredImage), CountTotalColors(ditheredImage)); @@ -211,6 +233,14 @@ private static void TrueColorBytesToWebSafeColorBytes(in byte[] input, ref byte[ } } + private static void TrueColorBytesToWebSafeColorDoubles(in double[] input, ref double[] output) + { + for (int i = 0; i < input.Length; i++) + { + output[i] = (Math.Round(input[i] * 51.0) / 51.0); + } + } + private static byte[,,] ReadTo3DBytes(Png image) { byte[,,] returnValue = new byte[image.Width, image.Height, 3]; @@ -227,6 +257,22 @@ private static void TrueColorBytesToWebSafeColorBytes(in byte[] input, ref byte[ return returnValue; } + private static double[,,] ReadTo3DDoubles(Png image) + { + double[,,] returnValue = new double[image.Width, image.Height, 3]; + for (int x = 0; x < image.Width; x++) + { + for (int y = 0; y < image.Height; y++) + { + Pixel pixel = image.GetPixel(x, y); + returnValue[x, y, 0] = pixel.R / 255.0; + returnValue[x, y, 1] = pixel.G / 255.0; + returnValue[x, y, 2] = pixel.B / 255.0; + } + } + return returnValue; + } + private static void WriteToBitmap(PngBuilder builder, int width, int height, byte[,,] bytes) { for (int x = 0; x < width; x++) @@ -239,6 +285,18 @@ private static void WriteToBitmap(PngBuilder builder, int width, int height, byt } } + private static void WriteToBitmap(PngBuilder builder, int width, int height, double[,,] doubles) + { + for (int x = 0; x < width; x++) + { + for (int y = 0; y < height; y++) + { + Pixel pixel = new Pixel((byte)(doubles[x, y, 0] * 255.0), (byte)(doubles[x, y, 1] * 255.0), (byte)(doubles[x, y, 2] * 255.0)); + builder.SetPixel(pixel, x, y); + } + } + } + private static void WriteToBitmap(PngBuilder builder, int width, int height, Func reader) { for (int x = 0; x < width; x++) @@ -252,6 +310,19 @@ private static void WriteToBitmap(PngBuilder builder, int width, int height, Fun } } + private static void WriteToBitmap(PngBuilder builder, int width, int height, Func reader) + { + for (int x = 0; x < width; x++) + { + for (int y = 0; y < height; y++) + { + double[] read = reader(x, y); + Pixel pixel = new Pixel((byte)(read[0] * 255.0), (byte)(read[1] * 255.0), (byte)(read[2] * 255.0)); + builder.SetPixel(pixel, x, y); + } + } + } + private static long GetImageTotalPixelSum(Png image) { long totalSum = 0; @@ -276,7 +347,7 @@ private static int CountTotalColors(Png image) { for (int j = 0; j < image.Height; j++) { - Pixel pixel = image.GetPixel(i,j); + Pixel pixel = image.GetPixel(i, j); unique.Add(pixel.GetHashCode()); } } diff --git a/tests/TempImageFormatTests.cs b/tests/TempImageFormatTests.cs new file mode 100644 index 0000000..aa5a547 --- /dev/null +++ b/tests/TempImageFormatTests.cs @@ -0,0 +1,225 @@ +using NUnit.Framework; +using BigGustave; +using System; +using System.IO; +using System.Collections.Generic; + +namespace tests +{ + public class TempImageFormatTests + { + [SetUp] + public void Setup() + { + } + + [Test, Description("Test that Data is correctly loaded as 3d")] + public void LoadTest3d() + { + // Arrange + + // Act + var stream = File.OpenRead("half.png"); + Png image = Png.Open(stream); + + Pixel pixel = image.GetPixel(0, 0); + + TempImageFormat test = new TempImageFormat(ReadTo3DBytes(image)); + byte[] firstPixel = test.GetPixelChannels(0, 0); + + // Assert + Assert.AreEqual(pixel.R, firstPixel[0]); + Assert.AreEqual(pixel.G, firstPixel[1]); + Assert.AreEqual(pixel.B, firstPixel[2]); + } + + [Test, Description("Test that Data is correctly loaded as 1d")] + public void LoadTest1d() + { + // Arrange + + // Act + var stream = File.OpenRead("half.png"); + Png image = Png.Open(stream); + + Pixel pixel = image.GetPixel(0, 0); + + TempImageFormat test = new TempImageFormat(ReadTo1DBytes(image), image.Width, image.Height, 3); + byte[] firstPixel = test.GetPixelChannels(0, 0); + + // Assert + Assert.AreEqual(pixel.R, firstPixel[0]); + Assert.AreEqual(pixel.G, firstPixel[1]); + Assert.AreEqual(pixel.B, firstPixel[2]); + } + + [Test, Description("Test that indexing works equally")] + public void IndexingTest() + { + // Arrange + int[] indexes = new int[] { 0, 1, 12, 37, 56, 132, 200 }; + + // Act + var stream = File.OpenRead("half.png"); + Png image = Png.Open(stream); + + TempImageFormat test3d = new TempImageFormat(ReadTo3DBytes(image)); + TempImageFormat test1d = new TempImageFormat(ReadTo1DBytes(image), image.Width, image.Height, 3); + + // Assert + for (int x = 0; x < indexes.Length; x++) + { + for (int y = 0; y < indexes.Length; y++) + { + byte[] pixel3d = test3d.GetPixelChannels(x, y); + byte[] pixel1d = test1d.GetPixelChannels(x, y); + + CollectionAssert.AreEqual(pixel3d, pixel1d, $"Pixels at {x} x {y} should be equal"); + } + } + } + + [Test, Description("Test that 1d copy works")] + public void CheckThat1dCopyWorks() + { + // Arrange + + // Act + var stream = File.OpenRead("half.png"); + Png image = Png.Open(stream); + byte[] bytes1d = ReadTo1DBytes(image); + TempImageFormat test1d_1 = new TempImageFormat(bytes1d, image.Width, image.Height, 3, createCopy: false); + TempImageFormat test1d_2 = new TempImageFormat(bytes1d, image.Width, image.Height, 3, createCopy: true); + bytes1d[0] = 0; + + byte[] firstPixel1 = test1d_1.GetPixelChannels(0, 0); + byte[] firstPixel2 = test1d_2.GetPixelChannels(0, 0); + + // Assert + Assert.AreEqual(0, firstPixel1[0]); + Assert.AreNotEqual(0, firstPixel2[0]); + } + + [Test, Description("Test that raw content works")] + public void CheckThatRawContentWorks() + { + // Arrange + + // Act + var stream = File.OpenRead("half.png"); + Png image = Png.Open(stream); + byte[] bytes1d = ReadTo1DBytes(image); + TempImageFormat test1d_1 = new TempImageFormat(bytes1d, image.Width, image.Height, 3, createCopy: true); + + // Assert + Assert.Greater(bytes1d.Length, 1000, "There should be some bytes in image data"); + CollectionAssert.AreEqual(bytes1d, test1d_1.GetRawContent()); + } + + [Test, Description("Test that GetQuantErrorsPerChannel for one channel works")] + public void CheckThatGetQuantErrorsPerChannelForOneChannelWorks() + { + // Arrange + byte[] imageBytes1d = new byte[1] { 0 }; + byte[] modifiedBytes = new byte[1] { 1 }; + double[] expected = new double[] { -1 }; + double[] actual = new double[1]; + + TempImageFormat test1d = new TempImageFormat(imageBytes1d, 1, 1, 1); + + // Act + test1d.GetQuantErrorsPerChannel(imageBytes1d, modifiedBytes, ref actual); + + // Assert + CollectionAssert.AreEqual(expected, actual); + } + + [Test, Description("Test that GetQuantErrorsPerChannel for three channels works")] + public void CheckThatGetQuantErrorsPerChannelForThreeChannelsWorks() + { + // Arrange + byte[] imageBytes1d = new byte[3] { 0, 127, 255 }; + byte[] modifiedBytes = new byte[3] { 0, 128, 254 }; + double[] expected = new double[] { 0, -1, 1 }; + double[] actual = new double[3]; + + TempImageFormat test1d = new TempImageFormat(imageBytes1d, 1, 1, 3); + + // Act + test1d.GetQuantErrorsPerChannel(imageBytes1d, modifiedBytes, ref actual); + + // Assert + CollectionAssert.AreEqual(expected, actual); + } + + [Test, Description("Test that ModifyPixelChannelsWithQuantError for one channel works")] + public void CheckThatModifyPixelChannelsWithQuantErrorForOneChannelWorks() + { + // Arrange + byte[] imageBytes1d = new byte[1] { 0 }; + double[] quantErrors = new double[1] { 1 }; + byte[] expected = new byte[1] { 1 }; + double multiplier = 1.0; + TempImageFormat test1d = new TempImageFormat(imageBytes1d, imageWidth: 1, imageHeight: 1, imageChannelsPerPixel: 1); + + // Act + test1d.ModifyPixelChannelsWithQuantError(ref imageBytes1d, quantErrors, multiplier); + + // Assert + CollectionAssert.AreEqual(expected, imageBytes1d); + } + + [Test, Description("Test that ModifyPixelChannelsWithQuantError for three channels works")] + public void CheckThatModifyPixelChannelsWithQuantErrorForThreeChannelsWorks() + { + // Arrange + byte[] imageBytes3d = new byte[3] { 30, 127, 200 }; + double[] quantErrors = new double[3] { -10, 0, 20 }; + byte[] expected = new byte[3] { 10, 127, 240 }; + double multiplier = 2.0; + TempImageFormat test1d = new TempImageFormat(imageBytes3d, imageWidth: 1, imageHeight: 1, imageChannelsPerPixel: 3); + + // Act + test1d.ModifyPixelChannelsWithQuantError(ref imageBytes3d, quantErrors, multiplier); + + // Assert + CollectionAssert.AreEqual(expected, imageBytes3d); + } + + private static byte[,,] ReadTo3DBytes(Png bitmap) + { + byte[,,] returnValue = new byte[bitmap.Width, bitmap.Height, 3]; + for (int x = 0; x < bitmap.Width; x++) + { + for (int y = 0; y < bitmap.Height; y++) + { + Pixel pixel = bitmap.GetPixel(x, y); + returnValue[x, y, 0] = pixel.R; + returnValue[x, y, 1] = pixel.G; + returnValue[x, y, 2] = pixel.B; + } + } + return returnValue; + } + + private static byte[] ReadTo1DBytes(Png bitmap) + { + int width = bitmap.Width; + int height = bitmap.Height; + int channelsPerPixel = 3; + byte[] returnValue = new byte[width * height * 3]; + for (int x = 0; x < width; x++) + { + for (int y = 0; y < height; y++) + { + Pixel pixel = bitmap.GetPixel(x, y); + int arrayIndex = y * width * channelsPerPixel + x * channelsPerPixel; + returnValue[arrayIndex + 0] = pixel.R; + returnValue[arrayIndex + 1] = pixel.G; + returnValue[arrayIndex + 2] = pixel.B; + } + } + return returnValue; + } + } +} \ No newline at end of file