第 3 课:颜色是怎么被存进电脑的
# 第 3 课:颜色是怎么被存进电脑的
本课目标:解开前两课一直埋着的悬念 —— 一个像素的颜色,到底怎么存进电脑? 我们会认识两大流派 RGB 和 YUV,搞懂一个关键问题:为什么直播和视频几乎都用 YUV,而不是你更熟悉的 RGB? 答案,又一次和「省数据」有关。
第 2 课我们说「像素就是一个带颜色的点」,但故意没讲「颜色怎么存」。现在补上。存颜色有两套主流办法:一套叫 RGB,直观、好理解;另一套叫 YUV,绕一点,却是视频世界的主角。我们从简单的 RGB 讲起。
# 3.1 RGB:用红、绿、蓝三种光调出一切
📍我们在哪:第 2 课说「像素是带颜色的点」,却一直没讲颜色怎么存。这一课补上,先从最直观的 RGB 讲起。
你可能听过「光的三原色是红绿蓝」。屏幕就是这么工作的:每个像素其实是三盏挨在一起的小灯 —— 一盏红、一盏绿、一盏蓝。让它们以不同亮度发光、再相加,就能混出几乎所有颜色:

这叫加色混合(additive):因为是「发光相加」,所以光越加越亮 —— 红 + 绿 = 黄,三色全开 = 白,三色全关 = 黑。(这和你小时候用颜料调色正好相反,颜料是「减色」,越混越黑。)
于是,一个像素的颜色,就是三个数:R、G、B,每个数表示对应那盏灯的亮度。通常每个数用1 个字节(8 位)存,取值 0~255:
![]()
- 3 个通道 = 3 个字节 = 24 位,能表示 256 × 256 × 256 ≈ 1678 万种颜色 —— 足够覆盖人眼能分辨的绝大多数颜色,所以「24 位真彩色」是常见标准。
- 有时还会加第 4 个通道 Alpha(透明度),变成 4 字节,排列成
RGBA、BGRA、BGRX(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 三张图看看:

仔细看这张图,关键就藏在里面:
- 单看灰色的 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 | 不省,每个像素一份色度 | 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 的格式,有两个你一定会反复见到的名字:NV12 和 I420(它俩都是 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,
...
};
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) { ... }
2
把这些整理一下,就是 OBS 的颜色格式全貌:

而「OBS 默认用哪种格式」?答案明明白白写在前端代码里 —— 默认就是 NV12(也就是 YUV 4:2:0):
// frontend/widgets/OBSBasic.cpp:845
config_set_default_string(activeConfiguration, "Video", "ColorFormat", "NV12");
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;
...
}
2
3
4
5
6
7
8
顺带一提,
libobs/media-io/format-conversion.h里有一组真实的格式转换函数,名字直接拼出了这些家族 ——compress_uyvx_to_nv12、compress_uyvx_to_i420、convert_uyvx_to_i444、decompress_420、decompress_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),有三个层层递进的理由:
- 省数据:4:2:0 是 1.5 字节/像素,RGB 是 3~4 字节。还没进入「编码」这一步,光是换个像素格式,数据就先瘦了一半,而且是免费的、肉眼无损的。
- 人眼无感:省掉的全是「色度」—— 你本来就看不太出来的部分。代价极小,收益极大。
- 下游天生就吃 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 }; // 取值范围:有限 / 完整
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:2→4: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 动手 / 观察(本课作业)
- 在 OBS 里看格式与警告:打开 OBS → 设置 → 高级 → 「颜色格式」,看到下拉里的
NV12(默认)、I420、I444…… 把它改成I444试试,留意 OBS 弹出的那条「主要用于录像、不推荐串流」的提示 —— 现在你完全懂它在说什么了。 - 亲手感受 RGB:用取色 / 调色工具(比如系统自带的拾色器)随便点一个颜色,看它的 R、G、B 三个 0~255 的值。
- 读一段源码:打开
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.h、frontend/widgets/OBSBasic.cpp:845(默认 NV12)、:1510(GetVideoFormatFromName)、frontend/data/locale/zh-CN.ini:1073(设置警告)
本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。
社区交流
讨论与留言