IconColorPicker.cs 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. using SkiaSharp;
  2. using System;
  3. using System.Collections.Generic;
  4. namespace Ryujinx.Ava.UI.Windows
  5. {
  6. static class IconColorPicker
  7. {
  8. private const int ColorsPerLine = 64;
  9. private const int TotalColors = ColorsPerLine * ColorsPerLine;
  10. private const int UvQuantBits = 3;
  11. private const int UvQuantShift = BitsPerComponent - UvQuantBits;
  12. private const int SatQuantBits = 5;
  13. private const int SatQuantShift = BitsPerComponent - SatQuantBits;
  14. private const int BitsPerComponent = 8;
  15. private const int CutOffLuminosity = 64;
  16. private readonly struct PaletteColor(int qck, byte r, byte g, byte b)
  17. {
  18. public int Qck => qck;
  19. public byte R => r;
  20. public byte G => g;
  21. public byte B => b;
  22. }
  23. public static SKColor GetFilteredColor(SKBitmap image)
  24. {
  25. SKColor color = GetColor(image);
  26. // We don't want colors that are too dark.
  27. // If the color is too dark, make it brighter by reducing the range
  28. // and adding a constant color.
  29. int luminosity = GetColorApproximateLuminosity(color.Red, color.Green, color.Blue);
  30. if (luminosity < CutOffLuminosity)
  31. {
  32. color = new SKColor(
  33. (byte)Math.Min(CutOffLuminosity + color.Red, byte.MaxValue),
  34. (byte)Math.Min(CutOffLuminosity + color.Green, byte.MaxValue),
  35. (byte)Math.Min(CutOffLuminosity + color.Blue, byte.MaxValue));
  36. }
  37. return color;
  38. }
  39. public static SKColor GetColor(SKBitmap image)
  40. {
  41. PaletteColor[] colors = new PaletteColor[TotalColors];
  42. Dictionary<int, int> dominantColorBin = new Dictionary<int, int>();
  43. SKColor[] buffer = GetBuffer(image);
  44. int i = 0;
  45. int maxHitCount = 0;
  46. for (int y = 0; y < image.Height; y++)
  47. {
  48. int yOffset = y * image.Width;
  49. for (int x = 0; x < image.Width && i < TotalColors; x++)
  50. {
  51. int offset = x + yOffset;
  52. SKColor pixel = buffer[offset];
  53. byte cr = pixel.Red;
  54. byte cg = pixel.Green;
  55. byte cb = pixel.Blue;
  56. int qck = GetQuantizedColorKey(cr, cg, cb);
  57. if (dominantColorBin.TryGetValue(qck, out int hitCount))
  58. {
  59. dominantColorBin[qck] = hitCount + 1;
  60. if (maxHitCount < hitCount)
  61. {
  62. maxHitCount = hitCount;
  63. }
  64. }
  65. else
  66. {
  67. dominantColorBin.Add(qck, 1);
  68. }
  69. colors[i++] = new PaletteColor(qck, cr, cg, cb);
  70. }
  71. }
  72. int highScore = -1;
  73. PaletteColor bestCandidate = default;
  74. for (i = 0; i < TotalColors; i++)
  75. {
  76. int score = GetColorScore(dominantColorBin, maxHitCount, colors[i]);
  77. if (highScore < score)
  78. {
  79. highScore = score;
  80. bestCandidate = colors[i];
  81. }
  82. }
  83. return new SKColor(bestCandidate.R, bestCandidate.G, bestCandidate.B);
  84. }
  85. public static SKColor[] GetBuffer(SKBitmap image)
  86. {
  87. SKColor[] pixels = new SKColor[image.Width * image.Height];
  88. for (int y = 0; y < image.Height; y++)
  89. {
  90. for (int x = 0; x < image.Width; x++)
  91. {
  92. pixels[x + y * image.Width] = image.GetPixel(x, y);
  93. }
  94. }
  95. return pixels;
  96. }
  97. private static int GetColorScore(Dictionary<int, int> dominantColorBin, int maxHitCount, PaletteColor color)
  98. {
  99. int hitCount = dominantColorBin[color.Qck];
  100. int balancedHitCount = BalanceHitCount(hitCount, maxHitCount);
  101. int quantSat = (GetColorSaturation(color) >> SatQuantShift) << SatQuantShift;
  102. int value = GetColorValue(color);
  103. // If the color is rarely used on the image,
  104. // then chances are that there's a better candidate, even if the saturation value
  105. // is high. By multiplying the saturation value with a weight, we can lower
  106. // it if the color is almost never used (hit count is low).
  107. int satWeighted = quantSat;
  108. int satWeight = balancedHitCount << 5;
  109. if (satWeight < 0x100)
  110. {
  111. satWeighted = (satWeighted * satWeight) >> 8;
  112. }
  113. // Compute score from saturation and dominance of the color.
  114. // We prefer more vivid colors over dominant ones, so give more weight to the saturation.
  115. int score = ((satWeighted << 1) + balancedHitCount) * value;
  116. return score;
  117. }
  118. private static int GetColorSaturation(PaletteColor color)
  119. {
  120. int cMax = Math.Max(Math.Max(color.R, color.G), color.B);
  121. if (cMax == 0)
  122. {
  123. return 0;
  124. }
  125. int cMin = Math.Min(Math.Min(color.R, color.G), color.B);
  126. int delta = cMax - cMin;
  127. return (delta << 8) / cMax;
  128. }
  129. private static int GetColorValue(PaletteColor color) => Math.Max(Math.Max(color.R, color.G), color.B);
  130. private static int BalanceHitCount(int hitCount, int maxHitCount) => (hitCount << 8) / maxHitCount;
  131. private static int GetColorApproximateLuminosity(byte r, byte g, byte b) => (r + g + b) / 3;
  132. private static int GetQuantizedColorKey(byte r, byte g, byte b)
  133. {
  134. int u = ((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128;
  135. int v = ((112 * r - 94 * g - 18 * b + 128) >> 8) + 128;
  136. return (v >> UvQuantShift) | ((u >> UvQuantShift) << UvQuantBits);
  137. }
  138. }
  139. }