|
@@ -1,21 +1,38 @@
|
|
-namespace Terminal.Gui;
|
|
|
|
|
|
+using System.Collections.ObjectModel;
|
|
|
|
+using ColorHelper;
|
|
|
|
+
|
|
|
|
+namespace Terminal.Gui;
|
|
|
|
|
|
/// <summary>
|
|
/// <summary>
|
|
/// Translates colors in an image into a Palette of up to 256 colors.
|
|
/// Translates colors in an image into a Palette of up to 256 colors.
|
|
/// </summary>
|
|
/// </summary>
|
|
public class ColorQuantizer
|
|
public class ColorQuantizer
|
|
{
|
|
{
|
|
- private Dictionary<Color, int> colorFrequency;
|
|
|
|
- public List<Color> Palette;
|
|
|
|
- private const int MaxColors = 256;
|
|
|
|
|
|
+ /// <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> ();
|
|
|
|
|
|
- public ColorQuantizer ()
|
|
|
|
- {
|
|
|
|
- colorFrequency = new Dictionary<Color, int> ();
|
|
|
|
- Palette = 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 ();
|
|
|
|
|
|
- public void BuildPalette (Color [,] pixels, IPaletteBuilder builder)
|
|
|
|
|
|
+ /// <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> ();
|
|
List<Color> allColors = new List<Color> ();
|
|
int width = pixels.GetLength (0);
|
|
int width = pixels.GetLength (0);
|
|
@@ -29,10 +46,10 @@ public class ColorQuantizer
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
- Palette = builder.BuildPalette(allColors,MaxColors);
|
|
|
|
|
|
+ Palette = PaletteBuildingAlgorithm.BuildPalette(allColors,MaxColors);
|
|
}
|
|
}
|
|
|
|
|
|
- public int GetNearestColor (Color toTranslate, IColorDistance distanceAlgorithm)
|
|
|
|
|
|
+ public int GetNearestColor (Color toTranslate)
|
|
{
|
|
{
|
|
// Simple nearest color matching based on Euclidean distance in RGB space
|
|
// Simple nearest color matching based on Euclidean distance in RGB space
|
|
double minDistance = double.MaxValue;
|
|
double minDistance = double.MaxValue;
|
|
@@ -40,8 +57,8 @@ public class ColorQuantizer
|
|
|
|
|
|
for (var index = 0; index < Palette.Count; index++)
|
|
for (var index = 0; index < Palette.Count; index++)
|
|
{
|
|
{
|
|
- Color color = Palette [index];
|
|
|
|
- double distance = distanceAlgorithm.CalculateDistance(color, toTranslate);
|
|
|
|
|
|
+ Color color = Palette.ElementAt(index);
|
|
|
|
+ double distance = DistanceAlgorithm.CalculateDistance(color, toTranslate);
|
|
|
|
|
|
if (distance < minDistance)
|
|
if (distance < minDistance)
|
|
{
|
|
{
|
|
@@ -90,6 +107,114 @@ public class EuclideanColorDistance : IColorDistance
|
|
return Math.Sqrt (rDiff * rDiff + gDiff * gDiff + bDiff * bDiff);
|
|
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
|
|
class MedianCutPaletteBuilder : IPaletteBuilder
|