diff --git a/Terminal.Gui/Drawing/Quant/ColorQuantizer.cs b/Terminal.Gui/Drawing/Quant/ColorQuantizer.cs
index 1529a3eea..4b32e1669 100644
--- a/Terminal.Gui/Drawing/Quant/ColorQuantizer.cs
+++ b/Terminal.Gui/Drawing/Quant/ColorQuantizer.cs
@@ -1,5 +1,4 @@
-using System.Collections.ObjectModel;
-using Terminal.Gui.Drawing.Quant;
+
namespace Terminal.Gui;
@@ -29,7 +28,7 @@ public class ColorQuantizer
///
/// Gets or sets the algorithm used to build the .
///
- public IPaletteBuilder PaletteBuildingAlgorithm { get; set; } = new KMeansPaletteBuilder (new EuclideanColorDistance ()) ;
+ public IPaletteBuilder PaletteBuildingAlgorithm { get; set; } = new MedianCutPaletteBuilder (new EuclideanColorDistance ()) ;
public void BuildPalette (Color [,] pixels)
{
diff --git a/Terminal.Gui/Drawing/Quant/IPaletteBuilder.cs b/Terminal.Gui/Drawing/Quant/IPaletteBuilder.cs
index e72de3769..999297cff 100644
--- a/Terminal.Gui/Drawing/Quant/IPaletteBuilder.cs
+++ b/Terminal.Gui/Drawing/Quant/IPaletteBuilder.cs
@@ -1,6 +1,17 @@
namespace Terminal.Gui;
+///
+/// Builds a palette of a given size for a given set of input colors.
+///
public interface IPaletteBuilder
{
+ ///
+ /// Reduce the number of to (or less)
+ /// using an appropriate selection algorithm.
+ ///
+ /// Color of every pixel in the image. Contains duplication in order
+ /// to support algorithms that weigh how common a color is.
+ /// The maximum number of colours that should be represented.
+ ///
List BuildPalette (List colors, int maxColors);
}
diff --git a/Terminal.Gui/Drawing/Quant/KMeansPaletteBuilder.cs b/Terminal.Gui/Drawing/Quant/KMeansPaletteBuilder.cs
deleted file mode 100644
index 0cf8bb0eb..000000000
--- a/Terminal.Gui/Drawing/Quant/KMeansPaletteBuilder.cs
+++ /dev/null
@@ -1,154 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-
-namespace Terminal.Gui.Drawing.Quant;
-
- ///
- /// that works well for images with high contrast images
- ///
- 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 BuildPalette (List colors, int maxColors)
- {
- // Convert colors to vectors
- List colorVectors = colors.Select (c => new ColorVector (c.R, c.G, c.B)).ToList ();
-
- // Perform K-Means Clustering
- List 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 KMeans (List colors, int k)
- {
- // Randomly initialize k centroids
- List centroids = InitializeCentroids (colors, k);
-
- List previousCentroids = new List ();
- 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 InitializeCentroids (List colors, int k)
- {
- return colors.OrderBy (c => random.Next ()).Take (k).ToList (); // Randomly select k initial centroids
- }
-
- private Dictionary> AssignColorsToClusters (List colors, List centroids)
- {
- var clusters = centroids.ToDictionary (c => c, c => new List ());
-
- 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 RecomputeCentroids (Dictionary> clusters)
- {
- var newCentroids = new List ();
-
- 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 currentCentroids, List 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);
- }
- }
-}
diff --git a/Terminal.Gui/Drawing/Quant/MedianCutPaletteBuilder.cs b/Terminal.Gui/Drawing/Quant/MedianCutPaletteBuilder.cs
index 9a49c9a64..cdafdfaa3 100644
--- a/Terminal.Gui/Drawing/Quant/MedianCutPaletteBuilder.cs
+++ b/Terminal.Gui/Drawing/Quant/MedianCutPaletteBuilder.cs
@@ -1,138 +1,123 @@
namespace Terminal.Gui;
-
public class MedianCutPaletteBuilder : IPaletteBuilder
{
+ private readonly IColorDistance _colorDistance;
+
+ public MedianCutPaletteBuilder (IColorDistance colorDistance)
+ {
+ _colorDistance = colorDistance;
+ }
+
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)
+ if (colors == null || colors.Count == 0 || maxColors <= 0)
{
- // Find the box with the largest brightness range and split it
- ColorBox boxToSplit = FindBoxWithLargestRange (boxes);
+ return new List ();
+ }
- if (boxToSplit == null || boxToSplit.Colors.Count == 0)
+ return MedianCut (colors, maxColors);
+ }
+
+ private List MedianCut (List colors, int maxColors)
+ {
+ var cubes = new List> () { colors };
+
+ // Recursively split color regions
+ while (cubes.Count < maxColors)
+ {
+ bool added = false;
+ cubes.Sort ((a, b) => Volume (a).CompareTo (Volume (b)));
+
+ var largestCube = cubes.Last ();
+ cubes.RemoveAt (cubes.Count - 1);
+
+ var (cube1, cube2) = SplitCube (largestCube);
+
+ if (cube1.Any ())
+ {
+ cubes.Add (cube1);
+ added = true;
+ }
+
+ if (cube2.Any ())
+ {
+ cubes.Add (cube2);
+ added = true;
+ }
+
+ if (!added)
{
break;
}
-
- // Split the box into two smaller boxes, based on luminance
- var splitBoxes = SplitBoxByLuminance (boxToSplit);
- boxes.Remove (boxToSplit);
- boxes.AddRange (splitBoxes);
}
- // Average the colors in each box to get the final palette
- return boxes.Select (box => box.GetWeightedAverageColor ()).ToList ();
+ // Calculate average color for each cube
+ return cubes.Select (AverageColor).Distinct().ToList ();
}
- // Find the box with the largest brightness range (based on luminance)
- private ColorBox FindBoxWithLargestRange (List boxes)
+ // Splits the cube based on the largest color component range
+ private (List, List) SplitCube (List cube)
{
- ColorBox largestRangeBox = null;
- double largestRange = 0;
+ var (component, range) = FindLargestRange (cube);
- foreach (var box in boxes)
+ // Sort by the largest color range component (either R, G, or B)
+ cube.Sort ((c1, c2) => component switch
{
- double range = box.GetBrightnessRange ();
- if (range > largestRange)
- {
- largestRange = range;
- largestRangeBox = box;
- }
- }
+ 0 => c1.R.CompareTo (c2.R),
+ 1 => c1.G.CompareTo (c2.G),
+ 2 => c1.B.CompareTo (c2.B),
+ _ => 0
+ });
- return largestRangeBox;
+ var medianIndex = cube.Count / 2;
+ var cube1 = cube.Take (medianIndex).ToList ();
+ var cube2 = cube.Skip (medianIndex).ToList ();
+
+ return (cube1, cube2);
}
- // Split a box at the median point based on brightness (luminance)
- private List SplitBoxByLuminance (ColorBox box)
+ private (int, int) FindLargestRange (List cube)
{
- var sortedColors = box.Colors.OrderBy (c => GetBrightness (c)).ToList ();
+ var minR = cube.Min (c => c.R);
+ var maxR = cube.Max (c => c.R);
+ var minG = cube.Min (c => c.G);
+ var maxG = cube.Max (c => c.G);
+ var minB = cube.Min (c => c.B);
+ var maxB = cube.Max (c => c.B);
- // Split the box at the median
- int medianIndex = sortedColors.Count / 2;
+ var rangeR = maxR - minR;
+ var rangeG = maxG - minG;
+ var rangeB = maxB - minB;
- var lowerHalf = sortedColors.Take (medianIndex).ToList ();
- var upperHalf = sortedColors.Skip (medianIndex).ToList ();
-
- return new List
- {
- new ColorBox(lowerHalf),
- new ColorBox(upperHalf)
- };
+ if (rangeR >= rangeG && rangeR >= rangeB) return (0, rangeR);
+ if (rangeG >= rangeR && rangeG >= rangeB) return (1, rangeG);
+ return (2, rangeB);
}
- // Calculate the brightness (luminance) of a color
- private static double GetBrightness (Color color)
+ private Color AverageColor (List cube)
{
- // Luminance formula (standard)
- return 0.299 * color.R + 0.587 * color.G + 0.114 * color.B;
+ var avgR = (byte)(cube.Average (c => c.R));
+ var avgG = (byte)(cube.Average (c => c.G));
+ var avgB = (byte)(cube.Average (c => c.B));
+
+ return new Color (avgR, avgG, avgB);
}
- // The ColorBox class to represent a subset of colors
- public class ColorBox
+ private int Volume (List cube)
{
- public List Colors { get; private set; }
-
- public ColorBox (List colors)
+ if (cube == null || cube.Count == 0)
{
- Colors = colors;
+ // Return a volume of 0 if the cube is empty or null
+ return 0;
}
- // Get the range of brightness (luminance) in this box
- public double GetBrightnessRange ()
- {
- double minBrightness = double.MaxValue, maxBrightness = double.MinValue;
+ var minR = cube.Min (c => c.R);
+ var maxR = cube.Max (c => c.R);
+ var minG = cube.Min (c => c.G);
+ var maxG = cube.Max (c => c.G);
+ var minB = cube.Min (c => c.B);
+ var maxB = cube.Max (c => c.B);
- foreach (var color in Colors)
- {
- double brightness = GetBrightness (color);
- if (brightness < minBrightness)
- {
- minBrightness = brightness;
- }
-
- if (brightness > maxBrightness)
- {
- maxBrightness = brightness;
- }
- }
-
- return maxBrightness - minBrightness;
- }
-
- // Calculate the average color in the box, weighted by brightness (darker colors have more weight)
- public Color GetWeightedAverageColor ()
- {
- double totalR = 0, totalG = 0, totalB = 0;
- double totalWeight = 0;
-
- foreach (var color in Colors)
- {
- double brightness = GetBrightness (color);
- double weight = 1.0 - brightness / 255.0; // Darker colors get more weight
-
- totalR += color.R * weight;
- totalG += color.G * weight;
- totalB += color.B * weight;
- totalWeight += weight;
- }
-
- // Normalize by the total weight
- totalR /= totalWeight;
- totalG /= totalWeight;
- totalB /= totalWeight;
-
- return new Color ((int)totalR, (int)totalG, (int)totalB);
- }
-
- // Calculate brightness (luminance) of a color
- private static double GetBrightness (Color color)
- {
- return 0.299 * color.R + 0.587 * color.G + 0.114 * color.B;
- }
+ return (maxR - minR) * (maxG - minG) * (maxB - minB);
}
-}
\ No newline at end of file
+}
diff --git a/Terminal.Gui/Drawing/SixelEncoder.cs b/Terminal.Gui/Drawing/SixelEncoder.cs
index dfe9f63a4..c8a50a54b 100644
--- a/Terminal.Gui/Drawing/SixelEncoder.cs
+++ b/Terminal.Gui/Drawing/SixelEncoder.cs
@@ -7,6 +7,29 @@ namespace Terminal.Gui;
///
public class SixelEncoder
{
+ /*
+
+ A sixel is a column of 6 pixels - with a width of 1 pixel
+
+ Column controlled by one sixel character:
+ [ ] - Bit 0 (top-most pixel)
+ [ ] - Bit 1
+ [ ] - Bit 2
+ [ ] - Bit 3
+ [ ] - Bit 4
+ [ ] - Bit 5 (bottom-most pixel)
+
+ Special Characters
+ The '-' acts like '\n'. It moves the drawing cursor
+ to beginning of next line
+
+ The '$' acts like the key. It moves drawing
+ cursor back to beginning of the current line
+ e.g. to draw more color layers.
+
+ */
+
+
///
/// Gets or sets the quantizer responsible for building a representative
/// limited color palette for images and for mapping novel colors in
@@ -39,29 +62,6 @@ public class SixelEncoder
return start + defaultRatios + completeStartSequence + noScaling + fillArea + pallette + pixelData + terminator;
}
-
- /*
-
- A sixel is a column of 6 pixels - with a width of 1 pixel
-
- Column controlled by one sixel character:
- [ ] - Bit 0 (top-most pixel)
- [ ] - Bit 1
- [ ] - Bit 2
- [ ] - Bit 3
- [ ] - Bit 4
- [ ] - Bit 5 (bottom-most pixel)
-
- Special Characters
- The '-' acts like '\n'. It moves the drawing cursor
- to beginning of next line
-
- The '$' acts like the key. It moves drawing
- cursor back to beginning of the current line
- e.g. to draw more color layers.
-
- */
-
/**
* This method is adapted from
* https://github.com/jerch/node-sixel/
diff --git a/UnitTests/Drawing/SixelEncoderTests.cs b/UnitTests/Drawing/SixelEncoderTests.cs
index 6f36e8990..3c8042710 100644
--- a/UnitTests/Drawing/SixelEncoderTests.cs
+++ b/UnitTests/Drawing/SixelEncoderTests.cs
@@ -37,6 +37,10 @@ public class SixelEncoderTests
var encoder = new SixelEncoder (); // Assuming SixelEncoder is the class that contains the EncodeSixel method
string result = encoder.EncodeSixel (pixels);
+ // Since image is only red we should only have 1 color definition
+ Color c1 = Assert.Single (encoder.Quantizer.Palette);
+
+ Assert.Equal (new Color(255,0,0),c1);
Assert.Equal (expected, result);
}