Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Sixel support #3734

Draft
wants to merge 31 commits into
base: v2_develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
4d1d740
Initial exploration of how to sixel encode
tznind Sep 9, 2024
f096062
Add ColorQuantizer
tznind Sep 9, 2024
943fa11
Work on SixelEncoder
tznind Sep 9, 2024
c6281dd
Build color palette using median cut instead of naive method
tznind Sep 11, 2024
b482306
Fix build errors
tznind Sep 11, 2024
3081765
Refactoring and comments
tznind Sep 11, 2024
e334bfd
Refactor and split into seperate files WIP
tznind Sep 11, 2024
891adec
Switch to a new WriteSixel algorithm
tznind Sep 13, 2024
484b75a
Output palette in Images scenario
tznind Sep 14, 2024
68d5e99
Fix ConvertToColorArray and namespaces
tznind Sep 14, 2024
d747867
Fix comment
tznind Sep 14, 2024
f103b04
Attribution for the WriteSixel method
tznind Sep 15, 2024
cbef6c5
Add comments
tznind Sep 15, 2024
eaa5c0e
Simplify and speed up palette building
tznind Sep 15, 2024
f8bb2f0
Fix infinite loop building palette
tznind Sep 15, 2024
f40b7b4
Fix test and make comments clearer
tznind Sep 22, 2024
93ce9a8
Add sixel test for grid 3x3 to make 12x12 checkerboard
tznind Sep 22, 2024
ef56998
Tidy up test file and comments in NetDriver
tznind Sep 23, 2024
a7c65bf
Move lab colors to UICatalog
tznind Sep 23, 2024
f07ab92
Switch to simpler and faster palette builder
tznind Sep 23, 2024
2378570
Fix early exit bug in palette builder and change to far more conserva…
tznind Sep 23, 2024
b9bb2ba
Merge branch 'v2_develop' into sixel-encoder-tinkering
tznind Sep 23, 2024
4571978
Fix fill area - y is not in sixels its in pixels
tznind Sep 25, 2024
3c6804a
Move license to top of page and credit both source repos
tznind Sep 25, 2024
6149c5f
Add 2 tabs to Image scenario - one for sixel one for basic
tznind Sep 26, 2024
050db7a
Merge branch 'v2_develop' into sixel-encoder-tinkering
tznind Sep 26, 2024
9322b9a
Output at specific position
tznind Sep 26, 2024
519d8c0
Investigate changing sixel to output as part of view render
tznind Sep 28, 2024
94cbc1c
Restore the static approach to rendering for now and fix dispose
tznind Sep 28, 2024
d16f1b6
Fix tabbing into sixel tab view
tznind Sep 28, 2024
18f185d
Fix scenario dispose
tznind Sep 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Terminal.Gui/Application/Application.Driver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,6 @@ public static partial class Application // Driver abstractions
/// </remarks>
[SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
public static string ForceDriver { get; set; } = string.Empty;

public static List<SixelToRender> Sixel = new List<SixelToRender> ();
}
9 changes: 9 additions & 0 deletions Terminal.Gui/ConsoleDrivers/NetDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1020,6 +1020,15 @@ public override void UpdateScreen ()
SetCursorPosition (lastCol, row);
Console.Write (output);
}

foreach (var s in Application.Sixel)
{
if (!string.IsNullOrWhiteSpace (s.SixelData))
{
SetCursorPosition (s.ScreenPosition.X, s.ScreenPosition.Y);
Console.Write (s.SixelData);
}
}
}

SetCursorPosition (0, 0);
Expand Down
70 changes: 70 additions & 0 deletions Terminal.Gui/Drawing/Quant/ColorQuantizer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@


namespace Terminal.Gui;

/// <summary>
/// Translates colors in an image into a Palette of up to <see cref="MaxColors"/> colors (typically 256).
/// </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="EuclideanColorDistance"/>
/// </summary>
public IColorDistance DistanceAlgorithm { get; set; } = new EuclideanColorDistance ();

/// <summary>
/// Gets or sets the algorithm used to build the <see cref="Palette"/>.
/// </summary>
public IPaletteBuilder PaletteBuildingAlgorithm { get; set; } = new PopularityPaletteWithThreshold (new EuclideanColorDistance (),5) ;

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 DistanceAlgorithm
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;
}
}
29 changes: 29 additions & 0 deletions Terminal.Gui/Drawing/Quant/EuclideanColorDistance.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
namespace Terminal.Gui;

/// <summary>
/// <para>
/// 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.
///</para>
/// <para>
/// Euclidean distance in RGB space is calculated as:
/// </para>
/// <code>
/// √((R2 - R1)² + (G2 - G1)² + (B2 - B1)²)
/// </code>
/// <remarks>Values vary from 0 to ~441.67 linearly</remarks>
///
/// <remarks>This distance metric is commonly used for comparing colors in RGB space, though
/// it doesn't account for perceptual differences in color.</remarks>
/// </summary>
public class EuclideanColorDistance : IColorDistance
{
/// <inheritdoc/>
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 changes: 18 additions & 0 deletions Terminal.Gui/Drawing/Quant/IColorDistance.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace Terminal.Gui;

/// <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);
}
17 changes: 17 additions & 0 deletions Terminal.Gui/Drawing/Quant/IPaletteBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Terminal.Gui;

/// <summary>
/// Builds a palette of a given size for a given set of input colors.
/// </summary>
public interface IPaletteBuilder
{
/// <summary>
/// Reduce the number of <paramref name="colors"/> to <paramref name="maxColors"/> (or less)
/// using an appropriate selection algorithm.
/// </summary>
/// <param name="colors">Color of every pixel in the image. Contains duplication in order
/// to support algorithms that weigh how common a color is.</param>
/// <param name="maxColors">The maximum number of colours that should be represented.</param>
/// <returns></returns>
List<Color> BuildPalette (List<Color> colors, int maxColors);
}
105 changes: 105 additions & 0 deletions Terminal.Gui/Drawing/Quant/PopularityPaletteWithThreshold.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
using Terminal.Gui;
using Color = Terminal.Gui.Color;

/// <summary>
/// Simple fast palette building algorithm which uses the frequency that a color is seen
/// to determine whether it will appear in the final palette. Includes a threshold where
/// by colors will be considered 'the same'. This reduces the chance of under represented
/// colors being missed completely.
/// </summary>
public class PopularityPaletteWithThreshold : IPaletteBuilder
{
private readonly IColorDistance _colorDistance;
private readonly double _mergeThreshold;

public PopularityPaletteWithThreshold (IColorDistance colorDistance, double mergeThreshold)
{
_colorDistance = colorDistance;
_mergeThreshold = mergeThreshold; // Set the threshold for merging similar colors
}

public List<Color> BuildPalette (List<Color> colors, int maxColors)
{
if (colors == null || colors.Count == 0 || maxColors <= 0)
{
return new ();
}

// Step 1: Build the histogram of colors (count occurrences)
Dictionary<Color, int> colorHistogram = new Dictionary<Color, int> ();

foreach (Color color in colors)
{
if (colorHistogram.ContainsKey (color))
{
colorHistogram [color]++;
}
else
{
colorHistogram [color] = 1;
}
}

// If we already have fewer or equal colors than the limit, no need to merge
if (colorHistogram.Count <= maxColors)
{
return colorHistogram.Keys.ToList ();
}

// Step 2: Merge similar colors using the color distance threshold
Dictionary<Color, int> mergedHistogram = MergeSimilarColors (colorHistogram, maxColors);

// Step 3: Sort the histogram by frequency (most frequent colors first)
List<Color> sortedColors = mergedHistogram.OrderByDescending (c => c.Value)
.Take (maxColors) // Keep only the top `maxColors` colors
.Select (c => c.Key)
.ToList ();

return sortedColors;
}

/// <summary>
/// Merge colors in the histogram if they are within the threshold distance
/// </summary>
/// <param name="colorHistogram"></param>
/// <returns></returns>
private Dictionary<Color, int> MergeSimilarColors (Dictionary<Color, int> colorHistogram, int maxColors)
{
Dictionary<Color, int> mergedHistogram = new Dictionary<Color, int> ();

foreach (KeyValuePair<Color, int> entry in colorHistogram)
{
Color currentColor = entry.Key;
var merged = false;

// Try to merge the current color with an existing entry in the merged histogram
foreach (Color mergedEntry in mergedHistogram.Keys.ToList ())
{
double distance = _colorDistance.CalculateDistance (currentColor, mergedEntry);

// If the colors are similar enough (within the threshold), merge them
if (distance <= _mergeThreshold)
{
mergedHistogram [mergedEntry] += entry.Value; // Add the color frequency to the existing one
merged = true;

break;
}
}

// If no similar color is found, add the current color as a new entry
if (!merged)
{
mergedHistogram [currentColor] = entry.Value;
}

// Early exit if we've reduced the colors to the maxColors limit
if (mergedHistogram.Count >= maxColors)
{
return mergedHistogram;
}
}

return mergedHistogram;
}
}
Loading
Loading