🎬 第十一章 SWScale图像缩放与颜色空间转换
📄 一句话概括

RGB 说「怎么发光」,YUV 说「多亮 + 偏什么色」。 同一个颜色两种说法,换的是坐标,不是信息。

📝 本节和前面几节的关系

第十章把声音讲完了,这一章换到画面这头的「换尺寸与换颜色」。 像素格式那一堆名字,根子都在这两个模型上。

一、两种模型

一个颜色可以用两套坐标描述。 模型 三个分量 谁在用


RGB 红、绿、蓝三盏灯 屏幕、图片 YUV 亮度加两个色差 视频、编码

RGB 是「三个灯各开多亮」,很直观,所以屏幕与图片都用它。 YUV 是「整体多亮、往哪个颜色偏」,对人眼更贴合——因为人眼对亮度敏感、对色度迟钝。 这个生理事实是整个视频编码的基础:既然分不清那么细的颜色, 色度就存一半分辨率,数据量立刻降下来。

二、六个典型色的换算

实测值,不是公式推的。 颜色 RGB Y U V


红 255,0,0 81 90 240 绿 0,255,0 145 54 34 蓝 0,0,255 41 240 110 灰 128,128,128 126 128 128 黑 0,0,0 16 128 128 白 255,255,255 235 128 128

黑不是 Y=0,白也不是 255。 亮度只用 16 到 235 这一段, 两头各留一点余量,这叫 limited range。 为什么留余量:模拟信号时代要给同步头与过冲留空间; 数字时代沿用了这个约定,因为大量设备与素材都是这么存的。 灰那一行 U=V=128,说明「没有颜色」。所以 YUV 里的灰度图就是 U=V=128 的图。

三、换回来会削顶

从 YUV 回到 RGB,超出范围的部分被削掉。 Y(UV=128) 换回的 RGB


0 0,0,0 16 0,0,0 128 130,130,130 235 255,255,255 255 255,255,255

0 与 16 都变成纯黑,235 与 255 都变成纯白。 因为目标只有 0 到 255,装不下的就削。 这条解释了「同一段视频在不同播放器上明暗不同」: 有人按 limited 解、有人按 full 解,差的就是 16 与 235 这两个边界。 所以「颜色不对」的第一嫌疑不是编码,是范围标错了。 查的时候先看 color_range 是 0(未指定)还是 1(limited)。

四、只动色度会怎样

亮度固定 128,色度从 0 扫到 255。 U=V RGB 是什么颜色


0 0,255,0 绿 64 27,207,0 黄绿 128 130,130,130 灰 192 232,51,255 粉紫 255 255,0,255 品红

亮度一直没变,颜色跑了一圈。 这说明「Y 管明暗、UV 管颜色」这条分工是成立的,不是比喻。 这条对调色很有用:想改明暗只动 Y,想改色调只动 UV, 两者互不干扰——这就是 YUV 比 RGB 更适合做色彩处理的理由。

五、为什么视频偏爱 YUV

三条理由,第三条是根本。 理由 说明


省带宽 色度降采样,一个像素只要 1.5 字节 黑白兼容 只取 Y 就是灰度图 符合人眼 对亮度敏感、对色度迟钝

1.5 字节是怎么来的:亮度全存(1 字节),两个色差各存四分之一(0.25 + 0.25),合计 1.5。 这就是 4:2:0 的账。 换成 RGB 要 3 字节,同样画质下 YUV 只用一半——这就是 yuv420p 在视频里占绝对主流的原因。 代价是细节损失:色度只有一半分辨率,所以纯红纯蓝的边缘容易出现「糊边」, 这是 4:2:0 的固有代价。

六、动手 Demo:Demo 85:RGB 与 YUV 之间到底怎么换

六个典型色从 RGB 换到 YUV,再反过来看削顶,最后只动色度看颜色怎么跑。

import numpy as np
import av

def to_yuv(rgbv):
    a = np.zeros((4, 4, 3), dtype=np.uint8)
    a[:, :] = rgbv
    f = av.VideoFrame.from_ndarray(a, format="rgb24")
    y = f.reformat(format="yuv420p").to_ndarray()
    return int(y[0, 0]), int(y[4, 0]), int(y[5, 0])

for v in ((255, 0, 0), (0, 255, 0), (0, 0, 255),
          (128, 128, 128), (0, 0, 0), (255, 255, 255)):
    print(v, to_yuv(v))

# 反过来:Y 从 0 到 255,看削顶
for y in (0, 16, 128, 235, 255):
    f = av.VideoFrame(4, 4, "yuv420p")
    for i, p in enumerate(f.planes):
        p.update(bytes([y if i == 0 else 128]) * p.buffer_size)
    print(y, f.reformat(format="rgb24").to_ndarray()[0, 0].tolist())

实跑输出:

--------------------------------------------------------------------
RGB 与 YUV 之间到底怎么换
--------------------------------------------------------------------

**① 两种模型各自管什么**

  模型     三个分量             谁用
  --------------------------------------------
  RGB      红、绿、蓝三盏灯     屏幕、图片
  YUV      亮度 + 两个色差      视频、编码

**RGB 是「怎么发光」,YUV 是「多亮 + 偏什么色」。**
**同一个颜色,两种描述方式,信息量一样。**

**② 六个典型色,从 RGB 换到 YUV**

  颜色     RGB             Y       U       V
  ------------------------------------------------
  红       255,0,0         81      90      240
  绿       0,255,0         145     54      34
  蓝       0,0,255         41      240     110
  灰       128,128,128     126     128     128
  黑       0,0,0           16      128     128
  白       255,255,255     235     128     128

**黑不是 Y=0,是 Y=**16**;白也不是 255,是 **235**。**
**这就是「limited range」**——亮度只用 16 到 235,两头留着余量。

**灰的那一行 U=V=128,说明「没有颜色」**。**所以 YUV 里「灰度图」就是 U=V=128 的图。**

**③ 从 YUV 换回 RGB**

  Y(UV=128)     换回的 RGB
  -----------------------------------
  0               [0, 0, 0]
  16              [0, 0, 0]
  128             [130, 130, 130]
  235             [255, 255, 255]
  255             [255, 255, 255]

**Y=0 与 Y=16 都变成纯黑,Y=235 与 Y=255 都变成纯白。**
**因为目标范围只有 0 到 255,超出的部分被「削」掉了。**

**这条解释了「为什么同一段视频在不同播放器上明暗略有不同」**:
**有人按 limited range 解,有人按 full range 解,差的就是这 16 与 235 的边界。**

**④ 只动色度,看颜色怎么跑**

  Y       U       V       RGB
  -------------------------------------------
  128     0       0       [0, 255, 0]
  128     64      64      [27, 207, 0]
  128     128     128     [130, 130, 130]
  128     192     192     [232, 51, 255]
  128     255     255     [255, 0, 255]

**亮度一直没变,颜色却从绿跑到灰再跑到品红。**
**这说明「Y 管明暗、UV 管颜色」这条分工是成立的。**

**⑤ 视频为什么用 YUV 不用 RGB**

  理由         说明
  -----------------------------------------------------
  省带宽       色度可以降采样,1.5 字节就够一个像素
  黑白兼容     只取 Y 就是灰度图
  人眼特性     对亮度敏感、对色度迟钝

**第三条是根本原因**:既然眼睛分不清那么细的颜色,
**色度就存一半分辨率——这就是 4:2:0 的来历,也是 yuv420p 占主流的理由。**

黑是 Y=16、白是 Y=235,不是 0 与 255,这是 limited range。 UV 落在 128 就是「没有颜色」,所以灰度图就是 U=V=128。

30 秒实验

  1. 把红色换成 (255,128,128),看 Y 与 U 怎么变(提示:亮度升、色度降)。
  2. 用 reformat(format="yuvj420p") 再试一次,看黑白的 Y 变成几(提示:0 与 255)。
  3. 把 Y=100、U=200、V=100 组合一下,猜它偏什么色再看答案。

常见误区

  1. 以为黑色是 Y=0。 limited range 里黑的 Y 是 16。
  2. 以为颜色不对一定是编码问题。 先查 color_range 有没有标错。
  3. 以为 YUV 无损。 4:2:0 的色度只有一半分辨率。
自测三问(点开对答案) 1. RGB 与 YUV 的本质区别是什么? —— RGB 按发光三原色分,YUV 按亮度与色差分。 2. limited range 里黑与白的 Y 各是多少? —— 黑 16、白 235。 3. 为什么视频用 YUV 而不是 RGB? —— 色度可以降采样,一个像素只要 1.5 字节,符合人眼特性。

相关笔记