3、RGB与HSV互转:色调、饱和度、明度的数学映射

好,咱们接着聊。上一节我们把RGB和HSV各自的特点捋清楚了,这一节就来点硬核的——怎么在这两个色彩空间之间来回切换。

说实话,我第一次看这个转换公式的时候,也觉得有点绕。但后来在项目中用多了,发现其实就那么几个关键步骤。你只要抓住「色调是角度、饱和度是纯度、明度是亮度」这个感觉,剩下的就是数学上的映射关系了。

3.1 从RGB到HSV:把立方体掰成圆锥

RGB是个立方体,HSV是个圆锥。转换的过程,说白了就是把立方体里的每个点,映射到圆锥里对应的位置。

我个人习惯先算明度V,再算饱和度S,最后算色调H。这个顺序最顺手。

第一步:求明度V

明度最简单,就是RGB三个通道的最大值:

V = max(R, G, B)

嗯,这里要注意,RGB的值通常归一化到[0, 1]区间。如果你拿到的数据是0-255,记得先除以255。

第二步:求饱和度S

饱和度反映的是「颜色有多纯」。公式是这样的:

S = (V - min(R, G, B)) / V   (当V ≠ 0时)
S = 0                         (当V = 0时)

为什么V=0时饱和度是0?你想想看,V=0意味着RGB全是0,那就是纯黑色。黑色没有颜色倾向,饱和度自然为0。

我的经验: 在实际图像处理中,V非常接近0时(比如小于0.001),直接让S=0可以避免除零错误。我曾经在这个坑里栽过,调试了半天才发现是除零问题。

第三步:求色调H

色调是最麻烦的,因为它是个角度。公式分三种情况:

如果 V == R:
    H = 60° × ((G - B) / (V - min(R,G,B)))
如果 V == G:
    H = 60° × (2 + (B - R) / (V - min(R,G,B)))
如果 V == B:
    H = 60° × (4 + (R - G) / (V - min(R,G,B)))

算完之后,如果H是负数,加360°让它落在[0°, 360°)区间。

你可能会问:为什么是60°?因为HSV色环上,红、黄、绿、青、蓝、品红这六个主色正好相隔60°。这个设计很巧妙,对吧?

3.2 完整代码示例:RGB转HSV

光讲公式不过瘾,咱们直接上代码。我习惯用Python写,因为NumPy处理图像太方便了:

import numpy as np

def rgb_to_hsv(rgb):
    """
    RGB转HSV,输入rgb为[0,1]范围的数组
    返回hsv,h在[0,360),s和v在[0,1]
    """
    r, g, b = rgb[0], rgb[1], rgb[2]
    
    # 求最大值和最小值
    v = max(r, g, b)
    min_val = min(r, g, b)
    delta = v - min_val
    
    # 计算饱和度
    if v == 0:
        s = 0
    else:
        s = delta / v
    
    # 计算色调
    if delta == 0:
        h = 0  # 无色调
    elif v == r:
        h = 60 * ((g - b) / delta)
    elif v == g:
        h = 60 * (2 + (b - r) / delta)
    elif v == b:
        h = 60 * (4 + (r - g) / delta)
    
    # 确保h在[0, 360)
    if h < 0:
        h += 360
    
    return np.array([h, s, v])
避坑指南: 我曾经在项目中直接用OpenCV的cvtColor函数,结果发现它默认的HSV范围是H:[0,180], S:[0,255], V:[0,255]。跟数学定义不一样!所以后来我养成了习惯:每次用库函数之前,先查一下它的取值范围。

3.3 从HSV回到RGB:逆映射

有去就有回。HSV转RGB稍微复杂一点,但核心思路是:根据H落在哪个60°区间,确定RGB三个通道的取值关系。

具体步骤是这样的:

  1. 先把H除以60,得到它落在哪个扇区(0-5)
  2. 计算中间变量:f = H/60 - floor(H/60)
  3. 计算三个临时值:p = V×(1-S), q = V×(1-S×f), t = V×(1-S×(1-f))
  4. 根据扇区查表,得到RGB

查表规则如下:

扇区 H范围 R G B
0 [0°, 60°) V t p
1 [60°, 120°) q V p
2 [120°, 180°) p V t
3 [180°, 240°) p q V
4 [240°, 300°) t p V
5 [300°, 360°) V p q

你看这个表,其实有规律:每个扇区里,RGB三个值分别是V、某个中间值、p。只是排列顺序不同。记住这个规律,代码写起来就快了。

3.4 HSV转RGB的Python实现

def hsv_to_rgb(hsv):
    """
    HSV转RGB,h在[0,360),s和v在[0,1]
    返回rgb在[0,1]范围
    """
    h, s, v = hsv[0], hsv[1], hsv[2]
    
    if s == 0:
        # 饱和度0,就是灰度
        return np.array([v, v, v])
    
    # 确定扇区
    h_i = int(h / 60) % 6
    f = h / 60 - h_i
    
    p = v * (1 - s)
    q = v * (1 - s * f)
    t = v * (1 - s * (1 - f))
    
    # 查表
    if h_i == 0:
        return np.array([v, t, p])
    elif h_i == 1:
        return np.array([q, v, p])
    elif h_i == 2:
        return np.array([p, v, t])
    elif h_i == 3:
        return np.array([p, q, v])
    elif h_i == 4:
        return np.array([t, p, v])
    elif h_i == 5:
        return np.array([v, p, q])
注意: 浮点数精度问题!当H非常接近60°、120°等边界值时,由于浮点误差,可能会落在错误的扇区。我建议在计算h_i之前,给H加一个很小的epsilon(比如1e-6),避免边界抖动。

3.5 实际应用中的选择

讲完了互转,你可能会问:什么时候用RGB,什么时候用HSV?

我个人经验是这样的:

  • 图像采集和显示:用RGB,因为硬件原生支持
  • 颜色识别和分割:用HSV,因为色调H对光照变化不敏感
  • 图像调色:用HSV,调整饱和度和明度比调整RGB三个通道直观得多
  • 深度学习预处理:看任务。分类任务用RGB多,但如果是颜色相关的任务(比如车牌识别),HSV往往效果更好

我记得有一次做交通信号灯识别,用RGB死活分不清红灯和橙灯。后来换成HSV,只根据H值范围做判断,准确率直接从82%飙到了96%。这就是色彩空间选择的威力。

好,这一节的内容就到这。公式虽然多,但你只要理解了「色调是角度、饱和度是纯度、明度是亮度」这个本质,剩下的就是查表和套公式的事。下一节我们聊聊色彩空间在图像预处理中的实战技巧。