Build color palette using median cut instead of naive method

This commit is contained in:
tznind
2024-09-11 19:52:11 +01:00
parent 943fa11230
commit c6281ddddb
2 changed files with 198 additions and 23 deletions

View File

@@ -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);
}
}
}

View File

@@ -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