4 / 32 课程导览与音视频基础

第 3 课:颜色是怎么被存进电脑的

# 第 3 课:颜色是怎么被存进电脑的

本课目标:解开前两课一直埋着的悬念 —— 一个像素的颜色,到底怎么存进电脑? 我们会认识两大流派 RGBYUV,搞懂一个关键问题:为什么直播和视频几乎都用 YUV,而不是你更熟悉的 RGB? 答案,又一次和「省数据」有关。

第 2 课我们说「像素就是一个带颜色的点」,但故意没讲「颜色怎么存」。现在补上。存颜色有两套主流办法:一套叫 RGB,直观、好理解;另一套叫 YUV,绕一点,却是视频世界的主角。我们从简单的 RGB 讲起。


# 3.1 RGB:用红、绿、蓝三种光调出一切

📍我们在哪:第 2 课说「像素是带颜色的点」,却一直没讲颜色怎么存。这一课补上,先从最直观的 RGB 讲起。

你可能听过「光的三原色是红绿蓝」。屏幕就是这么工作的:每个像素其实是三盏挨在一起的小灯 —— 一盏红、一盏绿、一盏蓝。让它们以不同亮度发光、再相加,就能混出几乎所有颜色:

RGB 加色混合

这叫加色混合(additive):因为是「发光相加」,所以光越加越亮 —— 红 + 绿 = 黄,三色全开 = 白,三色全关 = 黑。(这和你小时候用颜料调色正好相反,颜料是「减色」,越混越黑。)

于是,一个像素的颜色,就是三个数:R、G、B,每个数表示对应那盏灯的亮度。通常每个数用1 个字节(8 位)存,取值 0~255:

一个像素占几个字节

  • 3 个通道 = 3 个字节 = 24 位,能表示 256 × 256 × 256 ≈ 1678 万种颜色 —— 足够覆盖人眼能分辨的绝大多数颜色,所以「24 位真彩色」是常见标准。
  • 有时还会加第 4 个通道 Alpha(透明度),变成 4 字节,排列成 RGBABGRABGRX(X 表示「占位但不用」)。

你也许会奇怪:为什么是 BGRA 这种「蓝在前」的别扭顺序?这是历史和硬件遗留 —— 不同的显卡、系统当年对字节顺序有不同偏好。对新手来说,记住它们都是「RGB 三兄弟 + 可选的透明通道」,只是排列顺序不同就够了。OBS 在 GPU 上做画面合成时,内部主要用的就是 BGRA / BGRX 这类 RGB 格式 —— 这一点我们模块 3 讲图形子系统时会再遇到。


# 3.2 RGB 有个「省不下来」的毛病

📍承上启下:RGB 好懂,可它有个对视频致命的毛病 —— 省不下来。这一节点破它,顺便引出人眼「重明暗、轻颜色」的偏心。

RGB 又直观又好用,但它有个对视频很不友好的特点:R、G、B 三个通道地位完全平等,谁都不能少,必须老老实实每个像素存满 3 个字节。

可是人眼并不「平等」地看待信息。人眼有一个非常重要的偏心:

我们对「明暗」极其敏感,对「颜色」却相当迟钝。

这是进化留下的:在昏暗环境里能看清物体的轮廓和明暗(有没有危险),远比分辨它是青色还是蓝绿色重要。视网膜里负责明暗的细胞(视杆),也远多于负责颜色的细胞(视锥)。

RGB 完全没法利用这个偏心 —— 因为「明暗」被均匀地摊在了 R、G、B 三个数里,你没办法「只保留明暗、少存点颜色」。

要利用人眼这个弱点省数据,就得换一种存法,把明暗颜色拆开。这就是 YUV 登场的理由。


# 3.3 YUV:把「亮度」和「颜色」拆成两摊

📍承上启下:要利用人眼这个偏心,就得把明暗和颜色拆开存 —— 这正是 YUV 的思路,视频世界真正的主角登场了。

YUV 的思路很简单:不再存红绿蓝,而是把一个像素拆成两部分 ——

  • Y:亮度(luma),也就是「明暗」,等于把画面变成黑白照片时的那个灰度值;
  • U、V:色度(chroma),描述「在这个明暗基础上,偏什么颜色」。

我们把一张普通彩色图,真的拆成 Y、U、V 三张图看看:

YUV 拆解:亮度 Y + 色度 UV

仔细看这张图,关键就藏在里面:

  • 单看灰色的 Y(亮度),整幅画面几乎是完整的 —— 房子、太阳、草地的轮廓和细节全都在;
  • U、V(色度)看起来模糊、平淡得多,信息量明显小。

这正好印证了上一节:画面的细节,绝大部分都在「亮度」里;「颜色」本身没那么多信息,人眼也看不太出来。 既然如此,我们就可以对 Y 老老实实地存,而对 U、V —— 省着存。怎么省?下一节。

补充一句:Y 怎么从 RGB 算出来?用的是类似 Y = 0.299·R + 0.587·G + 0.114·B 的加权公式(绿色权重最大,因为人眼对绿最敏感)。上面那张 Y 图,就是程序按这个公式从原图算出来的。


# 3.4 色度抽样:YUV 真正省数据的地方

📍承上启下:拆成 Y 和 UV 还不算省,真正省数据的动作在这一节 —— 色度抽样,把你看不出差别的颜色偷偷少存一半。

「省着存 U、V」的专业说法,叫色度抽样(chroma subsampling):亮度 Y 每个像素都完整保留,但色度 UV 让相邻的几个像素共用一份。 反正人眼看不出差别。

它用 4:?:? 这种记号表示,常见三档:

色度抽样 4:4:4 / 4:2:2 / 4:2:0

抽样 色度怎么省 每像素大小 相对体积
4:4:4 不省,每个像素一份色度 3 字节 100%
4:2:2 横向每 2 个像素共用一份色度 2 字节 67%
4:2:0 每 2 × 2 共 4 个像素共用一份色度 1.5 字节 50%

看最后一行:4:2:0 直接把数据砍掉一半(从 3 字节降到 1.5 字节),而画面差别肉眼几乎看不出来 —— 因为省掉的全是你本来就不敏感的颜色信息。

这种 4:2:0 的格式,有两个你一定会反复见到的名字:NV12I420(它俩都是 8 位 4:2:0,只是内存排布略有不同)。它们就是视频世界里最主流的像素格式。

📖 先补:planar vs packed —— 同样是 YUV,存法有两种:**planar(平面)**把 Y、U、V 分成三块连续存(如 I420 三块);**packed(打包)**把它们交错着存(如 YUYV);NV12 是"两平面"(Y 一块 + UV 交错一块)。记住这个,下面 NV12/I420 的区别就清楚了。


# 3.5 源码印证:OBS 支持的颜色格式

📍承上启下:概念齐了,回代码点名 —— 看 OBS 的 enum video_format 把这些格式怎么分组,默认又为什么选 NV12。

现在回到代码。打开 libobs/media-io/video-io.h,OBS 把它支持的所有像素格式列在一个枚举里 —— 注意它的分组注释,本课讲的概念全在里面:

// libobs/media-io/video-io.h:34
enum video_format {
	VIDEO_FORMAT_NONE,

	/* planar 4:2:0 formats */          // ← 4:2:0!(3.4)
	VIDEO_FORMAT_I420, /* three-plane */
	VIDEO_FORMAT_NV12, /* two-plane, luma and packed chroma */

	/* packed 4:2:2 formats */          // ← 4:2:2
	VIDEO_FORMAT_YVYU,
	VIDEO_FORMAT_YUY2,
	VIDEO_FORMAT_UYVY,

	/* packed uncompressed formats */   // ← RGB 家族(3.1)
	VIDEO_FORMAT_RGBA,
	VIDEO_FORMAT_BGRA,
	VIDEO_FORMAT_BGRX,
	...
	/* planar 4:4:4 */                  // ← 4:4:4
	VIDEO_FORMAT_I444,
	...
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

源码里甚至有个工具函数,直接帮你把这些格式分成「YUV 家族」和「非 YUV(RGB)家族」两类:

// libobs/media-io/video-io.h:141
static inline bool format_is_yuv(enum video_format format) { ... }
1
2

把这些整理一下,就是 OBS 的颜色格式全貌:

OBS 颜色格式家族与源码

而「OBS 默认用哪种格式」?答案明明白白写在前端代码里 —— 默认就是 NV12(也就是 YUV 4:2:0):

// frontend/widgets/OBSBasic.cpp:845
config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12");
1
2

你在 OBS「设置 → 高级 → 颜色格式」下拉框里能选的那些(NV12 / I420 / I444 / …),则由这个函数翻译成上面的枚举:

// frontend/widgets/OBSBasic.cpp:1510
static inline enum video_format GetVideoFormatFromName(const char *name)
{
	if (astrcmpi(name, "I420") == 0)       return VIDEO_FORMAT_I420;
	else if (astrcmpi(name, "NV12") == 0)  return VIDEO_FORMAT_NV12;
	else if (astrcmpi(name, "I444") == 0)  return VIDEO_FORMAT_I444;
	...
}
1
2
3
4
5
6
7
8

顺带一提,libobs/media-io/format-conversion.h 里有一组真实的格式转换函数,名字直接拼出了这些家族 —— compress_uyvx_to_nv12compress_uyvx_to_i420convert_uyvx_to_i444decompress_420decompress_422…… OBS 经常需要在这些格式之间来回转换(比如把 GPU 合成出的 RGB 转成编码要的 NV12),这就是干这活的地方。

🗒️ 术语速记:RGB=红绿蓝三分量;YUV=亮度+两个色度(便于压缩);4:4:4/4:2:2/4:2:0=色度采样比例(4:2:0 色度只留 1/4,人眼不敏感);NV12=两平面 4:2:0;I420=三平面 4:2:0。


# 3.6 把问题答完:为什么直播都用 YUV 4:2:0

📍承上启下:铺垫全部到位,这一节正面回答本课标题 —— 直播为什么几乎清一色用 YUV 4:2:0,三个理由层层递进。

现在可以正面回答本课标题的问题了。直播 / 视频之所以几乎清一色用 YUV 4:2:0(NV12 / I420),有三个层层递进的理由:

  1. 省数据:4:2:0 是 1.5 字节/像素,RGB 是 3~4 字节。还没进入「编码」这一步,光是换个像素格式,数据就先瘦了一半,而且是免费的、肉眼无损的。
  2. 人眼无感:省掉的全是「色度」—— 你本来就看不太出来的部分。代价极小,收益极大。
  3. 下游天生就吃 YUV:摄像头采集出来的、视频编码器(H.264 / H.265)内部处理的,本来就是 YUV 4:2:0。用它,整条流水线最顺,不用反复转换。

这也解释了 OBS 设置界面里那条「劝告」。当你把颜色格式从 NV12 改成别的(比如 I444),OBS 会弹出警告:

「NV12/P010 之外的颜色格式主要用于录像,并不推荐在串流时使用。因为串流时需要进行颜色格式转换,可能会增加 CPU 使用率。」 —— frontend/data/locale/zh-CN.ini:1073

读懂本课后,这句话就一点都不神秘了:录制追求画质,可以用不抽样的 4:4:4(I444);但直播追求带宽和流畅,就该用省一半数据、又跟编码器对口的 4:2:0(NV12)。OBS 把这个工程经验,直接做成了默认值和一句提醒。


# 3.7 选修:色彩空间与范围(知道有这回事就行)

📍收束:主线讲完,留个选修小尾巴 —— 同一组 YUV 数字还要配一份「按哪套标准解释」的约定,不匹配就会偏色。

最后留个小尾巴,新手了解即可、不必深究。

同样一组 (Y, U, V) 数字,要还原成具体颜色,还需要一份「换算约定」,这叫色彩空间(color space)。常见的有 BT.601(老标清)、BT.709(高清)、BT.2020(超高清 / HDR)。约定不同,同样的数字算出来的颜色会略有差异。OBS 把它们也定义成了枚举:

// libobs/media-io/video-io.h:106 / :115
enum video_colorspace { VIDEO_CS_601, VIDEO_CS_709, VIDEO_CS_SRGB, VIDEO_CS_2100_PQ, ... };
enum video_range_type { VIDEO_RANGE_PARTIAL, VIDEO_RANGE_FULL };  // 取值范围:有限 / 完整
1
2
3

那个 video_range_type 也值得一提:亮度的取值,有的标准用满 0~255(Full),有的只用 16~235(Limited / Partial,电视时代的遗留)。如果录制端和播放端对这个约定不一致,画面就会发灰或发"过曝" —— 这就是很多人遇到过的「录出来的视频颜色不对」的常见原因之一。

新手现在只需记住一句话:(Y,U,V) 数字之外,还有一层「按哪套标准解释这些数字」的约定;约定不匹配,颜色就会偏。 真正需要调它,是以后做 HDR、跨设备校色时的事了。


# 3.8 本课小结

  • 存颜色有两大流派:
    • RGB —— 一个像素 = (R, G, B) 三个数,加色发光;直观,3~4 字节/像素;OBS 在 GPU 合成时用它(BGRA/BGRX)。
    • YUV —— 把像素拆成亮度 Y(明暗)和色度 UV(颜色)。
  • 人眼对明暗敏感、对颜色迟钝,所以画面细节几乎都在 Y 里 —— 这给了「省颜色」的空间。
  • 色度抽样就是利用它:4:4:4(不省)→ 4:2:24:2:0(每 2×2 共用一份色度,数据砍半,肉眼无损)。
  • 4:2:0 = NV12 / I420,是视频世界的默认像素格式;OBS 默认就是 NV12(OBSBasic.cpp:845),并提醒非 NV12/P010 主要用于录像。
  • 直播用 YUV 4:2:0 的三个理由:省数据、人眼无感、下游编码器天生吃它
  • 选修:(Y,U,V) 还需配一份色彩空间 / 范围约定(BT.601/709/2020,Full/Limited),不匹配会偏色。

# 3.9 动手 / 观察(本课作业)

  1. 在 OBS 里看格式与警告:打开 OBS → 设置 → 高级 → 「颜色格式」,看到下拉里的 NV12(默认)、I420I444…… 把它改成 I444 试试,留意 OBS 弹出的那条「主要用于录像、不推荐串流」的提示 —— 现在你完全懂它在说什么了。
  2. 亲手感受 RGB:用取色 / 调色工具(比如系统自带的拾色器)随便点一个颜色,看它的 R、G、B 三个 0~255 的值。
  3. 读一段源码:打开 libobs/media-io/video-io.h:34,顺着分组注释数一数 enum video_format 里有多少个格式,试着把它们归到本课讲的「RGB 家族 / 4:2:0 / 4:2:2 / 4:4:4」四类里。

# 下一课预告

视频这条线,我们已经讲完了「画面怎么存」(像素、分辨率、颜色)。下一课换条线 —— 声音

第 4 课:声音的本质就是一串数字 —— 声波是连续的,电脑是离散的,中间靠「采样」连接。我们会讲采样率、位深、声道、PCM,你会发现声音和视频在「数字化」这件事上,思路惊人地一致。然后回到 libobs/media-io/audio-io.h,看 OBS 怎么描述一段声音。


📁 本课配图:imgs/03-01 ~ imgs/03-05 📌 源码锚点:libobs/media-io/video-io.h:34(video_format)、:141(format_is_yuv)、:106/:115(色彩空间/范围)、libobs/media-io/format-conversion.hfrontend/widgets/OBSBasic.cpp:845(默认 NV12)、:1510(GetVideoFormatFromName)、frontend/data/locale/zh-CN.ini:1073(设置警告)

上次更新: 2026/07/14, 19:37:40

社区交流

讨论与留言

前往 GitHub Issues →

本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。

最近更新
第 1 课:专栏导论与安全边界
07-01
第 3 课:安装编译与调试环境
07-01
第 4 课:第三方库下载、编译与依赖管理
07-01
更多文章>