From 6a00e2f5158e3b28b968b40df02e728d8dbafe1b Mon Sep 17 00:00:00 2001 From: Diogo Trindade Date: Sun, 18 Oct 2020 19:43:41 +0100 Subject: [PATCH 1/2] Added Color quantizer service --- .../Services/ColorQuantizerService.cs | 162 ++++++++++++++++++ .../Interfaces/IColorQuantizerService.cs | 17 ++ 2 files changed, 179 insertions(+) create mode 100644 src/Artemis.Core/Services/ColorQuantizerService.cs create mode 100644 src/Artemis.Core/Services/Interfaces/IColorQuantizerService.cs diff --git a/src/Artemis.Core/Services/ColorQuantizerService.cs b/src/Artemis.Core/Services/ColorQuantizerService.cs new file mode 100644 index 000000000..2281c5850 --- /dev/null +++ b/src/Artemis.Core/Services/ColorQuantizerService.cs @@ -0,0 +1,162 @@ +using Artemis.Core.Services.Interfaces; +using SkiaSharp; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text; + +namespace Artemis.Core.Services +{ + public enum ColorType + { + Vibrant, + LightVibrant, + DarkVibrant, + Muted, + LightMuted, + DarkMuted + } + + public class ColorQuantizerService : IColorQuantizerService + { + #region Quantizer + 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 Cube(colors)); + + while (cubes.Count < amount) + { + Cube 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(); + } + + private class Cube + { + private readonly List _colors; + + internal Cube(IEnumerable colors) + { + 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 Cube? a, [NotNullWhen(returnValue: true)] out Cube? b) + { + if (_colors.Count < 2) + { + a = null; + b = null; + return false; + } + + int median = _colors.Count / 2; + + a = new Cube(_colors.GetRange(0, median)); + b = new Cube(_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) + ); + } + } + #endregion + + #region Vibrant Classifier + 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; + + 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; + } + #endregion + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Services/Interfaces/IColorQuantizerService.cs b/src/Artemis.Core/Services/Interfaces/IColorQuantizerService.cs new file mode 100644 index 000000000..d199e9163 --- /dev/null +++ b/src/Artemis.Core/Services/Interfaces/IColorQuantizerService.cs @@ -0,0 +1,17 @@ +using SkiaSharp; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Artemis.Core.Services.Interfaces +{ + /// + /// A service providing a pallette of colors in a bitmap based on vibrant.js + /// + public interface IColorQuantizerService : IArtemisService + { + public SKColor[] Quantize(IEnumerable colors, int amount); + + public SKColor FindColorVariation(IEnumerable colors, ColorType type, bool ignoreLimits = false); + } +} From 32639d3090bec825297ca9cb9e1b06e5a5176077 Mon Sep 17 00:00:00 2001 From: Diogo Trindade Date: Mon, 19 Oct 2020 18:49:30 +0100 Subject: [PATCH 2/2] added comments, moved helper class to separate file --- .../Services/ColorQuantizer/ColorCube.cs | 67 +++++++++++++++ .../ColorQuantizerService.cs | 81 ++----------------- .../Services/ColorQuantizer/ColorType.cs | 12 +++ .../Interfaces/IColorQuantizerService.cs | 30 +++++++ .../Interfaces/IColorQuantizerService.cs | 17 ---- 5 files changed, 117 insertions(+), 90 deletions(-) create mode 100644 src/Artemis.Core/Services/ColorQuantizer/ColorCube.cs rename src/Artemis.Core/Services/{ => ColorQuantizer}/ColorQuantizerService.cs (62%) create mode 100644 src/Artemis.Core/Services/ColorQuantizer/ColorType.cs create mode 100644 src/Artemis.Core/Services/ColorQuantizer/Interfaces/IColorQuantizerService.cs delete mode 100644 src/Artemis.Core/Services/Interfaces/IColorQuantizerService.cs 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/ColorQuantizerService.cs b/src/Artemis.Core/Services/ColorQuantizer/ColorQuantizerService.cs similarity index 62% rename from src/Artemis.Core/Services/ColorQuantizerService.cs rename to src/Artemis.Core/Services/ColorQuantizer/ColorQuantizerService.cs index 2281c5850..c90260f57 100644 --- a/src/Artemis.Core/Services/ColorQuantizerService.cs +++ b/src/Artemis.Core/Services/ColorQuantizer/ColorQuantizerService.cs @@ -2,36 +2,25 @@ using SkiaSharp; using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Text; namespace Artemis.Core.Services { - public enum ColorType - { - Vibrant, - LightVibrant, - DarkVibrant, - Muted, - LightMuted, - DarkMuted - } - + /// public class ColorQuantizerService : IColorQuantizerService { - #region Quantizer + /// 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 Cube(colors)); + Queue cubes = new Queue(amount); + cubes.Enqueue(new ColorCube(colors)); while (cubes.Count < amount) { - Cube cube = cubes.Dequeue(); + ColorCube cube = cubes.Dequeue(); if (cube.TrySplit(out var a, out var b)) { cubes.Enqueue(a); @@ -42,62 +31,7 @@ namespace Artemis.Core.Services return cubes.Select(c => c.GetAverageColor()).ToArray(); } - private class Cube - { - private readonly List _colors; - - internal Cube(IEnumerable colors) - { - 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 Cube? a, [NotNullWhen(returnValue: true)] out Cube? b) - { - if (_colors.Count < 2) - { - a = null; - b = null; - return false; - } - - int median = _colors.Count / 2; - - a = new Cube(_colors.GetRange(0, median)); - b = new Cube(_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) - ); - } - } - #endregion - - #region Vibrant Classifier + #region Constants private const float targetDarkLuma = 0.26f; private const float maxDarkLuma = 0.45f; private const float minLightLuma = 0.55f; @@ -111,7 +45,9 @@ namespace Artemis.Core.Services 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 @@ -157,6 +93,5 @@ namespace Artemis.Core.Services return totalValue / totalweight; } - #endregion } } \ 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); + } +} diff --git a/src/Artemis.Core/Services/Interfaces/IColorQuantizerService.cs b/src/Artemis.Core/Services/Interfaces/IColorQuantizerService.cs deleted file mode 100644 index d199e9163..000000000 --- a/src/Artemis.Core/Services/Interfaces/IColorQuantizerService.cs +++ /dev/null @@ -1,17 +0,0 @@ -using SkiaSharp; -using System; -using System.Collections.Generic; -using System.Text; - -namespace Artemis.Core.Services.Interfaces -{ - /// - /// A service providing a pallette of colors in a bitmap based on vibrant.js - /// - public interface IColorQuantizerService : IArtemisService - { - public SKColor[] Quantize(IEnumerable colors, int amount); - - public SKColor FindColorVariation(IEnumerable colors, ColorType type, bool ignoreLimits = false); - } -}