Lode 的计算机图形学教程

光与颜色

目录

返回目录

简介

本教程介绍颜色,颜色无疑是计算机图形学中非常重要的一个方面。

首先解释光是如何构成的,以及为什么我们能看到不同的颜色。

然后介绍几种颜色模型:首先是计算机使用的 RGB 颜色模型,并通过示例展示如何在 RGB 中进行颜色运算,接着介绍更直观的 HSL/HSV 模型。

之后提供代码,允许你在颜色模型之间进行转换,例如让用户使用 HSV 颜色模型选取颜色,然后将其转换为 RGB 以供计算机使用,或者创建彩虹渐变效果,或更改图像的颜色。


在介绍颜色模型之前,了解人眼和大脑如何将光转化为颜色是很重要的。

光本身是一种电磁波。电磁波与声波类似,都包含不同的频率,但电磁波是电磁性质的,可以在真空中传播。电磁波是由一个或多个频率组成的信号,例如微波炉使用的电磁波频率非常高,而无线电波的频率非常低。人眼只对非常窄的频率范围敏感,即 429 THz 到 750 THz 之间的频率(1 THz = 1 太赫兹 = 10^12 Hz)。其他所有电磁波都无法被看见。

单色光是由单一纯频率组成的光(这当然不是普遍情况,你看到的大多数光都是多色光)。单色光在眼睛看来是纯色,永远不会是白色或品红色。由于它只包含一个频率,单色光的波可以用正弦波来表示:



正弦波的高度是振幅,表示光的亮度。一个周期的宽度(称为 lambda)是光的波长,与频率成反比:由于光以 300000000 m/s 的速度传播,其波长为 300000000/f,其中 f 是频率。因此,可见光谱的波长范围约为 400 到 700 nm(1 nm = 10^-9 米)。



这个可见光谱展示了单色光所能呈现的所有颜色。某些光源(如激光和钠灯)发出单色光,但一般来说,光是多色的。例如,太阳发出白光,而白光包含所有频率!这意味着红、黄、绿、蓝和紫色光叠加在一起看起来就是白色!从物理角度来说,它根本不是白色,而是许多正弦曲线的叠加,但人类大脑使它看起来是白色的。因此,颜色是一种心理现象,而非物理现象。

光波是许多不同频率的叠加,即许多正弦曲线的叠加。每条正弦曲线都有自己的频率,也可以有自己的振幅。频谱显示了每个频率对应的振幅。有关频谱的更多信息,请参阅傅里叶变换章节的开头部分。

下面是一个这样的频谱示例:



这是我找到的一个黄色 LED 的频谱。频谱的顶部是主频,这是当具有该频谱的光照射时,我们的眼睛通常看到的颜色。如果这个黄色 LED 是单色的,频谱看起来会是这样:



对于眼睛来说,颜色看起来是一样的。这揭示了一个有趣的事实:具有不同频谱的光,在眼睛看来仍然可能是相同的!如果人眼能够将每一种频谱识别为不同的颜色,我们就能看到数不清的不同颜色,但人眼的工作方式不同,它将整个频谱转化为仅 3 个信号:检测到的红、绿、蓝的数量,这些信号的组合构成了我们能看到的颜色。

白光的频谱如下(曲线的高度并不重要):



光中所有频率的量相等,这时大脑才会感知为纯白色。在其他所有情况下,某个频率将占主导地位,大脑看到的颜色就是该频率对应的颜色。

黑光的频谱如下所示:



确实,完全没有光,每个频率的振幅都是零。黑色是大脑在没有光时赋予的颜色。

品红色是一种无法用单色光产生的颜色,其频谱可能如下所示:



蓝色和红色的振幅都很高,蓝色和红色频率的混合在大脑看来就是品红色或紫色。

眼睛与颜色感知

本节不讨论眼睛的物理结构和晶状体的工作原理,而是介绍眼睛和大脑如何区分不同的颜色。

光落在视网膜上,视网膜上有 2 种含有感光化学物质的细胞,即光感受器:视杆细胞和视锥细胞。视杆细胞只检测是否有光存在,在夜间非常重要。视杆细胞对整个频谱同时敏感,无法判断光的频率,因此无法提供任何颜色信息。要检测颜色,需要只对特定频率敏感的光感受器。这正是视锥细胞的作用:

视锥细胞有 3 种,分别对红色、绿色和蓝色敏感。这样的细胞并不只对单一频率敏感,它们之间有一定的重叠,只是主要对某个频率敏感。

例如,黄色的频率介于红色和绿色之间。这个黄色频率会同时激发红色和绿色视锥细胞,人脑将"红色和绿色视锥细胞都被激发"的信号转换为"黄色"。蓝色视锥细胞也会被黄光略微激发,但可以忽略不计。

如果落在眼睛上的光包含两个频率:红色和绿色,它也会同时激发红色和绿色视锥细胞,因此这种光看起来也是黄色的,即使它根本不包含任何黄色频率。

如果蓝光落在视网膜上,蓝色视锥细胞会被强烈激发,而绿色和红色视锥细胞只会发出微弱的信号。大脑将"主要是蓝色视锥细胞被激发"的信号转换为"蓝色"。

白光包含所有频率,因此当白光落在视网膜上时,3 种视锥细胞都被激发,大脑将"绿色、红色和蓝色视锥细胞全部被激发"的信号转换为"白色"。

以上解释了大脑如何从传入信号中产生不同的颜色"色相",但大脑还会根据传入信号的强度赋予光一定的亮度:如果信号非常强,大脑将其标记为非常明亮的红色、白色……但如果信号非常弱,颜色将接近黑色。此外,颜色还有"饱和度",它基于每种颜色类型给出的强度相对差异:如果红色信号非常强,但蓝色和绿色也相当强,颜色的饱和度将较低,呈红灰色或红白色。但如果红色信号非常强,而蓝色和绿色信号非常弱,则会出现非常纯正的红色。

由于不同的频谱对我们来说看起来完全相同,而某些动物具有不同类型的颜色感受器,因此对我们看起来相同的两种颜色,对某些动物来说可能看起来是两种不同的颜色。

上述过程在视网膜的每个位置分别发生,从而形成一幅复杂的二维图像,图像中的每个位置都可以有自己的颜色。

由于有 3 种视锥细胞,人可以区分 8(2^3)种主要颜色:

当然,你能区分的颜色远不止这 8 种,因为每种感受器类型可以有不同的激发程度。

色盲是指一种或多种类型的视锥细胞缺失或敏感度降低。例如,如果缺少红色视锥细胞,你只能区分主要含绿色的光和主要含蓝色的光。对于这样的人来说,主要含红色的光会显示为绿色,因为绿色感受器对红色的敏感度仍然高于蓝色感受器。缺少 2 种视锥细胞、只剩 1 种的人只能看到黑白色,因为现在只存在两种主要信号类型:"视锥细胞被激发"和"视锥细胞未被激发"。想象一下,如果人类有 4 种颜色感受器而不是只有 3 种,能看到多少更多的颜色。

最后还有一个问题:紫色在光谱的一端,而红色完全在另一端。紫色比红色感受器更接近蓝色感受器,所以你可能认为紫色光在眼睛看来就是纯蓝色。但紫色看起来更像紫罗兰色,暗示它含有一些红色,这是为什么呢?



原因在于紫色的频率非常高,对蓝色感受器来说也太高了,因此蓝色和红色感受器的信号都非常弱。相对而言,红色和蓝色信号会非常接近,大脑感知到的颜色就会更像紫罗兰色而不是蓝色!

RGB 颜色模型

RGB 颜色模型的工作原理与人眼的颜色感受器完全相同:RGB 颜色模型使用 3 个变量来描述颜色,即红(Red)、绿(Green)和蓝(Blue)。这些变量可以类比为神经中 3 种颜色感受器信号的强度。计算机或电视屏幕也是这样工作的:它有 3 种类型的像素单元,分别是红、绿、蓝,可以独立地使每种类型变亮或变暗,从而激发眼睛中正确的感受器以产生所需的颜色。如果用放大镜观察计算机屏幕的白色区域,你会发现白色实际上是由红、绿、蓝 3 种颜色组成的。这意味着计算机屏幕发出的白色与白色阳光不同:白色阳光包含所有频率(除了少数几个)的光子,而计算机屏幕只有 3 种频率。人眼无法区分这两种白色之间的差异。

RGB 颜色模型是你在计算机图形学中最常用的模型。它也被称为加法颜色模型,因为你将 3 个颜色分量相加来形成任何颜色。在 24 位颜色中,R、G、B 这 3 个分量各是一个 8 位变量,可以是 0 到 255 之间的整数。0 表示该颜色分量关闭(黑色),而 255 表示其处于最大强度。127 是半强度。这意味着颜色 0,0,0 是最深的黑色,颜色 255,0,0 是最亮的红色,颜色 0,255,0 是最亮的绿色,颜色 0,0,255 是最亮的蓝色。255,255,255 是最亮的白色,127,127,127 是灰色。32 位颜色与此相同,但额外添加了一个 8 位 Alpha 通道,可用于纹理的透明度等。

RGB 颜色模型不是很直观,下面是一个包含一些常见 RGB 值的表格:

以下是常见 RGB 颜色值的表格:

R G B 十六进制值
颜色
0 0 0 000000 黑色
255 0 0 FF0000 红色
0 255 0 00FF00 绿色
0 0 255 0000FF 蓝色
255 255 0 FFFF00 黄色
255 0 255 FF00FF 品红色
0 255 255 00FFFF 青色
255
128
128
FF8080
亮红色
128
255
128
80FF80
亮绿色
128
128
255
8080FF
亮蓝色
64 64 64 404040 深灰色
128 128 128 808080 中灰色
192 192 192 C0C0C0 亮灰色
255 255 255 FFFFFF 白色

通过这个表格,你应该能猜到 128,0,0 是深红色,255,128,192 是粉红色,16,16,16 是非常深的灰色。十六进制值是颜色的十六进制代码,例如在 HTML 中使用。

R、G、B 的值就是填入 QuickCG 函数(如 pset、drawLine、drawCircle)的参数,用于指定颜色。

在 RGB 颜色中,R、G、B 的值越高,颜色越亮,如果 R=G=B,颜色将是某种灰色。

如果设置 R=x、G=y、B=z,你可以在一个立方体上表示 RGB 颜色,其中原点是黑色,R=255、G=255、B=255 处的角是白色:

RGB 运算

通过对图像像素的 RGB 值进行计算,你可以实现各种颜色效果。

下面是一张可以对 RGB 颜色进行的操作表格,截图和代码将在后续章节中给出。这些操作适用于每通道 8 位的 24 位颜色模型,因此 255 是颜色的最大值。颜色通道也可以用 0.0 到 1.0 之间的浮点数表示,此时需要将值"255"替换为"1.0"。C 表示整个通道或总颜色,而 R、G、B 分别表示红色、绿色和蓝色通道。

操作
公式
效果
负片
255-C
返回相反的颜色,例如白色变为黑色,红色变为青色,……
变暗
C/p 或 C-p
将颜色除以某个常数(大于 1),或从中减去一个常数,使其变暗。
变亮
C*p 或 C+p
将颜色乘以某个常数(大于 1),或向其添加一个常数,使其变亮。
灰度
0.2126 * R + 0.7152 * G + 0.0722 * B
计算 3 个通道的加权平均值(此处基于 ITU Rec.709),以获得具有相同亮度的灰色。
移除通道
R=0,G=0 和/或 B=0
通过将一个或多个通道设置为 0,从图像中完全移除该颜色分量。
交换通道
R=G,G=R,……
交换两个颜色通道的值,以获得颜色完全不同的图像。

我们将在以下花朵的 PNG 图像上尝试所有这些公式:


负片图像

以下代码将加载一张 200*133 像素的 PNG 图像,计算其负片,并显示结果。ColorRGB 是包含 3 个整数 r、g、b 的结构体,用于描述 RGB 颜色。

int main(int argc, char *argv[])
{
  unsigned long w = 0, h = 0;
  std::vector<ColorRGB> image;
  loadImage(image, w, h, "pics/flower.png");
  screen(w, h, 0, "RGB Color");

  ColorRGB color; //the color for the pixels

  for(int y = 0; y < h; y++)
  for(int x = 0; x < w; x++)
  {
    //here the negative color is calculated!
    color.r = 255 - image[y * w + x].r;
    color.g = 255 - image[y * w + x].g;
    color.b = 255 - image[y * w + x].b;
    pset(x, y, color);
  }

  redraw();
  sleep();
  return 0;
}

image[y * w + x].r 是图像中像素 x、y 的红色分量,因此 255 - image[y * w + x].r 是其负片值。对每个颜色通道都执行此操作。以下是结果:



你也可以输入"color = RGB_White - image[y * w + x]"来替代这 3 行代码,因为 ColorRGB 结构体支持一些运算符。

更改亮度

要更改亮度,将 R、G、B 除以一个大于 1 的数使其变暗,或将它们乘以该数使其变亮。如果颜色分量超过 255,则截断为 255。

例如,要使图像变暗两倍,将上一示例中使图像变为负片的 3 行代码更改为:


        color.r = image[y * w + x].r / 2;
        color.g = image[y * w + x].g / 2;
        color.b = image[y * w + x].b / 2;



或者要使其变暗 1.5 倍,使用:

        color.r = int(image[y * w + x].r / 1.5);
        color.g = int(image[y * w + x].g / 1.5);
        color.b = int(image[y * w + x].b / 1.5);



要使其变亮两倍,使用:

        color.r = image[y * w + x].r * 2;
        color.g = image[y * w + x].g * 2;
        color.b = image[y * w + x].b * 2;

        if(color.r > 255) color.r = 255;
        if(color.g > 255) color.g = 255;
        if(color.b > 255) color.b = 255;



除了除法或乘法,你也可以加减一个数,在使图像变亮时这样做甚至能得到更好的效果:

        color.r = image[y * w + x].r + 50;
        color.g = image[y * w + x].g + 50;
        color.b = image[y * w + x].b + 50;

        if(color.r > 255) color.r = 255;
        if(color.g > 255) color.g = 255;
        if(color.b > 255) color.b = 255;



或者使其变暗:

        color.r = image[y * w + x].r - 50;
        color.g = image[y * w + x].g - 50;
        color.b = image[y * w + x].b - 50;

        if(color.r < 0) color.r = 0;
        if(color.g < 0) color.g = 0;
        if(color.b < 0) color.b = 0;



灰度

将图像转为灰度的一种方法是计算 3 个颜色分量的加权平均值(此处基于 ITU Rec. 709),并将该平均值用作灰色阴影的值。眼睛对蓝色不太敏感,因此蓝色的系数较小,对绿色更敏感,因此绿色的系数较大:

        color.r = color.g = color.b = 0.2126 * image[y * w + x].r + 0.7152 * image[y * w + x].g + 0.0722 * image[y * w + x].b;



交换与移除通道

通过移除通道,你可以完全去除图像中的某种颜色,例如如果你移除花朵的红色通道,你会得到这样的效果:

        color.r = 0;
        //red component set to zero
        color.g = image[y * w + x].g;
        color.b = image[y * w + x].b;



如果改为移除绿色通道,你会得到:



如果移除蓝色通道,你会得到:



最后一张图像看起来与原图非常相似,因为图像中没有太多蓝色,不过白色区域现在看起来是绿色或黄色,整体也变暗了一些。

交换两个通道可以得到颜色完全不同的结果,例如如果我们交换红色和绿色通道,花朵变为绿色,而背景变为红色调:

        color.r = image[y * w + x].g; //the green component of the image
        color.g = image[y * w + x].r; //the red component of the image
        color.b = image[y * w + x].b; //the blue component of the image



如果改为交换红色和蓝色通道,花朵自然变为蓝色:



要使花朵变为黄色,将 R 和 G 都设置为图像的红色通道:

        color.r = image[y * w + x].r; //the red component of the image
        color.g = image[y * w + x].r; //the red component of the image
        color.b = image[y * w + x].b; //the blue component of the image



HSL 颜色模型

HSL 是另一种用 3 个参数描述颜色的方式。RGB 是计算机屏幕的工作方式,但不太直观。HSL 更直观,但在用它绘制像素之前需要将其转换为 RGB。这种颜色模型最好的应用是,你可以轻松创建彩虹渐变,或使用该颜色模型更改图像的颜色、亮度或饱和度。

HSL 颜色显然具有参数 H、S 和 L,即色相(Hue)、饱和度(Saturation)和亮度(Lightness)。

色相表示光的颜色感觉,换句话说,颜色是红色、黄色、绿色、青色、蓝色、品红色……这种表示方式看起来与可见光谱几乎相同,只是右侧现在是品红色(红色和蓝色的组合),而不是紫色(频率高于蓝色的光):



色相是循环的,因此可以在圆上表示。360° 的色相与 0° 的色相看起来相同。



饱和度表示色相与中性灰色的差异程度。值从 0%(无颜色)到 100%(在给定光照百分比下给定色相的最大饱和度)。光的频谱越集中在一个波长附近,颜色的饱和度就越高。



亮度表示颜色的光照程度,0% 时颜色完全为黑色,50% 时颜色为纯色,100% 时变为白色。在 HSL 颜色中,最大亮度(L=255)的颜色始终是白色,无论色相或饱和度分量是什么。亮度定义为(maxColor+minColor)/2,其中 maxColor 是 R、G 或 B 分量中值最大的,minColor 是值最小的。请注意,HSL 和 HSV 的亮度并不是对实际感知亮度的良好近似,线性组合(如 0.2126*R + 0.7152*G + 0.0722*B 或 0.299*R + 0.587*G + 0.114*B)更为精确。HSL 和 HSV 是易于使用的颜色模型,但并不试图很好地模拟人类视觉系统。有关其他颜色模型,请参阅本教程末尾。



在本教程中,色相、饱和度和亮度将以 0-255 之间的数字表示,使 HSL 模型与 RGB 模型具有相同的 24 位。

HSL 颜色模型例如被用于 Paint Shop Pro 的颜色选取器中。

HSV 颜色模型

HSV 颜色模型(有时也称为 HSB)使用参数明度(Value)代替亮度(Lightness)。明度与亮度的工作方式不同,最大明度(V=255)的颜色可以是任何颜色,如红色、绿色、黄色、白色等,处于其最大亮度。明度定义为 maxColor,其中 maxColor 是 R、G 或 B 分量中值最大的。因此,红色(255,0,0)和白色(255,255,255)的明度确实都是 255。

在 HSL 中,亮度增加时表现如下:



在 HSV 中,明度的表现如下:



色相和饱和度参数与 HSL 中的非常相似。HSV 通常在表示饱和度方面更好,而 HSL 在表示亮度方面更好。但是,HSV 在降低非常亮的图像的亮度方面又更胜一筹。

通过比较它们的图表,我们可以更好地比较 HSL 和 HSV 模型:

以下是 S=255 时 HSL(左)和 HSV(右)的图表,横轴为色相,纵轴为亮度/明度(顶部为最大亮度):



HSL 曲线的顶部是白色,因为白色是最大亮度的颜色,而 HSV 曲线的顶部包含所有颜色,因为饱和度为 255,在 HSV 中,饱和度 255 必须是有色的,而白色的饱和度应为 0。HSV 曲线的顶部与 HSL 曲线的中心水平线相同,整个 HSV 曲线与 HSL 图像的下半部分完全相同。

以下是 L=255 和 V=255 时 HSL(左)和 HSV(右)的图表,横轴为色相,纵轴为饱和度(底部为最大饱和度):



HSL 曲线完全是白色,因为在 HSL 模型中,白色是唯一 L=255 的颜色。HSV 曲线现在显示所有一个或多个颜色分量等于 255 的颜色。这里的 HSV 图像与之前 S=255 时 HSL 图像的上半部分完全相同。

以下是 L=128 和 V=128 时 HSL(左)和 HSV(右)的图表,同样横轴为色相,纵轴为饱和度(底部为最大饱和度):



颜色模型转换

要绘制上面给出的图表,必须使用颜色模型转换函数:首先将颜色描述为 HSL 或 HSV,但要在屏幕上绘制,必须先将其转换为 RGB。从 RGB 转换到 HSL/HSV 也很有用,例如如果你加载一张 RGB 图像并想更改其色相,你必须先将其转换为 HSL 或 HSV,然后更改色相,再将其转换回 RGB。

颜色模型转换公式已在 QuickCG 的 QuickCG.cpp 文件中提供。

RGB 转 HSL

以下函数将 RGB 颜色转换为 HSL 颜色。你给它一个 ColorRGB,它返回一个 ColorHSL。ColorRGB 和 ColorHSL 都是包含 3 个整数的简单结构体,唯一的区别是它们的名称。RGBtoHSL 函数计算 ColorHSL 的值。

首先将变量 r、g、b、h、s、l 声明为浮点数。在内部,函数使用 0.0 到 1.0 之间的浮点数,以获得更好的精度。在函数末尾,结果可以很容易地转换回 0-255 的整数。该函数也可以很容易地修改以适应其他范围,例如如果你想每个颜色通道使用 16 位,或将色相表示为 0° 到 360° 之间的值。

ColorHSL RGBtoHSL(ColorRGB colorRGB)
{
    float r, g, b, h, s, l; //this function works with floats between 0 and 1
    r = colorRGB.r / 256.0;
    g = colorRGB.g / 256.0;
    b = colorRGB.b / 256.0;

然后定义 minColor 和 maxColor。minColor 是值最小的颜色分量的值,而 maxColor 是值最大的颜色分量的值。这两个变量是必需的,因为亮度定义为(minColor + maxColor)/ 2。

    float maxColor = max(r, max(g, b));
    float minColor = min(r, min(g, b));

如果 minColor 等于 maxColor,我们知道 R=G=B,因此颜色是某种灰色。这是一种简单情况,色相可以设置为任何值,饱和度必须设置为 0(只有这样才是灰色),亮度设置为 R=G=B,即灰色的深浅。

    //R == G == B, so it's a shade of gray
    {
        h = 0.0; //it doesn't matter what value it has
        s = 0.0;
        l = r; //doesn't matter if you pick r, g, or b
    }

如果 minColor 不等于 maxColor,我们有一种真实的颜色而不是灰色,因此需要更多计算:


    else
    {
        l = (minColor + maxColor) / 2;

        if(l < 0.5) s = (maxColor - minColor) / (maxColor + minColor);
        else s = (maxColor - minColor) / (2.0 - maxColor - minColor);

        if(r == maxColor) h = (g - b) / (maxColor - minColor);
        else if(g == maxColor) h = 2.0 + (b - r) / (maxColor - minColor);
        else h = 4.0 + (r - g) / (maxColor - minColor);

        h /= 6; //to bring it to a number between 0 and 1
        if(h < 0) h ++;
    }

最后,H、S 和 L 从 h、s、l 计算为 0 到 255 之间的整数,并作为结果"返回"。之所以说"返回",是因为 H、S 和 L 是通过引用传递给函数的。

    ColorHSL colorHSL;
    colorHSL.h = int(h * 255.0);
    colorHSL.s = int(s * 255.0);
    colorHSL.l = int(l * 255.0);
    return colorHSL;
}

HSL 转 RGB

这是相反的转换,因此该函数将计算 RGBtoHSL 函数的逆运算。

首先,在内部再次将小写字母变量定义为 0 到 1 之间的浮点数。还声明了一些用于计算的临时值。

ColorRGB HSLtoRGB(ColorHSL colorHSL)
{
    float r, g, b, h, s, l; //this function works with floats between 0 and 1
    float temp1, temp2, tempr, tempg, tempb;
    h = colorHSL.h / 256.0;
    s = colorHSL.s / 256.0;
    l = colorHSL.l / 256.0;

然后是一种简单情况:如果饱和度为 0,颜色将是灰度颜色,此时计算非常简单:r、g、b 都设置为亮度值。

    //If saturation is 0, the color is a shade of gray
    if(s == 0) r = g = b = l;

如果饱和度大于 0,则需要再次进行更多计算。红色、绿色和蓝色使用代码中定义的公式计算。

    //If saturation > 0, more complex calculations are needed
    else
    {
        //Set the temporary values
        if(l < 0.5) temp2 = l * (1 + s);
        else temp2 = (l + s) - (l * s);
        temp1 = 2 * l - temp2;
        tempr = h + 1.0 / 3.0;
        if(tempr > 1) tempr--;
        tempg = h;
        tempb = h - 1.0 / 3.0;
        if(tempb < 0) tempb++;

        //Red
        if(tempr < 1.0 / 6.0) r = temp1 + (temp2 - temp1) * 6.0 * tempr;
        else if(tempr < 0.5) r = temp2;
        else if(tempr < 2.0 / 3.0) r = temp1 + (temp2 - temp1) * ((2.0 / 3.0) - tempr) * 6.0;
        else r = temp1;

        //Green
        if(tempg < 1.0 / 6.0) g = temp1 + (temp2 - temp1) * 6.0 * tempg;
        else if(tempg < 0.5) g = temp2;
        else if(tempg < 2.0 / 3.0) g = temp1 + (temp2 - temp1) * ((2.0 / 3.0) - tempg) * 6.0;
        else g = temp1;

        //Blue
        if(tempb < 1.0 / 6.0) b = temp1 + (temp2 - temp1) * 6.0 * tempb;
        else if(tempb < 0.5) b = temp2;
        else if(tempb < 2.0 / 3.0) b = temp1 + (temp2 - temp1) * ((2.0 / 3.0) - tempb) * 6.0;
        else b = temp1;
    }

最后,结果作为 0 到 255 之间的整数返回。

    ColorRGB colorRGB;
    colorRGB.r = int(r * 255.0);
    colorRGB.g = int(g * 255.0);
    colorRGB.b = int(b * 255.0);
    return colorRGB;
}

RGB 转 HSV

RGBtoHSV 函数与 RGBtoHSL 函数非常相似,唯一的区别是现在使用变量 V(明度)代替 L(亮度),明度定义为 maxColor。这可以在函数开始时立即计算:

ColorHSV RGBtoHSV(ColorRGB colorRGB)
{
    float r, g, b, h, s, v; //this function works with floats between 0 and 1
    r = colorRGB.r / 256.0;
    g = colorRGB.g / 256.0;
    b = colorRGB.b / 256.0;
    float maxColor = max(r, max(g, b));
    float minColor = min(r, min(g, b));
    v = maxColor;

然后计算饱和度。如果颜色是黑色,饱和度的值无关紧要,可以设置为 0。这样做是为了避免除以零。

    if(maxColor == 0) //avoid division by zero when the color is black
    {
        s = 0;
    }
    else
    {
        s = (maxColor - minColor) / maxColor;
    }

最后计算色相。如果饱和度为 0,颜色是灰色,色相无关紧要。同样,这种情况单独处理以避免除以零。

    if(s == 0)
    {
        h = 0; //it doesn't matter what value it has
    }
    else
    {
        if(r == maxColor) h = (g - b) / (maxColor-minColor);
        else if(g == maxColor) h = 2.0 + (b - r) / (maxColor - minColor);
        else h = 4.0 + (r - g) / (maxColor - minColor);
        h /= 6.0; //to bring it to a number between 0 and 1
        if (h < 0) h++;
    }

最后,结果作为 0 到 255 之间的整数返回。

    ColorHSV colorHSV;
    colorHSV.h = int(h * 255.0);
    colorHSV.s = int(s * 255.0);
    colorHSV.v = int(v * 255.0);
    return colorHSV;
}

HSV 转 RGB

首先再次声明 0 到 1 之间的浮点数:

ColorRGB HSVtoRGB(ColorHSV colorHSV)
{
    float r, g, b, h, s, v; //this function works with floats between 0 and 1
    h = colorHSV.h / 256.0;
    s = colorHSV.s / 256.0;
    v = colorHSV.v / 256.0;

处理饱和度为零的简单情况:

    //If saturation is 0, the color is a shade of gray
    if(s == 0) r = g = b = v;

HSV 模型可以用六边形锥体来表示。对于六边形的每条边,分别计算一种情况:

    //If saturation > 0, more complex calculations are needed
    else
    {
        float f, p, q, t;
        int i;
        h *= 6; //to bring hue to a number between 0 and 6, better for the calculations
        i = int(floor(h));  //e.g. 2.7 becomes 2 and 3.01 becomes 3 or 4.9999 becomes 4
        f = h - i;  //the fractional part of h
        p = v * (1 - s);
        q = v * (1 - (s * f));
        t = v * (1 - (s * (1 - f)));
        switch(i)
        {
            case 0: r = v; g = t; b = p; break;
            case 1: r = q; g = v; b = p; break;
            case 2: r = p; g = v; b = t; break;
            case 3: r = p; g = q; b = v; break;
            case 4: r = t; g = p; b = v; break;
            case 5: r = v; g = p; b = q; break;
        }
    }

同样,结果作为 0 到 255 之间的整数"返回"。

    ColorRGB colorRGB;
    colorRGB.r = int(r * 255.0);
    colorRGB.g = int(g * 255.0);
    colorRGB.b = int(b * 255.0);
    return colorRGB;
}

HSL 与 HSV 运算

上面给出的函数已包含在 QuickCG 中,借助它们我们可以对图像进行 HSL 和 HSV 运算。

示例将再次在花朵图像上进行:



更改色相

无论使用 HSL 还是 HSV 模型更改色相,结果都是相同的,所以这里就用 HSL 吧,没有特别的原因。

以下代码将加载 PNG 图像,将其转换为 HSL,通过向色相添加某个值来更改色相,然后将其转换回 RGB 以显示(将此代码放在 main.cpp 文件的 main 函数中):

int main(int argc, char *argv[])
{
    ColorRGB colorRGB;
    ColorHSL colorHSL;

    unsigned long w, h;
    std::vector<ColorRGB> image;
    loadImage(image, w, h, "pics/flower.png");
    screen(w, h, 0, "RGB Color");

    for(int y = 0; y < h; y++)
    for(int x = 0; x < w; x++)
    {
        //store the color of the image in variables R, G and B
        colorRGB = image[y * w + x];
        //calculate H, S and L out of R, G and B
        colorHSL = RGBtoHSL(colorRGB);
        //change Hue
        colorHSL.h += int(42.5 * 1);
        colorHSL.h %= 255;
        //convert back to RGB
        colorRGB = HSLtoRGB(colorHSL);
        //plot the pixel
        pset(x, y, colorRGB);
    }
    redraw();
    sleep();
    return 0;
}

色相对 255 取模,因为它必须在 0 到 255 之间,且是循环的,所以 260 的色相与 5 的色相相同。选择 42.5 是因为它等于 255/6,表示 60° 的色相偏移。以下是色相偏移 0°、60°、120°、180°、240° 和 300° 的结果截图(360° 与 0° 的结果相同):





你也可以将色相设置为常数,使整张图像呈现相同的颜色。例如,这里将色相设置为 25,即橙色:



更改饱和度

你可以更改饱和度,使图像更鲜艳,或更像粉彩色,或变为灰度。这次,如果使用 HSL 或 HSV 颜色模型,结果会略有不同。

例如,要通过乘以 2.5 来增加饱和度,将更改色相的代码行改为:

    colorHSV.s = int(colorHSV.s * 2.5);
    if(colorHSV.s > 255) colorHSV.s = 255;

如果饱和度超过 255,则截断。左边是使用 HSL 的结果,右边是使用 HSV 的结果。结果与原图非常相似,因为原图的饱和度已经相当高了。只有背景变得更绿了一些。



如果改为乘以 0.5,则会将饱和度减半:



你也可以通过减去一个值来降低饱和度:

    colorHSV.s = colorHSV.s - 100;
    if(colorHSV.s < 0) colorHSV.s = 0;

背景现在将变为灰度,而饱和度较高的花朵仍然保留一些颜色:



如果将饱和度设置为 0,图像将变为灰度,使用 HSL 或 HSV 的方式略有不同,两者也都与将图像转为灰度的"平均"公式不同:



如果将所有像素的饱和度设置为 128,你会得到这样的效果。花朵和背景现在看起来同样鲜艳:



更改亮度

最后,HSL 和 HSV 也可用于更改图像的亮度。同样,HSL 和 HSV 模型的工作方式不同。在使含有白色或接近白色像素的图像变暗时,HSL 会产生较差的结果。



你也可以通过减去一个值来降低亮度:

    colorHSV.l -= 50;
    if(colorHSV.l < 0) colorHSV.l = 0;

以下是 HSL(左)和 HSV(右)的结果:



将所有像素的亮度设置为 192 会得到:



伽马校正

伽马校正通过将分量提升到某个幂次来实现,但需要先缩放到 0-1 范围(而不是 0-255),从而产生 0-1 范围内的新值。对于典型的计算机显示器,幂次的值大约为 2.2。当颜色值对应于光的数量时,这称为"线性" RGB 颜色。然而,在图像中,颜色通常不以线性格式存储,而是以 1/2.2 的幂次进行伽马压缩。这是因为计算机屏幕会自动进行相反的变换,具有将幂次提升到 2.2 的效果。

在对图像像素进行操作时,为了正确性,这些操作应在线性 RGB 中进行,而不是在压缩 RGB 中进行,否则非线性颜色值会影响你预期的线性或其他变换(例如对两个相邻像素取平均)。本教程的其余部分实际上忽略了这一事实(例如关于滤波的部分),但这是在实际应用中需要考虑的事项。

因此,在进行操作之前,要将压缩 RGB 转换为线性 RGB:

        color.r = 255.0 * std::pow(image[y * w + x].r / 255.0, 2.2);
        color.g = 255.0 * std::pow(image[y * w + x].g / 255.0, 2.2);
        color.b = 255.0 * std::pow(image[y * w + x].b / 255.0, 2.2);

转换回来:

        image[y * w + x].r = 255.0 * std::pow(color.r / 255.0, 1 / 2.2);
        image[y * w + x].g = 255.0 * std::pow(color.g / 255.0, 1 / 2.2);
        image[y * w + x].b = 255.0 * std::pow(color.b / 255.0, 1 / 2.2);


其他颜色模型

本文重点介绍了 RGB、HSL 和 HSV 颜色模型。然而,还有很多其他模型,具有各种专业应用,如电视、出版、数据压缩……此外,需要精确的数学计算,还需要考虑以下方面:伽马校正(上文已提及)、白点、人类视觉系统、色域、色度图……这些内容超出了本教程的范围。处理这些色彩空间最好使用浮点数,使用 0-255 的字节值会导致精度损失。

一个包含精确数学的好资源是 Bruce Lindeboom 的网站:http://www.brucelindbloom.com/

最后,我们简要列举一些其他颜色模型,此处不提供转换公式。



最后编辑:2004 年

版权所有 (c) 2004-2007 Lode Vandevenne,保留所有权利。