using System.Collections.ObjectModel;
using ColorHelper;
namespace Terminal.Gui;
///
/// Translates colors in an image into a Palette of up to 256 colors.
///
public class ColorQuantizer
{
///
/// Gets the current colors in the palette based on the last call to
/// .
///
public IReadOnlyCollection Palette { get; private set; } = new List ();
///
/// Gets or sets the maximum number of colors to put into the .
/// Defaults to 256 (the maximum for sixel images).
///
public int MaxColors { get; set; } = 256;
///
/// Gets or sets the algorithm used to map novel colors into existing
/// palette colors (closest match). Defaults to
///
public IColorDistance DistanceAlgorithm { get; set; } = new CIE94ColorDistance ();
///
/// Gets or sets the algorithm used to build the .
/// Defaults to
///
public IPaletteBuilder PaletteBuildingAlgorithm { get; set; } = new MedianCutPaletteBuilder ();
public void BuildPalette (Color [,] pixels)
{
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 = 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 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);
}
}
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;
}
}
///
public abstract double CalculateDistance (Color c1, Color c2);
}
///
/// 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.
///
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));
}
}
///
/// 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 .
///
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 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);
}
}
}