【图像处理】颜色空间——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时代,软件工程师必备概念全景图

相关推荐
码哥字节20 分钟前
你的PPT交给人改还是发链接?41k星Skill相反
ai编程
小江的记录本23 分钟前
【AI Agent】《2026年9月 AI Agent全栈开发技术选型专项面试宝典》
java·spring boot·python·spring·ai·面试·ai编程
魔术师Grace2 小时前
调用一次大模型,就算 Agent 吗?
aigc·agent·ai编程
zandy10113 小时前
网络支付合规三重门:Kimi Code、Cursor、GitHub Copilot等五款AI编程工具国内实战测评
github·copilot·ai编程
Java小白笔记6 小时前
AI Agent-CLI 的启动命令与跳过权限模式
人工智能·ai·ai编程
FairGuard手游加固6 小时前
iOS 游戏加固技术解析:代码混淆、资源加密、内存保护与重签名检测
安全·游戏·macos·unity·ios·objective-c
小虎AI生活7 小时前
阿里开源 Pixelle-Video:一段文案 5 分钟出片,我把短视频生产线跑通了
ai编程
思盛iOS签名上架7 小时前
iOS企业签名原理是什么?
macos·ios·cocoa
七牛云行业应用7 小时前
国产新模型选型:GLM-5.3、DeepSeek V4、GLM-5.3-Flash、Qwen3.8-Flash-Next、Kimi K3 怎么选
人工智能·大模型·ai编程
彼日花7 小时前
我做了一个开源项目,让 AI 记住我们解决过的问题:Usora
人工智能·agent·ai编程