From c6281ddddb58ac5c918b5c4f346631b15c815ed7 Mon Sep 17 00:00:00 2001 From: tznind Date: Wed, 11 Sep 2024 19:52:11 +0100 Subject: [PATCH] Build color palette using median cut instead of naive method --- Terminal.Gui/Drawing/ColorQuantizer.cs | 219 ++++++++++++++++++++++--- Terminal.Gui/Drawing/SixelEncoder.cs | 2 +- 2 files changed, 198 insertions(+), 23 deletions(-) diff --git a/Terminal.Gui/Drawing/ColorQuantizer.cs b/Terminal.Gui/Drawing/ColorQuantizer.cs index 945e1daf4..6e1e4b2e5 100644 --- a/Terminal.Gui/Drawing/ColorQuantizer.cs +++ b/Terminal.Gui/Drawing/ColorQuantizer.cs @@ -15,38 +15,24 @@ public class ColorQuantizer Palette = new List (); } - public void BuildColorPalette (Color [,] pixels) + public void BuildPalette (Color [,] pixels, IPaletteBuilder builder) { + List allColors = new List (); int width = pixels.GetLength (0); int height = pixels.GetLength (1); - // Count the frequency of each color for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { - Color color = pixels [x, y]; - if (colorFrequency.ContainsKey (color)) - { - colorFrequency [color]++; - } - else - { - colorFrequency [color] = 1; - } + allColors.Add (pixels [x, y]); } } - // Create a sorted list of colors based on frequency - var sortedColors = colorFrequency.OrderByDescending (kvp => kvp.Value).ToList (); - - // Build the Palette with the most frequent colors up to MaxColors - Palette = sortedColors.Take (MaxColors).Select (kvp => kvp.Key).ToList (); - - + Palette = builder.BuildPalette(allColors,MaxColors); } - public int GetNearestColor (Color toTranslate) + public int GetNearestColor (Color toTranslate, IColorDistance distanceAlgorithm) { // Simple nearest color matching based on Euclidean distance in RGB space double minDistance = double.MaxValue; @@ -55,7 +41,7 @@ public class ColorQuantizer for (var index = 0; index < Palette.Count; index++) { Color color = Palette [index]; - double distance = ColorDistance (color, toTranslate); + double distance = distanceAlgorithm.CalculateDistance(color, toTranslate); if (distance < minDistance) { @@ -66,13 +52,202 @@ public class ColorQuantizer return nearestIndex; } +} - private double ColorDistance (Color c1, Color c2) +public interface IPaletteBuilder +{ + List BuildPalette (List colors, int maxColors); +} + +/// +/// 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. +/// +public interface IColorDistance +{ + /// + /// Computes a similarity metric between two 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. + /// + /// The first color. + /// The second color. + /// A numeric value representing the distance between the two colors. + double CalculateDistance (Color c1, Color c2); +} + +/// +/// 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. +/// +public class EuclideanColorDistance : IColorDistance +{ + public double CalculateDistance (Color c1, Color c2) { - // Euclidean distance in RGB space 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); } +} + + +class MedianCutPaletteBuilder : IPaletteBuilder +{ + public List BuildPalette (List colors, int maxColors) + { + // Initial step: place all colors in one large box + List boxes = new List { 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 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 SplitBox (ColorBox box) + { + List result = new List (); + + // 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 Colors { get; private set; } + + public ColorBox (List 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); + } + } } \ No newline at end of file diff --git a/Terminal.Gui/Drawing/SixelEncoder.cs b/Terminal.Gui/Drawing/SixelEncoder.cs index 2063f0023..96b7c9d1f 100644 --- a/Terminal.Gui/Drawing/SixelEncoder.cs +++ b/Terminal.Gui/Drawing/SixelEncoder.cs @@ -162,7 +162,7 @@ public class SixelEncoder private string GetColorPallette (Color [,] pixels, out ColorQuantizer quantizer) { quantizer = new ColorQuantizer (); - quantizer.BuildColorPalette (pixels); + quantizer.BuildPaletteUsingMedianCut (pixels); // Color definitions in the format "#;;;;" - For type the 2 means RGB. The values range 0 to 100