【图像处理】颜色空间——RGB之外的世界

RGB 是相机记录颜色的方式,不是人类感知颜色的方式。 当你说"把这张图调得更鲜艳一点",你的意思是什么? RGB 不知道;HSV 知道;Lab 更知道。


一、RGB 的局限性:耦合的颜色表示

RGB 颜色空间用三个分量(Red、Green、Blue)直接描述光的组成。

问题 :RGB 三个通道是高度耦合的,颜色编辑极为不直观。

ini 复制代码
例:把一个橙色(R=255, G=128, B=0)调得"更亮":

目标:保持色相(橙)不变,只提高亮度

在 RGB 中怎么做?
  同等比例增加 R/G/B?→ (255, 128, 0) × 1.2 = (306, 154, 0)
  但 R 已经超出 255!需要截断 → (255, 154, 0)
  这改变了 R/G 的比例,也改变了色相(颜色偏黄了)

在 HSV 中怎么做?
  直接修改 V 值:V: 1.0 → 0.8(调暗)或配合曝光
  H 和 S 不变,色相和饱和度保持不变
  完美解耦!
操作 RGB 实现难度 HSV/Lab 实现难度
旋转色相 极难(需要矩阵变换) 简单(H += angle)
调整饱和度 难(S 与三通道均相关) 简单(S × factor)
调整亮度 中等(会影响色相) 简单(V × factor)
感知均匀的色差计算 不可靠 Lab 的 ΔE 公式

二、HSV 颜色空间:六边形锥体模型

HSV 用三个直觉化的分量描述颜色:

  • H(Hue,色相):颜色的"种类"(0°--360°)
  • S(Saturation,饱和度):颜色的"鲜艳程度"(0--1)
  • V(Value,明度):颜色的"明亮程度"(0--1)
ini 复制代码
几何模型(倒锥体):
         白色(S=0, V=1)
        ╱─────────────╲
       ╱ 红  黄  绿  青  ╲   ← 锥体顶面:高饱和色(V=1, S=1)
      ╱      蓝  品红      ╲
     ╱                      ╲
    ╲        H=0°(红)      ╱
     ╲    沿圆周旋转 H 角度  ╱
      ╲                    ╱
       ╲──────────────────╱
              黑色(V=0,任意 H/S)

各分量的直觉含义

ini 复制代码
H=0°,   S=1, V=1 → 纯红
H=60°,  S=1, V=1 → 纯黄
H=120°, S=1, V=1 → 纯绿
H=180°, S=1, V=1 → 纯青
H=240°, S=1, V=1 → 纯蓝
H=300°, S=1, V=1 → 纯品红

H=任意, S=0, V=1 → 白色(饱和度为 0 → 无色)
H=任意, S=任意, V=0 → 黑色(明度为 0)

三、RGB → HSV 转换推导

r, g, b ∈ [0, 1](归一化后的 RGB 值),定义:

ini 复制代码
M = max(r, g, b)   最大分量(决定明度)
m = min(r, g, b)   最小分量
C = M - m          色度(Chroma,范围跨度)

明度 V(最简单):

ini 复制代码
V = M

饱和度 S

ini 复制代码
S = C / M     (若 M > 0)
S = 0         (若 M = 0,即纯黑,无色相)

色相 H(六扇区分类):

ini 复制代码
M = r 时(色相在红色附近,300°--60°):
  H' = (g - b) / C
  H' 范围:-1 to +1

M = g 时(色相在绿色附近,60°--180°):
  H' = (b - r) / C + 2

M = b 时(色相在蓝色附近,180°--300°):
  H' = (r - g) / C + 4

最终 H = H' × 60°(转为度数)
若 H < 0:H += 360°

为什么是六扇区?

色相圆盘被 R/G/B 三个主色和 Y/C/M 三个补色分成 6 个 60° 区间。每个区间内,主导分量是 M(最大者),次主导分量线性插值产生过渡色相。六扇区分类确保 H 在正确的 0°--360° 范围内。

Swift 实现

swift 复制代码
func rgbToHSV(r: UInt8, g: UInt8, b: UInt8) -> (h: Double, s: Double, v: Double) {
    let rf = Double(r) / 255.0
    let gf = Double(g) / 255.0
    let bf = Double(b) / 255.0

    let M = max(rf, gf, bf)
    let m = min(rf, gf, bf)
    let C = M - m

    let v = M
    let s = M > 0 ? C / M : 0.0

    var h: Double = 0.0
    if C > 0 {
        switch M {
        case rf: h = (gf - bf) / C
                 if h < 0 { h += 6.0 }   // 处理负值(红色跨越 0°/360°)
        case gf: h = (bf - rf) / C + 2.0
        default: h = (rf - gf) / C + 4.0
        }
        h *= 60.0
    }

    return (h, s, v)
}

四、HSV → RGB 反向变换

已知 h ∈ [0°, 360°), s ∈ [0, 1], v ∈ [0, 1]

swift 复制代码
func hsvToRGB(h: Double, s: Double, v: Double) -> (r: UInt8, g: UInt8, b: UInt8) {
    if s == 0 {
        // 无色(灰色)
        let c = UInt8(v * 255)
        return (c, c, c)
    }

    let hSector = h / 60.0          // 扇区索引(0--5.999)
    let i = Int(hSector) % 6        // 整数扇区(0--5)
    let f = hSector - Double(i)     // 扇区内小数部分(0--1)

    // 三个中间值
    let p = v * (1 - s)             // 最小值(V × 无饱和度)
    let q = v * (1 - s * f)         // 递减插值
    let t = v * (1 - s * (1 - f))  // 递增插值

    let (r, g, b): (Double, Double, Double)
    switch i {
    case 0: (r, g, b) = (v, t, p)
    case 1: (r, g, b) = (q, v, p)
    case 2: (r, g, b) = (p, v, t)
    case 3: (r, g, b) = (p, q, v)
    case 4: (r, g, b) = (t, p, v)
    default:(r, g, b) = (v, p, q)
    }

    return (UInt8(r * 255), UInt8(g * 255), UInt8(b * 255))
}

p/q/t 三个中间值的几何意义

ini 复制代码
在扇区 0(H: 0°--60°,从红到黄):
  R = V(保持最大)
  G = t(从 p 线性增长到 V,G 通道递增)
  B = p(V × (1-S),最小值,保持为"底色")

p:去掉饱和度后的底色,等于灰度值 V×(1-S)
q:递减插值(在相邻扇区的过渡中下降)
t:递增插值(在相邻扇区的过渡中上升)

五、CIE Lab 颜色空间:感知均匀的设计哲学

问题 :HSV 虽然直观,但不是感知均匀的。

复制代码
在 HSV 中:
  将 H 从 0° 改为 10°(红色变为橙红)→ 人眼感知变化大
  将 H 从 240° 改为 250°(蓝色)    → 人眼感知变化小

同样的 10° 变化,感知到的差异不同!

CIE 1976 Lab 颜色空间的目标:在该空间中,相同的欧氏距离对应相同的感知差异

Lab 三个分量:

  • L(Lightness,亮度):0(黑)-- 100(白)
  • a:红--绿轴(负值 = 绿,正值 = 红)
  • b:蓝--黄轴(负值 = 蓝,正值 = 黄)

六、RGB → Lab 转换链

Lab 不能直接从 RGB 转换,需要经过中间步骤:

css 复制代码
sRGB(0--255)
    ↓ ① 归一化到 [0, 1]
线性化 sRGB(去 gamma)
    ↓ ② 伽马反校正
线性 sRGB(真实光物理量)
    ↓ ③ 线性变换(3×3 矩阵)
XYZ(D65 白点)
    ↓ ④ 非线性映射(含立方根)
CIE Lab

步骤 ①②:去伽马(Gamma Linearization)

相机存储的 sRGB 值经过伽马编码(约 γ=2.2),需要反解:

swift 复制代码
func linearize(_ c: Double) -> Double {
    // sRGB 标准的精确公式
    if c <= 0.04045 {
        return c / 12.92
    } else {
        return pow((c + 0.055) / 1.055, 2.4)
    }
}

为什么有两段公式?

sRGB 标准在暗部(c < 0.04045)用线性段代替幂函数,避免在极暗区域的数值不稳定和计算误差放大。

步骤 ③:线性 RGB → XYZ(D65 白点矩阵)

swift 复制代码
// IEC 61966-2-1(sRGB 标准矩阵,D65 白点)
func rgbToXYZ(r: Double, g: Double, b: Double) -> (x: Double, y: Double, z: Double) {
    let x = 0.4124564 * r + 0.3575761 * g + 0.1804375 * b
    let y = 0.2126729 * r + 0.7151522 * g + 0.0721750 * b
    let z = 0.0193339 * r + 0.1191920 * g + 0.9503041 * b
    return (x, y, z)
}

步骤 ④:XYZ → Lab

swift 复制代码
func xyzToLab(x: Double, y: Double, z: Double) -> (l: Double, a: Double, b: Double) {
    // D65 白点参考值(归一化,让白色 = L=100)
    let xn = 0.95047, yn = 1.00000, zn = 1.08883

    func f(_ t: Double) -> Double {
        // CIE Lab 的非线性映射(结合了立方根和线性段)
        let delta = 6.0 / 29.0
        if t > delta * delta * delta {
            return pow(t, 1.0 / 3.0)
        } else {
            return t / (3 * delta * delta) + 4.0 / 29.0
        }
    }

    let fx = f(x / xn)
    let fy = f(y / yn)
    let fz = f(z / zn)

    let l = 116 * fy - 16
    let a = 500 * (fx - fy)
    let b = 200 * (fy - fz)
    return (l, a, b)
}

完整转换链的 Swift 封装

swift 复制代码
func rgbToLab(r: UInt8, g: UInt8, b: UInt8) -> (l: Double, a: Double, b: Double) {
    let rLin = linearize(Double(r) / 255.0)
    let gLin = linearize(Double(g) / 255.0)
    let bLin = linearize(Double(b) / 255.0)
    let (x, y, z) = rgbToXYZ(r: rLin, g: gLin, b: bLin)
    return xyzToLab(x: x, y: y, z: z)
}

七、ΔE 色差(CIE76)

ΔE(Delta E)是 Lab 空间中两种颜色的欧氏距离,用于量化颜色差异:

swift 复制代码
func deltaE76(lab1: (l: Double, a: Double, b: Double),
              lab2: (l: Double, a: Double, b: Double)) -> Double {
    let dl = lab1.l - lab2.l
    let da = lab1.a - lab2.a
    let db = lab1.b - lab2.b
    return sqrt(dl*dl + da*da + db*db)
}

ΔE 的感知参考阈值

ΔE 值 感知程度
< 1.0 肉眼无法分辨
1.0 -- 2.0 专业人士仔细观察才能发现
2.0 -- 3.5 普通人侧面对比可以发现
3.5 -- 5.0 普通人正常观察可以发现
> 5.0 明显不同

应用

  • JPEG 压缩质量评估:比较原图和压缩图的平均 ΔE
  • 颜色匹配容差:电商商品颜色校准(ΔE < 2 视为合格)
  • Day 14 调色板去重:若两个代表色 ΔE < 5,合并为一个

八、实际应用:滤镜设计原理

8.1 HueRotateFilter(色相旋转)

swift 复制代码
public struct HueRotateFilter: ImageFilter {
    public let angle: Double   // 旋转角度(度),范围 -180 ~ +180

    public func apply(to bitmap: MLBitmap) -> MLBitmap {
        var result = bitmap
        for i in stride(from: 0, to: bitmap.pixels.count, by: 4) {
            let r = bitmap.pixels[i]
            let g = bitmap.pixels[i + 1]
            let b = bitmap.pixels[i + 2]

            var (h, s, v) = rgbToHSV(r: r, g: g, b: b)
            h = fmod(h + angle + 360, 360)   // 环绕旋转
            let (nr, ng, nb) = hsvToRGB(h: h, s: s, v: v)

            result.pixels[i]     = nr
            result.pixels[i + 1] = ng
            result.pixels[i + 2] = nb
        }
        return result
    }
}

8.2 SaturationFilter(饱和度调整)

swift 复制代码
public struct SaturationFilter: ImageFilter {
    public let factor: Double   // 1.0 = 不变,0.0 = 灰度,2.0 = 双倍饱和度

    public func apply(to bitmap: MLBitmap) -> MLBitmap {
        var result = bitmap
        for i in stride(from: 0, to: bitmap.pixels.count, by: 4) {
            let r = bitmap.pixels[i]
            let g = bitmap.pixels[i + 1]
            let b = bitmap.pixels[i + 2]

            var (h, s, v) = rgbToHSV(r: r, g: g, b: b)
            s = min(1.0, max(0.0, s * factor))   // 线性缩放,截断到 [0, 1]
            let (nr, ng, nb) = hsvToRGB(h: h, s: s, v: v)

            result.pixels[i]     = nr
            result.pixels[i + 1] = ng
            result.pixels[i + 2] = nb
        }
        return result
    }
}

8.3 VibranceFilter:智能饱和度(Vibrance vs Saturation 的核心差异)

Saturation (饱和度):对所有像素等量提升饱和度。

Vibrance (自然饱和度):对低饱和区域 提升更多,高饱和区域提升更少。

效果:皮肤(低饱和,容易变橘色)保持自然;天空和植物(高饱和)适度增强。

swift 复制代码
public struct VibranceFilter: ImageFilter {
    public let strength: Double   // 0.0 = 不变,1.0 = 最大 Vibrance

    public func apply(to bitmap: MLBitmap) -> MLBitmap {
        var result = bitmap
        for i in stride(from: 0, to: bitmap.pixels.count, by: 4) {
            let r = bitmap.pixels[i]
            let g = bitmap.pixels[i + 1]
            let b = bitmap.pixels[i + 2]

            var (h, s, v) = rgbToHSV(r: r, g: g, b: b)

            // 核心差异:权重与饱和度负相关
            // 饱和度低的像素(s 接近 0)→ weight 接近 1(提升力度大)
            // 饱和度高的像素(s 接近 1)→ weight 接近 0(基本不动)
            let weight = (1.0 - s) * strength

            s = min(1.0, s + weight * 0.5)

            let (nr, ng, nb) = hsvToRGB(h: h, s: s, v: v)
            result.pixels[i]     = nr
            result.pixels[i + 1] = ng
            result.pixels[i + 2] = nb
        }
        return result
    }
}

Vibrance vs Saturation 对比

像素类型 原饱和度 S Saturation(factor=1.5) Vibrance(strength=1.0)
皮肤色 S=0.20 0.30(+50%) 0.30(weight≈0.8,+40%)
草地 S=0.60 0.90(+50%) 0.72(weight≈0.4,+20%)
纯蓝天 S=0.90 1.00(截断,+11%) 0.925(weight≈0.1,+3%)

Vibrance 的本质是自适应饱和度:低饱和区域收益最大,高饱和区域几乎不动,避免过饱和带来的失真感。


九、三种颜色空间的使用场景对比

颜色空间 最适合的操作 不适合的操作
RGB 像素混合、Alpha 合成、计算机存储 色相旋转、饱和度调整、色差计算
HSV 色相旋转、饱和度/明度独立调整、颜色选择器 感知均匀的色差、颜色外观模型
Lab 感知均匀色差(ΔE)、颜色分类、压缩质量评估 实时滤镜(转换成本高)、简单亮度调整

十、小结

概念 核心内容
RGB 的耦合性 调色相需要同时改三个值,不直观
HSV 色相/饱和度/明度解耦,适合直觉化颜色编辑
六扇区 H 计算 按最大分量分类,六段线性插值,H ∈ [0°, 360°)
p/q/t 中间值 反向 HSV → RGB 的三个过渡插值
CIE Lab 感知均匀空间,相同欧氏距离 = 相同感知差异
转换链 sRGB → 线性化(去 gamma) → XYZ → Lab
ΔE(CIE76) Lab 空间欧氏距离,< 1 肉眼不可辨
Vibrance vs Saturation 权重 = 1-S,低饱和区域提升更多,避免过饱和

♥️喜欢我的内容,欢迎大家点赞、转发、关注。

♥️专注于技术+投资+认知三位一体的内容分享。

往期推荐:

经济机器是怎样运行的?

解决AI焦虑的唯一办法,建议收藏

图解Otsu算法

AI工业革命,有哪些能力最稀缺?

为什么很多创业者做出来的产品没人买?------读《The Mom Test》有感

巴菲特和芒格没有说的那些话

『纳瓦尔宝典』长期主义才是财富密码

『纳瓦尔宝典』独特知识才是真正资产

『纳瓦尔宝典』不要竞争,要变得不可替代!

我们终将成为自己选择的样子!!

你的环境,正在替你做决定!!

芒格的人生操作系统,构建复利人生的基石

如何选择真正适合你的职业?

决策矩阵

二阶思维

决策日志

AI时代,软件工程师必备概念全景图

相关推荐
财经资讯数据_灵砚智能2 小时前
基于全球经济类多源新闻的NLP情感分析与数据可视化(夜间-次晨)2026年6月6日
人工智能·python·ai·信息可视化·自然语言处理·ai编程·灵砚智能
CodeAI2 小时前
Prompt Engineering 进阶:6 个让输出更稳定的实用技巧
openai·ai编程
老梁agent2 小时前
LangChain4j AiServices 深度解析:声明式 Agent 编程的魔法背后
物联网·ai编程
用户029669769822 小时前
微信iPad协议的消息加密与安全传输机制
ios
坚果派·白晓明2 小时前
鸿蒙PC三方库使用:使用 AtomCode + Skills 自动完成鸿蒙化三方库spdlog集成
c++·华为·ai编程·harmonyos·skills·atomcode·c/c++三方库
DigitalOcean3 小时前
深度评测:RAG 向量数据库选型指南 —— OpenSearch、Weaviate、pgvector 怎么选?
数据库·ai编程
canonical_entropy3 小时前
吸引子引导与轨迹挖掘:AI Native Engineering 的收敛机制
数学·架构·ai编程
开开心心loky3 小时前
[OC 底层] (五) iOS 中常见的几种锁
macos·ios·cocoa
JavaGuide4 小时前
GitHub 6.2 万 Star!Claude Code / Codex 的项目知识图谱工具火了。
github·ai编程·claude