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