图像处理入门(第005期):颜色空间入门——RGB/HSV/CMYK/Lab

标签图像处理 颜色空间 RGB HSV OpenCV Python

系列:图像处理算法入门(每日20:00更新,更新至2026-12-31)

日期 :2026-08-14 | 难度 :入门 | 阅读时长:约15分钟


一、前言

颜色是图像最直观的属性,但"颜色"在计算机中的表示方式却远比人眼所见复杂。不同的应用场景需要不同的颜色描述方式:网页设计用 RGB,印刷出版用 CMYK,视频调色用 HSV,色彩科学研究用 Lab。理解这些颜色空间的定义、转换原理与适用边界,是图像处理工程师的基本功。

本期将系统梳理四种核心颜色空间------RGB、HSV、CMYK、Lab,从物理原理到数学公式,再到 Python 代码实践,帮助读者建立完整的颜色空间认知框架。


二、为什么需要多种颜色空间?

人眼通过三种视锥细胞感知颜色,但计算机存储、显示、处理图像时,面对的需求截然不同:

场景 核心需求 适宜颜色空间
屏幕显示、网页、拍照 发射光叠加 RGB
用户调色、颜色选择 直观感知(色相/饱和度/明度) HSV/HSL
印刷、喷墨、出版 颜料吸收光叠加 CMYK
色彩科学、色差计算 感知均匀性 Lab/Luv
视频压缩、传输 亮度与色度分离 YCbCr/YUV

关键洞察:没有"最好的"颜色空间,只有"最适合当前问题"的颜色空间。图像处理中,90% 的操作在 RGB 上完成,但剩余的 10% 需要在特定颜色空间才能获得正确结果。


三、RGB 颜色空间:加色法的数字基础

3.1 物理原理

RGB(Red, Green, Blue)基于加色法(Additive Color Mixing):将不同强度的红、绿、蓝三原色光叠加,产生各种颜色。这是发光设备(显示器、手机屏、投影仪)的工作基础。

3.2 数学表示

  • 每个通道取值范围: 0 , 255 0, 255 0,255(8位)或 0 , 1 0, 1 0,1(归一化)
  • 颜色向量: C = ( R , G , B ) \mathbf{C} = (R, G, B) C=(R,G,B)
  • 可表示颜色总数: 256 3 = 16 , 777 , 216 256^3 = 16,777,216 2563=16,777,216(约1677万色)

3.3 特性与局限

优点

  • 与硬件直接对应,计算简单
  • 三通道独立,便于并行处理

局限

  • 非感知均匀 : ( 200 , 0 , 0 ) (200, 0, 0) (200,0,0) 到 ( 200 , 1 , 0 ) (200, 1, 0) (200,1,0) 的视觉差异,远小于 ( 200 , 200 , 0 ) (200, 200, 0) (200,200,0) 到 ( 200 , 201 , 0 ) (200, 201, 0) (200,201,0) 的差异
  • 亮度信息分散在三个通道,调整亮度需同时修改 R、G、B
  • 不适合直接描述"色相""饱和度"等感知属性

3.4 代码实践:RGB 基础操作

python 复制代码
import cv2
import numpy as np
import matplotlib.pyplot as plt

# 设置中文字体(Windows环境)
plt.rcParams['font.sans-serif'] = ['SimHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False

def create_rgb_color_wheel(size=400):
    """
    创建RGB颜色轮可视化
    展示RGB三原色的叠加关系
    """
    wheel = np.zeros((size, size, 3), dtype=np.uint8)
    center = size // 2
    radius = size // 3
    
    for y in range(size):
        for x in range(size):
            dx, dy = x - center, y - center
            dist = np.sqrt(dx**2 + dy**2)
            if dist > radius * 1.5:
                continue
            
            angle = np.arctan2(dy, dx)  # -pi to pi
            # 将角度映射到RGB
            r = int(255 * (1 + np.cos(angle)) / 2)
            g = int(255 * (1 + np.cos(angle - 2*np.pi/3)) / 2)
            b = int(255 * (1 + np.cos(angle + 2*np.pi/3)) / 2)
            wheel[y, x] = [b, g, r]  # OpenCV使用BGR顺序
    
    return wheel

# 生成并展示颜色轮
wheel = create_rgb_color_wheel(300)

fig, axes = plt.subplots(2, 3, figsize=(12, 8))

# 原图
axes[0, 0].imshow(cv2.cvtColor(wheel, cv2.COLOR_BGR2RGB))
axes[0, 0].set_title('RGB颜色轮')
axes[0, 0].axis('off')

# 分离通道(OpenCV默认BGR)
b, g, r = cv2.split(wheel)
axes[0, 1].imshow(r, cmap='Reds')
axes[0, 1].set_title('R通道(红色)')
axes[0, 1].axis('off')

axes[0, 2].imshow(g, cmap='Greens')
axes[0, 2].set_title('G通道(绿色)')
axes[0, 2].axis('off')

axes[1, 0].imshow(b, cmap='Blues')
axes[1, 0].set_title('B通道(蓝色)')
axes[1, 0].axis('off')

# 合并对比:只保留R通道
r_only = np.zeros_like(wheel)
r_only[:, :, 2] = r  # BGR顺序,R在索引2
axes[1, 1].imshow(cv2.cvtColor(r_only, cv2.COLOR_BGR2RGB))
axes[1, 1].set_title('仅R通道显示')
axes[1, 1].axis('off')

# 灰度化(加权平均法)
gray = cv2.cvtColor(wheel, cv2.COLOR_BGR2GRAY)
axes[1, 2].imshow(gray, cmap='gray')
axes[1, 2].set_title('灰度转换(ITU-R BT.601)')
axes[1, 2].axis('off')

plt.tight_layout()
plt.savefig('005_rgb_color_wheel.png', dpi=150, bbox_inches='tight')
plt.show()
print("RGB颜色轮可视化已保存")

运行说明

  • 代码生成 RGB 颜色轮,展示三通道分离效果
  • 注意 OpenCV 默认使用 BGR 顺序,这是新手最常见的陷阱
  • cv2.cvtColor() 是颜色空间转换的核心函数

四、HSV 颜色空间:感知友好的圆柱模型

4.1 设计动机

RGB 对计算机友好,但对人类不友好。当你想"让图片更红一点"或"降低饱和度"时,在 RGB 上操作需要同时调整三个通道,且结果不可预测。

HSV(Hue, Saturation, Value)将颜色分解为三个感知属性

  • H(Hue,色相) :颜色的"种类",如红、黄、蓝。取值 0 , 360 ° 0, 360° 0,360°(OpenCV 中映射为 0 , 179 0, 179 0,179
  • S(Saturation,饱和度) :颜色的"纯度",0 为纯灰,100% 为纯色。取值 0 , 255 0, 255 0,255
  • V(Value,明度) :颜色的"亮度",0 为纯黑,100% 为最亮。取值 0 , 255 0, 255 0,255

4.2 几何模型

HSV 可可视化为一个六棱锥(或圆柱):

  • 中心轴:V(从黑到白)
  • 径向距离:S(从中心灰度到边缘纯色)
  • 环绕角度:H(色相环,0°=红,60°=黄,120°=绿,180°=青,240°=蓝,300°=品红)

4.3 RGB → HSV 转换公式

V = max ⁡ ( R , G , B ) V = \max(R, G, B) V=max(R,G,B)

S = { 0 if V = 0 V − min ⁡ ( R , G , B ) V otherwise S = \begin{cases} 0 & \text{if } V = 0 \\ \frac{V - \min(R,G,B)}{V} & \text{otherwise} \end{cases} S={0VV−min(R,G,B)if V=0otherwise

H = { 0 if S = 0 60 ° × G − B V − min ⁡ + 0 ° if V = R 60 ° × B − R V − min ⁡ + 120 ° if V = G 60 ° × R − G V − min ⁡ + 240 ° if V = B H = \begin{cases} 0 & \text{if } S = 0 \\ 60° \times \frac{G-B}{V-\min} + 0° & \text{if } V = R \\ 60° \times \frac{B-R}{V-\min} + 120° & \text{if } V = G \\ 60° \times \frac{R-G}{V-\min} + 240° & \text{if } V = B \end{cases} H=⎩ ⎨ ⎧060°×V−minG−B+0°60°×V−minB−R+120°60°×V−minR−G+240°if S=0if V=Rif V=Gif V=B

若 H < 0 H < 0 H<0,则 H ← H + 360 ° H \leftarrow H + 360° H←H+360°。

4.4 代码实践:RGB 与 HSV 互转

python 复制代码
def manual_rgb_to_hsv(r, g, b):
    """
    手动实现RGB到HSV转换(归一化[0,1]输入)
    返回H∈[0,360], S∈[0,1], V∈[0,1]
    """
    r, g, b = r / 255.0, g / 255.0, b / 255.0
    mx = max(r, g, b)
    mn = min(r, g, b)
    df = mx - mn
    
    if mx == mn:
        h = 0
    elif mx == r:
        h = (60 * ((g - b) / df) + 360) % 360
    elif mx == g:
        h = (60 * ((b - r) / df) + 120) % 360
    else:  # mx == b
        h = (60 * ((r - g) / df) + 240) % 360
    
    s = 0 if mx == 0 else df / mx
    v = mx
    
    return h, s, v


def manual_hsv_to_rgb(h, s, v):
    """
    手动实现HSV到RGB转换
    H∈[0,360], S∈[0,1], V∈[0,1]
    返回R,G,B∈[0,255]
    """
    c = v * s
    x = c * (1 - abs((h / 60) % 2 - 1))
    m = v - c
    
    if h < 60:
        r, g, b = c, x, 0
    elif h < 120:
        r, g, b = x, c, 0
    elif h < 180:
        r, g, b = 0, c, x
    elif h < 240:
        r, g, b = 0, x, c
    elif h < 300:
        r, g, b = x, 0, c
    else:
        r, g, b = c, 0, x
    
    r = int((r + m) * 255)
    g = int((g + m) * 255)
    b = int((b + m) * 255)
    return r, g, b


# 验证转换正确性
test_colors = [
    (255, 0, 0),    # 纯红
    (0, 255, 0),    # 纯绿
    (0, 0, 255),    # 纯蓝
    (255, 255, 0),  # 黄色
    (255, 0, 255),  # 品红
    (0, 255, 255),  # 青色
    (128, 128, 128),# 灰色
]

print("=" * 60)
print("手动RGB↔HSV转换验证")
print("=" * 60)
for rgb in test_colors:
    h, s, v = manual_rgb_to_hsv(*rgb)
    rgb_back = manual_hsv_to_rgb(h, s, v)
    print(f"RGB{rgb} → HSV({h:6.1f}°, {s:.2f}, {v:.2f}) → RGB{rgb_back}")

# 与OpenCV内置函数对比
print("\n" + "=" * 60)
print("与OpenCV cvtColor对比验证")
print("=" * 60)
for rgb in test_colors:
    # OpenCV使用BGR顺序,且H∈[0,179], S∈[0,255], V∈[0,255]
    bgr = np.uint8([[rgb[::-1]]])  # RGB转BGR,包装为1x1图像
    hsv_cv = cv2.cvtColor(bgr, cv2.COLOR_BGR2HSV)[0, 0]
    h, s, v = manual_rgb_to_hsv(*rgb)
    print(f"RGB{rgb}: 手动=({h:6.1f}°, {s*255:.0f}, {v*255:.0f}) | "
          f"OpenCV=({hsv_cv[0]*2:.0f}°, {hsv_cv[1]:.0f}, {hsv_cv[2]:.0f})")

输出解读

  • 手动实现与 OpenCV 结果一致,验证公式正确性
  • OpenCV 中 H 的范围是 0 , 179 0, 179 0,179(为了适配8位存储, 0.5 ° 0.5° 0.5° 精度),需 × 2 \times 2 ×2 还原为 0 , 360 ° 0, 360° 0,360°

4.5 实用场景:基于 HSV 的颜色分割

python 复制代码
def hsv_color_segmentation(image_path, target_hue, hue_range=15, min_sat=50, min_val=50):
    """
    基于HSV的特定颜色分割
    target_hue: 目标色相 [0, 360]
    """
    img = cv2.imread(image_path)
    if img is None:
        # 创建测试图像
        img = np.zeros((300, 400, 3), dtype=np.uint8)
        # 绘制多种颜色的圆
        colors = [(0, 0, 255), (0, 255, 0), (255, 0, 0), (0, 255, 255), (255, 255, 0)]
        for i, c in enumerate(colors):
            cv2.circle(img, (80 + i*70, 150), 50, c, -1)
    
    hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
    
    # OpenCV中H范围是[0,179]
    target_h = target_hue / 2
    lower_h = max(0, target_h - hue_range / 2)
    upper_h = min(179, target_h + hue_range / 2)
    
    lower = np.array([lower_h, min_sat, min_val])
    upper = np.array([upper_h, 255, 255])
    
    mask = cv2.inRange(hsv, lower, upper)
    result = cv2.bitwise_and(img, img, mask=mask)
    
    return img, mask, result


# 创建测试图像并分割红色
img, mask, result = hsv_color_segmentation(None, target_hue=0, hue_range=20)

fig, axes = plt.subplots(1, 3, figsize=(12, 4))
axes[0].imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
axes[0].set_title('原图(多色圆)')
axes[0].axis('off')

axes[1].imshow(mask, cmap='gray')
axes[1].set_title('HSV红色掩码')
axes[1].axis('off')

axes[2].imshow(cv2.cvtColor(result, cv2.COLOR_BGR2RGB))
axes[2].set_title('分割结果(仅红色)')
axes[2].axis('off')

plt.tight_layout()
plt.savefig('005_hsv_segmentation.png', dpi=150, bbox_inches='tight')
plt.show()
print("HSV颜色分割结果已保存")

关键洞察

  • HSV 分割对光照变化(V 通道)和饱和度变化具有一定鲁棒性
  • 在 OpenCV 中,红色跨越 H=0 边界,需要特殊处理(拆分为两段范围或使用 HSV 全空间)

五、CMYK 颜色空间:减色法的印刷基础

5.1 物理原理

CMYK(Cyan, Magenta, Yellow, Key/Black)基于减色法(Subtractive Color Mixing):颜料吸收(减去)部分波长,反射其余波长。这是印刷、绘画、染料的工作基础。

  • C(Cyan,青色):吸收红光,反射绿+蓝
  • M(Magenta,品红):吸收绿光,反射红+蓝
  • Y(Yellow,黄色):吸收蓝光,反射红+绿
  • K(Key/Black,黑色):三色素叠加深色不完美,单独黑色通道保证暗部纯度并节省彩色墨水

5.2 RGB → CMYK 转换公式

K = 1 − max ⁡ ( R ′ , G ′ , B ′ ) K = 1 - \max(R', G', B') K=1−max(R′,G′,B′)

C = 1 − R ′ − K 1 − K , M = 1 − G ′ − K 1 − K , Y = 1 − B ′ − K 1 − K C = \frac{1 - R' - K}{1 - K}, \quad M = \frac{1 - G' - K}{1 - K}, \quad Y = \frac{1 - B' - K}{1 - K} C=1−K1−R′−K,M=1−K1−G′−K,Y=1−K1−B′−K

其中 R ′ , G ′ , B ′ ∈ 0 , 1 R', G', B' \in 0, 1 R′,G′,B′∈0,1 为归一化 RGB。若 K = 1 K = 1 K=1(纯黑),则 C = M = Y = 0 C = M = Y = 0 C=M=Y=0。

5.3 为什么印刷需要 CMYK?

对比维度 RGB(屏幕) CMYK(印刷)
色域范围 宽(约1677万色) 窄(油墨物理限制)
黑色生成 三通道叠加 = 深灰 专用 K 通道 = 纯黑
暗部表现 容易 三色素叠加深色发污
成本 无额外成本 彩色墨水比黑墨贵3-5倍

关键洞察:直接印刷 RGB 图像会导致颜色失真、暗部发灰、成本浪费。专业出版流程必须包含 RGB→CMYK 转换环节。

5.4 代码实践:RGB 与 CMYK 互转

python 复制代码
def rgb_to_cmyk(r, g, b, scale=100):
    """
    RGB转CMYK
    输入: r,g,b ∈ [0, 255]
    输出: c,m,y,k ∈ [0, scale](默认百分比)
    """
    r_, g_, b_ = r / 255.0, g / 255.0, b / 255.0
    k = 1 - max(r_, g_, b_)
    
    if k == 1:  # 纯黑
        c = m = y = 0
    else:
        c = (1 - r_ - k) / (1 - k)
        m = (1 - g_ - k) / (1 - k)
        y = (1 - b_ - k) / (1 - k)
    
    return c * scale, m * scale, y * scale, k * scale


def cmyk_to_rgb(c, m, y, k, scale=100):
    """
    CMYK转RGB
    输入: c,m,y,k ∈ [0, scale]
    输出: r,g,b ∈ [0, 255]
    """
    c_, m_, y_, k_ = c / scale, m / scale, y / scale, k / scale
    r = 255 * (1 - c_) * (1 - k_)
    g = 255 * (1 - m_) * (1 - k_)
    b = 255 * (1 - y_) * (1 - k_)
    return int(r), int(g), int(b)


# 验证:印刷三原色
print("=" * 60)
print("RGB ↔ CMYK 转换验证")
print("=" * 60)
test_rgb = [
    (255, 0, 0),    # 红 → 应高M+Y
    (0, 255, 0),    # 绿 → 应高C+Y
    (0, 0, 255),    # 蓝 → 应高C+M
    (0, 0, 0),      # 黑 → K=100%
    (255, 255, 255),# 白 → CMYK=0
    (128, 128, 128),# 灰 → 高K,低CMY
]

for rgb in test_rgb:
    cmyk = rgb_to_cmyk(*rgb)
    rgb_back = cmyk_to_rgb(*cmyk)
    print(f"RGB{rgb} → CMYK({cmyk[0]:5.1f}%, {cmyk[1]:5.1f}%, {cmyk[2]:5.1f}%, {cmyk[3]:5.1f}%) → RGB{rgb_back}")


# 可视化:RGB vs CMYK 分解
fig, axes = plt.subplots(2, 4, figsize=(14, 6))

def show_cmyk_decomposition(rgb_color, ax_row):
    """展示某一颜色的CMYK分解"""
    c, m, y, k = rgb_to_cmyk(*rgb_color)
    
    # 创建色块
    height, width = 100, 100
    
    # 原色
    rgb_block = np.full((height, width, 3), rgb_color[::-1], dtype=np.uint8)  # BGR
    ax_row[0].imshow(cv2.cvtColor(rgb_block, cv2.COLOR_BGR2RGB))
    ax_row[0].set_title(f'RGB\n{rgb_color}')
    ax_row[0].axis('off')
    
    # C通道
    c_only = (255 * (1 - c/100), 255, 255)  # 减红 = 青
    c_block = np.full((height, width, 3), (255, 255, 255), dtype=np.uint8)
    c_block[:, :] = (int(255 * (1 - c/100)), 255, 255)  # BGR近似
    ax_row[1].imshow(cv2.cvtColor(c_block, cv2.COLOR_BGR2RGB))
    ax_row[1].set_title(f'C={c:.1f}%')
    ax_row[1].axis('off')
    
    # M通道
    m_block = np.full((height, width, 3), (255, int(255 * (1 - m/100)), 255), dtype=np.uint8)
    ax_row[2].imshow(cv2.cvtColor(m_block, cv2.COLOR_BGR2RGB))
    ax_row[2].set_title(f'M={m:.1f}%')
    ax_row[2].axis('off')
    
    # Y通道
    y_block = np.full((height, width, 3), (255, 255, int(255 * (1 - y/100))), dtype=np.uint8)
    ax_row[3].imshow(cv2.cvtColor(y_block, cv2.COLOR_BGR2RGB))
    ax_row[3].set_title(f'Y={y:.1f}%, K={k:.1f}%')
    ax_row[3].axis('off')

show_cmyk_decomposition((255, 0, 0), axes[0])  # 红色
show_cmyk_decomposition((0, 255, 0), axes[1])    # 绿色

plt.suptitle('RGB 颜色的 CMYK 分解示意', fontsize=14)
plt.tight_layout()
plt.savefig('005_cmyk_decomposition.png', dpi=150, bbox_inches='tight')
plt.show()
print("CMYK分解可视化已保存")

注意:CMYK 色彩管理涉及 ICC 特性文件、网点增益、纸张白度等复杂因素,上述简化公式仅用于理解原理。实际印刷需使用专业色彩管理引擎(如 Little-CMS)。


六、Lab 颜色空间:感知均匀的色彩科学标准

6.1 设计目标

Lab(也称 CIELAB、La b*)由国际照明委员会(CIE)于1976年制定,核心目标是感知均匀性(Perceptual Uniformity):

在 Lab 空间中,任意两个颜色之间的欧氏距离,与人类的视觉感知差异成正比。

这在色差计算、色彩质量管理、图像质量评估中至关重要。

6.2 三个通道的含义

  • L(Lightness,明度) *: 0 , 100 0, 100 0,100,0=纯黑,100=纯白(理论上可超100,如荧光)
  • *a(绿-红轴)**:负值=绿,正值=红
  • *b(蓝-黄轴)**:负值=蓝,正值=黄

Lab 的设计使得:

  • Δ E = ( Δ L ) 2 + ( Δ a ) 2 + ( Δ b ) 2 \Delta E = \sqrt{(\Delta L)^2 + (\Delta a)^2 + (\Delta b)^2} ΔE=(ΔL)2+(Δa)2+(Δb)2 能合理反映人眼感知色差
  • Δ E < 1 \Delta E < 1 ΔE<1:人眼几乎无法区分
  • Δ E = 1 − 2 \Delta E = 1-2 ΔE=1−2:受过训练的人可察觉
  • Δ E > 5 \Delta E > 5 ΔE>5:明显差异

6.3 转换路径:RGB → XYZ → Lab

Lab 不是直接从 RGB 转换,而是基于 CIE XYZ 色度空间:

  1. RGB → XYZ:通过3×3矩阵变换,依赖于具体 RGB 色域(sRGB、Adobe RGB、DCI-P3 等)
  2. XYZ → Lab:非线性变换,模拟人眼亮度的非线性响应

L ∗ = 116 ⋅ f ( Y / Y n ) − 16 L^* = 116 \cdot f(Y/Y_n) - 16 L∗=116⋅f(Y/Yn)−16

a ∗ = 500 ⋅ f ( X / X n ) − f ( Y / Y n ) a^* = 500 \cdot f(X/X_n) - f(Y/Y_n) a∗=500⋅f(X/Xn)−f(Y/Yn)

b ∗ = 200 ⋅ f ( Y / Y n ) − f ( Z / Z n ) b^* = 200 \cdot f(Y/Y_n) - f(Z/Z_n) b∗=200⋅f(Y/Yn)−f(Z/Zn)

其中:

f ( t ) = { t 1 / 3 t > ( 6 / 29 ) 3 1 3 ( 29 / 6 ) 2 t + 16 / 116 otherwise f(t) = \begin{cases} t^{1/3} & t > (6/29)^3 \\ \frac{1}{3}(29/6)^2 t + 16/116 & \text{otherwise} \end{cases} f(t)={t1/331(29/6)2t+16/116t>(6/29)3otherwise

X n , Y n , Z n X_n, Y_n, Z_n Xn,Yn,Zn 为参考白点的三刺激值(D65 标准光源下 X n = 95.047 , Y n = 100.0 , Z n = 108.883 X_n=95.047, Y_n=100.0, Z_n=108.883 Xn=95.047,Yn=100.0,Zn=108.883)。

6.4 代码实践:Lab 颜色空间与色差计算

python 复制代码
def rgb_to_xyz(r, g, b):
    """sRGB → XYZ (D65)"""
    def gamma_inv(c):
        c = c / 255.0
        return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4
    
    r_, g_, b_ = gamma_inv(r), gamma_inv(g), gamma_inv(b)
    
    # sRGB D65 转换矩阵
    x = r_ * 0.4124564 + g_ * 0.3575761 + b_ * 0.1804375
    y = r_ * 0.2126729 + g_ * 0.7151522 + b_ * 0.0721750
    z = r_ * 0.0193339 + g_ * 0.1191920 + b_ * 0.9503041
    
    return x * 100, y * 100, z * 100


def xyz_to_lab(x, y, z):
    """XYZ → Lab (D65)"""
    xn, yn, zn = 95.047, 100.0, 108.883
    
    def f(t):
        delta = 6/29
        return t ** (1/3) if t > delta**3 else t / (3 * delta**2) + 4/29
    
    l = 116 * f(y / yn) - 16
    a = 500 * (f(x / xn) - f(y / yn))
    b = 200 * (f(y / yn) - f(z / zn))
    
    return l, a, b


def rgb_to_lab(r, g, b):
    """RGB → Lab 完整转换"""
    x, y, z = rgb_to_xyz(r, g, b)
    return xyz_to_lab(x, y, z)


def delta_e_cie76(lab1, lab2):
    """CIE76 色差公式(欧氏距离)"""
    return np.sqrt(sum((a - b) ** 2 for a, b in zip(lab1, lab2)))


# 验证:计算常见颜色的Lab值
print("=" * 60)
print("RGB → Lab 转换与色差计算")
print("=" * 60)
colors = {
    '纯红': (255, 0, 0),
    '纯绿': (0, 255, 0),
    '纯蓝': (0, 0, 255),
    '纯白': (255, 255, 255),
    '纯黑': (0, 0, 0),
    '中灰': (128, 128, 128),
    '肤色1': (255, 224, 189),
    '肤色2': (255, 205, 148),  # 稍深的肤色
}

lab_values = {}
for name, rgb in colors.items():
    lab = rgb_to_lab(*rgb)
    lab_values[name] = lab
    print(f"{name:8s}: RGB{rgb} → Lab({lab[0]:7.2f}, {lab[1]:7.2f}, {lab[2]:7.2f})")

# 计算色差
print("\n" + "=" * 60)
print("色差分析(ΔE)")
print("=" * 60)
print(f"红 vs 绿:   ΔE = {delta_e_cie76(lab_values['纯红'], lab_values['纯绿']):.2f}")
print(f"白 vs 黑:   ΔE = {delta_e_cie76(lab_values['纯白'], lab_values['纯黑']):.2f}")
print(f"肤色1 vs 2: ΔE = {delta_e_cie76(lab_values['肤色1'], lab_values['肤色2']):.2f}")
print(f"红 vs 肤色1: ΔE = {delta_e_cie76(lab_values['纯红'], lab_values['肤色1']):.2f}")


# 使用OpenCV的Lab转换对比
print("\n" + "=" * 60)
print("手动实现 vs OpenCV Lab转换对比")
print("=" * 60)
for name, rgb in colors.items():
    manual = rgb_to_lab(*rgb)
    # OpenCV: RGB → Lab(注意OpenCV默认BGR,需要用COLOR_RGB2Lab)
    rgb_arr = np.uint8([[list(rgb)]])
    cv_lab = cv2.cvtColor(rgb_arr, cv2.COLOR_RGB2Lab)[0, 0]
    # OpenCV输出范围: L∈[0,100], a∈[0,255]映射到[-128,127], b∈[0,255]映射到[-128,127]
    cv_l = cv_lab[0] * 100 / 255  # 注意:OpenCV的L范围是[0,255]映射到[0,100]
    cv_a = cv_lab[1] - 128
    cv_b = cv_lab[2] - 128
    print(f"{name:8s}: 手动=({manual[0]:6.2f}, {manual[1]:6.2f}, {manual[2]:6.2f}) | "
          f"OpenCV=({cv_l:6.2f}, {cv_a:6.2f}, {cv_b:6.2f})")


# 可视化:Lab颜色空间切片
fig, axes = plt.subplots(1, 3, figsize=(15, 5))

# L=50时的ab平面
L_fix = 50
a_range = np.linspace(-128, 127, 256)
b_range = np.linspace(-128, 127, 256)
a_grid, b_grid = np.meshgrid(a_range, b_range)

# 将Lab转回RGB用于显示(简化近似,使用OpenCV)
lab_slice = np.zeros((256, 256, 3), dtype=np.uint8)
lab_slice[:, :, 0] = int(L_fix * 255 / 100)  # L
lab_slice[:, :, 1] = (a_grid + 128).astype(np.uint8)  # a
lab_slice[:, :, 2] = (b_grid + 128).astype(np.uint8)  # b
rgb_slice = cv2.cvtColor(lab_slice, cv2.COLOR_Lab2RGB)

axes[0].imshow(rgb_slice, extent=[-128, 127, -128, 127], origin='lower')
axes[0].set_xlabel('a* (绿 → 红)')
axes[0].set_ylabel('b* (蓝 → 黄)')
axes[0].set_title(f'Lab颜色空间切片 (L*={L_fix})')

# a=0时的Lb平面
a_fix = 0
L_range = np.linspace(0, 100, 256)
b_range = np.linspace(-128, 127, 256)
L_grid, b_grid = np.meshgrid(L_range, b_range)

lab_slice2 = np.zeros((256, 256, 3), dtype=np.uint8)
lab_slice2[:, :, 0] = (L_grid * 255 / 100).astype(np.uint8)
lab_slice2[:, :, 1] = 128  # a=0
lab_slice2[:, :, 2] = (b_grid + 128).astype(np.uint8)
rgb_slice2 = cv2.cvtColor(lab_slice2, cv2.COLOR_Lab2RGB)

axes[1].imshow(rgb_slice2, extent=[0, 100, -128, 127], origin='lower')
axes[1].set_xlabel('L* (黑 → 白)')
axes[1].set_ylabel('b* (蓝 → 黄)')
axes[1].set_title(f'Lab颜色空间切片 (a*={a_fix})')

# b=0时的La平面
b_fix = 0
L_range = np.linspace(0, 100, 256)
a_range = np.linspace(-128, 127, 256)
L_grid, a_grid = np.meshgrid(L_range, a_range)

lab_slice3 = np.zeros((256, 256, 3), dtype=np.uint8)
lab_slice3[:, :, 0] = (L_grid * 255 / 100).astype(np.uint8)
lab_slice3[:, :, 1] = (a_grid + 128).astype(np.uint8)
lab_slice3[:, :, 2] = 128  # b=0
rgb_slice3 = cv2.cvtColor(lab_slice3, cv2.COLOR_Lab2RGB)

axes[2].imshow(rgb_slice3, extent=[0, 100, -128, 127], origin='lower')
axes[2].set_xlabel('L* (黑 → 白)')
axes[2].set_ylabel('a* (绿 → 红)')
axes[2].set_title(f'Lab颜色空间切片 (b*={b_fix})')

plt.tight_layout()
plt.savefig('005_lab_color_space.png', dpi=150, bbox_inches='tight')
plt.show()
print("Lab颜色空间切片可视化已保存")

关键洞察

  • Lab 的感知均匀性使 Δ E \Delta E ΔE 成为工业色差标准(ISO/ASTM)
  • 高级色差公式(CIE94、CIEDE2000)在 CIE76 基础上加入了人眼对色度/亮度的不同敏感度加权
  • OpenCV 的 Lab 实现使用 D65 白点,但 L、a、b 的存储范围做了 8 位量化映射

七、四种颜色空间对比总结

维度 RGB HSV CMYK Lab
基础原理 加色法(光叠加) 感知属性分解 减色法(颜料叠加) 人眼感知均匀性
主要应用 显示、存储、计算 调色、分割、UI 印刷、出版 色差、质量评估、色彩科学
通道含义 物理光强 色相/饱和度/明度 油墨覆盖率 明度/绿红/蓝黄
通道相关性 高(三通道互相关) 低(感知解耦) 高(K依赖CMY) 低(近似正交)
感知均匀性 中等(V非均匀)
硬件友好性 最优 中等 印刷专用 差(需转换)
OpenCV支持 原生 cvtColor 需自行实现 cvtColor

八、产品推荐:从移动端到专业级

8.1 移动端产品(iOS/Android)

产品 平台 核心功能 颜色空间相关能力
Canva iOS/Android/Web 平面设计、模板设计 支持 HEX/RGB/HSV/CMYK 颜色输入,印刷输出自动转换
Snapseed iOS/Android 专业照片编辑 白平衡(色温/色调)、HSL selective调整、曲线
VSCO iOS/Android 胶片风格滤镜 基于 HSL 的精细调色,HSL 分离编辑
Procreate iPadOS 数字绘画 完整的颜色面板,支持 RGB/HSB/CMYK/Hex,颜色调和算法
PicsArt iOS/Android 创意编辑与合成 颜色替换工具、基于色相的选择性调整

8.2 专业级产品(桌面/企业)

产品 平台 核心功能 颜色空间相关能力
DaVinci Resolve Win/Mac/Linux 专业视频调色 完整色彩科学管线,支持 RCM 色彩管理、Lab 曲线、HDR 分级
Adobe Photoshop Win/Mac 图像编辑标杆 完整的颜色空间支持(32位/16位/8位),ICC 色彩管理,Lab 模式编辑
Adobe Illustrator Win/Mac 矢量设计 印刷色管理,Pantone 色库,CMYK 分色预览
Affinity Photo Win/Mac/iPad Photoshop 替代方案 16位/32位通道,ProPhoto RGB 工作空间,完整的 ICC 管理
Capture One Win/Mac RAW 处理与摄影工作流 行业领先的色彩引擎,自定义 ICC 特性文件,精确的色彩编辑工具
3D Slicer Win/Mac/Linux 医学影像处理 DICOM 色彩映射、伪彩色 LUT、窗宽窗位(基于灰度窗技术)
MATLAB Image Processing Toolbox Win/Mac/Linux 科研图像处理 完整的颜色空间转换函数、色彩科学计算、色差分析
GIMP 跨平台 开源图像编辑 支持 RGB/HSV/CMYK/Lab,颜色管理(Little-CMS),免费

8.3 开源工具库(开发者)

库/工具 语言 颜色空间支持 适用场景
OpenCV C++/Python RGB/BGR/HSV/HLS/Lab/Luv/YCbCr/XYZ 计算机视觉、实时处理
Pillow Python RGB/HSV/CMYK/Lab/YCbCr 图像读写、基础处理
colour-science Python 全色度学支持(CIE全系、ICC、光谱) 科研级色彩计算
Little-CMS C ICC 色彩管理 印刷级色彩管理引擎
colorio Python CIELAB、CAM16、Oklab 等 现代感知均匀色空间研究

九、常见问题与排查

问题 原因 解决方案
OpenCV 读取后颜色偏蓝/偏红 OpenCV 默认使用 BGR 顺序,而 matplotlib 期望 RGB 使用 cv2.cvtColor(img, cv2.COLOR_BGR2RGB) 转换
HSV 分割红色时结果不完整 红色跨越 H=0° 边界(在 OpenCV 中跨越 H=0/179) 拆分为两个范围:[0,10][170,179],或转换到 HSV 全空间
CMYK 转换后黑色不纯 简化公式未考虑灰成分替代(GCR)或底色去除(UCR) 专业场景使用 ICC 特性文件 + Little-CMS
Lab 色差与人眼感知不一致 CIE76 公式在强色度区不够精确 使用 CIE94 或 CIEDE2000 公式
屏幕显示与印刷输出色差大 RGB 色域与 CMYK 色域不匹配 使用色彩管理(ICC Profile),在 Photoshop 中开启 CMYK 预览

十、扩展练习

  1. 色相环生成器:修改 RGB 颜色轮代码,生成 HSV 色相环(固定 S=100%, V=100%),对比 RGB 与 HSV 色相环的视觉差异。

  2. 肤色检测器:基于 HSV 颜色空间,实现一个简单的肤色检测器。提示:人类肤色在 HSV 中大致为 H∈0,50,S∈0.2,0.6,V∈0.4,1.0

  3. CMYK 分色模拟器:编写一个程序,将 RGB 图像转换为 CMYK 四通道分色图,可视化每一通道的灰度分布,理解印刷分色的原理。


十一、总结

本期系统梳理了四种核心颜色空间:

  • RGB:计算机的基础,发光设备的原生语言,但非感知均匀
  • HSV:人类直觉的映射,调色与分割的首选,但需注意 H 的环形边界
  • CMYK:印刷行业的标准,减色法的物理实现,涉及专业的色彩管理流程
  • Lab:色彩科学的度量衡,感知均匀是其最大价值,色差计算的核心空间

掌握颜色空间转换,是图像处理从"能用"到"精确"的关键跨越。下期(第006期)将聚焦图像质量评价指标------PSNR、SSIM、MSE,学习如何用数学量化图像处理的效果。


系列导航

源码与数据 :本文完整代码见 code/005-color-spaces/ 目录

版权声明:本文版权归作者所有,转载请注明出处。代码示例遵循 MIT 协议。


参考资源

  • 1 CIE 15:2014, Colorimetry, 4th Edition
  • 2 OpenCV 官方文档: cv2.cvtColor 函数说明
  • 3 Lindbloom, B. (2001). RGB/XYZ Matrices. brucelindbloom.com
  • 4 Sharma, G., & Bala, R. (2017). Digital Color Imaging Handbook. CRC Press.
  • 5 Poynton, C. (2012). Digital Video and HD: Algorithms and Interfaces. Elsevier.
相关推荐
风流 少年1 小时前
Spring AI 2.0:Tool
java·python·spring
郝学胜-神的一滴1 小时前
干货版《算法导论》17:二叉树核心原理、遍历逻辑与高阶实操全解
数据结构·c++·python·算法·计算机·编程
WangY_ZQ1 小时前
【无标题】
windows·python
桐桐桐1 小时前
PDF 翻译工具横评与自建批量方案:从在线工具到 Python 自动化
python·pdf·自动化
evans在进步2 小时前
LeetCode 33:搜索旋转排序数组——Java 两阶段二分查找详解
java·python·leetcode
言乐62 小时前
Python游戏水平测试辅助系统2
开发语言·windows·python·游戏·django
jerryinwuhan4 小时前
《Python数据预处理》第四章
开发语言·python
雨晨源码(同名B站)4 小时前
基于Python的网易云音乐评论数据情感化分析系统 音乐爬虫信息可视化 |SnowNLP评论情感分析
开发语言·hadoop·爬虫·python·信息可视化·毕业设计
阿童木写作4 小时前
跨境电商图片翻译工具推荐:跨马翻译批量处理视频字幕与抠图
python·音视频