diff --git a/src/Artemis.Core/Services/ColorQuantizer/ColorCube.cs b/src/Artemis.Core/Services/ColorQuantizer/ColorCube.cs new file mode 100644 index 000000000..631b9a87b --- /dev/null +++ b/src/Artemis.Core/Services/ColorQuantizer/ColorCube.cs @@ -0,0 +1,67 @@ +using SkiaSharp; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace Artemis.Core.Services +{ + internal class ColorCube + { + private readonly List _colors; + + internal ColorCube(IEnumerable colors) + { + if (colors.Count() < 2) + { + _colors = colors.ToList(); + return; + } + + int redRange = colors.Max(c => c.Red) - colors.Min(c => c.Red); + int greenRange = colors.Max(c => c.Green) - colors.Min(c => c.Green); + int blueRange = colors.Max(c => c.Blue) - colors.Min(c => c.Blue); + + if (redRange > greenRange && redRange > blueRange) + _colors = colors.OrderBy(a => a.Red).ToList(); + else if (greenRange > blueRange) + _colors = colors.OrderBy(a => a.Green).ToList(); + else + _colors = colors.OrderBy(a => a.Blue).ToList(); + } + + internal bool TrySplit([NotNullWhen(returnValue: true)] out ColorCube? a, [NotNullWhen(returnValue: true)] out ColorCube? b) + { + if (_colors.Count < 2) + { + a = null; + b = null; + return false; + } + + int median = _colors.Count / 2; + + a = new ColorCube(_colors.GetRange(0, median)); + b = new ColorCube(_colors.GetRange(median, _colors.Count - median)); + + return true; + } + + internal SKColor GetAverageColor() + { + int r = 0, g = 0, b = 0; + + for (int i = 0; i < _colors.Count; i++) + { + r += _colors[i].Red; + g += _colors[i].Green; + b += _colors[i].Blue; + } + + return new SKColor( + (byte)(r / _colors.Count), + (byte)(g / _colors.Count), + (byte)(b / _colors.Count) + ); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Services/ColorQuantizer/ColorQuantizerService.cs b/src/Artemis.Core/Services/ColorQuantizer/ColorQuantizerService.cs new file mode 100644 index 000000000..c90260f57 --- /dev/null +++ b/src/Artemis.Core/Services/ColorQuantizer/ColorQuantizerService.cs @@ -0,0 +1,97 @@ +using Artemis.Core.Services.Interfaces; +using SkiaSharp; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Artemis.Core.Services +{ + /// + public class ColorQuantizerService : IColorQuantizerService + { + /// + public SKColor[] Quantize(IEnumerable colors, int amount) + { + if ((amount & (amount - 1)) != 0) + throw new ArgumentException("Must be power of two", nameof(amount)); + + Queue cubes = new Queue(amount); + cubes.Enqueue(new ColorCube(colors)); + + while (cubes.Count < amount) + { + ColorCube cube = cubes.Dequeue(); + if (cube.TrySplit(out var a, out var b)) + { + cubes.Enqueue(a); + cubes.Enqueue(b); + } + } + + return cubes.Select(c => c.GetAverageColor()).ToArray(); + } + + #region Constants + private const float targetDarkLuma = 0.26f; + private const float maxDarkLuma = 0.45f; + private const float minLightLuma = 0.55f; + private const float targetLightLuma = 0.74f; + private const float minNormalLuma = 0.3f; + private const float targetNormalLuma = 0.5f; + private const float maxNormalLuma = 0.7f; + private const float targetMutesSaturation = 0.3f; + private const float maxMutesSaturation = 0.3f; + private const float targetVibrantSaturation = 1.0f; + private const float minVibrantSaturation = 0.35f; + private const float weightSaturation = 3f; + private const float weightLuma = 5f; + #endregion + + /// + public SKColor FindColorVariation(IEnumerable colors, ColorType type, bool ignoreLimits = false) + { + (float targetLuma, float minLuma, float maxLuma, float targetSaturation, float minSaturation, float maxSaturation) = type switch + { + ColorType.Vibrant => (targetNormalLuma, minNormalLuma, maxNormalLuma, targetVibrantSaturation, minVibrantSaturation, 1f), + ColorType.LightVibrant => (targetLightLuma, minLightLuma, 1f, targetVibrantSaturation, minVibrantSaturation, 1f), + ColorType.DarkVibrant => (targetDarkLuma, 0f, maxDarkLuma, targetVibrantSaturation, minVibrantSaturation, 1f), + ColorType.Muted => (targetNormalLuma, minNormalLuma, maxNormalLuma, targetMutesSaturation, 0, maxMutesSaturation), + ColorType.LightMuted => (targetLightLuma, minLightLuma, 1f, targetMutesSaturation, 0, maxMutesSaturation), + ColorType.DarkMuted => (targetDarkLuma, 0, maxDarkLuma, targetMutesSaturation, 0, maxMutesSaturation), + _ => (0.5f, 0f, 1f, 0.5f, 0f, 1f) + }; + + var bestColorScore = float.MinValue; + var bestColor = SKColor.Empty; + foreach (var clr in colors) + { + clr.ToHsl(out float _, out float sat, out float luma); + sat /= 100f; + luma /= 100f; + + if (!ignoreLimits && (sat <= minSaturation || sat >= maxSaturation || luma <= minLuma || luma >= maxLuma)) + continue; + + var score = GetComparisonValue(sat, targetSaturation, luma, targetLuma); + if (score > bestColorScore) + { + bestColorScore = score; + bestColor = clr; + } + } + + return bestColor; + } + + private static float GetComparisonValue(float sat, float targetSaturation, float luma, float targetLuma) + { + static float InvertDiff(float value, float target) => 1 - Math.Abs(value - target); + const float totalweight = weightSaturation + weightLuma; + + float totalValue = (InvertDiff(sat, targetSaturation) * weightSaturation) + + (InvertDiff(luma, targetLuma) * weightLuma); + + return totalValue / totalweight; + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Services/ColorQuantizer/ColorType.cs b/src/Artemis.Core/Services/ColorQuantizer/ColorType.cs new file mode 100644 index 000000000..e6f598a50 --- /dev/null +++ b/src/Artemis.Core/Services/ColorQuantizer/ColorType.cs @@ -0,0 +1,12 @@ +namespace Artemis.Core.Services +{ + public enum ColorType + { + Vibrant, + LightVibrant, + DarkVibrant, + Muted, + LightMuted, + DarkMuted + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Services/ColorQuantizer/Interfaces/IColorQuantizerService.cs b/src/Artemis.Core/Services/ColorQuantizer/Interfaces/IColorQuantizerService.cs new file mode 100644 index 000000000..e5690867c --- /dev/null +++ b/src/Artemis.Core/Services/ColorQuantizer/Interfaces/IColorQuantizerService.cs @@ -0,0 +1,30 @@ +using SkiaSharp; +using System.Collections.Generic; + +namespace Artemis.Core.Services.Interfaces +{ + /// + /// A service providing a pallette of colors in a bitmap based on vibrant.js + /// + public interface IColorQuantizerService : IArtemisService + { + /// + /// Reduces an to a given amount of relevant colors. Based on the Median Cut algorithm + /// + /// The colors to quantize. + /// The number of colors that should be calculated. Must be a power of two. + /// The quantized colors. + public SKColor[] Quantize(IEnumerable colors, int amount); + + /// + /// Finds colors with certain characteristics in a given . + /// Vibrant variants are more saturated, while Muted colors are less. + /// Light and Dark colors have higher and lower lightness values, respectively. + /// + /// The colors to find the variations in + /// Which type of color to find + /// Ignore hard limits on whether a color is considered for each category. Result may be if this is true + /// The color found + public SKColor FindColorVariation(IEnumerable colors, ColorType type, bool ignoreLimits = false); + } +}