PopularityPaletteWithThreshold.cs 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. namespace Terminal.Gui.Drawing;
  2. /// <summary>
  3. /// Simple fast palette building algorithm which uses the frequency that a color is seen
  4. /// to determine whether it will appear in the final palette. Includes a threshold whereby
  5. /// colors will be considered 'the same'. This reduces the chance of underrepresented
  6. /// colors being missed completely.
  7. /// </summary>
  8. public class PopularityPaletteWithThreshold : IPaletteBuilder
  9. {
  10. private readonly IColorDistance _colorDistance;
  11. private readonly double _mergeThreshold;
  12. /// <summary>
  13. /// Creates a new instance with the given color grouping parameters.
  14. /// </summary>
  15. /// <param name="colorDistance">Determines which different colors can be considered the same.</param>
  16. /// <param name="mergeThreshold">Threshold for merging two colors together.</param>
  17. public PopularityPaletteWithThreshold (IColorDistance colorDistance, double mergeThreshold)
  18. {
  19. _colorDistance = colorDistance;
  20. _mergeThreshold = mergeThreshold; // Set the threshold for merging similar colors
  21. }
  22. /// <inheritdoc/>
  23. public List<Color> BuildPalette (List<Color>? colors, int maxColors)
  24. {
  25. if (colors is null || colors.Count == 0 || maxColors <= 0)
  26. {
  27. return [];
  28. }
  29. // Step 1: Build the histogram of colors (count occurrences)
  30. Dictionary<Color, int> colorHistogram = new ();
  31. foreach (Color color in colors)
  32. {
  33. if (!colorHistogram.TryAdd (color, 1))
  34. {
  35. colorHistogram [color]++;
  36. }
  37. }
  38. // If we already have fewer or equal colors than the limit, no need to merge
  39. if (colorHistogram.Count <= maxColors)
  40. {
  41. return colorHistogram.Keys.ToList ();
  42. }
  43. // Step 2: Merge similar colors using the color distance threshold
  44. Dictionary<Color, int> mergedHistogram = MergeSimilarColors (colorHistogram, maxColors);
  45. // Step 3: Sort the histogram by frequency (most frequent colors first)
  46. List<Color> sortedColors = mergedHistogram.OrderByDescending (c => c.Value)
  47. .Take (maxColors) // Keep only the top `maxColors` colors
  48. .Select (c => c.Key)
  49. .ToList ();
  50. return sortedColors;
  51. }
  52. /// <summary>
  53. /// Merge colors in the histogram if they are within the threshold distance
  54. /// </summary>
  55. /// <param name="colorHistogram"></param>
  56. /// <param name="maxColors"></param>
  57. /// <returns></returns>
  58. private Dictionary<Color, int> MergeSimilarColors (Dictionary<Color, int> colorHistogram, int maxColors)
  59. {
  60. Dictionary<Color, int> mergedHistogram = new ();
  61. foreach (KeyValuePair<Color, int> entry in colorHistogram)
  62. {
  63. Color currentColor = entry.Key;
  64. var merged = false;
  65. // Try to merge the current color with an existing entry in the merged histogram
  66. foreach (Color mergedEntry in mergedHistogram.Keys.ToList ())
  67. {
  68. double distance = _colorDistance.CalculateDistance (currentColor, mergedEntry);
  69. // If the colors are similar enough (within the threshold), merge them
  70. if (distance <= _mergeThreshold)
  71. {
  72. mergedHistogram [mergedEntry] += entry.Value; // Add the color frequency to the existing one
  73. merged = true;
  74. break;
  75. }
  76. }
  77. // If no similar color is found, add the current color as a new entry
  78. if (!merged)
  79. {
  80. mergedHistogram [currentColor] = entry.Value;
  81. }
  82. // Early exit if we've reduced the colors to the maxColors limit
  83. if (mergedHistogram.Count >= maxColors)
  84. {
  85. return mergedHistogram;
  86. }
  87. }
  88. return mergedHistogram;
  89. }
  90. }