给定一个系统(例如一个网站),允许用户自定义某些部分的背景色,但不允许自定义字体颜色(以保持选项的数量最小化),是否有一种方法可以通过编程来确定“浅色”或“深色”字体颜色是必要的?

我相信有一些算法,但我对颜色、光度等了解不够,无法自己找出答案。


当前回答

谢谢你写这篇文章。

对于可能感兴趣的人,这里有一个Delphi中的函数示例:

function GetContrastColor(ABGColor: TColor): TColor;
var
  ADouble: Double;
  R, G, B: Byte;
begin
  if ABGColor <= 0 then
  begin
    Result := clWhite;
    Exit; // *** EXIT RIGHT HERE ***
  end;

  if ABGColor = clWhite then
  begin
    Result := clBlack;
    Exit; // *** EXIT RIGHT HERE ***
  end;

  // Get RGB from Color
  R := GetRValue(ABGColor);
  G := GetGValue(ABGColor);
  B := GetBValue(ABGColor);

  // Counting the perceptive luminance - human eye favors green color...
  ADouble := 1 - (0.299 * R + 0.587 * G + 0.114 * B) / 255;

  if (ADouble < 0.5) then
    Result := clBlack  // bright colors - black font
  else
    Result := clWhite;  // dark colors - white font
end;

其他回答

我也遇到过类似的问题。我必须找到一种选择对比字体颜色的好方法,以便在色度/热图上显示文本标签。它必须是通用的方法,生成的颜色必须“好看”,这意味着简单地生成互补色并不是一个好的解决方案——有时它会生成奇怪的、非常强烈的颜色,很难观察和阅读。

经过长时间的测试和尝试解决这个问题,我发现最好的解决方案是选择白色字体的“深色”,和黑色字体的“明亮”的颜色。

下面是我在c#中使用的一个函数示例:

Color ContrastColor(Color color)
{
    int d = 0;
    
    // Counting the perceptive luminance - human eye favors green color...      
    double luminance = (0.299 * color.R + 0.587 * color.G + 0.114 * color.B)/255;
    
    if (luminance > 0.5)
       d = 0; // bright colors - black font
    else
       d = 255; // dark colors - white font
                
    return  Color.FromArgb(d, d, d);
}

这种方法在许多不同的颜色尺度(彩虹,灰度,热,冰,和许多其他)下进行了测试,这是我发现的唯一“通用”方法。

编辑 改变计算a的公式为“感知亮度”-它真的看起来更好!已经在我的软件中实现了,看起来很棒。

编辑2 @WebSeed提供了这个算法的一个很好的工作示例:http://codepen.io/WebSeed/full/pvgqEq/

简短的回答:

计算给定颜色的亮度(Y),并根据预先确定的中间对比度图将文本翻转为黑色或白色。对于典型的sRGB显示,当Y < 0.4(即40%)时切换为白色

再回答

毫不奇怪,这里几乎每个答案都有一些误解,和/或引用了不正确的系数。唯一接近的答案是Seirios,尽管它依赖于WCAG 2对比,而WCAG 2对比本身是不正确的。

如果我说“不足为奇”,部分原因是互联网上关于这个特定主题的大量错误信息。事实上,这一领域仍然是一个活跃的研究和悬而未决的科学的主题,增加了乐趣。我得出这个结论是由于过去几年对一种新的可读性对比预测方法的研究。

视知觉领域是密集而抽象的,也是不断发展的,因此误解的存在是很常见的。例如,HSV和HSL甚至在感知上都不接近准确。为此,您需要一个感知上统一的模型,如CIELAB或CIELUV或CIECAM02等。

一些误解甚至已经进入了标准,例如WCAG 2(1.4.3)的对比部分,在其大部分范围内都被证明是不正确的。

第一个解决办法:

这里许多答案中显示的系数为(。299, .587, .114)是错误的,因为它们属于一个被称为NTSC YIQ的早已过时的系统,这是几十年前北美的模拟广播系统。虽然在一些YCC编码规范中仍可用于向后兼容,但不应在sRGB上下文中使用它们。

sRGB和Rec.709 (HDTV)的系数为:

红色:0.2126 绿色:0.7152 蓝色:0.0722

其他颜色空间如Rec2020或AdobeRGB使用不同的系数,对于给定的颜色空间使用正确的系数是很重要的。

这些系数不能直接应用于8位sRGB编码的图像或颜色数据。编码的数据必须首先线性化,然后应用系数来找到给定像素或颜色的亮度(光值)。

对于sRGB,有一个分段变换,但由于我们只对感知的亮度对比感兴趣,以找到将文本从黑色“翻转”到白色的点,我们可以通过简单的gamma方法走捷径。

安迪的亮度和亮度捷径

将每个sRGB颜色除以255.0,然后提高到2.2的幂,然后乘以系数并将它们相加,得到估计的亮度。

 let Ys = Math.pow(sR/255.0,2.2) * 0.2126 +
          Math.pow(sG/255.0,2.2) * 0.7152 +
          Math.pow(sB/255.0,2.2) * 0.0722; // Andy's Easy Luminance for sRGB. For Rec709 HDTV change the 2.2 to 2.4

这里,Y是sRGB显示器的相对亮度,在0.0到1.0范围内。但这与感知无关,我们需要进一步的转换来适应我们人类对相对亮度的视觉感知,以及感知到的对比。

40%的翻转

但在此之前,如果你只是寻找一个基本点来将文本从黑色翻转到白色,反之亦然,那么作弊是使用我们刚刚推导出的Y,并使翻转点约为Y = 0.40;。对于大于0.4 Y的颜色,将文本设置为黑色#000,对于大于0.4 Y的颜色,将文本设置为白色#fff。

  let textColor = (Ys < 0.4) ? "#fff" : "#000"; // Low budget down and dirty text flipper.

为什么是40%而不是50%?人类对明暗和对比的感知不是线性的。对于自动照明显示器来说,在大多数典型条件下,0.4 Y恰好是中等对比度。

是的,它是不同的,是的,这是一个过度简化。但如果你要将文本翻转成黑色或白色,简单的答案是有用的。

知觉奖励轮

预测对特定颜色和亮度的感知仍然是一个活跃的研究课题,而不是完全确定的科学。CIELAB或LUV的L* (Lstar)已被用于预测感知亮度,甚至预测感知对比度。然而,L*在非常明确/受控的环境中很好地用于表面颜色,而不适用于自发光显示器。

虽然这不仅取决于显示类型和校准,还取决于你的环境和整个页面内容,如果你从上面取Y,并将它提高到^0.685到^0.75左右,你会发现0.5通常是将文本从白色翻转到黑色的中间点。

  let textColor = (Math.pow(Ys,0.75) < 0.5) ? "#fff" : "#000"; // perceptually based text flipper.

使用指数0.685将使文本颜色交换在较深的颜色上,而使用0.8将使文本颜色交换在较浅的颜色上。

空间频率双倍奖励回合

值得注意的是,对比度不仅仅是两种颜色之间的距离。空间频率,换句话说,字体的重量和大小,也是不可忽视的关键因素。

也就是说,你可能会发现当颜色处于中间位置时,你会想要增加字体的大小和重量。

  let textSize = "16px";
  let textWeight = "normal"; 
  let Ls = Math.pow(Ys,0.7);

  if (Ls > 0.33 && Ls < 0.66) {
      textSize = "18px";
      textWeight = "bold";
      }  // scale up fonts for the lower contrast mid luminances.

Hue R U

这超出了本文的范围,但上面我们忽略了色相和色度。色相和色度确实有影响,如Helmholtz Kohlrausch,上面简单的亮度计算并不总是预测饱和色相的强度。

为了预测感知的这些更微妙的方面,需要一个完整的外观模型。亨特,费尔希尔德,伯恩斯是值得一看的几位作家,如果你想坠入人类视觉感知的兔子洞……

出于这个狭窄的目的,我们可以稍微重新加权系数,知道绿色占亮度的大部分,纯蓝色和纯红色应该总是两种颜色中最暗的。使用标准系数往往会发生的情况是,含有大量蓝色或红色的中间颜色可能会在低于理想亮度的情况下变成黑色,而含有大量绿色成分的颜色可能会相反。

也就是说,我发现通过增加中间颜色的字体大小和粗细来解决这个问题是最好的。

把它们放在一起

因此,我们假设您将向这个函数发送一个十六进制字符串,它将返回一个样式字符串,可以发送到特定的HTML元素。

看看CODEPEN,灵感来自Seirios的作品:

CodePen:花式字体翻转

Codepen代码所做的其中一件事是为低对比度的中范围增加文本大小。下面是一个例子:

如果您想尝试其中一些概念,请访问SAPC开发网站https://www.myndex.com/SAPC/,点击“研究模式”提供交互式实验来演示这些概念。

启蒙的术语

亮度:Y(相对)或L(绝对cd/m2),一种光谱加权的光的线性度量。不要和“光度”混淆。 光度:随时间变化的光,在天文学上很有用。 亮度:由CIE定义的L* (Lstar)感知亮度。一些型号有相关的明度J*。

基于Gacek的答案,但直接返回颜色常数(其他修改见下文):

public Color ContrastColor(Color iColor)
{
  // Calculate the perceptive luminance (aka luma) - human eye favors green color... 
  double luma = ((0.299 * iColor.R) + (0.587 * iColor.G) + (0.114 * iColor.B)) / 255;

  // Return black for bright colors, white for dark colors
  return luma > 0.5 ? Color.Black : Color.White;
}

注意:我去掉了亮度值的反转,使明亮的颜色有一个更高的值,这对我来说似乎更自然,也是“默认”的计算方法。 (编辑:这在原来的答案中也被采用了)

我使用了与Gacek相同的常数,因为它们非常适合我。


你也可以使用下面的签名来实现这个扩展方法:

public static Color ContrastColor(this Color iColor)

然后,您可以轻松地将其称为via foregroundColor = backgroundColor.ContrastColor()。

作为Kotlin / Android扩展:

fun Int.getContrastColor(): Int {
    // Counting the perceptive luminance - human eye favors green color...
    val a = 1 - (0.299 * Color.red(this) + 0.587 * Color.green(this) + 0.114 * Color.blue(this)) / 255
    return if (a < 0.5) Color.BLACK else Color.WHITE
}

您可以在任何色相背景上有任何色相文本,并确保它是易读的。我一直都这么做。在Javascript中有一个关于可读的彩色文本的公式- STW* 正如它在那个链接上所说的那样,这个公式是逆伽马调整计算的变化,尽管IMHO更易于管理。 该链接右侧的菜单及其相关页面使用随机生成的颜色作为文本和背景,始终清晰可辨。所以,是的,显然这是可以做到的,没有问题。