namespace Terminal.Gui; /// /// Translates colors in an image into a Palette of up to 256 colors. /// public class ColorQuantizer { private Dictionary colorFrequency; public List Palette; private const int MaxColors = 256; public ColorQuantizer () { colorFrequency = new Dictionary (); Palette = new List (); } public void BuildPalette (Color [,] pixels, IPaletteBuilder builder) { List allColors = new List (); 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 = builder.BuildPalette(allColors,MaxColors); } public int GetNearestColor (Color toTranslate, IColorDistance distanceAlgorithm) { // 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 [index]; double distance = distanceAlgorithm.CalculateDistance(color, toTranslate); if (distance < minDistance) { minDistance = distance; nearestIndex = index; } } return nearestIndex; } } 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) { 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); } } }