Browse Source

Switch ColorHelper.ToDisplayName() to an HSV color space algorithm that uses all components

pull/10439/head
robloo 4 years ago
parent
commit
a92d7abcc7
  1. 120
      src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs

120
src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs

@ -11,11 +11,11 @@ namespace Avalonia.Controls.Primitives
/// </summary>
public static class ColorHelper
{
private static readonly Dictionary<Color, string> _cachedDisplayNames = new Dictionary<Color, string>();
private static readonly Dictionary<KnownColor, double> _cachedKnownColorHues = new Dictionary<KnownColor, double>();
private static readonly Dictionary<HsvColor, string> _cachedDisplayNames = new Dictionary<HsvColor, string>();
private static readonly Dictionary<KnownColor, string> _cachedKnownColorNames = new Dictionary<KnownColor, string>();
private static readonly object _displayNameCacheMutex = new object();
private static readonly object _knownColorCacheMutex = new object();
private static readonly KnownColor[] _knownColors = (KnownColor[])Enum.GetValues(typeof(KnownColor));
/// <summary>
/// Gets the relative (perceptual) luminance/brightness of the given color.
@ -62,7 +62,36 @@ namespace Avalonia.Controls.Primitives
/// <returns>The approximate color display name.</returns>
public static string ToDisplayName(Color color)
{
// Without rounding, there are 16,777,216 possible RGB colors (without alpha).
var hsvColor = color.ToHsv();
// Handle extremes that are outside the below algorithm
if (color.A == 0x00)
{
return GetDisplayName(KnownColor.Transparent);
}
// HSV ----------------------------------------------------------------------
//
// There are far too many possible HSV colors to cache and search through
// for performance reasons. Therefore, the HSV color is rounded.
// Rounding is tolerable in this algorithm because it is perception based.
// Hue is the most important for user perception so is rounded the least.
// Then there is a lot of loss in rounding the saturation and value components
// which are not as closely related to perceived color.
//
// Hue : Round to nearest int (0..360)
// Saturation : Round to the nearest 1/10 (0..1)
// Value : Round to the nearest 1/10 (0..1)
// Alpha : Is ignored in this algorithm
//
// Rounding results in ~36_000 values to cache in the worse case.
//
// RGB ----------------------------------------------------------------------
//
// The original algorithm worked in RGB color space.
// If this code is every adjusted to work in RGB again note the following:
//
// Without rounding, there are 16_777_216 possible RGB colors (without alpha).
// This is too many to cache and search through for performance reasons.
// It is also needlessly large as there are only ~140 known/named colors.
// Therefore, rounding of the input color's component values is done to
@ -71,58 +100,29 @@ namespace Avalonia.Controls.Primitives
// The rounding value of 5 is specially chosen.
// It is a factor of 255 and therefore evenly divisible which improves
// the quality of the calculations.
double rounding = 5;
var roundedColor = new Color(
0xFF,
Convert.ToByte(Math.Round(color.R / rounding) * rounding),
Convert.ToByte(Math.Round(color.G / rounding) * rounding),
Convert.ToByte(Math.Round(color.B / rounding) * rounding));
var hsvColor = color.ToHsv();
// Handle extremes that are outside the below algorithm
if (color.A == 0x00)
{
return GetDisplayName(KnownColor.Transparent);
}
else if (hsvColor.S <= 0.0)
{
return GetDisplayName(KnownColor.White);
}
else if (hsvColor.V <= 0.0)
{
return GetDisplayName(KnownColor.Black);
}
var roundedHsvColor = new HsvColor(
1.0,
Math.Round(hsvColor.H, 0),
Math.Round(hsvColor.S, 1),
Math.Round(hsvColor.V, 1));
// Attempt to use a previously cached display name
lock (_displayNameCacheMutex)
{
if (_cachedDisplayNames.TryGetValue(roundedColor, out var displayName))
if (_cachedDisplayNames.TryGetValue(roundedHsvColor, out var displayName))
{
return displayName;
}
}
// Build KnownColor caches if they don't already exist
// Build the KnownColor name cache if it doesn't already exist
lock (_knownColorCacheMutex)
{
if (_cachedKnownColorHues.Count == 0 ||
_cachedKnownColorNames.Count == 0)
if (_cachedKnownColorNames.Count == 0)
{
_cachedKnownColorHues.Clear();
_cachedKnownColorNames.Clear();
var knownColors = (KnownColor[])Enum.GetValues(typeof(KnownColor));
for (int i = 1; i < knownColors.Length; i++) // Skip 'None' so start at 1
for (int i = 1; i < _knownColors.Length; i++) // Skip 'None' so start at 1
{
KnownColor knownColor = knownColors[i];
// Transparent is skipped since alpha is ignored making it equivalent to White
if (knownColor == KnownColor.Transparent)
{
continue;
}
double hue = KnownColors.ToColor(knownColor).ToHsv().H;
KnownColor knownColor = _knownColors[i];
// Some known colors have the same numerical value. For example:
// - Aqua = 0xff00ffff
@ -131,11 +131,6 @@ namespace Avalonia.Controls.Primitives
// This is not possible to represent in a dictionary which requires
// unique values. Therefore, only the first value is used.
if (!_cachedKnownColorHues.ContainsKey(knownColor))
{
_cachedKnownColorHues.Add(knownColor, hue);
}
if (!_cachedKnownColorNames.ContainsKey(knownColor))
{
_cachedKnownColorNames.Add(knownColor, GetDisplayName(knownColor));
@ -144,24 +139,29 @@ namespace Avalonia.Controls.Primitives
}
}
// Find the closest known color by finding nearest Hue
// Since Hue is the best measure of human perception of the color itself
// it is not necessary to check other components (Saturation, Value).
// Find the closest known color by measuring 3D Euclidean distance (ignore alpha)
// This is done in HSV color space to most closely match user-perception
var closestKnownColor = KnownColor.None;
var closestKnownColorHueDiff = double.PositiveInfinity;
var closestKnownColorDistance = double.PositiveInfinity;
lock (_knownColorCacheMutex)
for (int i = 1; i < _knownColors.Length; i++) // Skip 'None' so start at 1
{
foreach (var hueEntry in _cachedKnownColorHues)
KnownColor knownColor = _knownColors[i];
// Transparent is skipped since alpha is ignored making it equivalent to White
if (knownColor != KnownColor.Transparent)
{
// Closest hue before or after is allowed
// Therefore, use an absolute value
double difference = Math.Abs(hsvColor.H - hueEntry.Value);
HsvColor knownHsvColor = KnownColors.ToColor(knownColor).ToHsv();
double distance = Math.Sqrt(
Math.Pow((roundedHsvColor.H - knownHsvColor.H), 2.0) +
Math.Pow((roundedHsvColor.S - knownHsvColor.S), 2.0) +
Math.Pow((roundedHsvColor.V - knownHsvColor.V), 2.0));
if (difference < closestKnownColorHueDiff)
if (distance < closestKnownColorDistance)
{
closestKnownColor = hueEntry.Key;
closestKnownColorHueDiff = difference;
closestKnownColor = knownColor;
closestKnownColorDistance = distance;
}
}
}
@ -182,7 +182,7 @@ namespace Avalonia.Controls.Primitives
lock (_displayNameCacheMutex)
{
_cachedDisplayNames.Add(roundedColor, displayName);
_cachedDisplayNames.Add(roundedHsvColor, displayName);
}
return displayName;

Loading…
Cancel
Save