Browse Source

Refactor and split into seperate files WIP

tznind 11 months ago
parent
commit
e334bfd00d

+ 0 - 378
Terminal.Gui/Drawing/ColorQuantizer.cs

@@ -1,378 +0,0 @@
-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
-{
-    /// <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> ();
-
-    /// <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 ();
-
-    /// <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);
-        int height = pixels.GetLength (1);
-
-        for (int x = 0; x < width; x++)
-        {
-            for (int y = 0; y < height; y++)
-            {
-                allColors.Add (pixels [x, y]);
-            }
-        }
-
-        Palette = PaletteBuildingAlgorithm.BuildPalette(allColors,MaxColors);
-    }
-
-    public int GetNearestColor (Color toTranslate)
-    {
-        // Simple nearest color matching based on Euclidean distance in RGB space
-        double minDistance = double.MaxValue;
-        int nearestIndex = 0;
-
-        for (var index = 0; index < Palette.Count; index++)
-        {
-            Color color = Palette.ElementAt(index);
-            double distance = DistanceAlgorithm.CalculateDistance(color, toTranslate);
-
-            if (distance < minDistance)
-            {
-                minDistance = distance;
-                nearestIndex = index;
-            }
-        }
-
-        return nearestIndex;
-    }
-}
-
-public interface IPaletteBuilder
-{
-    List<Color> BuildPalette (List<Color> colors, int maxColors);
-}
-
-/// <summary>
-/// Interface for algorithms that compute the relative distance between pairs of colors.
-/// This is used for color matching to a limited palette, such as in Sixel rendering.
-/// </summary>
-public interface IColorDistance
-{
-    /// <summary>
-    /// Computes a similarity metric between two <see cref="Color"/> instances.
-    /// A larger value indicates more dissimilar colors, while a smaller value indicates more similar colors.
-    /// The metric is internally consistent for the given algorithm.
-    /// </summary>
-    /// <param name="c1">The first color.</param>
-    /// <param name="c2">The second color.</param>
-    /// <returns>A numeric value representing the distance between the two colors.</returns>
-    double CalculateDistance (Color c1, Color c2);
-}
-
-/// <summary>
-/// Calculates the distance between two colors using Euclidean distance in 3D RGB space.
-/// This measures the straight-line distance between the two points representing the colors.
-/// </summary>
-public class EuclideanColorDistance : IColorDistance
-{
-    public double CalculateDistance (Color c1, Color c2)
-    {
-        int rDiff = c1.R - c2.R;
-        int gDiff = c1.G - c2.G;
-        int bDiff = c1.B - c2.B;
-        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
-{
-    public List<Color> BuildPalette (List<Color> colors, int maxColors)
-    {
-        // Initial step: place all colors in one large box
-        List<ColorBox> boxes = new List<ColorBox> { new ColorBox (colors) };
-
-        // Keep splitting boxes until we have the desired number of colors
-        while (boxes.Count < maxColors)
-        {
-            // Find the box with the largest range and split it
-            ColorBox boxToSplit = FindBoxWithLargestRange (boxes);
-
-            if (boxToSplit == null || boxToSplit.Colors.Count == 0)
-            {
-                break;
-            }
-
-            // Split the box into two smaller boxes
-            var splitBoxes = SplitBox (boxToSplit);
-            boxes.Remove (boxToSplit);
-            boxes.AddRange (splitBoxes);
-        }
-
-        // Average the colors in each box to get the final palette
-        return boxes.Select (box => box.GetAverageColor ()).ToList ();
-    }
-
-    // Find the box with the largest color range (R, G, or B)
-    private ColorBox FindBoxWithLargestRange (List<ColorBox> boxes)
-    {
-        ColorBox largestRangeBox = null;
-        int largestRange = 0;
-
-        foreach (var box in boxes)
-        {
-            int range = box.GetColorRange ();
-            if (range > largestRange)
-            {
-                largestRange = range;
-                largestRangeBox = box;
-            }
-        }
-
-        return largestRangeBox;
-    }
-
-    // Split a box at the median point in its largest color channel
-    private List<ColorBox> SplitBox (ColorBox box)
-    {
-        List<ColorBox> result = new List<ColorBox> ();
-
-        // Find the color channel with the largest range (R, G, or B)
-        int channel = box.GetLargestChannel ();
-        var sortedColors = box.Colors.OrderBy (c => GetColorChannelValue (c, channel)).ToList ();
-
-        // Split the box at the median
-        int medianIndex = sortedColors.Count / 2;
-
-        var lowerHalf = sortedColors.Take (medianIndex).ToList ();
-        var upperHalf = sortedColors.Skip (medianIndex).ToList ();
-
-        result.Add (new ColorBox (lowerHalf));
-        result.Add (new ColorBox (upperHalf));
-
-        return result;
-    }
-
-    // Helper method to get the value of a color channel (R = 0, G = 1, B = 2)
-    private static int GetColorChannelValue (Color color, int channel)
-    {
-        switch (channel)
-        {
-            case 0: return color.R;
-            case 1: return color.G;
-            case 2: return color.B;
-            default: throw new ArgumentException ("Invalid channel index");
-        }
-    }
-
-    // The ColorBox class to represent a subset of colors
-    public class ColorBox
-    {
-        public List<Color> Colors { get; private set; }
-
-        public ColorBox (List<Color> colors)
-        {
-            Colors = colors;
-        }
-
-        // Get the color channel with the largest range (0 = R, 1 = G, 2 = B)
-        public int GetLargestChannel ()
-        {
-            int rRange = GetColorRangeForChannel (0);
-            int gRange = GetColorRangeForChannel (1);
-            int bRange = GetColorRangeForChannel (2);
-
-            if (rRange >= gRange && rRange >= bRange)
-            {
-                return 0;
-            }
-
-            if (gRange >= rRange && gRange >= bRange)
-            {
-                return 1;
-            }
-
-            return 2;
-        }
-
-        // Get the range of colors for a given channel (0 = R, 1 = G, 2 = B)
-        private int GetColorRangeForChannel (int channel)
-        {
-            int min = int.MaxValue, max = int.MinValue;
-
-            foreach (var color in Colors)
-            {
-                int value = GetColorChannelValue (color, channel);
-                if (value < min)
-                {
-                    min = value;
-                }
-
-                if (value > max)
-                {
-                    max = value;
-                }
-            }
-
-            return max - min;
-        }
-
-        // Get the overall color range across all channels (for finding the box to split)
-        public int GetColorRange ()
-        {
-            int rRange = GetColorRangeForChannel (0);
-            int gRange = GetColorRangeForChannel (1);
-            int bRange = GetColorRangeForChannel (2);
-
-            return Math.Max (rRange, Math.Max (gRange, bRange));
-        }
-
-        // Calculate the average color in the box
-        public Color GetAverageColor ()
-        {
-            int totalR = 0, totalG = 0, totalB = 0;
-
-            foreach (var color in Colors)
-            {
-                totalR += color.R;
-                totalG += color.G;
-                totalB += color.B;
-            }
-
-            int count = Colors.Count;
-            return new Color (totalR / count, totalG / count, totalB / count);
-        }
-    }
-}

+ 17 - 0
Terminal.Gui/Drawing/Quant/CIE76ColorDistance.cs

@@ -0,0 +1,17 @@
+namespace Terminal.Gui.Drawing.Quant;
+
+/// <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));
+    }
+}

+ 45 - 0
Terminal.Gui/Drawing/Quant/CIE94ColorDistance.cs

@@ -0,0 +1,45 @@
+namespace Terminal.Gui.Drawing.Quant;
+
+/// <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)
+                         );
+    }
+}

+ 71 - 0
Terminal.Gui/Drawing/Quant/ColorQuantizer.cs

@@ -0,0 +1,71 @@
+using System.Collections.ObjectModel;
+
+namespace Terminal.Gui.Drawing.Quant;
+
+/// <summary>
+/// Translates colors in an image into a Palette of up to 256 colors.
+/// </summary>
+public class ColorQuantizer
+{
+    /// <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> ();
+
+    /// <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 ();
+
+    /// <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);
+        int height = pixels.GetLength (1);
+
+        for (int x = 0; x < width; x++)
+        {
+            for (int y = 0; y < height; y++)
+            {
+                allColors.Add (pixels [x, y]);
+            }
+        }
+
+        Palette = PaletteBuildingAlgorithm.BuildPalette (allColors, MaxColors);
+    }
+
+    public int GetNearestColor (Color toTranslate)
+    {
+        // Simple nearest color matching based on Euclidean distance in RGB space
+        double minDistance = double.MaxValue;
+        int nearestIndex = 0;
+
+        for (var index = 0; index < Palette.Count; index++)
+        {
+            Color color = Palette.ElementAt (index);
+            double distance = DistanceAlgorithm.CalculateDistance (color, toTranslate);
+
+            if (distance < minDistance)
+            {
+                minDistance = distance;
+                nearestIndex = index;
+            }
+        }
+
+        return nearestIndex;
+    }
+}

+ 16 - 0
Terminal.Gui/Drawing/Quant/EuclideanColorDistance.cs

@@ -0,0 +1,16 @@
+namespace Terminal.Gui.Drawing.Quant;
+
+/// <summary>
+/// Calculates the distance between two colors using Euclidean distance in 3D RGB space.
+/// This measures the straight-line distance between the two points representing the colors.
+/// </summary>
+public class EuclideanColorDistance : IColorDistance
+{
+    public double CalculateDistance (Color c1, Color c2)
+    {
+        int rDiff = c1.R - c2.R;
+        int gDiff = c1.G - c2.G;
+        int bDiff = c1.B - c2.B;
+        return Math.Sqrt (rDiff * rDiff + gDiff * gDiff + bDiff * bDiff);
+    }
+}

+ 18 - 0
Terminal.Gui/Drawing/Quant/IColorDistance.cs

@@ -0,0 +1,18 @@
+namespace Terminal.Gui.Drawing.Quant;
+
+/// <summary>
+/// Interface for algorithms that compute the relative distance between pairs of colors.
+/// This is used for color matching to a limited palette, such as in Sixel rendering.
+/// </summary>
+public interface IColorDistance
+{
+    /// <summary>
+    /// Computes a similarity metric between two <see cref="Color"/> instances.
+    /// A larger value indicates more dissimilar colors, while a smaller value indicates more similar colors.
+    /// The metric is internally consistent for the given algorithm.
+    /// </summary>
+    /// <param name="c1">The first color.</param>
+    /// <param name="c2">The second color.</param>
+    /// <returns>A numeric value representing the distance between the two colors.</returns>
+    double CalculateDistance (Color c1, Color c2);
+}

+ 6 - 0
Terminal.Gui/Drawing/Quant/IPaletteBuilder.cs

@@ -0,0 +1,6 @@
+namespace Terminal.Gui.Drawing.Quant;
+
+public interface IPaletteBuilder
+{
+    List<Color> BuildPalette (List<Color> colors, int maxColors);
+}

+ 154 - 0
Terminal.Gui/Drawing/Quant/KMeansPaletteBuilder.cs

@@ -0,0 +1,154 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Terminal.Gui.Drawing.Quant;
+
+    /// <summary>
+    /// <see cref="IPaletteBuilder"/> that works well for images with high contrast images
+    /// </summary>
+    public class KMeansPaletteBuilder : IPaletteBuilder
+    {
+        private readonly int maxIterations;
+        private readonly Random random = new Random ();
+        private readonly IColorDistance colorDistance;
+
+        public KMeansPaletteBuilder (IColorDistance distanceAlgorithm, int maxIterations = 100)
+        {
+            colorDistance = distanceAlgorithm;
+            this.maxIterations = maxIterations;
+        }
+
+        public List<Color> BuildPalette (List<Color> colors, int maxColors)
+        {
+            // Convert colors to vectors
+            List<ColorVector> colorVectors = colors.Select (c => new ColorVector (c.R, c.G, c.B)).ToList ();
+
+            // Perform K-Means Clustering
+            List<ColorVector> centroids = KMeans (colorVectors, maxColors);
+
+            // Convert centroids back to colors
+            return centroids.Select (v => new Color ((int)v.R, (int)v.G, (int)v.B)).ToList ();
+        }
+
+        private List<ColorVector> KMeans (List<ColorVector> colors, int k)
+        {
+            // Randomly initialize k centroids
+            List<ColorVector> centroids = InitializeCentroids (colors, k);
+
+            List<ColorVector> previousCentroids = new List<ColorVector> ();
+            int iterations = 0;
+
+            // Repeat until convergence or max iterations
+            while (!HasConverged (centroids, previousCentroids) && iterations < maxIterations)
+            {
+                previousCentroids = centroids.Select (c => new ColorVector (c.R, c.G, c.B)).ToList ();
+
+                // Assign each color to the nearest centroid
+                var clusters = AssignColorsToClusters (colors, centroids);
+
+                // Recompute centroids
+                centroids = RecomputeCentroids (clusters);
+
+                iterations++;
+            }
+
+            return centroids;
+        }
+
+        private List<ColorVector> InitializeCentroids (List<ColorVector> colors, int k)
+        {
+            return colors.OrderBy (c => random.Next ()).Take (k).ToList (); // Randomly select k initial centroids
+        }
+
+        private Dictionary<ColorVector, List<ColorVector>> AssignColorsToClusters (List<ColorVector> colors, List<ColorVector> centroids)
+        {
+            var clusters = centroids.ToDictionary (c => c, c => new List<ColorVector> ());
+
+            foreach (var color in colors)
+            {
+                // Find the nearest centroid using the injected IColorDistance implementation
+                var nearestCentroid = centroids.OrderBy (c => colorDistance.CalculateDistance (c.ToColor (), color.ToColor ())).First ();
+                clusters [nearestCentroid].Add (color);
+            }
+
+            return clusters;
+        }
+
+        private List<ColorVector> RecomputeCentroids (Dictionary<ColorVector, List<ColorVector>> clusters)
+        {
+            var newCentroids = new List<ColorVector> ();
+
+            foreach (var cluster in clusters)
+            {
+                if (cluster.Value.Count == 0)
+                {
+                    // Reinitialize the centroid with a random color if the cluster is empty
+                    newCentroids.Add (InitializeRandomCentroid ());
+                }
+                else
+                {
+                    // Recompute the centroid as the mean of the cluster's points
+                    double avgR = cluster.Value.Average (c => c.R);
+                    double avgG = cluster.Value.Average (c => c.G);
+                    double avgB = cluster.Value.Average (c => c.B);
+
+                    newCentroids.Add (new ColorVector (avgR, avgG, avgB));
+                }
+            }
+
+            return newCentroids;
+        }
+
+        private bool HasConverged (List<ColorVector> currentCentroids, List<ColorVector> previousCentroids)
+        {
+            // Skip convergence check for the first iteration
+            if (previousCentroids.Count == 0)
+            {
+                return false; // Can't check for convergence in the first iteration
+            }
+
+            // Check if the length of current and previous centroids are different
+            if (currentCentroids.Count != previousCentroids.Count)
+            {
+                return false; // They haven't converged if they don't have the same number of centroids
+            }
+
+            // Check if the centroids have changed between iterations using the injected distance algorithm
+            for (int i = 0; i < currentCentroids.Count; i++)
+            {
+                if (colorDistance.CalculateDistance (currentCentroids [i].ToColor (), previousCentroids [i].ToColor ()) > 1.0) // Use a larger threshold
+                {
+                    return false; // Centroids haven't converged yet if any of them have moved significantly
+                }
+            }
+
+            return true; // Centroids have converged if all distances are below the threshold
+        }
+
+        private ColorVector InitializeRandomCentroid ()
+        {
+            // Initialize a random centroid by picking random color values
+            return new ColorVector (random.Next (0, 256), random.Next (0, 256), random.Next (0, 256));
+        }
+
+        private class ColorVector
+        {
+            public double R { get; }
+            public double G { get; }
+            public double B { get; }
+
+            public ColorVector (double r, double g, double b)
+            {
+                R = r;
+                G = g;
+                B = b;
+            }
+
+            // Convert ColorVector back to Color for use with the IColorDistance interface
+            public Color ToColor ()
+            {
+                return new Color ((int)R, (int)G, (int)B);
+            }
+        }
+}

+ 52 - 0
Terminal.Gui/Drawing/Quant/LabColorDistance.cs

@@ -0,0 +1,52 @@
+using ColorHelper;
+
+namespace Terminal.Gui.Drawing.Quant;
+
+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);
+}

+ 161 - 0
Terminal.Gui/Drawing/Quant/MedianCutPaletteBuilder.cs

@@ -0,0 +1,161 @@
+namespace Terminal.Gui.Drawing.Quant;
+
+public class MedianCutPaletteBuilder : IPaletteBuilder
+{
+    public List<Color> BuildPalette (List<Color> colors, int maxColors)
+    {
+        // Initial step: place all colors in one large box
+        List<ColorBox> boxes = new List<ColorBox> { new ColorBox (colors) };
+
+        // Keep splitting boxes until we have the desired number of colors
+        while (boxes.Count < maxColors)
+        {
+            // Find the box with the largest range and split it
+            ColorBox boxToSplit = FindBoxWithLargestRange (boxes);
+
+            if (boxToSplit == null || boxToSplit.Colors.Count == 0)
+            {
+                break;
+            }
+
+            // Split the box into two smaller boxes
+            var splitBoxes = SplitBox (boxToSplit);
+            boxes.Remove (boxToSplit);
+            boxes.AddRange (splitBoxes);
+        }
+
+        // Average the colors in each box to get the final palette
+        return boxes.Select (box => box.GetAverageColor ()).ToList ();
+    }
+
+    // Find the box with the largest color range (R, G, or B)
+    private ColorBox FindBoxWithLargestRange (List<ColorBox> boxes)
+    {
+        ColorBox largestRangeBox = null;
+        int largestRange = 0;
+
+        foreach (var box in boxes)
+        {
+            int range = box.GetColorRange ();
+            if (range > largestRange)
+            {
+                largestRange = range;
+                largestRangeBox = box;
+            }
+        }
+
+        return largestRangeBox;
+    }
+
+    // Split a box at the median point in its largest color channel
+    private List<ColorBox> SplitBox (ColorBox box)
+    {
+        List<ColorBox> result = new List<ColorBox> ();
+
+        // Find the color channel with the largest range (R, G, or B)
+        int channel = box.GetLargestChannel ();
+        var sortedColors = box.Colors.OrderBy (c => GetColorChannelValue (c, channel)).ToList ();
+
+        // Split the box at the median
+        int medianIndex = sortedColors.Count / 2;
+
+        var lowerHalf = sortedColors.Take (medianIndex).ToList ();
+        var upperHalf = sortedColors.Skip (medianIndex).ToList ();
+
+        result.Add (new ColorBox (lowerHalf));
+        result.Add (new ColorBox (upperHalf));
+
+        return result;
+    }
+
+    // Helper method to get the value of a color channel (R = 0, G = 1, B = 2)
+    private static int GetColorChannelValue (Color color, int channel)
+    {
+        switch (channel)
+        {
+            case 0: return color.R;
+            case 1: return color.G;
+            case 2: return color.B;
+            default: throw new ArgumentException ("Invalid channel index");
+        }
+    }
+
+    // The ColorBox class to represent a subset of colors
+    public class ColorBox
+    {
+        public List<Color> Colors { get; private set; }
+
+        public ColorBox (List<Color> colors)
+        {
+            Colors = colors;
+        }
+
+        // Get the color channel with the largest range (0 = R, 1 = G, 2 = B)
+        public int GetLargestChannel ()
+        {
+            int rRange = GetColorRangeForChannel (0);
+            int gRange = GetColorRangeForChannel (1);
+            int bRange = GetColorRangeForChannel (2);
+
+            if (rRange >= gRange && rRange >= bRange)
+            {
+                return 0;
+            }
+
+            if (gRange >= rRange && gRange >= bRange)
+            {
+                return 1;
+            }
+
+            return 2;
+        }
+
+        // Get the range of colors for a given channel (0 = R, 1 = G, 2 = B)
+        private int GetColorRangeForChannel (int channel)
+        {
+            int min = int.MaxValue, max = int.MinValue;
+
+            foreach (var color in Colors)
+            {
+                int value = GetColorChannelValue (color, channel);
+                if (value < min)
+                {
+                    min = value;
+                }
+
+                if (value > max)
+                {
+                    max = value;
+                }
+            }
+
+            return max - min;
+        }
+
+        // Get the overall color range across all channels (for finding the box to split)
+        public int GetColorRange ()
+        {
+            int rRange = GetColorRangeForChannel (0);
+            int gRange = GetColorRangeForChannel (1);
+            int bRange = GetColorRangeForChannel (2);
+
+            return Math.Max (rRange, Math.Max (gRange, bRange));
+        }
+
+        // Calculate the average color in the box
+        public Color GetAverageColor ()
+        {
+            int totalR = 0, totalG = 0, totalB = 0;
+
+            foreach (var color in Colors)
+            {
+                totalR += color.R;
+                totalG += color.G;
+                totalB += color.B;
+            }
+
+            int count = Colors.Count;
+            return new Color (totalR / count, totalG / count, totalB / count);
+        }
+    }
+}

+ 19 - 11
Terminal.Gui/Drawing/SixelEncoder.cs

@@ -1,10 +1,19 @@
-namespace Terminal.Gui;
+using Terminal.Gui.Drawing.Quant;
+
+namespace Terminal.Gui;
 
 /// <summary>
 /// Encodes a images into the sixel console image output format.
 /// </summary>
 public class SixelEncoder
 {
+    /// <summary>
+    /// Gets or sets the quantizer responsible for building a representative
+    /// limited color palette for images and for mapping novel colors in
+    /// images to their closest palette color
+    /// </summary>
+    public ColorQuantizer Quantizer { get; set; } = new ();
+
     /// <summary>
     /// Encode the given bitmap into sixel encoding
     /// </summary>
@@ -21,9 +30,9 @@ public class SixelEncoder
 
         string fillArea = GetFillArea (pixels);
 
-        string pallette = GetColorPallette (pixels, out var quantizer);
+        string pallette = GetColorPallette (pixels );
 
-        string pixelData = WriteSixel (pixels, quantizer);
+        string pixelData = WriteSixel (pixels);
 
         const string terminator = "\u001b\\"; // End sixel sequence
 
@@ -43,7 +52,7 @@ public class SixelEncoder
        [ ]  - Bit 5 (bottom-most pixel)
     */
 
-    private string WriteSixel (Color [,] pixels, ColorQuantizer quantizer)
+    private string WriteSixel (Color [,] pixels)
     {
         StringBuilder sb = new StringBuilder ();
         int height = pixels.GetLength (1);
@@ -55,7 +64,7 @@ public class SixelEncoder
         {
             int p = y * width;
             Color cachedColor = pixels [0, y];
-            int cachedColorIndex = quantizer.GetNearestColor (cachedColor );
+            int cachedColorIndex = Quantizer.GetNearestColor (cachedColor );
             int count = 1;
             int c = -1;
 
@@ -63,7 +72,7 @@ public class SixelEncoder
             for (int x = 0; x < width; x++)
             {
                 Color color = pixels [x, y];
-                int colorIndex = quantizer.GetNearestColor (color);
+                int colorIndex = Quantizer.GetNearestColor (color);
 
                 if (colorIndex == cachedColorIndex)
                 {
@@ -159,19 +168,18 @@ public class SixelEncoder
 
 
 
-    private string GetColorPallette (Color [,] pixels, out ColorQuantizer quantizer)
+    private string GetColorPallette (Color [,] pixels)
     {
-        quantizer = new ColorQuantizer ();
-        quantizer.BuildPalette (pixels);
+        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
 
         StringBuilder paletteSb = new StringBuilder ();
 
-        for (int i = 0; i < quantizer.Palette.Count; i++)
+        for (int i = 0; i < Quantizer.Palette.Count; i++)
         {
-            var color = quantizer.Palette.ElementAt (i);
+            var color = Quantizer.Palette.ElementAt (i);
             paletteSb.AppendFormat ("#{0};2;{1};{2};{3}",
                                     i,
                                     color.R * 100 / 255,

+ 4 - 0
UICatalog/Scenarios/Images.cs

@@ -6,6 +6,7 @@ using SixLabors.ImageSharp;
 using SixLabors.ImageSharp.PixelFormats;
 using SixLabors.ImageSharp.Processing;
 using Terminal.Gui;
+using Terminal.Gui.Drawing.Quant;
 using Color = Terminal.Gui.Color;
 
 namespace UICatalog.Scenarios;
@@ -103,6 +104,8 @@ public class Images : Scenario
                                    Application.Refresh ();
                                };
 
+
+
         var btnSixel = new Button () { X = Pos.Right (btnOpenImage) + 2, Y = 0, Text = "Output Sixel" };
         btnSixel.Accept += (s, e) => { imageView.OutputSixel ();};
         win.Add (btnSixel);
@@ -169,6 +172,7 @@ public class Images : Scenario
             }
 
             var encoder = new SixelEncoder ();
+            encoder.Quantizer.PaletteBuildingAlgorithm = new KMeansPaletteBuilder (new EuclideanColorDistance());
 
             var encoded = encoder.EncodeSixel (ConvertToColorArray (_fullResImage));