IconColorPicker.cs 6.3 KB

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