第14章:颜色科学
14.1 颜色物理
14.1.1 光与颜色
颜色是人类视觉系统对可见光波长的感知结果。理解颜色的物理基础对于正确处理数字图像至关重要。
可见光谱
电磁波谱中的可见光部分:
紫外线 ←────────── 可见光 ──────────→ 红外线
│ │
│ 380nm ──────────── 780nm │
│ │
│ 紫 蓝 青 绿 黄 橙 红 │
│ ■ ■ ■ ■ ■ ■ ■ │
│ │
人眼的感光细胞:
视锥细胞(Cones):颜色视觉,明视觉
- S 型:短波长敏感(蓝色),峰值约 420nm
- M 型:中波长敏感(绿色),峰值约 530nm
- L 型:长波长敏感(红色),峰值约 560nm
视杆细胞(Rods):亮度视觉,暗视觉
│
响应 │ S M L
强度 │ ╱╲ ╱╲ ╱╲
│ ╱ ╲ ╱ ╲ ╱ ╲
│ ╱ ╲╱ ╲ ╲
│╱ ╲ ╲
└────────────────────────► 波长
400 500 600 700 nm14.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:正确的颜色运算关键要点
- RGB 是加色模型,适用于显示器
- HSV/HSL 更符合人类对颜色的认知
- CIE Lab 是感知均匀的颜色空间
- Gamma 校正确保正确的亮度显示
- 色彩管理通过 ICC Profile 保持颜色一致
下一章预告:在第15章中,我们将学习前沿技术与发展方向,包括光线追踪、WebGPU 和神经渲染。
文档版本:v1.0
字数统计:约 10,000 字
