第3章

颜色格式

1 序


颜色常用颜色空间来表示。颜色空间是用一种数学方法形象化表示颜色,人们用它来指定和产生颜色。例如,

  • 对于人来说,我们可以通过色调、饱和度和明度来定义颜色;
  • 对于显示设备来说,人们使用红、绿和蓝磷光体的发光量来描述颜色;
  • 对于打印或者印刷设备来说,人们使用青色、品红色、黄色和黑色的反射和吸收来产生指定的颜色。

颜色空间有设备相关和设备无关之分。

设备相关的颜色空间是指颜色空间指定生成的颜色与生成颜色的设备有关。例如,RGB颜色空间是与显示系统相关的颜色空间,计算机显示器使用RGB来显示颜色,用像素值(例如,R=250,G=123,B=23)生成的颜色将随显示器的亮度和对比度的改变而改变。

设备无关的颜色空间是指颜色空间指定生成的颜色与生成颜色的设备无关,例如,CIE Lab*颜色空间就是设备无关的颜色空间,它构建在HSV(hue, saturation and value)颜色空间的基础上,用该空间指定的颜色无论在什么设备上生成的颜色都相同。

2 目录


颜色格式 的子部分

3.1 RGBA


RGB 的基本概念

RGB 代表红(Red)、绿(Green)、蓝(Blue)。这三种颜色被称为光的三原色。在 RGB 色彩模式下,通过不同强度比例的红、绿、蓝三种光的混合,可以产生出各种各样的颜色。

从物理学角度来看,光是一种电磁波,而人眼能够感知到的可见光波段内,红、绿、蓝这三种颜色的光具有独特的波长范围。红色光的波长大约在 620 - 750 纳米之间,绿色光波长约为 495 - 570 纳米,蓝色光波长则在 450 - 480 纳米左右。当这三种颜色的光以不同的能量强度同时作用于人眼时,大脑就会感知到不同的色彩。 例如,当红色光以最强的强度发出,而绿色光和蓝色光强度为零时,我们看到的就是纯粹的红色;同理,单独最强强度的绿色光呈现绿色,单独最强强度的蓝色光呈现蓝色。而当三种光强度相等时,就会产生白色光。

RGB的一些具体格式

其中 RGB、RGBA是比较常用的。

RGB24

RGB24 是一种 24 位的像素格式,它使用 8 位来表示红、绿和蓝的颜色分量。也被称作 RGB888

typedef struct
{
    unsigned char r;
    unsigned char g;
    unsigned char b;
}RGB,RGB24,RGB888;

#define RGB(r,g,b) ((unsigned int)(((unsigned char)(r)|(((unsigned char)(g))<<8))|(((unsigned char)(b))<<16)))

注:windows 平台下,一般使用BGR。颜色分量顺序为 BGR。

RGBA32

RGBA32 是一种 32 位的像素格式,它使用 8 位来表示红、绿、蓝和透明度的颜色分量。也被称作 RGB8888

typedef union
{
    unsigned int rgba;
    struct
    {
        unsigned char r;
        unsigned char g;
        unsigned char b;
        unsigned char a;
    };
}RGBA,RGBA32,RGBA8888;

#define RGBA(r,g,b,a) ((unsigned int)(((unsigned char)(r)|(((unsigned char)(g))<<8))|(((unsigned char)(b))<<16)|(((unsigned char)(a))<<24)))

RGB555

RGB555 是一种 16 位的像素格式,它使用 5 位来表示红、绿和蓝的颜色分量。其中的1位作为透明分量(可以不使用),共 16 位。

typedef struct
{
    unsigned short r : 5;
    unsigned short g : 5;
    unsigned short b : 5;
    unsigned short a : 1;
}RGBA555;

//各分量取值:
#define RGB555_MASK_RED 0x7C00
#define RGB555_MASK_GREEN 0x03E0
#define RGB555_MASK_BLUE 0x001F
#define RGB555_MASK_ALPHA 0x8000
R = (wPixel & RGB555_MASK_RED) >> 10; // 取值范围0-31
G = (wPixel & RGB555_MASK_GREEN) >> 5; // 取值范围0-31
B = wPixel & RGB555_MASK_BLUE; // 取值范围0-31
A = (wPixel & RGB555_MASK_ALPHA) >> 15;

#define RGB555(r,g,b,a) (unsigned short)( (r|0x08 << 10) | (g|0x08 << 5) | b|0x08 | (a << 15) )

RGB565

RGB565使用16位表示一个像素,这16位中的5位用于R,6位用于G,5位用于B。程序中通常使用一个字(WORD,一个字等于两个字节)来操作一个像素。当读出一个像素后,这个字的各个位意义如下:

typedef struct
{
    unsigned short r : 5;
    unsigned short g : 6;
    unsigned short b : 5;
}RGB565;

//各分量取值:
#define RGB565_MASK_RED 0xF800
#define RGB565_MASK_GREEN 0x07E0
#define RGB565_MASK_BLUE 0x001F
R = (wPixel & RGB565_MASK_RED) >> 11;
G = (wPixel & RGB565_MASK_GREEN) >> 5;
B = wPixel & RGB565_MASK_BLUE;

#define RGB(r,g,b) (unsigned int)( (r|0x08 << 11) | (g|0x08 << 6) | b|0x08 )

3.2 HSL/HSV


HSL是一种将RGB色彩模型中的点在圆柱坐标系中的表示法。这两种表示法试图做到比基于笛卡尔坐标系的几何结构RGB更加直观。是运用最广的颜色系统之一

HSL即色相、饱和度、亮度(英语:Hue, Saturation, Lightness)。 色相(H)是色彩的基本属性,就是平常所说的颜色名称,如红色、黄色等。 饱和度(S)是指色彩的纯度,越高色彩越纯,低则逐渐变灰,取0-100%的数值。 明度(V),亮度(L),取0-100%。

HSV即色相、饱和度、明度(英语:Hue, Saturation, Value),又称HSB,其中B即英语:Brightness。 HSL和HSV二者都把颜色描述在圆柱坐标系内的点,这个圆柱的中心轴取值为自底部的黑色到顶部的白色而在它们中间的是灰色,绕这个轴的角度对应于“色相”,到这个轴的距离对应于“饱和度”,而沿着这个轴的高度对应于“亮度”、“色调”或“明度”。 这两种表示在目的上类似,但在方法上有区别。二者在数学上都是圆柱,但HSV(色相、饱和度、明度)在概念上可以被认为是颜色的倒圆锥体(黑点在下顶点,白色在上底面圆心),HSL在概念上表示了一个双圆锥体和圆球体(白色在上顶点,黑色在下顶点,最大横切面的圆心是半程灰色)。注意尽管在HSL和HSV中“色相”指称相同的性质,它们的“饱和度”的定义是明显不同的。 因为HSL和HSV是设备依赖的RGB的简单变换,(h,s,l)或 (h,s,v)三元组定义的颜色依赖于所使用的特定RGB“加法原色”。每个独特的RGB设备都伴随着一个独特的HSL和HSV空间。但是 (h,s,l)或 (h,s,v)三元组在被约束于特定RGB空间比如sRGB的时候就更明确了。

HSL、HSV与RGB的转换

HSL和HSV在数学上定义为在RGB空间中的颜色的R,G和B的坐标的变换。

HSL与RGB的转换

// 定义结构体表示 HSL 颜色
typedef struct {
    float h; // 色调,范围 [0, 360]
    float s; // 饱和度,范围 [0, 1]
    float l; // 亮度,范围 [0, 1]
} HSL;

// 将 RGB 转换为 HSL
HSL rgb_to_hsl(int r, int g, int b) {
    // 归一化 RGB 值
    float rf = r / 255.0;
    float gf = g / 255.0;
    float bf = b / 255.0;

    // 计算最大值和最小值
    float max = fmaxf(fmaxf(rf, gf), bf);
    float min = fminf(fminf(rf, gf), bf);
    float delta = max - min;

    HSL hsl = {0, 0, 0};

    // 计算亮度 L
    hsl.l = (max + min) / 2.0;

    if (delta == 0) {
        hsl.h = 0; // 色调不确定,设为 0
        hsl.s = 0; // 饱和度为 0
    } else {
        // 计算饱和度 S
        if (hsl.l < 0.5) {
            hsl.s = delta / (max + min);
        } else {
            hsl.s = delta / (2.0 - max - min);
        }

        // 计算色调 H
        if (rf == max) {
            hsl.h = (gf - bf) / delta;
        } else if (gf == max) {
            hsl.h = 2 + (bf - rf) / delta;
        } else {
            hsl.h = 4 + (rf - gf) / delta;
        }

        hsl.h *= 60.0; // 转换为度数
        if (hsl.h < 0) {
            hsl.h += 360.0;
        }
    }

    return hsl;
}

// 将 HSL 转换为 RGB
RGB hsl_to_rgb(HSL hsl) {
    RGB rgb = {0, 0, 0};

    if (hsl.s == 0) {
        // 如果饱和度为 0,则是灰度颜色
        rgb.r = rgb.g = rgb.b = (int)(hsl.l * 255);
        return rgb;
    }

    float c = (1 - fabs(2 * hsl.l - 1)) * hsl.s;
    float x = c * (1 - fabs(fmod(hsl.h / 60.0, 2) - 1));
    float m = hsl.l - c / 2;

    float r, g, b;

    if (hsl.h >= 0 && hsl.h < 60) {
        r = c; g = x; b = 0;
    } else if (hsl.h >= 60 && hsl.h < 120) {
        r = x; g = c; b = 0;
    } else if (hsl.h >= 120 && hsl.h < 180) {
        r = 0; g = c; b = x;
    } else if (hsl.h >= 180 && hsl.h < 240) {
        r = 0; g = x; b = c;
    } else if (hsl.h >= 240 && hsl.h < 300) {
        r = x; g = 0; b = c;
    } else if (hsl.h >= 300 && hsl.h < 360) {
        r = c; g = 0; b = x;
    }

    rgb.r = (int)((r + m) * 255);
    rgb.g = (int)((g + m) * 255);
    rgb.b = (int)((b + m) * 255);

    return rgb;
}

代码解释:

  • 归一化:将输入的 RGB 值从 [0, 255] 范围归一化到 [0, 1] 范围。
  • 计算最大值和最小值:用于后续计算亮度和饱和度。
  • 计算亮度 L:根据最大值和最小值的平均值计算。
  • 计算饱和度 S:根据最大值和最小值的差值以及亮度 L 计算。
  • 计算色调 H:根据最大值对应的 RGB 分量计算,并转换为度数。

HSV与RGB的转换

// 定义结构体表示 RGB 颜色
typedef struct {
    int r; // 红色分量,范围 [0, 255]
    int g; // 绿色分量,范围 [0, 255]
    int b; // 蓝色分量,范围 [0, 255]
} RGB;

// 定义结构体表示 HSV 颜色
typedef struct {
    float h; // 色调,范围 [0, 360)
    float s; // 饱和度,范围 [0, 1]
    float v; // 明度,范围 [0, 1]
} HSV;

// 将 HSV 转换为 RGB
RGB hsv_to_rgb(HSV hsv) {
    RGB rgb = {0, 0, 0};
    float c = hsv.v * hsv.s;
    float x = c * (1 - fabs(fmod(hsv.h / 60.0, 2) - 1));
    float m = hsv.v - c;

    if (hsv.h >= 0 && hsv.h < 60) {
        rgb.r = (int)((c + m) * 255);
        rgb.g = (int)((x + m) * 255);
        rgb.b = (int)(m * 255);
    } else if (hsv.h >= 60 && hsv.h < 120) {
        rgb.r = (int)((x + m) * 255);
        rgb.g = (int)((c + m) * 255);
        rgb.b = (int)(m * 255);
    } else if (hsv.h >= 120 && hsv.h < 180) {
        rgb.r = (int)(m * 255);
        rgb.g = (int)((c + m) * 255);
        rgb.b = (int)((x + m) * 255);
    } else if (hsv.h >= 180 && hsv.h < 240) {
        rgb.r = (int)(m * 255);
        rgb.g = (int)((x + m) * 255);
        rgb.b = (int)((c + m) * 255);
    } else if (hsv.h >= 240 && hsv.h < 300) {
        rgb.r = (int)((x + m) * 255);
        rgb.g = (int)(m * 255);
        rgb.b = (int)((c + m) * 255);
    } else if (hsv.h >= 300 && hsv.h < 360) {
        rgb.r = (int)((c + m) * 255);
        rgb.g = (int)(m * 255);
        rgb.b = (int)((x + m) * 255);
    }

    return rgb;
}

// 将 HSV 转换为 RGB
RGB hsv_to_rgb(HSV hsv) {
    RGB rgb = {0, 0, 0};
    float c = hsv.v * hsv.s;
    float x = c * (1 - fabs(fmod(hsv.h / 60.0, 2) - 1));
    float m = hsv.v - c;

    if (hsv.h >= 0 && hsv.h < 60) {
        rgb.r = (int)((c + m) * 255);
        rgb.g = (int)((x + m) * 255);
        rgb.b = (int)(m * 255);
    } else if (hsv.h >= 60 && hsv.h < 120) {
        rgb.r = (int)((x + m) * 255);
        rgb.g = (int)((c + m) * 255);
        rgb.b = (int)(m * 255);
    } else if (hsv.h >= 120 && hsv.h < 180) {
        rgb.r = (int)(m * 255);
        rgb.g = (int)((c + m) * 255);
        rgb.b = (int)((x + m) * 255);
    } else if (hsv.h >= 180 && hsv.h < 240) {
        rgb.r = (int)(m * 255);
        rgb.g = (int)((x + m) * 255);
        rgb.b = (int)((c + m) * 255);
    } else if (hsv.h >= 240 && hsv.h < 300) {
        rgb.r = (int)((x + m) * 255);
        rgb.g = (int)(m * 255);
        rgb.b = (int)((c + m) * 255);
    } else if (hsv.h >= 300 && hsv.h < 360) {
        rgb.r = (int)((c + m) * 255);
        rgb.g = (int)(m * 255);
        rgb.b = (int)((x + m) * 255);
    }

    return rgb;
}

代码解释:

  • 定义结构体:RGB 和 HSV 结构体分别用于存储 RGB 和 HSV 颜色值。
  • 计算临时变量:
  • c:色度,计算公式为 V * S。
  • x:辅助变量,计算公式为 c * (1 - |(H / 60) % 2 - 1|)。
  • m:偏移量,计算公式为 V - c。
  • 根据色调 H 的范围选择相应的 RGB 值:通过条件判断,根据 H 的值选择合适的 RGB 分量。

3.3 YUV


简介

YUV,是一种颜色编码方法。常使用在各个视频处理组件中。 YUV在对照片或视频编码时,考虑到人类的感知能力,允许降低色度的带宽。 YUV是编译true-color颜色空间(color space)的种类,Y’UV, YUV, YCbCr,YPbPr等专有名词都可以称为YUV,彼此有重叠。“Y”表示明亮度(Luminance或Luma),也就是灰阶值,“U”和“V”表示的则是色度(Chrominance或Chroma),作用是描述影像色彩及饱和度,用于指定像素的颜色。

Y’代表明亮度(luma;brightness)而U与V存储色度(色讯;chrominance;color)部分;亮度(luminance)记作Y,而Y’的prime符号记作伽玛校正。 YUVFormats分成两个格式: 紧缩格式(packedformats):将Y、U、V值存储成MacroPixels数组,和RGB的存放方式类似。 平面格式(planarformats):将Y、U、V的三个分量分别存放在不同的矩阵中。 紧缩格式(packedformat)中的YUV是混合在一起的,对于YUV常见格式有AYUV格式(4:4:4采样、打包格式);YUY2、UYVY(采样、打包格式),有UYVY、YUYV等。平面格式(planarformats)是指每Y分量,U分量和V分量都是以独立的平面组织的,也就是说所有的U分量必须在Y分量后面,而V分量在所有的U分量后面,此一格式适用于采样(subsample)。平面格式(planarformat)有I420(4:2:0)、YV12、IYUV等。

常用的YUV格式

为节省带宽起见,大多数YUV格式平均使用的每像素位数都少于24位。主要的抽样(subsample)格式有YCbCr4:2:0、YCbCr4:2:2、YCbCr4:1:1和YCbCr4:4:4。YUV的表示法称为A:B:C表示法:

  • 4:4:4表示完全取样。
  • 4:2:2表示2:1的水平取样,垂直完全采样。
  • 4:2:0表示2:1的水平取样,垂直2:1采样。
  • 4:1:1表示4:1的水平取样,垂直完全采样。

最常用Y:UV记录的比重通常1:1或2:1,DVD-Video是以YUV4:2:0的方式记录,也就是我们俗称的I420,YUV4:2:0并不是说只有U(即Cb),V(即Cr)一定为0,而是指U:V互相援引,时见时隐,也就是说对于每一个行,只有一个U或者V分量,如果一行是4:2:0的话,下一行就是4:0:2,再下一行是4:2:0…以此类推。

至于其他常见的YUV格式有YUY2、YUYV、YVYU、UYVY、AYUV、Y41P、Y411、Y211、IF09、IYUV、YV12、YVU9、YUV411、YUV420等。

YUY2及常见表示方法

YUY2(和YUYV)格式为像素保留Y,而UV在水平空间上相隔二个像素采样一次(Y0U0Y1V0),(Y2U2Y3V2)…其中,(Y0U0Y1V0)就是一个macro-pixel(宏像素),它表示了2个像素,(Y2U2Y3V2)是另外的2个像素。以此类推,再如:Y41P(和Y411)格式为每个像素保留Y分量,而UV分量在水平方向上每4个像素采样一次。一个宏像素为12个字节,实际表示8个像素。

图像数据中YUV分量排列顺序如下:(U0Y0V0Y1U4Y2V4Y3Y4Y5Y6Y7)

YVYUUYVY

YVYU,UYVY格式跟YUY2类似,只是排列顺序有所不同。Y211格式是Y每2个像素采样一次,而UV每4个像素采样一次。AYUV格式则有一Alpha通道。

YV12

YV12格式与IYUV类似,每个像素都提取Y,在UV提取时,将图像2x2的矩阵,每个矩阵提取一个U和一个V。YV12格式和I420格式的不同处在V平面和U平面的位置不同。在YV12格式中,V平面紧跟在Y平面之后,然后才是U平面(即:YVU);但I420则是相反(即:YUV)。NV12与YV12类似,效果一样,YV12中U和V是连续排列的,而在NV12中,U和V就交错排列的。

3.4 XYZ


XYZ模型

CIE 1931 XYZ色彩空间(也叫做CIE 1931色彩空间)是其中一个最先采用数学方式来定义的色彩空间,它由国际照明委员会(CIE)于1931年创立。

XYZ 色彩空间作用

XYZ 色彩空间是为了解决更精确地定义色彩而提出来的, XYZ 三个分量中, XY代表的是色度, 其中Y分量既可以代表亮度也可以代表色度, 三个分量的单位都是 cd/m2 , (或者叫做nit)。我们无法用RGB来精确定义颜色, 因为,不同的设备显示的RGB都是不一样的,不同的设备, 显示同一个RGB, 在人眼看出来是千差万别的, 如果我们用XYZ定义一个设备的色彩空间, 这样就精确多了!

转换公式

XYZ 转 RGB

 R = 3.2406 * X + -1.5372 * Y + -0.4986 * Z
 G = -0.9689 * X + 1.8758 * Y + 0.0415 * Z
 B = 0.0557 * X + -0.2040 * Y + 1.0570 * Z

RGB 转 XYZ

X = 0.4124 * R + 0.3576 * G + 0.1805 * B
Y = 0.2126 * R + 0.7152 * G + 0.0722 * B
Z = 0.0193 * R + 0.1192 * G + 0.9505 * B

3.5 LAB


Lab色彩模型是由照度(L)和有关色彩的a, b三个要素组成。L表示照度(Luminosity),相当于亮度,a表示从红色至绿色的范围,b表示从蓝色至黄色的范围。L的值域由0到100,L=50时,就相当于50%的黑;a和b的值域都是由+120至-120,其中+120 a就是红色,渐渐过渡到-120 a的时候就变成绿色;同样原理,+120 b是黄色,-120 b是蓝色。所有的颜色就以这三个值交互变化所组成。

详情请在网络上搜索。

3.6 CMYK


CMYK代表印刷上用的四种颜色,C代表青色(Cyan),M代表洋红色或者品红(Magenta),Y代表黄色(Yellow),K代表黑色(Black)。因为在实际应用中,青色、洋红色和黄色很难叠加形成真正的黑色,最多不过是褐色而已。因此才引入了K——黑色。黑色的作用是强化暗调,加深暗部色彩。

详情请在网络上搜索。