Skip to content

第14章:颜色科学

14.1 颜色物理

14.1.1 光与颜色

颜色是人类视觉系统对可见光波长的感知结果。理解颜色的物理基础对于正确处理数字图像至关重要。

可见光谱

电磁波谱中的可见光部分:

紫外线  ←────────── 可见光 ──────────→  红外线
        │                              │
        │  380nm  ────────────  780nm  │
        │                              │
        │   紫  蓝  青  绿  黄  橙  红   │
        │   ■   ■   ■   ■   ■   ■   ■   │
        │                              │


人眼的感光细胞:

视锥细胞(Cones):颜色视觉,明视觉
- S 型:短波长敏感(蓝色),峰值约 420nm
- M 型:中波长敏感(绿色),峰值约 530nm
- L 型:长波长敏感(红色),峰值约 560nm

视杆细胞(Rods):亮度视觉,暗视觉



响应  │    S      M    L
强度  │   ╱╲    ╱╲   ╱╲
      │  ╱  ╲  ╱  ╲ ╱  ╲
      │ ╱    ╲╱    ╲    ╲
      │╱            ╲    ╲
      └────────────────────────► 波长
       400   500   600   700 nm

14.1.2 色彩三要素

色彩的三个属性

1. 色相(Hue)
   - 颜色的基本属性
   - 红、橙、黄、绿、青、蓝、紫
   
   ┌──────────────────────────────┐
   │ 红 ─ 橙 ─ 黄 ─ 绿 ─ 蓝 ─ 紫 ─ 红 │
   └──────────────────────────────┘
   0°                           360°

2. 饱和度(Saturation)
   - 颜色的纯度/鲜艳程度
   - 从灰色到纯色
   
   灰 ●─────────────────────● 纯色
   0%                       100%

3. 亮度(Brightness/Value/Lightness)
   - 颜色的明暗程度
   - 从黑到白
   
   黑 ●─────────────────────● 白
   0%                       100%

14.2 颜色空间

14.2.1 RGB 颜色空间

RGB 颜色空间

基于加色混合(光的混合):

        R(红)




    G(绿)──●────── B(蓝)


RGB 立方体:

           白(1,1,1)
            ╱│
           ╱ │
    黄────●──│──── 青
         ╱   │    ╱
        ╱    │   ╱
       ╱     │  ╱
   红────────●────── 蓝
            ╱│
           ╱ │
    绿────●  │
         │   │
         │   │
         │   │
         ●───●
    (0,0,0) 黑


应用:
- 显示器(RGB)
- 数字图像
- 网页颜色

14.2.2 HSV/HSL 颜色空间

HSV(色相-饱和度-明度)

圆柱体模型:

         明度

         白 ●

           │  ╭────╮
           │ ╱      ╲
           │╱   S=1  ╲  ← 色相环
           ●──────────●
          ╱ ╲  S=0  ╱
         ╱   ╲    ╱
        ╱     ╲  ╱
       ●───────●



HSL vs HSV:

HSL(Lightness):
- L=0 是黑色
- L=0.5 是纯色
- L=1 是白色

HSV(Value):
- V=0 是黑色
- V=1 是纯色或白色
- 需要 S=0 且 V=1 才是白色


应用:
- 颜色选择器
- 图像编辑
- 色调调整

14.2.3 颜色空间转换

javascript
/**
 * 颜色空间转换
 */
class ColorSpace {
    /**
     * RGB 转 HSV
     * @param r, g, b [0, 255]
     * @returns {h, s, v} h [0, 360], s,v [0, 1]
     */
    static rgbToHsv(r, g, b) {
        r /= 255;
        g /= 255;
        b /= 255;
        
        const max = Math.max(r, g, b);
        const min = Math.min(r, g, b);
        const d = max - min;
        
        let h, s, v;
        v = max;
        
        if (max === 0) {
            s = 0;
        } else {
            s = d / max;
        }
        
        if (d === 0) {
            h = 0;
        } else if (max === r) {
            h = 60 * (((g - b) / d) % 6);
        } else if (max === g) {
            h = 60 * ((b - r) / d + 2);
        } else {
            h = 60 * ((r - g) / d + 4);
        }
        
        if (h < 0) h += 360;
        
        return { h, s, v };
    }
    
    /**
     * HSV 转 RGB
     */
    static hsvToRgb(h, s, v) {
        const c = v * s;
        const x = c * (1 - Math.abs((h / 60) % 2 - 1));
        const m = v - c;
        
        let r, g, b;
        
        if (h < 60) {
            [r, g, b] = [c, x, 0];
        } else if (h < 120) {
            [r, g, b] = [x, c, 0];
        } else if (h < 180) {
            [r, g, b] = [0, c, x];
        } else if (h < 240) {
            [r, g, b] = [0, x, c];
        } else if (h < 300) {
            [r, g, b] = [x, 0, c];
        } else {
            [r, g, b] = [c, 0, x];
        }
        
        return {
            r: Math.round((r + m) * 255),
            g: Math.round((g + m) * 255),
            b: Math.round((b + m) * 255)
        };
    }
    
    /**
     * RGB 转 HSL
     */
    static rgbToHsl(r, g, b) {
        r /= 255;
        g /= 255;
        b /= 255;
        
        const max = Math.max(r, g, b);
        const min = Math.min(r, g, b);
        const d = max - min;
        const l = (max + min) / 2;
        
        let h, s;
        
        if (d === 0) {
            h = 0;
            s = 0;
        } else {
            s = d / (1 - Math.abs(2 * l - 1));
            
            if (max === r) {
                h = 60 * (((g - b) / d) % 6);
            } else if (max === g) {
                h = 60 * ((b - r) / d + 2);
            } else {
                h = 60 * ((r - g) / d + 4);
            }
        }
        
        if (h < 0) h += 360;
        
        return { h, s, l };
    }
    
    /**
     * HSL 转 RGB
     */
    static hslToRgb(h, s, l) {
        const c = (1 - Math.abs(2 * l - 1)) * s;
        const x = c * (1 - Math.abs((h / 60) % 2 - 1));
        const m = l - c / 2;
        
        let r, g, b;
        
        if (h < 60) {
            [r, g, b] = [c, x, 0];
        } else if (h < 120) {
            [r, g, b] = [x, c, 0];
        } else if (h < 180) {
            [r, g, b] = [0, c, x];
        } else if (h < 240) {
            [r, g, b] = [0, x, c];
        } else if (h < 300) {
            [r, g, b] = [x, 0, c];
        } else {
            [r, g, b] = [c, 0, x];
        }
        
        return {
            r: Math.round((r + m) * 255),
            g: Math.round((g + m) * 255),
            b: Math.round((b + m) * 255)
        };
    }
    
    /**
     * RGB 转 CMYK
     */
    static rgbToCmyk(r, g, b) {
        r /= 255;
        g /= 255;
        b /= 255;
        
        const k = 1 - Math.max(r, g, b);
        
        if (k === 1) {
            return { c: 0, m: 0, y: 0, k: 1 };
        }
        
        const c = (1 - r - k) / (1 - k);
        const m = (1 - g - k) / (1 - k);
        const y = (1 - b - k) / (1 - k);
        
        return { c, m, y, k };
    }
    
    /**
     * CMYK 转 RGB
     */
    static cmykToRgb(c, m, y, k) {
        const r = 255 * (1 - c) * (1 - k);
        const g = 255 * (1 - m) * (1 - k);
        const b = 255 * (1 - y) * (1 - k);
        
        return {
            r: Math.round(r),
            g: Math.round(g),
            b: Math.round(b)
        };
    }
}

14.3 CIE 颜色系统

14.3.1 CIE XYZ

CIE XYZ 颜色空间(1931)

设备无关的颜色空间,基于人眼的色彩响应。

XYZ 的含义:
- Y:亮度(luminance)
- X、Z:色度信息

色度图(xy chromaticity diagram):

    y
    │      ╱─────────╲
  0.8│    ╱   绿      ╲
    │   ╱              ╲
  0.6│  │       ●       │
    │ │     白点      │
  0.4│  │              │
    │   ╲    红      ╱
  0.2│    ╲──────────╱
    │        蓝
  0 └──────────────────────► x
    0      0.2    0.4    0.6


马蹄形曲线:光谱轨迹(纯色)
内部点:可混合的颜色
三角形:特定 RGB 色域

14.3.2 CIE Lab

CIE Lab 颜色空间

感知均匀的颜色空间。

L*a*b* 的含义:
- L*:亮度(0-100)
- a*:绿(-) 到 红(+)
- b*:蓝(-) 到 黄(+)

         +a*(红)


   -b*(蓝) ┼───────────► +b*(黄)


         -a*(绿)


优点:
1. 感知均匀:相等的数值差对应相等的感知差
2. 设备无关
3. 适合颜色比较

应用:
- 颜色差异计算
- 色彩校正
- 图像处理

14.3.3 Lab 转换

javascript
/**
 * CIE 颜色空间转换
 */
class CIEColorSpace {
    // D65 白点(标准日光)
    static D65 = { X: 95.047, Y: 100.0, Z: 108.883 };
    
    /**
     * RGB 转 XYZ
     */
    static rgbToXyz(r, g, b) {
        // sRGB 线性化
        r = this.srgbToLinear(r / 255);
        g = this.srgbToLinear(g / 255);
        b = this.srgbToLinear(b / 255);
        
        // RGB 到 XYZ 矩阵(sRGB -> D65)
        const X = r * 0.4124564 + g * 0.3575761 + b * 0.1804375;
        const Y = r * 0.2126729 + g * 0.7151522 + b * 0.0721750;
        const Z = r * 0.0193339 + g * 0.1191920 + b * 0.9503041;
        
        return { X: X * 100, Y: Y * 100, Z: Z * 100 };
    }
    
    /**
     * XYZ 转 RGB
     */
    static xyzToRgb(X, Y, Z) {
        X /= 100;
        Y /= 100;
        Z /= 100;
        
        // XYZ 到 RGB 矩阵
        let r = X * 3.2404542 + Y * -1.5371385 + Z * -0.4985314;
        let g = X * -0.9692660 + Y * 1.8760108 + Z * 0.0415560;
        let b = X * 0.0556434 + Y * -0.2040259 + Z * 1.0572252;
        
        // 伽马校正
        r = this.linearToSrgb(r);
        g = this.linearToSrgb(g);
        b = this.linearToSrgb(b);
        
        return {
            r: Math.round(Math.max(0, Math.min(255, r * 255))),
            g: Math.round(Math.max(0, Math.min(255, g * 255))),
            b: Math.round(Math.max(0, Math.min(255, b * 255)))
        };
    }
    
    /**
     * XYZ 转 Lab
     */
    static xyzToLab(X, Y, Z) {
        const xn = X / this.D65.X;
        const yn = Y / this.D65.Y;
        const zn = Z / this.D65.Z;
        
        const fx = this.labF(xn);
        const fy = this.labF(yn);
        const fz = this.labF(zn);
        
        const L = 116 * fy - 16;
        const a = 500 * (fx - fy);
        const b = 200 * (fy - fz);
        
        return { L, a, b };
    }
    
    /**
     * Lab 转 XYZ
     */
    static labToXyz(L, a, b) {
        const fy = (L + 16) / 116;
        const fx = a / 500 + fy;
        const fz = fy - b / 200;
        
        const xn = this.labFInv(fx);
        const yn = this.labFInv(fy);
        const zn = this.labFInv(fz);
        
        return {
            X: xn * this.D65.X,
            Y: yn * this.D65.Y,
            Z: zn * this.D65.Z
        };
    }
    
    /**
     * RGB 转 Lab
     */
    static rgbToLab(r, g, b) {
        const xyz = this.rgbToXyz(r, g, b);
        return this.xyzToLab(xyz.X, xyz.Y, xyz.Z);
    }
    
    /**
     * Lab 转 RGB
     */
    static labToRgb(L, a, b) {
        const xyz = this.labToXyz(L, a, b);
        return this.xyzToRgb(xyz.X, xyz.Y, xyz.Z);
    }
    
    /**
     * 计算两个 Lab 颜色的差异(Delta E 2000)
     */
    static deltaE2000(lab1, lab2) {
        // 简化版 CIE Delta E 2000
        const dL = lab2.L - lab1.L;
        const da = lab2.a - lab1.a;
        const db = lab2.b - lab1.b;
        
        const C1 = Math.sqrt(lab1.a * lab1.a + lab1.b * lab1.b);
        const C2 = Math.sqrt(lab2.a * lab2.a + lab2.b * lab2.b);
        const dC = C2 - C1;
        
        const dH = Math.sqrt(da * da + db * db - dC * dC) || 0;
        
        const sL = 1;
        const sC = 1 + 0.045 * (C1 + C2) / 2;
        const sH = 1 + 0.015 * (C1 + C2) / 2;
        
        const dE = Math.sqrt(
            Math.pow(dL / sL, 2) +
            Math.pow(dC / sC, 2) +
            Math.pow(dH / sH, 2)
        );
        
        return dE;
    }
    
    // 辅助函数
    static srgbToLinear(c) {
        return c <= 0.04045 
            ? c / 12.92 
            : Math.pow((c + 0.055) / 1.055, 2.4);
    }
    
    static linearToSrgb(c) {
        return c <= 0.0031308 
            ? c * 12.92 
            : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
    }
    
    static labF(t) {
        const delta = 6 / 29;
        return t > Math.pow(delta, 3) 
            ? Math.pow(t, 1/3) 
            : t / (3 * delta * delta) + 4 / 29;
    }
    
    static labFInv(t) {
        const delta = 6 / 29;
        return t > delta 
            ? Math.pow(t, 3) 
            : 3 * delta * delta * (t - 4 / 29);
    }
}

14.4 颜色转换

14.4.1 常用颜色转换

javascript
/**
 * 颜色工具类
 */
class ColorUtils {
    /**
     * 十六进制转 RGB
     */
    static hexToRgb(hex) {
        hex = hex.replace('#', '');
        
        if (hex.length === 3) {
            hex = hex.split('').map(c => c + c).join('');
        }
        
        const num = parseInt(hex, 16);
        
        return {
            r: (num >> 16) & 255,
            g: (num >> 8) & 255,
            b: num & 255
        };
    }
    
    /**
     * RGB 转十六进制
     */
    static rgbToHex(r, g, b) {
        return '#' + [r, g, b]
            .map(x => x.toString(16).padStart(2, '0'))
            .join('');
    }
    
    /**
     * 颜色混合
     */
    static blend(color1, color2, ratio = 0.5) {
        return {
            r: Math.round(color1.r * (1 - ratio) + color2.r * ratio),
            g: Math.round(color1.g * (1 - ratio) + color2.g * ratio),
            b: Math.round(color1.b * (1 - ratio) + color2.b * ratio)
        };
    }
    
    /**
     * 计算相对亮度
     */
    static relativeLuminance(r, g, b) {
        const rsrgb = r / 255;
        const gsrgb = g / 255;
        const bsrgb = b / 255;
        
        const rlin = rsrgb <= 0.03928 
            ? rsrgb / 12.92 
            : Math.pow((rsrgb + 0.055) / 1.055, 2.4);
        const glin = gsrgb <= 0.03928 
            ? gsrgb / 12.92 
            : Math.pow((gsrgb + 0.055) / 1.055, 2.4);
        const blin = bsrgb <= 0.03928 
            ? bsrgb / 12.92 
            : Math.pow((bsrgb + 0.055) / 1.055, 2.4);
        
        return 0.2126 * rlin + 0.7152 * glin + 0.0722 * blin;
    }
    
    /**
     * 计算对比度
     */
    static contrastRatio(color1, color2) {
        const l1 = this.relativeLuminance(color1.r, color1.g, color1.b);
        const l2 = this.relativeLuminance(color2.r, color2.g, color2.b);
        
        const lighter = Math.max(l1, l2);
        const darker = Math.min(l1, l2);
        
        return (lighter + 0.05) / (darker + 0.05);
    }
    
    /**
     * 生成互补色
     */
    static complementary(r, g, b) {
        const hsv = ColorSpace.rgbToHsv(r, g, b);
        hsv.h = (hsv.h + 180) % 360;
        return ColorSpace.hsvToRgb(hsv.h, hsv.s, hsv.v);
    }
    
    /**
     * 生成三色组
     */
    static triadic(r, g, b) {
        const hsv = ColorSpace.rgbToHsv(r, g, b);
        
        return [
            ColorSpace.hsvToRgb(hsv.h, hsv.s, hsv.v),
            ColorSpace.hsvToRgb((hsv.h + 120) % 360, hsv.s, hsv.v),
            ColorSpace.hsvToRgb((hsv.h + 240) % 360, hsv.s, hsv.v)
        ];
    }
    
    /**
     * 生成类似色
     */
    static analogous(r, g, b, angle = 30) {
        const hsv = ColorSpace.rgbToHsv(r, g, b);
        
        return [
            ColorSpace.hsvToRgb((hsv.h - angle + 360) % 360, hsv.s, hsv.v),
            ColorSpace.hsvToRgb(hsv.h, hsv.s, hsv.v),
            ColorSpace.hsvToRgb((hsv.h + angle) % 360, hsv.s, hsv.v)
        ];
    }
}

14.5 Gamma 校正

14.5.1 Gamma 的概念

Gamma 校正

显示器的亮度响应是非线性的:

输出亮度 = 输入电压^γ

CRT 显示器的 γ ≈ 2.2


问题:
如果直接存储线性值,暗部会有色带:

线性存储(8位):

│    只有少数值
│    用于暗部
│ ╭────────────
│╱
└────────────────
 暗       亮


解决方案:sRGB 伽马编码

存储时:先做伽马压缩(γ=1/2.2)
显示时:显示器自带伽马扩展

结果:亮度分布更均匀


sRGB 传递函数:

编码(线性 → sRGB):
C_srgb = { 12.92 × C_lin,              如果 C_lin ≤ 0.0031308
         { 1.055 × C_lin^(1/2.4) - 0.055, 否则

解码(sRGB → 线性):
C_lin = { C_srgb / 12.92,               如果 C_srgb ≤ 0.04045
        { ((C_srgb + 0.055) / 1.055)^2.4, 否则

14.5.2 Gamma 校正实现

javascript
/**
 * Gamma 校正
 */
class GammaCorrection {
    /**
     * sRGB 伽马解码(sRGB -> 线性)
     */
    static srgbToLinear(value) {
        const c = value / 255;
        
        if (c <= 0.04045) {
            return c / 12.92;
        }
        return Math.pow((c + 0.055) / 1.055, 2.4);
    }
    
    /**
     * sRGB 伽马编码(线性 -> sRGB)
     */
    static linearToSrgb(value) {
        if (value <= 0.0031308) {
            return Math.round(value * 12.92 * 255);
        }
        return Math.round((1.055 * Math.pow(value, 1/2.4) - 0.055) * 255);
    }
    
    /**
     * 简单伽马校正
     */
    static gammaCorrect(value, gamma) {
        return Math.pow(value / 255, gamma) * 255;
    }
    
    /**
     * 图像伽马校正
     */
    static correctImage(image, gamma) {
        const result = image.clone();
        
        // 预计算查找表
        const lut = new Uint8Array(256);
        const invGamma = 1 / gamma;
        
        for (let i = 0; i < 256; i++) {
            lut[i] = Math.round(255 * Math.pow(i / 255, invGamma));
        }
        
        for (let i = 0; i < result.data.length; i++) {
            if (result.channels === 4 && (i + 1) % 4 === 0) continue;
            result.data[i] = lut[result.data[i]];
        }
        
        return result;
    }
    
    /**
     * 在线性空间进行颜色混合
     */
    static linearBlend(color1, color2, ratio) {
        // 转换到线性空间
        const lin1 = {
            r: this.srgbToLinear(color1.r),
            g: this.srgbToLinear(color1.g),
            b: this.srgbToLinear(color1.b)
        };
        
        const lin2 = {
            r: this.srgbToLinear(color2.r),
            g: this.srgbToLinear(color2.g),
            b: this.srgbToLinear(color2.b)
        };
        
        // 在线性空间混合
        const mixed = {
            r: lin1.r * (1 - ratio) + lin2.r * ratio,
            g: lin1.g * (1 - ratio) + lin2.g * ratio,
            b: lin1.b * (1 - ratio) + lin2.b * ratio
        };
        
        // 转回 sRGB
        return {
            r: this.linearToSrgb(mixed.r),
            g: this.linearToSrgb(mixed.g),
            b: this.linearToSrgb(mixed.b)
        };
    }
}

14.6 色彩管理

14.6.1 色彩管理系统

色彩管理

问题:不同设备显示相同颜色可能看起来不同

    相机          显示器         打印机
    ┌──┐          ┌──┐          ┌──┐
    │  │          │  │          │  │
    └──┘          └──┘          └──┘
    拍摄           编辑          输出
      │             │             │
      └─────────────┴─────────────┘
             如何保持颜色一致?


解决方案:ICC 色彩管理

设备 A ──► PCS(连接空间)──► 设备 B
        ICC Profile      ICC Profile

PCS = Profile Connection Space(Lab 或 XYZ)


工作流程:
1. 输入:设备空间 → PCS
2. 处理:在 PCS 中进行
3. 输出:PCS → 设备空间


色域映射(Gamut Mapping):

当源色域超出目标色域时:

  ┌─────────────────┐
  │    源色域        │
  │  ┌───────────┐  │
  │  │  目标色域  │  │
  │  │           │  │
  │  └───────────┘  │
  └─────────────────┘

策略:
- 感知:整体压缩
- 相对色度:裁剪
- 绝对色度:精确匹配
- 饱和度:保持饱和

14.6.2 色域检查

javascript
/**
 * 色域检查与映射
 */
class GamutMapping {
    // sRGB 色域边界(近似)
    static sRGB_GAMUT = {
        rPrimary: { x: 0.64, y: 0.33 },
        gPrimary: { x: 0.30, y: 0.60 },
        bPrimary: { x: 0.15, y: 0.06 }
    };
    
    /**
     * 检查 RGB 值是否在色域内
     */
    static isInGamut(r, g, b) {
        return r >= 0 && r <= 255 &&
               g >= 0 && g <= 255 &&
               b >= 0 && b <= 255;
    }
    
    /**
     * 裁剪到色域
     */
    static clipToGamut(r, g, b) {
        return {
            r: Math.max(0, Math.min(255, Math.round(r))),
            g: Math.max(0, Math.min(255, Math.round(g))),
            b: Math.max(0, Math.min(255, Math.round(b)))
        };
    }
    
    /**
     * 保持亮度的色域映射(感知映射)
     */
    static mapToGamut(r, g, b) {
        if (this.isInGamut(r, g, b)) {
            return { r: Math.round(r), g: Math.round(g), b: Math.round(b) };
        }
        
        // 转换到 HSL
        const hsl = ColorSpace.rgbToHsl(
            Math.max(0, Math.min(255, r)),
            Math.max(0, Math.min(255, g)),
            Math.max(0, Math.min(255, b))
        );
        
        // 降低饱和度直到在色域内
        let s = hsl.s;
        let step = s / 10;
        
        while (s > 0) {
            const rgb = ColorSpace.hslToRgb(hsl.h, s, hsl.l);
            if (this.isInGamut(rgb.r, rgb.g, rgb.b)) {
                return rgb;
            }
            s -= step;
        }
        
        // 回退到灰度
        const gray = Math.round(hsl.l * 255);
        return { r: gray, g: gray, b: gray };
    }
}

14.7 本章小结

颜色空间对比

空间用途特点
RGB显示、存储加色、设备相关
HSV/HSL编辑、选择直观
CMYK印刷减色
XYZ转换中间设备无关
Lab色差、处理感知均匀

关键转换

RGB ←→ HSV/HSL:颜色编辑
RGB ←→ XYZ:设备转换的中间步骤
XYZ ←→ Lab:感知均匀处理
RGB ←→ Linear:正确的颜色运算

关键要点

  1. RGB 是加色模型,适用于显示器
  2. HSV/HSL 更符合人类对颜色的认知
  3. CIE Lab 是感知均匀的颜色空间
  4. Gamma 校正确保正确的亮度显示
  5. 色彩管理通过 ICC Profile 保持颜色一致

下一章预告:在第15章中,我们将学习前沿技术与发展方向,包括光线追踪、WebGPU 和神经渲染。


文档版本:v1.0
字数统计:约 10,000 字

如有转载或 CV 的请标注本站原文地址