mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2026-01-01 08:50:25 +01:00
Refactor and split into seperate files WIP
This commit is contained in:
@@ -1,378 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
17
Terminal.Gui/Drawing/Quant/CIE76ColorDistance.cs
Normal file
17
Terminal.Gui/Drawing/Quant/CIE76ColorDistance.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
namespace Terminal.Gui.Drawing.Quant;
|
||||
|
||||
/// <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));
|
||||
}
|
||||
}
|
||||
45
Terminal.Gui/Drawing/Quant/CIE94ColorDistance.cs
Normal file
45
Terminal.Gui/Drawing/Quant/CIE94ColorDistance.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
namespace Terminal.Gui.Drawing.Quant;
|
||||
|
||||
/// <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)
|
||||
);
|
||||
}
|
||||
}
|
||||
71
Terminal.Gui/Drawing/Quant/ColorQuantizer.cs
Normal file
71
Terminal.Gui/Drawing/Quant/ColorQuantizer.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Terminal.Gui.Drawing.Quant;
|
||||
|
||||
/// <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;
|
||||
}
|
||||
}
|
||||
16
Terminal.Gui/Drawing/Quant/EuclideanColorDistance.cs
Normal file
16
Terminal.Gui/Drawing/Quant/EuclideanColorDistance.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace Terminal.Gui.Drawing.Quant;
|
||||
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
18
Terminal.Gui/Drawing/Quant/IColorDistance.cs
Normal file
18
Terminal.Gui/Drawing/Quant/IColorDistance.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace Terminal.Gui.Drawing.Quant;
|
||||
|
||||
/// <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);
|
||||
}
|
||||
6
Terminal.Gui/Drawing/Quant/IPaletteBuilder.cs
Normal file
6
Terminal.Gui/Drawing/Quant/IPaletteBuilder.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Terminal.Gui.Drawing.Quant;
|
||||
|
||||
public interface IPaletteBuilder
|
||||
{
|
||||
List<Color> BuildPalette (List<Color> colors, int maxColors);
|
||||
}
|
||||
154
Terminal.Gui/Drawing/Quant/KMeansPaletteBuilder.cs
Normal file
154
Terminal.Gui/Drawing/Quant/KMeansPaletteBuilder.cs
Normal file
@@ -0,0 +1,154 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Terminal.Gui.Drawing.Quant;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="IPaletteBuilder"/> that works well for images with high contrast images
|
||||
/// </summary>
|
||||
public class KMeansPaletteBuilder : IPaletteBuilder
|
||||
{
|
||||
private readonly int maxIterations;
|
||||
private readonly Random random = new Random ();
|
||||
private readonly IColorDistance colorDistance;
|
||||
|
||||
public KMeansPaletteBuilder (IColorDistance distanceAlgorithm, int maxIterations = 100)
|
||||
{
|
||||
colorDistance = distanceAlgorithm;
|
||||
this.maxIterations = maxIterations;
|
||||
}
|
||||
|
||||
public List<Color> BuildPalette (List<Color> colors, int maxColors)
|
||||
{
|
||||
// Convert colors to vectors
|
||||
List<ColorVector> colorVectors = colors.Select (c => new ColorVector (c.R, c.G, c.B)).ToList ();
|
||||
|
||||
// Perform K-Means Clustering
|
||||
List<ColorVector> centroids = KMeans (colorVectors, maxColors);
|
||||
|
||||
// Convert centroids back to colors
|
||||
return centroids.Select (v => new Color ((int)v.R, (int)v.G, (int)v.B)).ToList ();
|
||||
}
|
||||
|
||||
private List<ColorVector> KMeans (List<ColorVector> colors, int k)
|
||||
{
|
||||
// Randomly initialize k centroids
|
||||
List<ColorVector> centroids = InitializeCentroids (colors, k);
|
||||
|
||||
List<ColorVector> previousCentroids = new List<ColorVector> ();
|
||||
int iterations = 0;
|
||||
|
||||
// Repeat until convergence or max iterations
|
||||
while (!HasConverged (centroids, previousCentroids) && iterations < maxIterations)
|
||||
{
|
||||
previousCentroids = centroids.Select (c => new ColorVector (c.R, c.G, c.B)).ToList ();
|
||||
|
||||
// Assign each color to the nearest centroid
|
||||
var clusters = AssignColorsToClusters (colors, centroids);
|
||||
|
||||
// Recompute centroids
|
||||
centroids = RecomputeCentroids (clusters);
|
||||
|
||||
iterations++;
|
||||
}
|
||||
|
||||
return centroids;
|
||||
}
|
||||
|
||||
private List<ColorVector> InitializeCentroids (List<ColorVector> colors, int k)
|
||||
{
|
||||
return colors.OrderBy (c => random.Next ()).Take (k).ToList (); // Randomly select k initial centroids
|
||||
}
|
||||
|
||||
private Dictionary<ColorVector, List<ColorVector>> AssignColorsToClusters (List<ColorVector> colors, List<ColorVector> centroids)
|
||||
{
|
||||
var clusters = centroids.ToDictionary (c => c, c => new List<ColorVector> ());
|
||||
|
||||
foreach (var color in colors)
|
||||
{
|
||||
// Find the nearest centroid using the injected IColorDistance implementation
|
||||
var nearestCentroid = centroids.OrderBy (c => colorDistance.CalculateDistance (c.ToColor (), color.ToColor ())).First ();
|
||||
clusters [nearestCentroid].Add (color);
|
||||
}
|
||||
|
||||
return clusters;
|
||||
}
|
||||
|
||||
private List<ColorVector> RecomputeCentroids (Dictionary<ColorVector, List<ColorVector>> clusters)
|
||||
{
|
||||
var newCentroids = new List<ColorVector> ();
|
||||
|
||||
foreach (var cluster in clusters)
|
||||
{
|
||||
if (cluster.Value.Count == 0)
|
||||
{
|
||||
// Reinitialize the centroid with a random color if the cluster is empty
|
||||
newCentroids.Add (InitializeRandomCentroid ());
|
||||
}
|
||||
else
|
||||
{
|
||||
// Recompute the centroid as the mean of the cluster's points
|
||||
double avgR = cluster.Value.Average (c => c.R);
|
||||
double avgG = cluster.Value.Average (c => c.G);
|
||||
double avgB = cluster.Value.Average (c => c.B);
|
||||
|
||||
newCentroids.Add (new ColorVector (avgR, avgG, avgB));
|
||||
}
|
||||
}
|
||||
|
||||
return newCentroids;
|
||||
}
|
||||
|
||||
private bool HasConverged (List<ColorVector> currentCentroids, List<ColorVector> previousCentroids)
|
||||
{
|
||||
// Skip convergence check for the first iteration
|
||||
if (previousCentroids.Count == 0)
|
||||
{
|
||||
return false; // Can't check for convergence in the first iteration
|
||||
}
|
||||
|
||||
// Check if the length of current and previous centroids are different
|
||||
if (currentCentroids.Count != previousCentroids.Count)
|
||||
{
|
||||
return false; // They haven't converged if they don't have the same number of centroids
|
||||
}
|
||||
|
||||
// Check if the centroids have changed between iterations using the injected distance algorithm
|
||||
for (int i = 0; i < currentCentroids.Count; i++)
|
||||
{
|
||||
if (colorDistance.CalculateDistance (currentCentroids [i].ToColor (), previousCentroids [i].ToColor ()) > 1.0) // Use a larger threshold
|
||||
{
|
||||
return false; // Centroids haven't converged yet if any of them have moved significantly
|
||||
}
|
||||
}
|
||||
|
||||
return true; // Centroids have converged if all distances are below the threshold
|
||||
}
|
||||
|
||||
private ColorVector InitializeRandomCentroid ()
|
||||
{
|
||||
// Initialize a random centroid by picking random color values
|
||||
return new ColorVector (random.Next (0, 256), random.Next (0, 256), random.Next (0, 256));
|
||||
}
|
||||
|
||||
private class ColorVector
|
||||
{
|
||||
public double R { get; }
|
||||
public double G { get; }
|
||||
public double B { get; }
|
||||
|
||||
public ColorVector (double r, double g, double b)
|
||||
{
|
||||
R = r;
|
||||
G = g;
|
||||
B = b;
|
||||
}
|
||||
|
||||
// Convert ColorVector back to Color for use with the IColorDistance interface
|
||||
public Color ToColor ()
|
||||
{
|
||||
return new Color ((int)R, (int)G, (int)B);
|
||||
}
|
||||
}
|
||||
}
|
||||
52
Terminal.Gui/Drawing/Quant/LabColorDistance.cs
Normal file
52
Terminal.Gui/Drawing/Quant/LabColorDistance.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using ColorHelper;
|
||||
|
||||
namespace Terminal.Gui.Drawing.Quant;
|
||||
|
||||
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);
|
||||
}
|
||||
161
Terminal.Gui/Drawing/Quant/MedianCutPaletteBuilder.cs
Normal file
161
Terminal.Gui/Drawing/Quant/MedianCutPaletteBuilder.cs
Normal file
@@ -0,0 +1,161 @@
|
||||
namespace Terminal.Gui.Drawing.Quant;
|
||||
|
||||
public 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,19 @@
|
||||
namespace Terminal.Gui;
|
||||
using Terminal.Gui.Drawing.Quant;
|
||||
|
||||
namespace Terminal.Gui;
|
||||
|
||||
/// <summary>
|
||||
/// Encodes a images into the sixel console image output format.
|
||||
/// </summary>
|
||||
public class SixelEncoder
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the quantizer responsible for building a representative
|
||||
/// limited color palette for images and for mapping novel colors in
|
||||
/// images to their closest palette color
|
||||
/// </summary>
|
||||
public ColorQuantizer Quantizer { get; set; } = new ();
|
||||
|
||||
/// <summary>
|
||||
/// Encode the given bitmap into sixel encoding
|
||||
/// </summary>
|
||||
@@ -21,9 +30,9 @@ public class SixelEncoder
|
||||
|
||||
string fillArea = GetFillArea (pixels);
|
||||
|
||||
string pallette = GetColorPallette (pixels, out var quantizer);
|
||||
string pallette = GetColorPallette (pixels );
|
||||
|
||||
string pixelData = WriteSixel (pixels, quantizer);
|
||||
string pixelData = WriteSixel (pixels);
|
||||
|
||||
const string terminator = "\u001b\\"; // End sixel sequence
|
||||
|
||||
@@ -43,7 +52,7 @@ public class SixelEncoder
|
||||
[ ] - Bit 5 (bottom-most pixel)
|
||||
*/
|
||||
|
||||
private string WriteSixel (Color [,] pixels, ColorQuantizer quantizer)
|
||||
private string WriteSixel (Color [,] pixels)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder ();
|
||||
int height = pixels.GetLength (1);
|
||||
@@ -55,7 +64,7 @@ public class SixelEncoder
|
||||
{
|
||||
int p = y * width;
|
||||
Color cachedColor = pixels [0, y];
|
||||
int cachedColorIndex = quantizer.GetNearestColor (cachedColor );
|
||||
int cachedColorIndex = Quantizer.GetNearestColor (cachedColor );
|
||||
int count = 1;
|
||||
int c = -1;
|
||||
|
||||
@@ -63,7 +72,7 @@ public class SixelEncoder
|
||||
for (int x = 0; x < width; x++)
|
||||
{
|
||||
Color color = pixels [x, y];
|
||||
int colorIndex = quantizer.GetNearestColor (color);
|
||||
int colorIndex = Quantizer.GetNearestColor (color);
|
||||
|
||||
if (colorIndex == cachedColorIndex)
|
||||
{
|
||||
@@ -159,19 +168,18 @@ public class SixelEncoder
|
||||
|
||||
|
||||
|
||||
private string GetColorPallette (Color [,] pixels, out ColorQuantizer quantizer)
|
||||
private string GetColorPallette (Color [,] pixels)
|
||||
{
|
||||
quantizer = new ColorQuantizer ();
|
||||
quantizer.BuildPalette (pixels);
|
||||
Quantizer.BuildPalette (pixels);
|
||||
|
||||
|
||||
// Color definitions in the format "#<index>;<type>;<R>;<G>;<B>" - For type the 2 means RGB. The values range 0 to 100
|
||||
|
||||
StringBuilder paletteSb = new StringBuilder ();
|
||||
|
||||
for (int i = 0; i < quantizer.Palette.Count; i++)
|
||||
for (int i = 0; i < Quantizer.Palette.Count; i++)
|
||||
{
|
||||
var color = quantizer.Palette.ElementAt (i);
|
||||
var color = Quantizer.Palette.ElementAt (i);
|
||||
paletteSb.AppendFormat ("#{0};2;{1};{2};{3}",
|
||||
i,
|
||||
color.R * 100 / 255,
|
||||
|
||||
@@ -6,6 +6,7 @@ using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using Terminal.Gui;
|
||||
using Terminal.Gui.Drawing.Quant;
|
||||
using Color = Terminal.Gui.Color;
|
||||
|
||||
namespace UICatalog.Scenarios;
|
||||
@@ -103,6 +104,8 @@ public class Images : Scenario
|
||||
Application.Refresh ();
|
||||
};
|
||||
|
||||
|
||||
|
||||
var btnSixel = new Button () { X = Pos.Right (btnOpenImage) + 2, Y = 0, Text = "Output Sixel" };
|
||||
btnSixel.Accept += (s, e) => { imageView.OutputSixel ();};
|
||||
win.Add (btnSixel);
|
||||
@@ -169,6 +172,7 @@ public class Images : Scenario
|
||||
}
|
||||
|
||||
var encoder = new SixelEncoder ();
|
||||
encoder.Quantizer.PaletteBuildingAlgorithm = new KMeansPaletteBuilder (new EuclideanColorDistance());
|
||||
|
||||
var encoded = encoder.EncodeSixel (ConvertToColorArray (_fullResImage));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user