mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2026-01-01 16:59:35 +01:00
Build color palette using median cut instead of naive method
This commit is contained in:
@@ -15,38 +15,24 @@ public class ColorQuantizer
|
||||
Palette = new List<Color> ();
|
||||
}
|
||||
|
||||
public void BuildColorPalette (Color [,] pixels)
|
||||
public void BuildPalette (Color [,] pixels, IPaletteBuilder builder)
|
||||
{
|
||||
List<Color> allColors = new List<Color> ();
|
||||
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<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)
|
||||
{
|
||||
// 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<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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 "#<index>;<type>;<R>;<G>;<B>" - For type the 2 means RGB. The values range 0 to 100
|
||||
|
||||
Reference in New Issue
Block a user