什么是计算RGB值的人眼对比度差异的高效方法?

3
为了检查灰度中的两种颜色是否过于接近,无法被人眼区分。如果选择了“危险”颜色,则希望能够向用户生成警告。因此,根据结果,我们可以决定对于视力差的人们,是否应将其中一种颜色更改为白色或黑色,以增强可读性。
例如,十六进制颜色#9d5fb0(紫色)和#318261(绿色)将变成几乎相同的灰色调。从HSB中看,B值与另一个只有1%的差异,因此健康的人眼无法真正看到区别。或者在这种情况下,8位K值不同2%。
我已经学习了亮度方法,这是一种更复杂的方式来判断人眼如何看待颜色的灰调。但是,如何通过程序实现这一点超出了我的当前理解范围。一旦我理解了数学原理,我可以用PHPJS编写它。
为了从CSS、屏幕pixel或图像object文件中选择值,我们应该始终将输入处理为RGB,对吗?
类似于:
$result = grayScaleDifference('#9d5fb0','#318261');

或者

$result = 8bitK_difference('#9d5fb0','#318261');

或者

$result = luminanceDifference('#9d5fb0','#318261');

那么在不改变或转换实际图像或颜色对象的情况下,最好的脚本样式公式是什么?


人类只能检测到256个离散的灰度级别,因此可以将每种颜色转换为灰度并映射到0-255范围内,然后查看它们之间的差异是否大于1(或者对于更大的差异可能是5,因为您需要一些容易看到的差异)。 - bob
@bob 人类可以看到各种不同的灰度。问题在于,什么时候一种灰度与另一种灰度之间存在“刚好可察觉差异”(JND)?在正常视觉中,光亮视觉下的对比敏感度(JND)约为1%,介于8 cd/m<sup>2</sup>和520 cd/m<sup>2</sup>之间。但是,人类的视觉范围远大于8-520,因为我们适应了不同的条件,并且在较暗或较亮的条件下具有不同的对比敏感度。续:... - Myndex
@Myndex 看起来灰度转换已经完成了,因为问题不是“我如何为颜色X选择正确的灰色”。因此,重要的部分是比较 OP 算法生成的灰色结果。 - James
我知道你相信这是因为你已经回答了那个问题。也许楼主可以澄清一下。 - James
感谢 @2x2p 接受我的答案! :) - Myndex
显示剩余5条评论
5个回答

9

跟进回答

我发这篇跟进回答不仅是为了澄清我的初始答案(我也刚编辑了一下),还为了添加各种概念的代码片段。在 R´G´B´ 转 Y 的每个步骤都很重要,而且必须按照描述的顺序进行,否则结果将失败。

定义:

sRGB:sRGB 是 Web 的三原色颜色模型,也是大多数计算机显示器使用的标准。它使用与 HDTV 标准 Rec709 相同的三原色和白点。sRGB 与 Rec709 的不同之处仅在于转移曲线,通常称为 gamma。

Gamma:这是用于存储和传输图像的各种方法中使用的曲线。它通常类似于人类视觉的感知曲线。在数字领域中,gamma 的作用是给予图像较暗区域更多的权重,以便它们由更多的位定义,从而避免出现“条带”等伪影。

亮度(记为LY):光的线性测量或表示方式(即没有gamma 曲线)。作为一种测量方式,它通常是 cd/m2。作为一种表示方法,它是 CIEXYZ 中的 Y,通常介于 0(黑色)到 100(白色)之间。亮度特征具有基于人类对不同波长的光的感知的光谱加权。但是,亮度在光明/暗淡方面是线性的,即如果 100 个光子的光测量为 10,则 20 将是 200 个光子的光。

L*(也称为 L 星):根据 CIELAB(L*a*b*)定义的感知亮度。在亮度在光数量方面是线性的情况下,L* 基于感知,因此在光量方面是非线性的,其曲线旨在匹配人眼的光视觉(近似 gamma 是 ^0.43)。

亮度 vs L:* 在亮度(写作 Y 或 L)和亮度(写作 L*)中,0 和 100 是相同的,但在中间它们非常不同。我们所认为的中灰色位于 L* 的正中央,即 50,但与此相关的亮度(Y)为 18.4。在 sRGB 中,这是 #777777 或 46.7%。

对比度:定义两个 L 或两个 Y 值之间的差异的术语。有多种对比度方法和标准。一种常见的方法是韦伯对比度,即 ΔL/L。对比度通常以比例(3:1)或百分比(70%)表示。

从 sRGB 派生亮度 (Y)

步骤零(非十六进制)

如果需要,将HEX颜色值转换为整数值三元组,其中#00 = 0#FF = 255
第一步(8位转10进制):
通过除以255来将8位sRGB值转换为十进制值:
decimal = R´8bit / 255     G´decimal = G´8bit / 255     B´decimal = B´8bit / 255 如果您的sRGB值是16位,则通过除以65535来转换为十进制。
第二步(线性化,简单版本):
将每个颜色通道的值提高到2.2次方,与sRGB显示器相同。这对于大多数应用程序都可以使用。但是,如果您需要进行多次进入和退出sRGB伽马编码空间,则请使用下面更准确的版本。
R´^2.2 = Rlin   G´^2.2 = Glin   B´^2.2 = Blin 第二步(线性化,精确版本):
如果您要进行图像操作并多次往返于伽马编码空间中,则请使用此版本,而不是上述简单的^2.2版本。
function sRGBtoLin(colorChannel) {
        // Send this function a decimal sRGB gamma encoded color value
        // between 0.0 and 1.0, and it returns a linearized value.

    if ( colorChannel <= 0.04045 ) {
            return colorChannel / 12.92;
        } else {
            return Math.pow((( colorChannel + 0.055)/1.055),2.4));
        }
    }

编辑以添加澄清:我上面引用的sRGB线性化使用了官方IEC标准的正确阈值,而旧的WCAG2数学方法使用了一个错误的阈值(已知的公开错误)。然而,阈值差异并不影响WCAG 2的结果,而是受其他因素的困扰。

第三步(光谱加权亮度)

人眼有三种类型的锥体对红、绿和蓝光敏感。但我们的光谱敏感度不均匀,我们对绿色(555nm)最敏感,而蓝色则垫底。亮度通过以下系数进行光谱加权以反映这一点:

   Rlin * 0.2126 + Glin * 0.7152 + Blin * 0.0722 = Y = L

将每个线性化的颜色通道乘以其系数并将它们相加即可找到L,即亮度。

第四步(对比度确定)

有许多不同的方法来确定对比度,以及各种标准。某些方程式在特定应用中效果更好。

WCAG 2.x
在WCAG 2.0和2.1中列出的当前网页对比度指南是带有偏移量的简单对比度:

   C = ((Llighter + 0.05) / (Ldarker + 0.05)) : 1

这给出了一个比率,WCAG规定非文本的比率为3:1,文本为4.5:1,以满足“AA”级别。

然而,由于各种原因,它是一个薄弱的例子。我在一份当前的GitHub问题(WCAG #695)中指出了缺陷,并一直在研究替代方案。

编辑以添加(2021年1月):

取代旧的WCAG 2对比度的是APCA:

Advanced Perceptual Contrast Algorithm”

它是新的WCAG 3的一部分,是一个重大的飞跃。虽然稳定,但我仍然认为它是测试版,并且因为它有点复杂,所以暂时最好链接到SAPC/APCA GitHub库

文献中一些先前开发的对比度方法:

修改的韦伯公式
Hwang/Peli Modified Weber 提供了一种更好的对比度评估方法,适用于计算机显示器 / sRGB。

   C = (L – L) / (L + 0.1)

请注意,我选择了闪烁因素为0.1而不是0.05,基于最近的一些试验。该值仍有待确定,可能有不同的取值更好。

LAB差异
另一个我比其他方法更喜欢的选择是将线性化亮度(L)转换为感知亮度L*,然后将其相减以找到差异。

将Y转换为L*:

function YtoLstar(Y) {
        // Send this function a luminance value between 0.0 and 1.0,
        // and it returns L* - perceptual lightness

    if ( Y <= (216/24389) {       // The CIE standard states 0.008856 but 216/24389 is the intent for 0.008856451679036
            return Y * (24389/27);  // The CIE standard states 903.3, but 24389/27 is the intent, making 903.296296296296296
        } else {
            return Math.pow(Y,(1/3)) * 116 - 16;
        }
    }

一旦将L转换为L*,有一个有用的对比图形简单地表示为:

    C = L较亮 – L较暗**

这里的结果可能需要缩放以与其他方法类似。缩放约为1.6或1.7似乎效果很好。

还有许多其他确定对比度的方法,但这些是最常见的。然而,一些应用程序使用其他对比方法会更好。另外一些方法是迈克尔逊对比、感知对比长度(PCL)和鲍曼/萨波林斯基。

此外,如果您正在寻找超越亮度或明度差异的颜色差异,则CIELAB在这方面有一些有用的方法。

侧记:

平均RGB不好!

OP 2x2p提到了一个经常引用的公式,用于使颜色的灰度值为:

    GRAY = round((R + G + B) / 3);

他指出了它看起来非常不准确,并且确实——完全错误。 R、G和B的光谱加权很大,不能忽略。绿色的亮度比蓝色高一个数量级。您不能只将所有三个通道相加再除以三,从而得到与特定颜色实际亮度接近的任何东西。

我认为这可能是由于一种称为HSI(色调、饱和度、强度)的颜色控制引起的混淆。但是,此控件不是(也从未打算)是感知均匀的! HSI和HSV都只是计算机中调整颜色值的"便利工具"。它们两者都不是感知均匀的,它们使用的数学严格用于支持软件中调整颜色值的"简单"方法。

OP的示例颜色

2x2p发布了他的代码,使用'#318261','#9d5fb0'作为测试颜色。以下是它们在我的电子表格上的外观,以及在转换过程的每个步骤中的每个值(使用"准确的"sRGB方法):

enter image description here

两者都接近#777777的中灰色。还注意到,虽然亮度L仅为18,但感知明度L*为50。


(2) Gamma解码应该在之前和之后进行夹紧,但这通常被忽略在定义中(可能包括WCAG)。 - Ryan
1
@Myndex 我非常清楚它的缺陷以及你对更好方法的贡献。没注意到是你在这里。感谢你编辑答案并标记。当声明0...255时,我认为许多人没有意识到要夹紧。事实上,这是文献中的一个常见错误。标记它有助于阻止错误信息的传播。我认为很多开发人员进入这个领域时并没有意识到它有多么技术化,有多少错误信息存在,并且会迷失在文字中。希望你有美好的一天。 - Ryan
@Ryan 那是一个有趣的观点,Ryan。由于我始于1975年编写带行号的Fortran IV程序并将其保存到纸带上,因此我可能会过分假设对我来说似乎很明显的事情,例如0-255字面意义上意味着它应该被夹在0-255之间。同时,我讨厌说话时显得“低人一等”或过于学究,这是一个永恒存在的问题,如果你看看我的帖子、文章和白皮书的长度就知道了... - Myndex
1
@Myndex 你比其他人更关心,所以你会看到这个意义!其他人可能因为匆忙/超负荷而错过了“我需要夹紧这个进出来的东西才能有效”的意思。甚至有些专业人士也会犯这个错误...或者在更早的步骤上。你可能知道一个现成的例子:Mozilla最近实施了Machado等人的方法。对于色盲,曾尝试使用XYZ Y亮度。但是,它从sRGB中得到了NTSC亮度...。我本来要发起一项PR,但我不知道同行评审接受的单色视觉模拟是什么。Chromium也采用了Machado,但使用了正确的sRGB转换...但我认为没有伽马值。 - Ryan
1
谢谢您!特别是关于过时的RGB线性化值的警告。我已经使用您推荐的替换值更新了我的一个程序,视觉效果立即更加有用/正确。我注意到最明显的改进是对深度饱和红色的处理。 - diopside
显示剩余8条评论

6

亮度对比和感知

您要寻找的是如何评估亮度对比

您肯定走在了正确的道路上——6% 的男性有色盲,他们依靠亮度对比而不是颜色对比。我在这里有一张图表来演示这个问题。

顺便提一下,术语应该是“亮度”,而不是亮度。亮度是指随着时间推移而发出的光线,通常用于天文学。当我们谈论色度学时,我们使用亮度这个术语,它是光的另一种度量,并由 CIEXYZ(CIE 1931)定义。

恰好我正在研究对比度评估方法,以提供一些新的、更准确的标准。您可以在GitHub 上关注一些进展情况,并在我的感知研究页面上了解更多信息。

事实并非像人们想象的那样简单,因为有许多因素影响人们对对比度的感知。目前在 GitHub 的讨论中有很多关于这个话题的讨论。

确定亮度

亮度是光的谱加权但否则线性的度量。谱加权基于人类三色视觉感知不同波长的光。这是 CIE 1931 实验和 CIEXYZ(亮度是 XYZ 中的 Y)等结果颜色空间中的测量部分。

虽然 XYZ 是光的线性模型,但人类感知非常非线性。因此,XYZ 不是感知均匀的。尽管如此,对于您的目的,您只需要知道与灰色补丁相比,颜色的等效亮度是多少。

假设您从 sRGB 视频开始(即网络和计算机标准颜色空间),首先需要删除伽马编码,然后应用谱加权。

我在 Stack 上发表了很多关于伽马的帖子,但如果您想要一个明确的解释,我建议您查看Poynton 的 Gamma FAQ。

将 sRGB 转换为线性 (gamma 1.0)。

1) R´G´B´ 值从 8 位整数 (0-255) 转换为小数 (0.0 - 1.0),通过分别将每个通道除以 255。 R´G´B´ 值必须为 0 到 1 才能进行下面的计算。此外,这里有一个链接,其中包含一个用于将单个数字(如 6 位十六进制)转换为 RGB 通道的代码片段。

2) 线性化每个通道。懒人的方法是应用一个 2.2 的幂函数曲线,这是电脑显示器显示图像数据的方式——对于判断颜色的亮度来说,这样做是可以的:

R´^2.2 = Rlin G´^2.2 = Glin B´^2.2 = Blin

3) 另一种(更精确)的方法:如果您正在进行图像处理并在 sRGB 和线性之间来回转换,则有一种更精确的方法,在维基百科上有介绍。但是,这里有一个来自我的电子表格的代码片段,我用它来实现类似的目的:

  =IF( A1 <= 0.04045 ; A1 / 12.92 ; POWER((( A1 + 0.055)/1.055) ; 2.4))

这表明,对于小于0.04045的值,只需除以12.92,但对于大于该值的值,则进行偏移并应用2.4次方 - 请注意,在“懒惰的方式”中,我们使用了2.2,但由于偏移/线性化,曲线几乎相同。
执行步骤2或步骤3中的任一操作,但不要同时执行两者。
4)最后,应用光谱加权系数,并将三个通道相加:
Rlin * 0.2126 + Glin * 0.7152 + Blin * 0.0722 = Y
这样就得到了Y,即给定颜色的亮度。亮度也称为L,但不要与感知亮度L*(Lstar)混淆,后者不是亮度。
确定感知对比度
现在,如果您想确定两个样本之间的差异,则有许多方法。韦伯对比度本质上是ΔL / L,自19世纪以来一直是标准。但对于计算机监视器显示的刺激,我建议采用一些更现代的方法。例如以下修改以获得更好的感知结果:
(Llighter – Ldarker) / (Llighter + 0.1)
还有“感知对比度长度”,鲍曼-萨波林斯基以及其他一些方法,包括我正在开发的一些方法。您还可以转换为基于人类感知的CIELAB(L*a*b*),然后只需从L*1中减去L*2
此外,还有许多其他因素会影响对比度感知,例如字体大小和重量、填充(请参见Bartleson-Breneman周围效应)以及其他因素。
如果您有任何疑问,请告诉我。

你还应该像你在Web内容无障碍性准则问题页面上(https://github.com/w3c/wcag/issues/695)所做的那样强调,“颜色对比度”只是可读性等式中的一部分,其他因素如字体大小和字重也起到了作用。 - Peter O.
@PeterO。实际上,感知对比度是一个相互关联的因素集合,其中包括(但不限于)字体大小、字体粗细、周围DIV填充、环境光、屏幕反射、负或正模式(即白色背景黑色字体)以及亮度对比度等。更不用说由于眼睛年龄和视觉障碍而产生的变化了。但是,我正在尝试在StackExchange上找到回答问题和写书之间的“正确平衡”,哈哈... 8-) 话虽如此,两种颜色之间的对比度在实际使用中并不足以确定。也许我会编辑这篇文章。 - Myndex
哇,Myndex 的回答非常详细。首先,我在我的问题中更正了亮度这个词。现在我将尝试基于此制作什么功能。我知道字体大小和重量以及环境条件也起着作用。但我目前专注于背景颜色与前景颜色与字体颜色之间的对比。例如,在灰度模式下,测试菜单颜色是否延伸到背景颜色上会失败。还有其他方法可以确保更好的文本对比度,例如给文本一个几乎看不见的微小阴影,这在视频字幕中经常使用。但那是另一个话题。暂时就这样,谢谢! - 2x2p
欢迎@2x2p——有趣的是你提到了BG/FG/Font三元组,因为这是我的研究重点之一。关于这个问题已经有一些研究,可以参考Bartleson-Breneman Surround Effects,其中一个标准中没有包含的内容是使用DIV文本容器填充文本周围的颜色与整体背景色存在明显差异。这是我希望在今年能够得到更好答案的几个问题之一。 - Myndex
@Myndex,我今天尝试将你的答案转换为JS,想知道我最后的假设是否正确? - 2x2p

0

为了更好的语法和使用方便,我将整个理论放入一个对象内的单一解析器中运行。

解析器将从颜色 318261 中一步计算这些值:

返回的对象将如下所示:

hex: "#318261"
rgb: {
  r: 49,
  g: 130,
  b: 97
}
int: 10313648
dec: {
  r: 0.19215686274509805,
  g: 0.5098039215686274,
  b: 0.3803921568627451
}
lin: {
  r: 0.030713443732993635,
  g: 0.2232279573168085,
  b: 0.11953842798834562
}
y: 0.17481298771137443
lstar: 48.86083783595441

JavaScript可以使用十六进制颜色字符串作为参数调用对象内部解析器。十六进制字符串可以如下所示:000#000000000#000000。 有两种处理结果的方法。

A:将返回的对象全部放入变量中:

var result = Color_Parser.parseHex('318261');
var lstar = result.lstar;

B:解析一次,然后访问上次解析结果的各个部分。例如,只选择L*对比度值就可以这样做:

Color_Parser.parseHex('#ABC');
var lstar = Color_Parser.result.lstar;

这是完整的代码:

const Color_Parser = {
  version: '1.0.0.beta',
  name: 'Color_Parser',
  result: null, // the parser output
  loging: true, // set to false to disable writing each step to console log
  parseHex: function(_input) {
    if (this.loging) {
      console.log(this.name + ', input: ' + _input);
    }
    this.result = {};
    // pre flight checks
    if (!_input) {
      this.result.error = true;
      console.log(this.name + ', error');
      return this.result;
    }
    // first convert shorthand Hex strings to full strings
    this.result.hex = String(_input);
    if (this.result.hex.length == 3) {
      this.result.hex = '#' + this.result.hex.substr(0, 1) + this.result.hex.substr(0, 1) + this.result.hex.substr(1, 1) + this.result.hex.substr(1, 1) + this.result.hex.substr(2, 1) + this.result.hex.substr(2, 1);
    }
    if (this.result.hex.length == 4) {
      this.result.hex = '#' + this.result.hex.substr(1, 1) + this.result.hex.substr(1, 1) + this.result.hex.substr(2, 1) + this.result.hex.substr(2, 1) + this.result.hex.substr(3, 1) + this.result.hex.substr(3, 1);
    }
    if (this.result.hex.length == 6) {
      this.result.hex = '#' + this.result.hex;
    }
    if (this.loging) {
      console.log(this.name + ', added to result: ' + this.result.hex);
    }
    // second get int values from the string segments as channels
    this.result.rgb = {
      r: null,
      g: null,
      b: null
    };
    this.result.rgb.r = parseInt(this.result.hex.substr(1, 2), 16);
    this.result.rgb.g = parseInt(this.result.hex.substr(3, 2), 16);
    this.result.rgb.b = parseInt(this.result.hex.substr(5, 2), 16);
    if (this.loging) {
      console.log(this.name + ', added to result: ' + this.result.rgb);
    }
    // third get the combined color int value
    this.result.int = ((this.result.rgb.r & 0x0ff) << 16) | ((this.result.rgb.g & 0x0ff) << 8) | (this.result.rgb.b & 0x0ff);
    if (this.loging) {
      console.log(this.name + ', added to result: ' + this.result.int);
    }
    // fourth turn 8 bit channels to decimal
    this.result.dec = {
      r: null,
      g: null,
      b: null
    };
    this.result.dec.r = this.result.rgb.r / 255.0; // red channel to decimal
    this.result.dec.g = this.result.rgb.g / 255.0; // green channel to decimal
    this.result.dec.b = this.result.rgb.b / 255.0; // blue channel to decimal
    if (this.loging) {
      console.log(this.name + ', added to result: ' + this.result.dec);
    }
    // fifth linearize each channel
    this.result.lin = {
      r: null,
      g: null,
      b: null
    };
    for (var i = 0, len = 3; i < len; i++) {
      if (this.result.dec[['r', 'g', 'b'][i]] <= 0.04045) {
        this.result.lin[['r', 'g', 'b'][i]] = this.result.dec[['r', 'g', 'b'][i]] / 12.92;
      } else {
        this.result.lin[['r', 'g', 'b'][i]] = Math.pow(((this.result.dec[['r', 'g', 'b'][i]] + 0.055) / 1.055), 2.4);
      }
    }
    if (this.loging) {
      console.log(this.name + ', added to result: ' + this.result.lin);
    }
    // get Y from linear result
    this.result.y = (0.2126 * (this.result.lin.r)); // red channel
    this.result.y += (0.7152 * (this.result.lin.g)); // green channel
    this.result.y += (0.0722 * (this.result.lin.b)); // blue channel
    if (this.loging) {
      console.log(this.name + ', added to result: ' + this.result.y);
    }
    // get L* contrast from Y 
    if (this.result.y <= (216 / 24389)) {
      this.result.lstar = this.result.y * (24389 / 27);
    } else {
      this.result.lstar = Math.pow(this.result.y, (1 / 3)) * 116 - 16;
    }
    if (this.loging) {
      console.log(this.name + ', added to result: ' + this.result.lstar);
    }
    // compute grayscale is to be continued hereafter
    // compute inverted rgb color
    this.result.invert = {
      r: null,
      g: null,
      b: null,
      hex: null
    };
    this.result.invert.r = (255 - this.result.rgb.r);
    this.result.invert.g = (255 - this.result.rgb.g);
    this.result.invert.b = (255 - this.result.rgb.b);
    // reverse compute hex from inverted rgb          
    this.result.invert.hex = this.result.invert.b.toString(16); // begin with blue channel
    if (this.result.invert.hex.length < 2) {
      this.result.invert.hex = '0' + this.result.invert.hex;
    }
    this.result.invert.hex = this.result.invert.g.toString(16) + this.result.invert.hex;
    if (this.result.invert.hex.length < 4) {
      this.result.invert.hex = '0' + this.result.invert.hex;
    }
    this.result.invert.hex = this.result.invert.r.toString(16) + this.result.invert.hex;
    if (this.result.invert.hex.length < 6) {
      this.result.invert.hex = '0' + this.result.invert.hex;
    }
    this.result.invert.hex = '#' + this.result.invert.hex;
    this.result.error = false;
    if (this.loging) {
      console.log(this.name + ', final output:');
    }
    if (this.loging) {
      console.log(this.result);
    }
    return this.result;
  }
}

0

也许这是有所帮助的东西。(从古老的 JavaScript 代码中提取)。

我相信这最初是为了数学上确定文本颜色在背景颜色上是否真正可读而开发的。

颜色对比度

由(WCAG Version 2)定义

http://www.w3.org/TR/2008/REC-WCAG20-20081211

对比度比率可以从1到21不等。

第1.4.3节

  • 高度可见:(增强)最小对比度比率为7:1
  • 普通文本:最小对比度比率为4.5:1
  • 大型文本:最小对比度比率为3:1

这个contrastRatio函数会输出一个介于1和21之间的数字,作为比率中的第一个数字。

例如:n:1,其中“n”是此方法的结果

数字越高,阅读性就越好。

function getLum(rgb) {

    var i, x;
    var a = []; // so we don't mutate
    for (i = 0; i < rgb.length; i++) {
        x = rgb[i] / 255;
        a[i] = x <= 0.03928 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4);
    }
    return 0.2126 * a[0] + 0.7152 * a[1] + 0.0722 * a[2];

}


var RE_HEX_RGB = /[a-f0-9]{6}|[a-f0-9]{3}/i;

function HEX_RGB(str) {
    var match = str.toString(16).match(RE_HEX_RGB);
    if (!match) {
        return [0, 0, 0];
    }

    var colorString = match[0];

    // Expand 3 character shorthand triplet e.g. #FFF -> #FFFFFF
    if (match[0].length === 3) {
        var Astr = colorString.split('');
        for (var i = 0; i < Astr.length; i++) {
            var ch = Astr[i];
            Astr[i] = ch + ch;
        }
        colorString = Astr.join('');
    }

    var integer = parseInt(colorString, 16);

    return [
        (integer >> 16) & 0xFF,
        (integer >> 8) & 0xFF,
        integer & 0xFF
    ];
};


function contrastRatio(rgb1, rgb2) {
    var l1 = getLum(rgb1);
    var l2 = getLum(rgb2);
    return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
}


var c1 = '#9d5fb0';
var c2 = '#318261';

var cr = contrastRatio( HEX_RGB(c1), HEX_RGB(c2) );
console.log("cr", cr);

1
WCAG的数学计算在多个层面上都是错误的。Github上有开放问题详细说明了这一点。首先,正确的sRGB阈值是0.04045而不是0.03928。但另一个问题是对比度方程无法解决人类感知问题。当然,如果你只关心“符合WCAG标准”,那么可以使用它们的数学公式,但是请注意,正在开发替代方程来解决这些问题。 - Myndex

0

这是我根据Myndex之前写的更新后的代码。

对于测试示例purple,我使用十六进制#9d5fb0(代表R:157, G:95, B:176),对于绿色,我使用十六进制#318261(代表R:49, G:130, B:97

JS:

    function HexToRGB(hex) {
      // to allow shorthand input like #FFF or FFFFFF without # sign make it #FFFFFF
      hex = String(hex);
      if(hex.length==3){hex='#'+hex.substr(0, 1)+hex.substr(0, 1)+hex.substr(1, 1)+hex.substr(1, 1)+hex.substr(2, 1)+hex.substr(2, 1);}
      if(hex.length==4){hex='#'+hex.substr(1, 1)+hex.substr(1, 1)+hex.substr(2, 1)+hex.substr(2, 1)+hex.substr(3, 1)+hex.substr(3, 1);}
      if(hex.length==6){hex='#'+hex;}
      let R = parseInt(hex.substr(1, 2),16);
      let G = parseInt(hex.substr(3, 2),16);
      let B = parseInt(hex.substr(5, 2),16);
      console.log("rgb from "+hex+" = "+[R,G,B]);   
      return [R,G,B];
    }

程序员常用的灰度计算公式是:

GRAY = round((R + G + B) / 3);

JS实现:

    function RGBToGRAY(rgb) {
      let avg = parseInt((rgb[0]+rgb[1]+rgb[2])/3);
      return [avg,avg,avg];
    }

这将把紫色转换为#8f8f8f,因为平均值= 143

这将把绿色转换为#5c5c5c,因为平均值= 92

92和143之间的差异太大,会错误地通过我的预期测试。 Adobe的模拟将相同的示例转换为灰度:

十六进制#777777代表

十六进制#747474代表

116和119之间的差异明显很小,应该未能通过我的预期差异测试。因此,RGBToGRAY方法被证明不准确。

现在,正如Myndex所解释的那样,我们应该使其线性并应用伽马2.2校正。

R´^2.2 = Rlin G´^2.2 = Glin B´^2.2 = Blin

JS:

    function linearFromRGB(rgb) {
      // make it decimal
      let R = rgb[0]/255.0; // red channel decimal
      let G = rgb[1]/255.0; // green channel decimal
      let B = rgb[2]/255.0; // blue channel decimal
      // apply gamma
      let gamma = 2.2;
      R = Math.pow(R, gamma); // linearize red
      G = Math.pow(G, gamma); // linearize green
      B = Math.pow(B, gamma); // linearize blue
      let linear = [R,G,B];
      console.log('linearized rgb = '+linear);  
      return linear;
    }

紫色的伽马校正线性结果现在为R:0.3440,G:0.1139,B:0.4423,绿色的结果为R:0.0265,G:0.2271,B:0.1192

现在通过应用系数来获取亮度L或(XYZ比例中的Y)如下:

Y = Rlin * 0.2126 + Glin * 0.7152 + Blin * 0.0722

JS

    function luminanceFromLin(rgblin) {
      let Y = (0.2126 * (rgblin[0])); // red channel
      Y = Y + (0.7152 * (rgblin[1])); // green channel
      Y = Y + (0.0722 * (rgblin[2])); // blue channel
      console.log('luminance from linear = '+Y);       
      return Y;
    }

现在计算两个Y(或L)值之间的感知对比度:

(Llighter - Ldarker) / (Llighter + 0.1)

JS

    function perceivedContrast(Y1,Y2){
      let C = ((Math.max(Y1,Y2)-Math.min(Y1,Y2))/(Math.max(Y1,Y2)+0.1));
      console.log('perceived contrast from '+Y1+','+Y2+' = '+C); 
      return C;      
    }

现在所有上述功能都合并为一个步骤的输入/输出

    function perceivedContrastFromHex(hex1,hex2){
      let lin1 = linearFromRGB(HexToRGB(hex1));
      let lin2 = linearFromRGB(HexToRGB(hex2));
      let y1 = luminanceFromLin(lin1);
      let y2 = luminanceFromLin(lin2);
      return perceivedContrast(y1,y2);
    }

最后进行测试

    var P = perceivedContrastFromHex('#318261','#9d5fb0');
    // compares the purple and green example
    alert(P);
    // shows 0.034369592139888626

    var P = perceivedContrastFromHex('#000','#fff'); 
    // compares pure black and white
    alert(P);
    // shows 0.9090909090909091

嗨@2X2p,我认为你已经明白了!问题是:你是否会检查非常接近的对比度,就像这个例子一样?如果是的话,那么您可能希望使用更准确的sRGB线性化,在我的后续答案中提供代码片段。原因是,使用深色进行接近对比度检查可能需要更多的精度,具体取决于您的需求。您可以在我的后续截图中看到电子表格屏幕上较暗值的偏差更大。我正在电子表格中使用“准确”的sRGB线性化。 - Myndex
我稍后将实现更准确的sRGB线性化。对于第一个版本来说,这已经非常有帮助了。仍然有一个问题,什么是告诉我们两种颜色足够不同的%差异? - 2x2p
1
啊哈!这可是百万美元的问题,不是吗?我希望我能给你一个“绝对”的答案,但是——没有。事实上,这是我目前研究的主要焦点(这也是我写这些冗长答案的部分原因,哈哈)。对于我给你的ModWeber方程式,我认为60%是正常视力个体的合理最低值。70%更加“标准”,并且偏向于那些有视力障碍的人。事实是,正常视力的阈值对比仅为1%至2%。但那不太可读。当使用“比率”时,3:1是相当典型的。链接在下一篇文章中。 - Myndex
1
以下是我一些感知研究的链接。在Myndex.com感知页面上,可以查看CE-09实验,比较不同数学的差异。您可能还想阅读我在GitHub上关于WCAG对比标准的长篇问题NASA有一套非常好的关于对比度和显示的页面。最后,您是否阅读过伽马FAQ和颜色FAQ? Charles Poynton博士解释得比我更好。 - Myndex

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接