从像素格式到色彩空间,系统理解 RGB、YUV、NV12 与 YUV420P

前言:为什么像素格式和色彩空间总让人越学越乱

只要开始接触音视频、Camera、FFmpeg、OpenCV、编码器或者渲染链路,几乎很快就会碰到这几个词:

  • RGB
  • YUV
  • NV12
  • YUV420P
  • 色彩空间
  • 像素格式

很多人最开始都会把它们混成一团。

典型表现是:

  • 把 RGB / YUV 当成“文件格式”
  • 把 NV12 / YUV420P 当成“色彩空间”
  • 知道它们都和图像有关,但不知道差别到底在哪
  • 碰到 FFmpeg、OpenCV、编码器之间格式不匹配时,只能靠试
  • 明明图能显示出来,但颜色不对、画面发绿、发紫、偏灰,又说不清是哪层出了问题

我觉得这里最容易乱的根本原因,不是你记性不好,而是很多资料一上来就把两个层次混着讲:

  1. 颜色怎么表达
  2. 数据怎么存放

前者更接近“色彩空间”或颜色表示方式,后者更接近“像素格式”或内存布局方式。

而工程里你真正会频繁碰到的问题,往往正是这两层交叉造成的。

所以这篇文章的目标,就是先把这两个层次拆开,再把它们重新串起来。

重点讲清下面这些问题:

  • RGB 和 YUV 到底分别在表达什么
  • 为什么视频系统更偏爱 YUV
  • NV12 和 YUV420P 到底是什么关系
  • 什么叫 planar,什么叫 packed
  • 为什么 FFmpeg、OpenCV、Camera、编码器总在格式上“打架”
  • 工程里该怎么判断当前数据到底是什么

而且这篇我会尽量按工程思维来写,多放结构图、内存图和代码,不只停在概念层。


一、先给结论:RGB / YUV 解决的是“颜色表达”,NV12 / YUV420P 解决的是“数据存放”

如果你现在只想先记住一句最重要的话,那就是:

RGB / YUV 更像是在讲颜色信息怎么表达,而 NV12 / YUV420P 更像是在讲这些数据在内存里怎么排。

这句话可以直接帮你把很多混乱先切开。

也就是说:

  • RGB / YUV,更偏“颜色模型 / 颜色表示方式”
  • NV12 / YUV420P,更偏“像素格式 / 采样方式 / 内存布局”

当然工程里它们不会完全彼此独立,因为一旦你选择了某种颜色表示方式,后面通常会对应某种具体的像素格式。

但先把这两层区分开,是理解后面全部内容的关键。


二、先看全局:一张图把几个概念的位置放清楚

先上总图。

flowchart TD
    A[图像颜色信息] --> B[颜色表示方式]
    B --> C1[RGB]
    B --> C2[YUV / YCbCr]

    C2 --> D[采样方式]
    D --> D1[4:4:4]
    D --> D2[4:2:2]
    D --> D3[4:2:0]

    D3 --> E[具体像素格式]
    E --> E1[YUV420P]
    E --> E2[NV12]
    E --> E3[NV21]

    C1 --> F[RGB像素格式]
    F --> F1[RGB24]
    F --> F2[BGR24]
    F --> F3[RGBA]

这张图最想表达的是:

  1. 先有颜色信息要表达
  2. 然后有 RGB / YUV 这类表达方式
  3. 如果走 YUV 路线,还会继续涉及采样方式
  4. 采样方式再进一步落成具体像素格式

所以你以后再看到一个格式时,可以先问自己:

它是在讲颜色怎么表示,还是在讲数据怎么存?

这一步就能过滤掉很多混乱。


三、RGB 到底是什么,它为什么直观但不一定适合视频系统

3.1 RGB 的本质

RGB 是最直观的一种颜色表示方式。

它的核心思想非常简单:

用红、绿、蓝三个通道的强度组合,表达一个像素的颜色。

比如一个像素可以表示成:

1
2
3
4
R = 255, G = 0, B = 0   -> 红色
R = 0, G = 255, B = 0 -> 绿色
R = 0, G = 0, B = 255 -> 蓝色
R = 255, G = 255, B = 255 -> 白色

从人类直觉上看,这很好理解。

3.2 RGB 为什么适合显示,却不总适合视频编码

因为 RGB 的优点是:

  • 直观
  • 适合显示设备理解
  • 很多图形接口和图像处理库天然支持

但它的问题也很明显:

  • 数据量大
  • 不方便利用视觉冗余
  • 对视频压缩不够友好

视频系统往往更关心:

  • 带宽
  • 存储
  • 压缩效率
  • 传输成本

所以虽然显示端很喜欢 RGB,但编码和传输链路通常更偏爱 YUV。


四、YUV 到底是什么,它和 RGB 的根本差别在哪

4.1 YUV 的核心思想

YUV 的直觉理解可以先抓住一句话:

它把亮度信息和色度信息拆开了。

通常可以粗略理解成:

  • Y:亮度信息
  • U / V:色度信息

在更严格的数字视频语境里,很多时候更准确的说法其实是 YCbCr,但工程沟通里大家经常仍然习惯说 YUV。为了不把文章讲得太绕,这里我们先沿用工程上更常见的叫法。

4.2 为什么把亮度和色度拆开很重要

因为人眼对亮度变化更敏感,对色彩细节没那么敏感。

这意味着:

  • 亮度信息要尽量保住
  • 色度信息可以适当降低采样精度

一旦接受这个前提,系统就可以大幅减少数据量,而主观视觉效果又不至于差太多。

这也是 YUV 在视频系统里特别重要的根本原因。

4.3 RGB 和 YUV 的角色差异

你可以先这样粗暴记:

  • RGB 更像显示友好格式
  • YUV 更像压缩友好格式

这不是绝对规则,但对大多数视频工程场景来说非常有帮助。


五、一张图看懂 RGB 和 YUV 的角色差别

flowchart LR
    A[原始图像内容] --> B1[RGB 路线]
    A --> B2[YUV 路线]

    B1 --> C1[更直观]
    B1 --> C2[更适合显示/图形系统]
    B1 --> C3[数据量相对大]

    B2 --> D1[亮度与色度分离]
    B2 --> D2[更适合压缩]
    B2 --> D3[更适合视频编码链路]

这张图虽然简单,但可以帮你建立一个很稳的直觉:

RGB 更偏显示侧,YUV 更偏视频侧。

后面你看 Camera、编码器、FFmpeg、播放器时,很多格式选择就会更容易理解。


六、为什么视频系统更偏爱 YUV,而不是 RGB

这个问题很重要,因为它关系到后面所有具体格式的存在意义。

6.1 核心原因:YUV 更适合做色度降采样

YUV 最大的工程价值之一,就是它允许:

在不明显伤害主观视觉的情况下,减少色度数据量。

这就是后面你经常看到的:

  • 4:4:4
  • 4:2:2
  • 4:2:0

这些采样方式的来源。

6.2 亮度保得更多,色度省一点

因为人眼更关心亮度细节,所以系统通常会尽量保留 Y,而对 U/V 做降采样。

这会带来两个直接好处:

  • 数据量下降
  • 压缩效率更高

6.3 工程上的连锁影响

一旦视频链路主要走 YUV 路线,后面很多模块都会围绕它来设计:

  • Camera 输出
  • 编码器输入
  • FFmpeg frame 格式
  • 硬件编解码器支持
  • 显示前色彩转换

这也是为什么很多时候你在系统里看到的是 NV12、YUV420P,而不是 RGB24。


七、4:4:4、4:2:2、4:2:0 到底在表达什么

这一块是很多人最容易发晕的地方。

我们先不讲特别学院派的定义,先抓工程直觉。

7.1 它们本质上在讲什么

它们描述的是:

亮度和色度分别采样得有多密。

你可以把它理解成:

  • Y 通常采得更完整
  • U/V 视情况采得更稀疏

7.2 为什么会这样设计

因为色度对人眼没那么敏感,所以系统可以少存一点 U/V,换更低的数据量。

7.3 一张示意图先建立直觉

flowchart TD
    A[4个像素块] --> B1[4:4:4]
    A --> B2[4:2:2]
    A --> B3[4:2:0]

    B1 --> C1[Y完整 + U完整 + V完整]
    B2 --> C2[Y完整 + U横向减半 + V横向减半]
    B3 --> C3[Y完整 + U/V横纵都减采样]

这张图先不用追特别细的数学定义,你只要先记住:

从 4:4:4 到 4:2:0,色度信息是在逐步减少的。

而 4:2:0 正是视频系统里最常见的一种方案。


八、YUV420P 和 NV12 到底是什么关系

终于进入最容易在工程里出问题的地方了。

很多人知道它们都很常见,也知道它们都和 YUV420 有关,但说不清差别。

一句话先说结论:

YUV420P 和 NV12 都属于 4:2:0 路线,但它们主要差在内存布局。

也就是说:

  • 它们都在表达类似的采样思想
  • 但它们存数据的方式不一样

这就是“像素格式”层真正开始起作用的地方。


九、YUV420P 的内存布局到底长什么样

YUV420P 也常被理解成一种 planar 格式。

所谓 planar,就是:

不同分量分开放。

它的典型布局是:

  • 先放一整块 Y 平面
  • 再放一整块 U 平面
  • 最后放一整块 V 平面

9.1 结构示意图

1
2
3
Y Y Y Y Y Y Y Y ...   <- Y plane
U U U U ... <- U plane
V V V V ... <- V plane

9.2 更直观一点的二维理解

假设一张 4x4 图像:

1
2
3
4
5
6
7
8
9
10
11
12
13
Y plane (4x4)
Y00 Y01 Y02 Y03
Y10 Y11 Y12 Y13
Y20 Y21 Y22 Y23
Y30 Y31 Y32 Y33

U plane (2x2)
U00 U01
U10 U11

V plane (2x2)
V00 V01
V10 V11

因为是 4:2:0,所以 U 和 V 的分辨率会比 Y 更低。

9.3 YUV420P 的特点

  • 结构清楚
  • 每个平面分开,处理时比较直观
  • FFmpeg 里非常常见
  • 很多软件处理链路更容易直接操作

十、NV12 的内存布局到底长什么样

NV12 也是 4:2:0,但它不是 Y、U、V 三个平面完全分开,而是:

  • 先是一整块 Y 平面
  • 然后是交错存放的 UV 平面

10.1 结构示意图

1
2
Y Y Y Y Y Y Y Y ...   <- Y plane
U V U V U V U V ... <- interleaved UV plane

10.2 二维理解

还是用 4x4 图像举例:

1
2
3
4
5
6
7
8
9
Y plane (4x4)
Y00 Y01 Y02 Y03
Y10 Y11 Y12 Y13
Y20 Y21 Y22 Y23
Y30 Y31 Y32 Y33

UV plane (2x2 pairs)
U00 V00 U01 V01
U10 V10 U11 V11

注意这里不是单独的 U 平面和 V 平面,而是 UV 交错排在一起。

10.3 NV12 的特点

  • 也是 4:2:0
  • 数据量和 YUV420P 同量级
  • 更适合很多硬件编解码和图像处理路径
  • 在 Camera / 硬件视频链路里特别常见

十一、YUV420P 和 NV12 一张图看清楚差别

flowchart LR
    A[YUV420 采样思想] --> B1[YUV420P]
    A --> B2[NV12]

    B1 --> C1[Y plane]
    B1 --> C2[U plane]
    B1 --> C3[V plane]

    B2 --> D1[Y plane]
    B2 --> D2[UV interleaved plane]

所以你以后再看到这两个格式时,最稳的理解应该是:

  • 共同点:都属于 YUV420 路线
  • 关键差别:内存布局不同

这就是工程里很多“格式兼容”问题的根源之一。


十二、planar 和 packed 到底怎么理解

这两个词在图像和视频处理中非常高频。

12.1 planar 是什么

planar 的意思是:

不同通道 / 分量分开放。

例如:

  • Y 一整块
  • U 一整块
  • V 一整块

YUV420P 就是典型的 planar 风格。

12.2 packed 是什么

packed 的意思是:

同一个像素相关的数据尽量挨着放。

比如 RGB24 通常可以理解成:

1
R G B  R G B  R G B  ...

每个像素的三个分量放在一起。

12.3 NV12 算什么

NV12 介于两者之间,常被理解成一种半平面(semi-planar)结构:

  • Y 单独一个 plane
  • UV 交错作为另一个 plane

所以它既不是像 RGB24 那种完全 packed,也不是像 YUV420P 那种三平面完全拆开。


十三、为什么 FFmpeg、OpenCV、Camera、编码器经常在像素格式上“打架”

这部分非常工程。

因为你真正踩坑的地方,不在概念,而在模块对格式的要求经常不一致。

13.1 一个典型链路可能长这样

flowchart LR
    A[Camera 输出 NV12] --> B[OpenCV 处理 BGR]
    B --> C[FFmpeg 编码器要求 YUV420P]
    C --> D[显示链路可能又要 RGB / RGBA]

你会发现,一条链路里不同模块喜欢的数据格式可能完全不同。

13.2 为什么会这样

因为每个模块的目标不一样:

  • Camera / 硬件链路更看重采集效率和硬件兼容
  • OpenCV 更偏图像处理与算法接口友好
  • 编码器更偏压缩友好
  • 显示链路更偏渲染友好

所以它们并不会天然统一成同一个格式。

13.3 这类问题的典型表现

  • 颜色不对
  • 画面偏绿 / 偏紫
  • 编码器初始化失败
  • 图能看但质量不对
  • 性能突然很差,因为中间做了太多转换

所以工程里一定要有一个习惯:

每过一个模块,都明确当前数据格式是什么。

这句话看起来朴素,但非常有用。


十四、一段 FFmpeg 代码,怎么看 frame 的像素格式

先给一段非常实用的代码。

14.1 打印 AVFrame 的像素格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <libavutil/pixdesc.h>
#include <libavutil/pixfmt.h>
#include <libavutil/frame.h>
#include <stdio.h>

void print_frame_pixfmt(const AVFrame *frame) {
const char *name = av_get_pix_fmt_name((enum AVPixelFormat)frame->format);
if (name) {
printf("frame format: %s\n", name);
} else {
printf("frame format: unknown (%d)\n", frame->format);
}

printf("width=%d, height=%d\n", frame->width, frame->height);
printf("linesize: [%d, %d, %d, %d]\n",
frame->linesize[0], frame->linesize[1],
frame->linesize[2], frame->linesize[3]);
}

这段代码的意义不只是打印一个字符串,而是在提醒你:

frame 不只是“有图像”,它还有格式语义。

而这个格式语义,决定了你后面该怎么访问数据、怎么转换、能不能直接送编码器。


十五、一段 FFmpeg 代码,怎么做像素格式转换

很多时候你拿到的 frame 格式,不是编码器或后处理模块想要的格式,这时就得转。

FFmpeg 里最常见的是用 libswscale

15.1 从一种格式转到另一种格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <libswscale/swscale.h>
#include <libavutil/imgutils.h>

struct SwsContext *sws = sws_getContext(
src_w, src_h, AV_PIX_FMT_NV12,
dst_w, dst_h, AV_PIX_FMT_YUV420P,
SWS_BILINEAR, NULL, NULL, NULL);

AVFrame *dst = av_frame_alloc();
dst->format = AV_PIX_FMT_YUV420P;
dst->width = dst_w;
dst->height = dst_h;
av_frame_get_buffer(dst, 32);

sws_scale(sws,
(const uint8_t * const *)src->data,
src->linesize,
0,
src_h,
dst->data,
dst->linesize);

这段代码非常有代表性,因为它体现了工程里一个高频现实:

模块之间经常需要做格式桥接。

而格式桥接的成本,不只是代码复杂度,还包括:

  • CPU 开销
  • 内存带宽
  • 延迟
  • 额外 copy

所以格式转换并不是“无害小动作”,它会直接影响性能。


十六、一段 OpenCV 代码,怎么把 YUV 数据转成 BGR 处理

如果你做算法或图像处理,OpenCV 这层很常见。

16.1 NV12 转 BGR

1
2
3
4
5
6
7
8
9
#include <opencv2/opencv.hpp>

int width = 1920;
int height = 1080;

// 假设 nv12_data 指向连续的 NV12 数据缓冲区
cv::Mat nv12(height * 3 / 2, width, CV_8UC1, nv12_data);
cv::Mat bgr;
cv::cvtColor(nv12, bgr, cv::COLOR_YUV2BGR_NV12);

16.2 YUV420P 转 BGR

1
2
3
cv::Mat yuv420p(height * 3 / 2, width, CV_8UC1, yuv420p_data);
cv::Mat bgr;
cv::cvtColor(yuv420p, bgr, cv::COLOR_YUV2BGR_I420);

这两段代码非常适合帮助建立一个现实认识:

同样看起来都是“YUV420”,OpenCV 转换入口也不一样,因为布局不一样。

这正好对应我们前面讲的:

  • YUV420P 和 NV12 的采样思想接近
  • 但内存布局不同

十七、一张时序图看懂一帧图像在系统里为什么总要来回转格式

这个现象在工程里特别常见。

sequenceDiagram
    participant Cam as Camera
    participant ISP as ISP/Preprocess
    participant Algo as OpenCV/AI
    participant Enc as Encoder
    participant Disp as Display

    Cam->>ISP: 输出 NV12 / Raw
    ISP->>Algo: 转成 BGR/RGB 便于算法处理
    Algo->>Enc: 处理后转回 YUV420/NV12
    Enc->>Disp: 解码后可能再转 RGB/RGBA 显示

这张时序图说明了一个很重要的工程事实:

很多系统不是只转换一次格式,而是在不同目标之间不断桥接。

而你真正要做的,不只是“把它转成功”,更要思考:

  • 有没有必要每次都转
  • 哪一步可以少转一次
  • 能不能走硬件路径
  • 能不能减少 copy

这就是格式问题为什么会升级成性能问题。


十八、工程里最常见的几个坑

这一块你后面大概率会频繁遇到。

18.1 把颜色模型和像素格式混了

比如:

  • 说 NV12 是一种色彩空间
  • 说 YUV420P 是一种颜色模型

更稳的说法应该是:

  • YUV 更偏颜色表示方式
  • NV12 / YUV420P 更偏具体像素格式和内存布局

18.2 忽略 linesize / stride

很多人访问图像数据时直接按 width * bytes_per_pixel 走,结果图像错乱。

因为真实内存布局里,经常存在对齐和 stride。

所以工程里读 frame 数据时,一定不能忘:

  • data[]
  • linesize[]

18.3 以为“YUV420 都一样”

不是。

哪怕都叫 4:2:0,布局不同就足以让整个处理逻辑完全不同。

18.4 模块一多,格式语义丢了

比如:

  • Camera 给的是 NV12
  • OpenCV 里变成 BGR
  • 编码器要 YUV420P
  • 最后显示要 RGB

如果中间没人维护清楚“当前格式是什么”,问题迟早会出现。


十九、面试里怎么讲这件事,才不会显得很散

如果面试官问你:

RGB、YUV、NV12、YUV420P 你怎么理解?

不建议一上来就背定义。

更好的讲法应该是:

19.1 先分层

RGB 和 YUV 更偏颜色信息怎么表达,NV12 和 YUV420P 则是基于 YUV 采样思想的具体像素格式和内存布局。

19.2 再讲核心差别

RGB 更直观、偏显示友好;YUV 把亮度和色度分开,更适合视频压缩和传输。NV12 和 YUV420P 都属于 4:2:0 路线,但 YUV420P 是 Y/U/V 三平面分开,NV12 是 Y 平面加 UV 交错平面。

19.3 最后讲工程意义

工程里这些格式差异会直接影响:

  • Camera 输出
  • OpenCV 处理
  • FFmpeg 编码输入
  • 显示渲染链路
  • 性能和 copy 次数

如果你能按这个顺序讲,回答会明显更像工程师,而不是只会背几个术语。


总结:理解像素格式和色彩空间,本质上是在建立图像数据的“阅读能力”

回到这篇文章最开始的问题:

RGB、YUV、NV12、YUV420P 到底该怎么系统理解?

我觉得最稳的主线是这样:

  1. RGB / YUV 先看作颜色表达方式
  2. 4:4:4 / 4:2:2 / 4:2:0 再看作采样方式
  3. NV12 / YUV420P 再落到具体像素格式和内存布局
  4. 工程里再进一步关注:当前数据在哪一层、当前模块希望吃什么格式、格式转换代价有多大

如果把这条主线立住,后面很多原本容易混的东西都会突然顺起来:

  • 为什么 Camera 喜欢 NV12
  • 为什么编码器常见 YUV420 路线
  • 为什么 OpenCV 喜欢 BGR
  • 为什么显示前常常要转 RGB
  • 为什么格式转换会成为性能瓶颈

所以我觉得,理解这部分内容的真正价值,不只是为了记住几个格式名字,而是为了建立一种能力:

看到一份图像数据时,知道它是怎么表达颜色的,又是怎么在内存里排布的。

这其实就是图像系统最基础的“阅读能力”。


从像素格式到色彩空间,系统理解 RGB、YUV、NV12 与 YUV420P
https://breaker505.github.io/2026/03/19/pixel-format-and-color-space-rgb-yuv-nv12-yuv420p/
作者
爱发呆的鱼
发布于
2026年3月19日
许可协议