/ src / Ryujinx / UI / Windows / IconColorPicker.cs
IconColorPicker.cs
  1  using SkiaSharp;
  2  using System;
  3  using System.Collections.Generic;
  4  
  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  
 12          private const int UvQuantBits = 3;
 13          private const int UvQuantShift = BitsPerComponent - UvQuantBits;
 14  
 15          private const int SatQuantBits = 5;
 16          private const int SatQuantShift = BitsPerComponent - SatQuantBits;
 17  
 18          private const int BitsPerComponent = 8;
 19  
 20          private const int CutOffLuminosity = 64;
 21  
 22          private readonly struct PaletteColor
 23          {
 24              public int Qck { get; }
 25              public byte R { get; }
 26              public byte G { get; }
 27              public byte B { get; }
 28  
 29              public PaletteColor(int qck, byte r, byte g, byte b)
 30              {
 31                  Qck = qck;
 32                  R = r;
 33                  G = g;
 34                  B = b;
 35              }
 36          }
 37  
 38          public static SKColor GetFilteredColor(SKBitmap image)
 39          {
 40              var color = GetColor(image);
 41  
 42  
 43              // We don't want colors that are too dark.
 44              // If the color is too dark, make it brighter by reducing the range
 45              // and adding a constant color.
 46              int luminosity = GetColorApproximateLuminosity(color.Red, color.Green, color.Blue);
 47              if (luminosity < CutOffLuminosity)
 48              {
 49                  color = new SKColor(
 50                      (byte)Math.Min(CutOffLuminosity + color.Red, byte.MaxValue),
 51                      (byte)Math.Min(CutOffLuminosity + color.Green, byte.MaxValue),
 52                      (byte)Math.Min(CutOffLuminosity + color.Blue, byte.MaxValue));
 53              }
 54  
 55              return color;
 56          }
 57  
 58          public static SKColor GetColor(SKBitmap image)
 59          {
 60              var colors = new PaletteColor[TotalColors];
 61              var dominantColorBin = new Dictionary<int, int>();
 62  
 63              var buffer = GetBuffer(image);
 64  
 65              int w = image.Width;
 66              int w8 = w << 8;
 67              int h8 = image.Height << 8;
 68  
 69  #pragma warning disable IDE0059 // Unnecessary assignment
 70              int xStep = w8 / ColorsPerLine;
 71              int yStep = h8 / ColorsPerLine;
 72  #pragma warning restore IDE0059
 73  
 74              int i = 0;
 75              int maxHitCount = 0;
 76  
 77              for (int y = 0; y < image.Height; y++)
 78              {
 79                  int yOffset = y * image.Width;
 80  
 81                  for (int x = 0; x < image.Width && i < TotalColors; x++)
 82                  {
 83                      int offset = x + yOffset;
 84  
 85                      SKColor pixel = buffer[offset];
 86                      byte cr = pixel.Red;
 87                      byte cg = pixel.Green;
 88                      byte cb = pixel.Blue;
 89  
 90                      var qck = GetQuantizedColorKey(cr, cg, cb);
 91  
 92                      if (dominantColorBin.TryGetValue(qck, out int hitCount))
 93                      {
 94                          dominantColorBin[qck] = hitCount + 1;
 95  
 96                          if (maxHitCount < hitCount)
 97                          {
 98                              maxHitCount = hitCount;
 99                          }
100                      }
101                      else
102                      {
103                          dominantColorBin.Add(qck, 1);
104                      }
105  
106                      colors[i++] = new PaletteColor(qck, cr, cg, cb);
107                  }
108              }
109  
110              int highScore = -1;
111              PaletteColor bestCandidate = default;
112  
113              for (i = 0; i < TotalColors; i++)
114              {
115                  var score = GetColorScore(dominantColorBin, maxHitCount, colors[i]);
116  
117                  if (highScore < score)
118                  {
119                      highScore = score;
120                      bestCandidate = colors[i];
121                  }
122              }
123  
124              return new SKColor(bestCandidate.R, bestCandidate.G, bestCandidate.B);
125          }
126  
127          public static SKColor[] GetBuffer(SKBitmap image)
128          {
129              var pixels = new SKColor[image.Width * image.Height];
130  
131              for (int y = 0; y < image.Height; y++)
132              {
133                  for (int x = 0; x < image.Width; x++)
134                  {
135                      pixels[x + y * image.Width] = image.GetPixel(x, y);
136                  }
137              }
138  
139              return pixels;
140          }
141  
142          private static int GetColorScore(Dictionary<int, int> dominantColorBin, int maxHitCount, PaletteColor color)
143          {
144              var hitCount = dominantColorBin[color.Qck];
145              var balancedHitCount = BalanceHitCount(hitCount, maxHitCount);
146              var quantSat = (GetColorSaturation(color) >> SatQuantShift) << SatQuantShift;
147              var value = GetColorValue(color);
148  
149              // If the color is rarely used on the image,
150              // then chances are that theres a better candidate, even if the saturation value
151              // is high. By multiplying the saturation value with a weight, we can lower
152              // it if the color is almost never used (hit count is low).
153              var satWeighted = quantSat;
154              var satWeight = balancedHitCount << 5;
155              if (satWeight < 0x100)
156              {
157                  satWeighted = (satWeighted * satWeight) >> 8;
158              }
159  
160              // Compute score from saturation and dominance of the color.
161              // We prefer more vivid colors over dominant ones, so give more weight to the saturation.
162              var score = ((satWeighted << 1) + balancedHitCount) * value;
163  
164              return score;
165          }
166  
167          private static int BalanceHitCount(int hitCount, int maxHitCount)
168          {
169              return (hitCount << 8) / maxHitCount;
170          }
171  
172          private static int GetColorApproximateLuminosity(byte r, byte g, byte b)
173          {
174              return (r + g + b) / 3;
175          }
176  
177          private static int GetColorSaturation(PaletteColor color)
178          {
179              int cMax = Math.Max(Math.Max(color.R, color.G), color.B);
180  
181              if (cMax == 0)
182              {
183                  return 0;
184              }
185  
186              int cMin = Math.Min(Math.Min(color.R, color.G), color.B);
187              int delta = cMax - cMin;
188              return (delta << 8) / cMax;
189          }
190  
191          private static int GetColorValue(PaletteColor color)
192          {
193              return Math.Max(Math.Max(color.R, color.G), color.B);
194          }
195  
196          private static int GetQuantizedColorKey(byte r, byte g, byte b)
197          {
198              int u = ((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128;
199              int v = ((112 * r - 94 * g - 18 * b + 128) >> 8) + 128;
200              return (v >> UvQuantShift) | ((u >> UvQuantShift) << UvQuantBits);
201          }
202      }
203  }