可见光谱的RGB值

71
我需要一个算法或函数来将可见光谱的每个波长映射到对应的RGB值。 RGB系统和光的波长之间是否存在结构关系?像这张图片一样: alt text
(来源:kms at www1.appstate.edu)

如果这是无关的,我很抱歉 :-]


6
这个问题可能会给你一些启示。https://dev59.com/bnM_5IYBdhLWcg3wNwUv - GWW
光谱颜色和RGB颜色具有无限到1的关系,因此没有唯一的关系。一个RGB颜色映射到无限多个光谱颜色。 - Sebastian Mach
请查看此处:https://dev59.com/vH3aa4cB1Zd3GeqPZCXb#22149027,是的,这只能用于波长-> RGB 转换,而不能反过来...(波长有颜色,但颜色不是波长...) - Spektre
基于真实线性光谱数据,我添加了新的波长到RGB转换。 - Spektre
12个回答

59

最近我发现我的光谱颜色不正常,因为它们是基于非线性和偏移的数据。所以我进行了一些研究和数据编译,发现大多数光谱图像都是不正确的。此外,颜色范围也不匹配,所以我从这一点开始只使用线性化的真实光谱数据,如此(原链接现已失效):

sun real linear spectra

这是我的纠正输出:

spectral colors

  • 第一个光谱是我找到的最好的渲染光谱,但仍然与真实光谱相差很远
  • 第二个是从地球上获取的太阳线性光谱
  • 最后一个是我当前的颜色输出

以下是RGB图表:

这是两个图表的合并:

graph merge

现在是代码:

void spectral_color(double &r,double &g,double &b,double l) // RGB <0,1> <- lambda l <400,700> [nm]
    {
    double t;  r=0.0; g=0.0; b=0.0;
         if ((l>=400.0)&&(l<410.0)) { t=(l-400.0)/(410.0-400.0); r=    +(0.33*t)-(0.20*t*t); }
    else if ((l>=410.0)&&(l<475.0)) { t=(l-410.0)/(475.0-410.0); r=0.14         -(0.13*t*t); }
    else if ((l>=545.0)&&(l<595.0)) { t=(l-545.0)/(595.0-545.0); r=    +(1.98*t)-(     t*t); }
    else if ((l>=595.0)&&(l<650.0)) { t=(l-595.0)/(650.0-595.0); r=0.98+(0.06*t)-(0.40*t*t); }
    else if ((l>=650.0)&&(l<700.0)) { t=(l-650.0)/(700.0-650.0); r=0.65-(0.84*t)+(0.20*t*t); }
         if ((l>=415.0)&&(l<475.0)) { t=(l-415.0)/(475.0-415.0); g=             +(0.80*t*t); }
    else if ((l>=475.0)&&(l<590.0)) { t=(l-475.0)/(590.0-475.0); g=0.8 +(0.76*t)-(0.80*t*t); }
    else if ((l>=585.0)&&(l<639.0)) { t=(l-585.0)/(639.0-585.0); g=0.84-(0.84*t)           ; }
         if ((l>=400.0)&&(l<475.0)) { t=(l-400.0)/(475.0-400.0); b=    +(2.20*t)-(1.50*t*t); }
    else if ((l>=475.0)&&(l<560.0)) { t=(l-475.0)/(560.0-475.0); b=0.7 -(     t)+(0.30*t*t); }
    }
//--------------------------------------------------------------------------

在哪里

  • l是波长,单位为[nm],可用值为l = < 400.0 , 700.0 >
  • r,g,b是颜色组件,范围为< 0.0 , 1.0 >

@Broseph,用1 / sqrt改变简单低阶多项式对我来说听起来不好(速度,准确性)。如果你想要速度或代码简单性,请使用表+插值。 - Spektre
你的渲染似乎没有足够的紫色。光谱的左端大多是蓝色的,而即使是从棱镜中形成的彩虹,在短波长端也非常紫色。 - Ruslan
1
@Ruslan 如果你的监视器使用不同的波长来表示RGB,那么所有东西都将被彻底搞砸,因为你需要重新整合这些不同波长的X、Y、Z曲线才能再次获得正确的结果... - Spektre
2
你第一段中的“像这样”的链接已经失效了,无法加载:https://www.cfa.harvard.edu/ssp/images/SolarCCD.jpg(未找到)。 - Sohail Si
2
@SohailSi 谢谢你提醒我,我从我的档案中添加了低质量的图像(原始图像是反向排序的,大小为2.9兆字节的jpg,超出了imgur 2兆字节的限制,因此无法发布)。 - Spektre
显示剩余8条评论

22
将波长转换为RGB颜色
首先,您需要参考CIE 1964补充标准色度观察者图表(存档)

https://istack.dev59.com/sEeR5.webp

查找所需波长的CIE颜色匹配函数数值。

例如,我想获取455纳米光的颜色:

enter image description here

对于我们期望的波长:

╔════════════╦═══════════════════════════════╦═════════════════════════════╗
║ Wavelength ║ CIE color matching functions  ║  Chromacity coordinates     ║
║   λ nm     ║    X     │     Y    │    Z    ║    x    │    y    │    z    ║
╟────────────╫──────────┼──────────┼─────────╫─────────┼─────────┼─────────╢ 
║ 455        ║ 0.342957 │ 0.106256 │ 1.90070 ║ 0.14594 │ 0.04522 │ 0.80884 ║
╚════════════╩══════════╧══════════╧═════════╩═════════╧═════════╧═════════╝

注意:色度坐标仅是从CIE色匹配函数计算得出的:

x = X / (X+Y+Z)
y = Y / (X+Y+Z)
z = Z / (Z+Y+Z)

鉴于:
X+Y+Z = 0.342257+0.106256+1.90070 = 2.349913

我们进行计算:
x = 0.342257 / 2.349913 = 0.145945
y = 0.106256 / 2.349913 = 0.045217
z = 1.900700 / 2.349913 = 0.808838

您现在已经使用两种不同的颜色空间指定了您的455纳米光:
- XYZ: (0.342957, 0.106256, 1.900700) - xyz: (0.145945, 0.045217, 0.808838)
我们还可以添加第三个颜色空间:xyY。
x = x = 0.145945
y = y = 0.045217
Y = y = 0.045217

我们现在有三种不同颜色空间中指定的455纳米光:
- XYZ: (0.342957, 0.106256, 1.900700) - xyz: (0.145945, 0.045217, 0.808838) - xyY: (0.145945, 0.045217, 0.045217)
因此,我们已经将纯单色发射光的波长转换为XYZ颜色。现在我们想将其转换为RGB。
如何将XYZ转换为RGB?
XYZ、xyz和xyY是使用绝对物理描述颜色的绝对颜色空间。
与此同时,人们使用的每个实际颜色空间都依赖于某个白点。然后,颜色被描述为相对于该白点。
例如,
  • 在RGB中,(255,255,255)表示"白色"
  • 在Lab中,(100, 0, 0)表示"白色"
  • 在LCH中,(100, 0, 309)表示"白色"
  • 在HSL中,(240, 0, 100)表示"白色"
  • 在HSV中,(240, 0, 100)表示"白色"

但是实际上并不存在所谓的白色。你如何定义白色呢?是太阳光的颜色吗?

  • 是在一天的什么时间?
  • 有多少云层遮挡?
  • 位于哪个纬度?
  • 地球上的哪个位置?

有些人用他们(非常橙色的)白炽灯泡的白色来表示白色。有些人用荧光灯的颜色。白色没有绝对的物理定义 - 白色存在于我们的大脑中。

因此我们必须选择一个白色

我们必须选择一个白色的。(你真的得选择一个白色的。) 而且有很多白色可供选择: 我会为您选择一个白色。与sRGB使用的相同的白色:
- D65 - 北欧夏季晴朗日的白天照明
D65(由于地球大气层的原因,其颜色接近于6504K,但并非完全如此)的颜色为:
- XYZ_D65:(0.95047, 1.00000, 1.08883)
有了这个,您可以将您的XYZ转换为Lab(或Luv)- 一种能够平等表达所有理论颜色的颜色空间。现在我们有了第四个颜色空间表示我们的445nm单色光发射的颜色。
  • XYZ: (0.342957, 0.106256, 1.900700)
  • xyz: (0.145945, 0.045217, 0.808838)
  • xyY: (0.145945, 0.045217, 0.045217)
  • Lab: (38.94259, 119.14058, -146.08508) (假设d65)

但你想要RGB

Lab(和Luv)是相对于某个白点的颜色空间。即使你被迫选择了一个任意的白点,你仍然可以表示出所有可能的颜色。

而RGB不是这样的。对于RGB:

  • 颜色不仅相对于某个白点
  • 还相对于三个主要颜色:红色绿色蓝色
如果你指定了一个RGB颜色为(255, 0, 0),那就意味着你想要的是"纯红"。但是红色并没有明确定义。"红色"、"绿色"或者"蓝色"这些概念并不存在。彩虹是连续的,没有箭头指示:
“这是红色”
而且这也意味着我们必须选择三个主要颜色。你必须选择你的三个主要颜色来定义"红色"、"绿色"和"蓝色"。而且你有很多不同的红色、绿色和蓝色的定义可供选择:
- CIE 1931 - ROMM RGB - Adobe Wide Gamut RGB - DCI-P3 - NTSC (1953) - Apple RGB - sRGB - Japanese NTSC - PAL/SECAM - Adobe RGB 98 - scRGB
我会替你选择。我会选择这三种颜色
  • 红色: xyY = (0.6400, 0.3300, 0.2126)
  • 绿色: xyY = (0.3000, 0.6000, 0.7152)
  • 蓝色: xyY = (0.1500, 0.0600, 0.0722)

这些也是国际委员会在1996年选择的原色。

他们制定了一个标准,要求每个人都使用:

  • 白点: D65日光
  • 红色: (0.6400, 0.3300, 0.2126)
  • 绿色: (0.3000, 0.6000, 0.7152)
  • 蓝色: (0.1500, 0.0600, 0.0722)

他们将这个标准称为sRGB

最后的努力

现在我们已经选择了我们的

  • 白点
  • 三个原色

我们现在可以将你的XYZ颜色转换为RGB:

  • RGB = (1.47450, -178.21694, 345.59392)

不幸的是,这个RGB值存在一些问题:

  • 你的显示器无法显示负数的绿色(-178.21694),这意味着它是超出你的显示器能够显示的颜色范围。
  • 你的显示器无法显示超过255的蓝色(345.59392),显示器只能显示与蓝色相同的程度,不能更加蓝。这意味着它是超出你的显示器能够显示的颜色范围。

因此,我们需要四舍五入:

  • XYZ: (0.342957, 0.106256, 1.900700)
  • xyz: (0.145945, 0.045217, 0.808838)
  • xyY: (0.145945, 0.045217, 0.045217)
  • Lab: (38.94259, 119.14058, -146.08508) (d65)
  • RGB: (1, 0, 255) (sRGB)

现在我们有了最接近的波长为455纳米的光的sRGB近似值:

enter image description here


你选择了一个合理的白点,但问题在于许多显示器的默认白点与D65相差很远(因此默认情况下远离sRGB兼容性)。我一直在使用这样的显示器,它们有黄色调的白色,更接近于D50(通过校准调整RGB最大值将其降低三倍),以及那些具有11000K白色的显示器。 - Ruslan
顺便提一下,你忘记了伽马校正。sRGB规定了一个非线性,可以建模为2.2的伽马值,因此在将最终的RGB值显示在屏幕上之前,应该将其提高到1/2.2的幂。 - Ruslan
@Ruslan 我没有这样做。此外,2.2 的概念需要消失 - 我们已经使用 sRGB 超过 20 年了。 - Ian Boyd
不确定您在这个链接中的意思。我的观点是,如果您显示#00007f#0000ff,则前一种颜色的亮度不会是后者的一半,而是接近于后者亮度的22%。我也不确定您想要什么样的想法消失。如果您的意思是分段线性和2.4的细节,那么它与2.2的差异与真实世界监视器的伽马曲线的不完美相比相当微不足道。 - Ruslan
1
差不多一年后,我学会了欣赏你对正确的sRGB传输函数的坚持。以前我不够聪明,没有考虑到2.2近似值的相对误差,在低RGB值时显得非常显著,有时甚至在高值时也能注意到。尽管真实的显示器偏离sRGB,但校准应该将传输函数修正为分段版本,而不是2.2次方版本。 - Ruslan
显示剩余5条评论

15

可见波长的近似RGB值

来源:Dan Bruton - Color Science

原始FORTRAN代码位于(http://www.physics.sfasu.edu/astro/color/spectra.html)

将返回平滑(连续)的光谱,红色成分较重。

w - 波长,R、G和B - 颜色分量

忽略伽马和强度,则简单地表示为:

if w >= 380 and w < 440:
    R = -(w - 440.) / (440. - 380.)
    G = 0.0
    B = 1.0
elif w >= 440 and w < 490:
    R = 0.0
    G = (w - 440.) / (490. - 440.)
    B = 1.0
elif w >= 490 and w < 510:
    R = 0.0
    G = 1.0
    B = -(w - 510.) / (510. - 490.)
elif w >= 510 and w < 580:
    R = (w - 510.) / (580. - 510.)
    G = 1.0
    B = 0.0
elif w >= 580 and w < 645:
    R = 1.0
    G = -(w - 645.) / (645. - 580.)
    B = 0.0
elif w >= 645 and w <= 780:
    R = 1.0
    G = 0.0
    B = 0.0
else:
    R = 0.0
    G = 0.0
    B = 0.0

@FrustratedWithFormsDesigner 不是,它是梯度。w在表达式中,所以它们是f(w)。 - Andrey
R=-(w - 440.) / (440. - 350.) 这些不是为中间值吗?这里的 R、G、B 是浮点数,不是整数。 - Rup
1
算法的来源是什么 - 是您自己从一些颜色的频率表中得出的,还是这是一个标准计算? - Rup
@Rup 这不是我的,但我认为它与http://en.wikipedia.org/wiki/File:Computer_color_spectrum.svg相符(请参见光谱下面的RGB分解)。 - Andrey
@msw 取决于应用程序。如果您编写科学应用程序,则是不好的,但如果您想创建滚动彩虹,则可以接受。 - Andrey
显示剩余7条评论

7

频率和色调之间存在关系,但由于感知、显示器色域和校准等复杂原因,除了昂贵的实验设备外,你能做到的最好只是粗略近似。

请参见http://en.wikipedia.org/wiki/HSL_and_HSV获取数学知识,并注意你需要自己推测色调与频率的映射关系。我认为这种经验性的映射关系不可能是线性的。


整个 Hue 到 RGB 的映射数学都是近似(线性)的。Hue 到频率可以映射为波长 390 到 750 纳米映射到色调 [0,1]。频率 = 光速 / 波长。 - Andrey
7
注意我的使用词语"empirical",然后尝试自己实践一下,记住我们的感知是三色性和明显非线性的,显示器是一个不同的三色性并且也极其非线性。整个校准行业就是围绕这些效应建立起来的。 - msw
色相是RGB立方体中白-黑轴周围的角度。当然,它与波长有某种关系,但这更多是偶然而非设计。请注意,该平面的一个六分之一部分具有不映射到任何波长的颜色。 - Cris Luengo

6

我认为答案未能解决实际问题。

RGB值通常来源于XYZ色彩空间,该空间是标准人类观察者函数、照明和每个波长范围内样本的相对功率的组合,范围大约为360-830。

我不确定您想要达到什么目的,但可以计算出一个相对“准确”的RGB值,用于样本的每个离散光谱带@ 10nm都是完全饱和的。变换如下:光谱->XYZ->RGB。请查看Bruce Lindbloom的网站了解数学知识。从XYZ中,您还可以轻松计算色调饱和度色度测量值,例如L*a*b*


6
如果您想要精确匹配,那么唯一的解决方案是将x、y、z颜色匹配函数与光谱值进行卷积,从而得到一个(与设备无关的)XYZ颜色表示,您可以随后将其转换为(与设备有关的)RGB。
这在这里有所描述: http://www.cs.rit.edu/~ncs/color/t_spectr.html 您可以在此处找到用于卷积的x、y、z颜色匹配函数: http://cvrl.ioo.ucl.ac.uk/cmfs.htm

5
这就是颜色配置文件所涉及的大部分内容。基本上,对于给定设备(扫描仪、相机、显示器、打印机等),颜色配置文件会告诉您特定输入集将产生哪些实际光颜色。
此外,请注意,对于大多数真实设备,您只需要处理少量离散的光波长,并且中间颜色是通过混合可用的两个相邻波长的不同数量来产生的,而不是直接产生该波长。鉴于我们以相同的方式感知颜色,这并不是真正的问题,但根据您关心的原因,了解这一点可能是值得的。
如果没有颜色配置文件(或等效信息),您就缺乏将RGB值映射到颜色所需的信息。纯红的RGB值通常会映射到该设备能够产生/感应到的最红色的颜色(同样,纯蓝色会映射到最蓝色的颜色,等等)-但是最红色或最蓝色可以并且会根据设备而变化(广泛变化),在某些情况下甚至不仅仅是设备本身-例如,在打印机中更换墨水可能会改变其特性。

2
Patapom几乎正确:对于每个波长,您需要计算CIE XYZ值,然后使用标准公式将其转换为(例如)sRGB(如果您幸运的话,您可以找到可以直接使用的代码来执行此转换)。因此,关键步骤是获取XYZ值。幸运的是,对于单波长光,这很容易:XYZ色匹配函数只是列出给定波长的XYZ值的表格。所以只需查找即可。如果您有更复杂光谱的光,例如黑体辐射,则必须平均XYZ响应时间和光中每个波长的量。

2
我不是程序员,也不是医生。我只是一名音乐家,拥有两只眼睛(如同其他人一样)。
所以……我知道电磁波在无线电和电视范围内具有对数比例的模式。为什么在可见光范围内会不同呢?
在无线电和电视世界中,我们使用简单的方程:根据极端频率之间的比率将给定范围分成给定数量的部分。
例如:如果我们的范围从100 MHz开始,到200 MHz结束,我们的比率为2(200等于100乘以2)。
因此,如果我们必须将该范围分成10个相等的部分,我们必须使用以下方程式:
第一个频率(100 MHz)乘以2的10次根。
将新值乘以2的10次根。
以此类推。
为什么我们使用2的10次根?很简单:请记住这不是线性比例尺,而是对数比例尺(与音符完全相同)。
因此,基于该方程式,由于我们知道可见光谱在780纳米至380纳米之间(大约为384.02 THz和789.26 THz;两个值都非常接近,因为它是根据个人光学能力变量计算),我们只需要知道这些频率之间的比率:
789.26/384.02=2.055
此外,我们知道RGB大致相当于这些极端频率:
384.02 THz = 95,0,0 (hx=5F0000)
788.92 THz = 97,0,97 (hx=610061)
同时,我们知道该点之间所有可能的RGB组合=1595。
因此,对于所有这些值,我们有一个简单的方程式:
RGB = 95, 0, 0 = 384.02 THz
RGB = 96, 0, 0 = 384.02 THz乘以(2.055的1595次根)= 384.19 THz
RGB = 97, 0, 0 = 384.19 THz乘以(2.055的1595次根)= 384.37 THz
如此类推。
很简单的比例尺。
以上仅为我个人的意见。

1

我发现Spektre的答案很有用,因为许多人可能无法应用其他答案中严格的CIE基于方法,但仍希望有一个基于物理实际的即插即用解决方案。

为此,我通过使用波长作为参数,将Spektre的数据与二次B样条拟合而成了修订后的算法。这样做的优点是RGB颜色随波长平滑变化(具有连续的一阶导数),并且稍微简单一些,因为大部分计算已经提前完成。这种形式也适用于矢量(SIMD)处理,如果相关的话。

Plot of RGB curves fitted to Spektre's data

在Javascript中:

function wavelengthToRGB (λ) {
    const C=[
        350,
            3.08919e-5,-2.16243e-2, 3.78425e+0,
            0.00000e+0, 0.00000e+0, 0.00000e+0,
            4.33926e-5,-3.03748e-2, 5.31559e+0,
        397,
           -5.53952e-5, 4.68877e-2,-9.81537e+0,
            6.13203e-5,-4.86883e-2, 9.66463e+0,
            4.41410e-4,-3.46401e-1, 6.80468e+1,
        423,
           -3.09111e-5, 2.61741e-2,-5.43445e+0,
            1.85633e-4,-1.53857e-1, 3.19077e+1,
           -4.58520e-4, 4.14940e-1,-9.29768e+1,
        464,
            2.86786e-5,-2.91252e-2, 7.39499e+0,
           -1.66581e-4, 1.72997e-1,-4.39224e+1,
            4.37994e-7,-1.09728e-2, 5.83495e+0,
        514,
            2.06226e-4,-2.11644e-1, 5.43024e+1,
           -6.65652e-5, 7.01815e-2,-1.74987e+1,
            9.41471e-5,-1.07306e-1, 3.05925e+1,
        565,
           -2.78514e-4, 3.36113e-1,-1.00439e+2,
           -1.79851e-4, 1.98194e-1,-5.36623e+1,
            1.12142e-5,-1.35916e-2, 4.11826e+0,
        606,
           -1.44403e-4, 1.73570e-1,-5.11884e+1,
            2.47312e-4,-3.19527e-1, 1.03207e+2,
            0.00000e+0, 0.00000e+0, 0.00000e+0,
        646,
            6.24947e-5,-9.37420e-2, 3.51532e+1,
            0.00000e+0, 0.00000e+0, 0.00000e+0,
            0.00000e+0, 0.00000e+0, 0.00000e+0,
        750
    ];
    let [r,g,b] = [0,0,0];
    if (λ >= C[0] && λ < C[C.length-1]) {
        for (let i=0; i<C.length; i+=10) {
            if (λ < C[i+10]) {
                const λ2 = λ*λ;
                r = C[i+1]*λ2 + C[i+2]*λ + C[i+3];
                g = C[i+4]*λ2 + C[i+5]*λ + C[i+6];
                b = C[i+7]*λ2 + C[i+8]*λ + C[i+9];
                break;
            }
        }
    }
    return [r,g,b];
}

该函数中的数组包含每个区间(以nm为单位)的边界波长,每个边界之间有三组λ²、λ¹和λ⁰系数 - 分别对应红色、绿色和蓝色。

如果您想使用不同的单位,可以相应地转换边界值(但如果使用倒数单位,例如THz、eV或cm-1,则需要反转搜索顺序)。

如果要直接生成8位颜色分量,还可以将所有系数预乘以255(并强制转换为int类型)。


这可能会生成大于1和小于0的RGB值。这似乎超出了边界。例如,尝试使用506和524。 - Anthony Michael Cook
1
@AnthonyMichaelCook 很好的发现 - 我已经编辑了代码以纠正这个问题。现在对于所有的λ,它应该返回[0,1]的值(并将值夹在范围[350,750]之外的部分设为零)。 - bobtato

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