Browse Source

Refactoring and comments

tznind 11 months ago
parent
commit
30817654b8
2 changed files with 143 additions and 20 deletions
  1. 139 14
      Terminal.Gui/Drawing/ColorQuantizer.cs
  2. 4 6
      Terminal.Gui/Drawing/SixelEncoder.cs

+ 139 - 14
Terminal.Gui/Drawing/ColorQuantizer.cs

@@ -1,21 +1,38 @@
-namespace Terminal.Gui;
+using System.Collections.ObjectModel;
+using ColorHelper;
+
+namespace Terminal.Gui;
 
 /// <summary>
 /// Translates colors in an image into a Palette of up to 256 colors.
 /// </summary>
 public class ColorQuantizer
 {
-    private Dictionary<Color, int> colorFrequency;
-    public List<Color> Palette;
-    private const int MaxColors = 256;
+    /// <summary>
+    /// Gets the current colors in the palette based on the last call to
+    /// <see cref="BuildPalette"/>.
+    /// </summary>
+    public IReadOnlyCollection<Color> Palette { get; private set; } = new List<Color> ();
 
-    public ColorQuantizer ()
-    {
-        colorFrequency = new Dictionary<Color, int> ();
-        Palette = new List<Color> ();
-    }
+    /// <summary>
+    /// Gets or sets the maximum number of colors to put into the <see cref="Palette"/>.
+    /// Defaults to 256 (the maximum for sixel images).
+    /// </summary>
+    public int MaxColors { get; set; } = 256;
+
+    /// <summary>
+    /// Gets or sets the algorithm used to map novel colors into existing
+    /// palette colors (closest match). Defaults to <see cref="CIE94ColorDistance"/>
+    /// </summary>
+    public IColorDistance DistanceAlgorithm { get; set; } = new CIE94ColorDistance ();
 
-    public void BuildPalette (Color [,] pixels, IPaletteBuilder builder)
+    /// <summary>
+    /// Gets or sets the algorithm used to build the <see cref="Palette"/>.
+    /// Defaults to <see cref="MedianCutPaletteBuilder"/>
+    /// </summary>
+    public IPaletteBuilder PaletteBuildingAlgorithm { get; set; } = new MedianCutPaletteBuilder ();
+
+    public void BuildPalette (Color [,] pixels)
     {
         List<Color> allColors = new List<Color> ();
         int width = pixels.GetLength (0);
@@ -29,10 +46,10 @@ public class ColorQuantizer
             }
         }
 
-        Palette = builder.BuildPalette(allColors,MaxColors);
+        Palette = PaletteBuildingAlgorithm.BuildPalette(allColors,MaxColors);
     }
 
-    public int GetNearestColor (Color toTranslate, IColorDistance distanceAlgorithm)
+    public int GetNearestColor (Color toTranslate)
     {
         // Simple nearest color matching based on Euclidean distance in RGB space
         double minDistance = double.MaxValue;
@@ -40,8 +57,8 @@ public class ColorQuantizer
 
         for (var index = 0; index < Palette.Count; index++)
         {
-            Color color = Palette [index];
-            double distance = distanceAlgorithm.CalculateDistance(color, toTranslate);
+            Color color = Palette.ElementAt(index);
+            double distance = DistanceAlgorithm.CalculateDistance(color, toTranslate);
 
             if (distance < minDistance)
             {
@@ -90,6 +107,114 @@ public class EuclideanColorDistance : IColorDistance
         return Math.Sqrt (rDiff * rDiff + gDiff * gDiff + bDiff * bDiff);
     }
 }
+public abstract class LabColorDistance : IColorDistance
+{
+    // Reference white point for D65 illuminant (can be moved to constants)
+    private const double RefX = 95.047;
+    private const double RefY = 100.000;
+    private const double RefZ = 108.883;
+
+    // Conversion from RGB to Lab
+    protected LabColor RgbToLab (Color c)
+    {
+        var xyz = ColorHelper.ColorConverter.RgbToXyz (new RGB (c.R, c.G, c.B));
+
+        // Normalize XYZ values by reference white point
+        double x = xyz.X / RefX;
+        double y = xyz.Y / RefY;
+        double z = xyz.Z / RefZ;
+
+        // Apply the nonlinear transformation for Lab
+        x = (x > 0.008856) ? Math.Pow (x, 1.0 / 3.0) : (7.787 * x) + (16.0 / 116.0);
+        y = (y > 0.008856) ? Math.Pow (y, 1.0 / 3.0) : (7.787 * y) + (16.0 / 116.0);
+        z = (z > 0.008856) ? Math.Pow (z, 1.0 / 3.0) : (7.787 * z) + (16.0 / 116.0);
+
+        // Calculate Lab values
+        double l = (116.0 * y) - 16.0;
+        double a = 500.0 * (x - y);
+        double b = 200.0 * (y - z);
+
+        return new LabColor (l, a, b);
+    }
+
+    // LabColor class encapsulating L, A, and B values
+    protected class LabColor
+    {
+        public double L { get; }
+        public double A { get; }
+        public double B { get; }
+
+        public LabColor (double l, double a, double b)
+        {
+            L = l;
+            A = a;
+            B = b;
+        }
+    }
+
+    /// <inheritdoc />
+    public abstract double CalculateDistance (Color c1, Color c2);
+}
+
+/// <summary>
+/// This is the simplest method to measure color difference in the CIE Lab color space. The Euclidean distance in Lab
+/// space is more aligned with human perception than RGB space, as Lab attempts to model how humans perceive color differences.
+/// </summary>
+public class CIE76ColorDistance : LabColorDistance
+{
+    public override double CalculateDistance (Color c1, Color c2)
+    {
+        var lab1 = RgbToLab (c1);
+        var lab2 = RgbToLab (c2);
+
+        // Euclidean distance in Lab color space
+        return Math.Sqrt (Math.Pow (lab1.L - lab2.L, 2) + Math.Pow (lab1.A - lab2.A, 2) + Math.Pow (lab1.B - lab2.B, 2));
+    }
+}
+
+/// <summary>
+/// CIE94 improves on CIE76 by introducing adjustments for chroma (color intensity) and lightness.
+/// This algorithm considers human visual perception more accurately by scaling differences in lightness and chroma.
+/// It is better but slower than <see cref="CIE76ColorDistance"/>.
+/// </summary>
+public class CIE94ColorDistance : LabColorDistance
+{
+    // Constants for CIE94 formula (can be modified for different use cases like textiles or graphics)
+    private const double kL = 1.0;
+    private const double kC = 1.0;
+    private const double kH = 1.0;
+
+    public override double CalculateDistance (Color first, Color second)
+    {
+        var lab1 = RgbToLab (first);
+        var lab2 = RgbToLab (second);
+
+        // Delta L, A, B
+        double deltaL = lab1.L - lab2.L;
+        double deltaA = lab1.A - lab2.A;
+        double deltaB = lab1.B - lab2.B;
+
+        // Chroma values for both colors
+        double c1 = Math.Sqrt (lab1.A * lab1.A + lab1.B * lab1.B);
+        double c2 = Math.Sqrt (lab2.A * lab2.A + lab2.B * lab2.B);
+        double deltaC = c1 - c2;
+
+        // Delta H (calculated indirectly)
+        double deltaH = Math.Sqrt (Math.Pow (deltaA, 2) + Math.Pow (deltaB, 2) - Math.Pow (deltaC, 2));
+
+        // Scaling factors
+        double sL = 1.0;
+        double sC = 1.0 + 0.045 * c1;
+        double sH = 1.0 + 0.015 * c1;
+
+        // CIE94 color difference formula
+        return Math.Sqrt (
+            Math.Pow (deltaL / (kL * sL), 2) +
+            Math.Pow (deltaC / (kC * sC), 2) +
+            Math.Pow (deltaH / (kH * sH), 2)
+        );
+    }
+}
 
 
 class MedianCutPaletteBuilder : IPaletteBuilder

+ 4 - 6
Terminal.Gui/Drawing/SixelEncoder.cs

@@ -45,8 +45,6 @@ public class SixelEncoder
 
     private string WriteSixel (Color [,] pixels, ColorQuantizer quantizer)
     {
-        var distanceAlgorithm = new EuclideanColorDistance ();
-
         StringBuilder sb = new StringBuilder ();
         int height = pixels.GetLength (1);
         int width = pixels.GetLength (0);
@@ -57,7 +55,7 @@ public class SixelEncoder
         {
             int p = y * width;
             Color cachedColor = pixels [0, y];
-            int cachedColorIndex = quantizer.GetNearestColor (cachedColor,distanceAlgorithm );
+            int cachedColorIndex = quantizer.GetNearestColor (cachedColor );
             int count = 1;
             int c = -1;
 
@@ -65,7 +63,7 @@ public class SixelEncoder
             for (int x = 0; x < width; x++)
             {
                 Color color = pixels [x, y];
-                int colorIndex = quantizer.GetNearestColor (color,distanceAlgorithm);
+                int colorIndex = quantizer.GetNearestColor (color);
 
                 if (colorIndex == cachedColorIndex)
                 {
@@ -164,7 +162,7 @@ public class SixelEncoder
     private string GetColorPallette (Color [,] pixels, out ColorQuantizer quantizer)
     {
         quantizer = new ColorQuantizer ();
-        quantizer.BuildPalette (pixels,new MedianCutPaletteBuilder ());
+        quantizer.BuildPalette (pixels);
 
 
         // Color definitions in the format "#<index>;<type>;<R>;<G>;<B>" - For type the 2 means RGB.  The values range 0 to 100
@@ -173,7 +171,7 @@ public class SixelEncoder
 
         for (int i = 0; i < quantizer.Palette.Count; i++)
         {
-            var color = quantizer.Palette [i];
+            var color = quantizer.Palette.ElementAt (i);
             paletteSb.AppendFormat ("#{0};2;{1};{2};{3}",
                                     i,
                                     color.R * 100 / 255,