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

时间:2019-05-18 12:07:01

标签: javascript php image colors rgb

为了检查灰度中的两种颜色是否太近而无法被人眼识别。如果希望选择“危险”颜色,我希望能够向用户发出警告。因此,根据结果,我们可以决定是否为视力不好的人将两种颜色之一更改为白色或黑色,以增强可读性。 例如,十六进制颜色#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');

那么在不更改或转换实际图像或颜色对象的情况下,比较它们的最佳脚本样式公式是什么?

5 个答案:

答案 0 :(得分:3)

亮度对比度和感知力

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

您绝对是对的,只有6%的男性患有色盲,他们依靠亮度对比而不是颜色对比。我有一个chart here that demonstrates问题。

仅供参考,“亮度”不是亮度。发光度是指随时间的发射光,通常用于天文学中。在谈论比色法时,我们使用术语亮度(一种不同的光度),由CIEXYZ(CIE 1931)定义。

碰巧的是,我正在研究对比度评估方法,以提供一些新的,更准确的标准。您可以在GitHub和我的perception research页面上关注一些进度。

它不像人们想象的那样直接,因为有许多因素会影响人类对对比度的感知。此刻,GitHub线程中有很多讨论。

确定亮度

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

尽管XYZ是光线的线性模型,但人类的感知却是非常非线性的。因此,XYZ在感知上并不均匀。不过,出于您的目的,您只想知道彩色与灰色色块的等效亮度是什么。

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

我已经在Stack上发表了很多关于伽玛的文章,但是如果您需要明确的解释,我建议Poynton's Gamma FAQ.

将sRGB转换为线性(伽玛1.0)。

1)通过将每个通道分别除以255,将R´G´B´值从8位整数(0-255)转换为十进制(0.0-1.0)。 R “ G´B”值必须为0到1,以下数学才能起作用。另外,here's a link to a post带有一个代码段,用于将单个数字(如6位十六进制)转换为RGB通道。

2)线性化每个频道。懒惰的方法是应用2.2的幂曲线,这是计算机监视器显示图像数据的方式-出于判断颜色亮度的目的,这很好:

R´^ 2.2 = R lin G´^ 2.2 = G lin B´^ 2.2 = B lin

3)一种 ALTERNATE (更准确)的方法:如果您要进行图像处理并从sRGB到线性地来回移动,则有一种更准确的方法,即{{3} }而且,这是我的电子表格中的一个代码段,我将其用于类似目的:

  =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)最后,应用频谱加权系数,并将三个通道求和:

R lin * 0.2126 + G lin * 0.7152 + B lin * 0.0722 = Y

这会为您提供Y,即给定颜色的亮度。亮度也称为 L ,但不要与感知亮度的L *(Lstar)混淆,而不是亮度)。

确定感知对比度

现在,如果要确定两个样本之间的差异,可以使用多种方法。韦伯对比度本质上是ΔL/ L,自19世纪以来一直是标准。但是对于计算机监视器显示的刺激,我建议一些更现代的方法。例如,进行以下修改以获得更好的感知效果:

(L lighter – L darker )/(L lighter + 0.1)

还有Bowman-Sapolinski和“ Perceptual Contrast Length”,以及其他一些我正在研究的东西。您还可以转换为基于人类感知的CIELAB (L*a*b*),然后从L * 2 中减去L * 1

此外,还有许多其他影响对比度感知的因素,例如字体大小和粗细,填充(请参阅Bartleson-Breneman Surround Effects)和其他因素。

如有任何疑问,请告诉我。

答案 1 :(得分:2)

跟进答案

我将其发布为后续答案,不仅要澄清我的初始答案(我也刚刚编辑过),而且还要添加各种概念的代码段。从R´G´B´到Y的每个步骤都很重要,并且必须按照所描述的顺序进行,否则结果将失败。

定义:

sRGB :sRGB是一种三色颜色模型,它是Web的标准,并在大多数计算机显示器上使用。它使用与HDTV标准Rec709相同的原色和白点。 sRGB与Rec709的区别仅在于传输曲线,通常称为伽玛。

伽玛:这是一条曲线,可与各种图像编码方法一起用于存储和传输。它通常类似于人类视觉的感知曲线。在数字技术中,伽玛的作用是赋予图像较暗的区域更多的权重,以便用更多的位来定义它们,以避免诸如“条带化”之类的伪影。

亮度:(用 L Y 表示):光的线性度量或表示形式(即,没有伽玛曲线)。作为度量,通常为cd / m 2 。作为表示,在CIEXYZ中为Y,通常为0 (黑色)至100 (白色)。亮度具有光谱加权功能,基于人类对不同波长的光的感知。但是,亮度在明暗方面是线性的-也就是说,如果100个光子测量10个光子,那么20个将是200个光子。

L * (又名Lstar):感知亮度,由CIELAB定义(L * a * b *)在亮度与光量成线性关系的情况下,L *基于感知,在光量方面也是非线性的,其曲线旨在与人眼的明视觉相匹配(伽玛约为^ 0.43)。

亮度与L *: 0和100在亮度(Y或L)和亮度(L *)中都是相同,但在中间有很大的不同。我们确定为中间灰色的是位于L *的中间50,但与亮度(Y)的18.4有关。在sRGB中为#777777或46.7%。

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

从sRGB得出亮度(Y)

STEP ZERO(un-HEX)

如果需要,可以将十六进制颜色值转换为三元组整数值,其中#00 = 0#FF = 255

第1步(从8位到十进制)

除以255,将8位sRGB值转换为十进制:

十进制 = R´ 8bit / 255ÂG´十进制 = G´ 8bit / 255 B´ decimal = B´ 8bit / 255

如果sRGB值为16位,则除以65535,将其转换为十进制。

第2步(线性化,简单版)

将每个颜色通道提高到2.2的功效,与sRGB显示屏相同。这对于大多数应用程序来说都是很好的。但是,如果您需要多次往返sRGB gamma编码空间,请使用下面更准确的版本。

R´^ 2.2 = R lin G´^ 2.2 = G lin ^ 2.2 = B lin

第2步(线性化,准确版本)

如果您要进行图像处理以及在伽马编码空间内外进行多次往返操作,请使用此版本而不是上面的简单^ 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));
        }
    }

第3步(光谱加权亮度)

正常的人眼具有三种类型的视锥细胞,它们对红色,绿色和蓝色的光敏感。但是我们的光谱灵敏度并不均匀,因为我们对绿色(555 nm)最敏感,而蓝色是遥远的最后位置。使用以下系数对亮度进行光谱加权以反映这一点:

R lin * 0.2126 + G lin * 0.7152 + B lin * 0.0722 = Y = L < / p>

将每个线性化颜色通道的系数乘以它们的总和,以求出L(亮度)。

第4步(对比度确定)

有许多不同的方法来确定对比度,以及各种标准。根据特定的应用,有些方程比其他方程更好。

WCAG
WCAG 2.0和2.1中列出的当前网页标准是简单的对比:

C =((L 打火机 + 0.05)/(L 打火机 + 0.05)):1

这给出一个比率,WCAG的标准将非文本指定为3:1,将文本指定为4.5:1。

但是,由于多种原因,这是一个微不足道的例子。据记录,我指出了当前GitHub问题(695)中的缺陷,并一直在研究替代方法。

修改过的韦伯
Hwang/Peli Modified Weber适用于计算机显示器/ sRGB,可以更好地评估对比度。

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

请注意,根据最近的一些实验,我将耀斑系数选择为0.1,而不是0.05。这个值虽然是待定,但另一个值可能会更好。

实验室差异
我碰巧比别人更喜欢的另一种选择是将线性亮度( 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)和Bowman / Sapolinski。

此外,如果您正在寻找亮度或亮度差异之外的色差,那么CIELAB在这方面有一些有用的方法。

注意事项:

平均RGB无布宜诺!

OP 2x2p提到了一个常用的等式,用于将颜色的灰度表示为:

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

他指出了它看起来多么不准确,的确是-这是完全错误的。 R,G和B的频谱加权很大,因此不能忽略。通过 MAGNITUDE量级,GREEN比BLUE的亮度更高。不能将所有三个通道加在一起然后除以3,而得到的任何值都接近特定颜色的实际亮度。

我相信对此的困惑可能来自于称为 HSI (色相,饱和度,强度)的颜色控制。但是这种控制并不是(永远不会)在感知上是统一的!像HSV一样,HSI只是在计算机中操纵颜色值的“便利”。两者在感知上都不统一,并且它们使用的数学严格地支持在软件中调整颜色值的“简便”方法。

OP的样品色

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

enter image description here

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

答案 2 :(得分:0)

也许这可以有所帮助。 (从不错的旧式js加密中提取)。

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

颜色对比度

由(WCAG版本2)定义

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

对比度范围从1到21

第1.4.3节

  • 高度可见:(增强)最小对比度为7到1-7:1
  • 普通文本:最小对比度为4.5到1-4.5:1
  • 大文本:最小对比度为3到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);

答案 3 :(得分:0)

这是我根据Myndex之前编写的内容更新的代码。

对于测试示例,紫色使用十六进制#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 =圆((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代表 R:119,G:119,B:119

十六进制#747474代表 R:116,G:116,B:116

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

现在,如Myndex所解释的,我们应该使其线性并应用gamma 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 < / em>

现在通过应用系数获得亮度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)值之间的感知对比度:

(较浅– Ldarker)/(较浅+ 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

答案 4 :(得分:0)

为了获得更好的语法和易用性,我将整个理论放入了一个对象的单个解析器中,该对象运行如下。

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

Object: 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      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 color      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 is to be continued hereafter

        this.result.error = false; 
        if(this.loging){console.log(this.name+', final output:');}
        if(this.loging){console.log(this.result);}
        return this.result;
        }
    }

请发表评论,如果您发现代码有误。