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 }