跳至正文
老丹的足迹 —— 代码写给机器,游记写给自己,感悟写给时间
老丹的足迹 老丹的足迹
老丹的足迹 老丹的足迹
  • 首页
  • 示例页面
  • 首页
  • 示例页面
老丹的足迹 老丹的足迹
老丹的足迹 老丹的足迹
  • 首页
  • 示例页面
  • 首页
  • 示例页面

YUV色彩空间完全指南:从概念原理到工程实战

一、YUV是什么——从RGB说起

要理解YUV,得先从我们最熟悉的RGB说起。

RGB:人眼看到的世界

RGB(红绿蓝)是我们最熟悉的色彩模型。计算机显示器、手机屏幕、电视,它们的工作原理就是通过控制每个像素点里红、绿、蓝三个子像素的亮度,混合出各种颜色。比如(255,0,0)是纯红,(0,255,0)是纯绿,(255,255,255)是白色,(0,0,0)是黑色。

RGB的好处是直观、符合人类对颜色的直觉理解。但它有一个致命的问题:三个通道的数据量太大了。如果一张1920×1080的图片,每个像素用3个字节(每个通道8位)表示,那么一张图就是1920×1080×3≈6.2MB。如果是每秒30帧的视频,一秒钟就是186MB的数据量——这个数据量对存储和传输来说都太大了。

YUV:为压缩而生的色彩模型

YUV的核心思想是:把颜色拆成”亮度”和”色度”两部分,然后对色度进行大幅压缩,因为人眼对亮度比对颜色更敏感。

这个思想来源于上世纪50年代彩色电视的发明。当时为了兼容黑白电视,工程师们设计了一套新方案:传输一个亮度信号(Y),让黑白电视直接显示;再叠加两个色度信号(U和V),让彩色电视用来还原颜色。黑白电视看彩色节目时,它只接收Y信号,看到的是黑白画面。

具体来说:

  • Y(Luma,亮度):描述像素的明暗程度,从黑到白。这就是黑白电视接收的信号。
  • U(Cb,蓝色色度):描述蓝色和黄色的偏移量。
  • V(Cr,红色色度):描述红色和青色的偏移量。

YUV到RGB的转换是一个线性变换:

Y  = 0.299*R + 0.587*G + 0.114*B
U  = 0.492*(B - Y) = -0.147*R - 0.289*G + 0.436*B
V  = 0.877*(R - Y) = 0.615*R - 0.515*G - 0.100*B

反过来,RGB可以从YUV还原:

R = Y + 1.140*V
G = Y - 0.395*U - 0.581*V
B = Y + 2.032*U

二、为什么YUV能压缩——人眼的弱点

YUV能大幅压缩视频体积的根本原因,是人眼视觉系统(HVS,Human Visual System)的特性。

人眼对亮度敏感,对颜色不敏感

人眼的视网膜上有两种感光细胞:视杆细胞和视锥细胞。视杆细胞负责感知亮度,数量多达1.2亿个,非常敏感;视锥细胞负责感知颜色,只有600万个,主要集中在视网膜中央凹区域。这意味着,人眼对亮度的分辨率远高于对颜色的分辨率。

一个经典的例子:如果你把一张彩色照片的色度信息完全去掉(变成黑白),人眼依然能辨认出绝大多数的内容。但如果把亮度信息去掉(只剩下颜色信息),画面就会变得完全无法辨认。

色度子采样

基于这个原理,视频编码中普遍采用色度子采样(Chroma Subsampling)技术:保留完整的亮度信息(Y),但丢弃一部分色度信息(U和V)。

最常见的采样格式是 4:2:0:

  • Y:每个像素都有完整的亮度信息
  • U和V:每4个像素(2×2的块)共享一组色度信息

这意味着,色度信息的数据量只有亮度的四分之一。加上亮度本身占三分之二的数据量,4:2:0格式的总数据量只有原始的50%——效果是肉眼几乎分辨不出差别。

采样格式Y分辨率U/V分辨率数据量常见用途
4:4:4全分辨率全分辨率100%专业后期制作、计算机图形
4:2:2全分辨率水平一半66.7%专业视频制作、广播
4:2:0全分辨率水平和垂直各一半50%绝大多数消费级视频、网络视频

三、YUV数据在内存里到底怎么排布的

概念上你知道YUV分成Y、U、V三个分量,但它们在内存里怎么放,直接决定你怎么填数据。这是写代码时遇到的第一道坎。

3.1 平面格式(Planar):三个分量分开存放

以 yuv420p 为例(P代表Planar),一帧1920×1080的数据在内存中是这样排列的:

地址偏移量 0          →  1920×1080 字节
┌───────────────────────────────┐
│  Y平面:每个像素1字节,共 1920×1080 = 2,073,600 字节       │
└───────────────────────────────┘
地址偏移量 2,073,600 →  + 960×540 字节
┌───────────────────────────────┐
│  U平面:每4个像素共享1个U值,共 960×540 = 518,400 字节     │
└───────────────────────────────┘
地址偏移量 2,592,000 →  + 960×540 字节
┌───────────────────────────────┐
│  V平面:每4个像素共享1个V值,共 960×540 = 518,400 字节     │
└───────────────────────────────┘
总大小:2,073,600 + 518,400 + 518,400 = 3,110,400 字节

在代码中的体现: FFmpeg的AVFrame结构体里,data[0]指向Y平面,data[1]指向U平面,data[2]指向V平面。

3.2 半平面格式(Semi-Planar):U和V交错

以 nv12 为例,这是很多硬件编解码器(如NVIDIA NVENC、Intel QSV)的默认输出格式:

地址偏移量 0          →  1920×1080 字节
┌───────────────────────────────┐
│  Y平面:同planar,每个像素1字节                             │
└───────────────────────────────┘
地址偏移量 2,073,600 →  960×540×2 字节
┌───────────────────────────────┐
│  UV交错平面:U0 V0 U1 V1 U2 V2 ...                         │
└───────────────────────────────┘

FFmpeg中,data[0]指向Y,data[1]指向UV交错平面。U和V不再分开存储。

3.3 紧缩格式(Packed):三个分量完全交错

现在已不常见,但在某些老代码或相机原始数据中可能遇到。以 YUYV 为例:

Y0 U0 Y1 V0 Y2 U1 Y3 V1 ...

Y和UV完全交错排列,无法用三个独立指针来访问,只能按固定步长从同一个缓冲区中读取。

3.4 快速对照表

格式名称内存布局Y平面U平面V平面常见场景
yuv420pplanar独立独立独立软编解码默认,x265输入
yuv422pplanar独立独立独立专业制作
yuv444pplanar独立独立独立计算机图形
nv12semi-planar独立UV交错–NVIDIA/Intel硬解输出
nv21semi-planar独立VU交错–Android摄像头
yuyv422packed与UV交错与Y交错与Y交错老式采集设备

四、行对齐(linesize / stride)——最容易踩的坑

这是实际开发中最常见的一个坑。你以为一行的数据就是width个字节,但实际上不是。

4.1 什么是行对齐

CPU读取内存时,如果数据地址是某个数的倍数(比如16字节或32字节对齐),读取效率会更高。为了利用这个特性,FFmpeg和许多底层库在分配帧内存时,每一行的字节数会向上取整到对齐边界。

比如一张1920×1080的YUV帧:

  • linesize[0](Y平面的行字节数)可能是 1920(不对齐)
  • 也可能是 2048(做了64字节对齐,1920向上取整到64的倍数)
  • 对于U/V平面(960宽),可能是 960 或 1024

4.2 这个坑长什么样

// ❌ 错误做法:把linesize当成width来用
memcpy(frame->data[0], source_data, width * height);
// 如果linesize[0]是2048,而width是1920,那么每行末尾有128个字节没被覆盖
// 画面会出现斜向的偏移条纹

如果linesize[0]是2048,而width是1920,每一行末尾有128个字节是“填充”的垃圾数据(或者上一行的数据)。如果按width * height来拷贝,那些填充位置的数据不会被覆盖,显示出来就是画面出现斜向的偏移条纹。

4.3 正确做法

// ✅ 逐行拷贝,每行只拷贝有效数据
uint8_t* dst = frame->data[0];
uint8_t* src = source_y_data;
for (int y = 0; y < height; y++) {
    memcpy(dst, src, width);
    dst += frame->linesize[0];  // 跳到下一行(跳过了填充区域)
    src += width;
}
// U和V平面同理,注意宽度和高度都是原来的1/2

4.4 硬件解码的特别说明

NVIDIA硬解输出的帧,linesize经常是256字节对齐(比如1920→2048)。如果直接把这个帧数据交给CPU做软件处理,务必逐行拷贝,否则画面必花。这就是为什么FFmpeg中要用av_frame_make_writable()和sws_scale()来做格式转换——它们内部正确处理了linesize对齐。

五、颜色范围 + 颜色矩阵 —— 画面发灰的元凶

这是实际转码中最常见的问题:为什么我转出来的视频颜色发灰/发暗/偏色?

5.1 两个独立的概念

颜色范围(Color Range) 决定YUV各分量的取值范围:

  • Limited(TV Range):Y取16-235(16是黑,235是白),UV取16-240。用于电视广播。
  • Full(PC Range):Y取0-255,UV取0-255。用于电脑显示器。

颜色矩阵(Color Matrix) 决定YUV和RGB之间的转换公式:

  • bt709:高清视频(720p以上),大多数网络视频
  • bt601:标清视频(DVD、老式电视)
  • bt2020:4K/8K和HDR视频

5.2 组合起来才有正确画面

这两个参数必须同时配对,否则颜色就偏。常见组合如下:

视频来源颜色矩阵颜色范围说明
网络流媒体(YouTube、B站)bt709Limited最常见,绝大多数情况
蓝光光盘bt709Limited同上
电脑录屏(OBS默认)bt709Full容易误判为Limited
DVDbt601Limited老标准,容易忽略
HDR 4Kbt2020Limited需要显示器支持
iPhone拍摄的HEVCbt709Limited大部分情况

5.3 错误组合导致的问题

源实际范围错误处理(当作)现象
电脑录屏FullLimited画面发灰,黑色变深灰
电视视频LimitedFull画面发白,对比度降低
bt709视频bt709bt601肤色偏红/偏绿

5.4 在FFmpeg中如何正确指定

# 源是电脑录屏(Full Range, bt709)
ffmpeg -i input.mp4 -color_range pc -color_matrix bt709 -c:v libx265 output.mp4

# 源是普通网络视频(Limited Range, bt709)——大多数情况默认即可
ffmpeg -i input.mp4 -c:v libx265 output.mp4

# 如果源的范围信息被错误标记了,强制指定
ffmpeg -i input.mp4 -color_range pc -color_primaries bt709 -color_trc bt709 -c:v libx265 output.mp4

5.5 如何在代码中设置

在FFmpeg的AVFrame中:

frame->color_range = AVCOL_RANGE_MPEG;   // Limited
// 或
frame->color_range = AVCOL_RANGE_JPEG;   // Full

frame->color_primaries = AVCOL_PRI_BT709;
frame->color_trc = AVCOL_TRC_BT709;
frame->colorspace = AVCOL_SPC_BT709;

如果不设置,FFmpeg会使用默认值(通常是Limited + bt709),可能导致与源不匹配。

六、RGB→YUV转换 —— 手写转换逻辑

当你从屏幕截图、OpenGL渲染或摄像头拿到RGB数据时,需要自己转成YUV再喂给x265。

6.1 BT.709 Full Range(PC范围)

范围0-255,转换公式:

Y  = clamp(round( 0.2126*R + 0.7152*G + 0.0722*B ), 0, 255)
U  = clamp(round(-0.1146*R - 0.3854*G + 0.5000*B + 128), 0, 255)
V  = clamp(round( 0.5000*R - 0.4542*G - 0.0458*B + 128), 0, 255)

6.2 BT.709 Limited Range(TV范围)

范围Y:16-235, UV:16-240,把Full范围的YUV缩放到Limited范围:

Y_limited  = (Y_full / 255.0) * 219 + 16    // 219 = 235-16
UV_limited = (UV_full / 255.0) * 224 + 16   // 224 = 240-16

在整数域计算(避免浮点):

// 使用16位精度(查表法常用)
int Y = (77*R + 150*G + 29*B) >> 8;      // 0-255
int U = ((-43*R - 84*G + 127*B) >> 8) + 128;
int V = ((127*R - 107*G - 20*B) >> 8) + 128;

// 缩放到Limited范围
Y = (Y * 219 + 127) / 255 + 16;
U = (U * 224 + 127) / 255 + 16;
V = (V * 224 + 127) / 255 + 16;

6.3 在实际项目中,99%的情况不需要手写

除非你在做极底层的优化,否则用FFmpeg的libswscale库搞定一切,它内部做了SIMD加速和查表优化:

struct SwsContext* swsCtx = sws_getContext(
    src_width, src_height, AV_PIX_FMT_RGB24,  // 输入RGB
    dst_width, dst_height, AV_PIX_FMT_YUV420P, // 输出YUV
    SWS_BILINEAR, nullptr, nullptr, nullptr
);

// 执行转换
sws_scale(swsCtx, src_data, src_linesize, 0, src_height, dst_data, dst_linesize);

七、硬解输出格式转换 —— 高性能转码避不开的问题

如果用硬件加速解码(NVIDIA NVENC、Intel QSV),解码器输出的帧格式大概率不是yuv420p,不能直接喂给x265。

7.1 各硬件平台的输出格式

硬件平台常见输出格式说明
NVIDIA (cuda)AV_PIX_FMT_CUDAGPU内存中的帧,CPU无法直接访问
NVIDIA (nvdec)AV_PIX_FMT_NV12显存中的NV12格式
Intel QSVAV_PIX_FMT_QSV显存中的特定格式
Intel VAAPIAV_PIX_FMT_VAAPILinux下Intel硬解的格式
Apple VideoToolboxAV_PIX_FMT_VIDEOTOOLBOXmacOS/iOS硬解格式

7.2 正确的处理方式

不能直接把硬解输出的帧传给x265。标准做法是:先用FFmpeg的滤镜(filter)或sws_scale做格式转换,转成yuv420p。

// 方式一:用滤镜(推荐)
// 创建滤镜图:hwupload → 解码 → 格式转换
// 代码较复杂,但性能最好,支持零拷贝路径

// 方式二:用sws_scale(简单但需注意GPU→CPU内存拷贝)
// 先把帧从GPU内存拷到CPU内存(av_hwframe_transfer_data)
// 然后用sws_scale转成yuv420p

在FFmpeg命令行中,这个过程由hwdownload和format滤镜自动完成:

ffmpeg -hwaccel cuda -i input.mp4 \
       -vf "hwdownload,format=yuv420p" \
       -c:v libx265 output.mp4

7.3 为什么硬解格式不能直接用

根本原因有三条:

  1. 内存位置不同:硬解输出在GPU显存中,x265是CPU软件编码器,读取不到GPU内存。
  2. 内存布局不同:硬解输出可能是NV12(半平面),x265需要yuv420p(平面)。
  3. 对齐方式不同:硬解输出的linesize对齐往往是256字节,x265虽然也能处理,但需要正确识别。

八、为什么GPU处理视频占优

这个问题是前面所有内容的逻辑延伸。既然我们讲了YUV的数据格式、行对齐、硬解输出,那自然会问:为什么要用GPU?它到底好在哪里?

8.1 CPU vs GPU 的架构差异

CPU和GPU的设计哲学从根上就是不同的。

CPU 的设计目标是 “低延迟” 。它有几个高性能的核心(比如8核、16核),每个核心都配有巨大的缓存、分支预测器、乱序执行单元——这些复杂电路是为了让单条指令执行得尽可能快。CPU擅长逻辑判断、分支跳转、串行计算这类复杂任务。

GPU 的设计目标是 “高吞吐量” 。它有成千上万个核心(比如NVIDIA RTX 4090有16384个CUDA核心),每个核心都很简单,没有复杂的缓存和分支预测,但它们能同时执行同样的操作。GPU擅长大量数据并行计算。

用一个类比来理解:

CPU像一位顶级数学教授,能做微积分,但一次只能做一道题。
GPU像一万个小学生,虽然只会加减乘除,但可以同时做一万道简单的算术题。

视频处理恰好是“一万道简单的算术题”这种场景。

8.2 视频处理为什么适合GPU

视频处理的核心操作——缩放、色彩空间转换(RGB↔YUV)、滤波、运动估计、DCT变换、量化——都有一个共同特点:一个像素的计算,不依赖于其他像素(或者只依赖于相邻的几个像素)。帧与帧之间、像素与像素之间可以独立并行处理。

视频处理操作并行程度说明
YUV↔RGB转换极高每个像素独立计算,公式完全相同
缩放(双线性/双三次)极高每个输出像素由周围几个输入像素计算得出,相互独立
降噪/锐化高每个像素依赖周围一个窗口内的像素,但窗口之间独立
编码中的运动估计中等每个宏块/CTU可以独立搜索参考帧,但需要参考帧的数据
编码中的熵编码低上下文依赖性强,难以并行(这是CPU的领域)

所以,GPU在视频处理中的优势分布是不均匀的:

  • 解码、缩放、色彩转换、滤镜:几乎可以100%交给GPU,效率极高。
  • 编码中的运动估计、变换、量化:宏块级别可并行,GPU能做。
  • 编码中的熵编码(CABAC等):依赖性强,GPU不擅长。

这就是为什么硬件编码器(NVENC、QSV)在编码质量上略逊于CPU软件编码(x265)——GPU把能并行的部分做得飞快,但在需要复杂决策(率失真优化、码率控制)和串行处理(熵编码)的环节,不得不做简化来换取速度。

8.3 内存带宽的维度

视频处理数据量大。1080p YUV420P一帧是3MB,4K一帧是12MB,60fps的4K视频每秒需要处理720MB的原始数据。

GPU的内存带宽远高于CPU:

  • 高端CPU内存带宽:约50-100 GB/s
  • 高端GPU内存带宽:约1000-2000 GB/s(GDDR6X/HBM)

对于“大量数据在内存里反复搬运”的视频处理任务,更高的内存带宽直接转化为更快的处理速度。

8.4 实际数据对比

场景CPU(x265, medium)GPU(NVIDIA NVENC)
4K→1080p H.265编码15-25 fps300-500 fps
画质(VMAF评分)基准(更高)略低3-5分
功耗100-150W30-50W
CPU占用100%<10%

GPU转码速度快了十几二十倍,功耗还更低。代价是画质略低(但多数场景肉眼难察觉),文件略大(同等画质下体积大约多10-20%)。

8.5 什么场景选GPU,什么场景选CPU

场景推荐原因
实时直播/推流GPU延迟敏感,速度优先
大量视频批量转码GPU吞吐量高,省时间
手机视频压缩存档GPU速度快,画质差异可接受
电影级母版制作CPU画质最高,体积最小,时间不重要
低码率流媒体(低带宽档位)CPU同码率下CPU画质更好,避免卡顿
个人NAS视频存档GPU速度快,画质差异肉眼看不出

8.6 GPU也不是万能的

  1. 画质略低:GPU编码器目标是“快”,在率失真优化、码率控制等需要复杂决策的环节做了简化。
  2. 格式受限:GPU硬解只支持特定格式和参数组合(比如不支持10-bit、特定profile),不是所有视频都能硬解。
  3. 通用性差:不同GPU(NVIDIA/AMD/Intel)的编码单元不同,需要分别适配,而x265在所有CPU上表现一致。
  4. 不能完全替代CPU:解码、缩放、滤镜可交给GPU,但封装、音频处理、部分编码决策仍需CPU参与。转码管线里CPU和GPU是协同工作的。

九、工程实战 —— 一张完整的决策表

当你拿到一个视频要转码时,按这个流程走:

1. 源视频是什么格式?
   ├─ H.264/H.265软解 → 输出yuv420p → 直接喂x265 ✅
   ├─ H.264/H.265硬解(NVIDIA/Intel)
   │   └─ 输出nv12/qsv/cuda → 需hwdownload + 格式转换 → 再喂x265 ⚠️
   └─ 屏幕截图/摄像头/OpenGL渲染(RGB)
       └─ 需sws_scale转成yuv420p → 再喂x265 ⚠️

2. 颜色参数怎么设?
   ├─ 来源是网络视频(YouTube/B站)→ bt709 + Limited ✅
   ├─ 来源是电脑录屏 → bt709 + Full ⚠️(不要默认!)
   ├─ 来源是DVD → bt601 + Limited ⚠️
   └─ 来源是HDR 4K → bt2020 + Limited ⚠️

3. 用CPU还是GPU?
   ├─ 实时/批量场景 → GPU(NVENC/QSV)
   ├─ 画质至上 → CPU(x265)
   └─ 两者配合:GPU解码缩放 + CPU编码(复杂场景的最佳实践)

4. 输出时要不要设置颜色元数据?
   └─ 在AVFrame中设置color_range/color_primaries/color_trc/colorspace
       └─ 让封装后的MP4带上正确的颜色元数据,播放器才能正确显示 ✅

总结

YUV是视频编码的”世界语”——几乎所有的视频编码器(x264、x265、AV1等)都工作在YUV色彩空间上。理解YUV的意义在于:

层次内容
概念层YUV把颜色拆成亮度和色度,利用人眼弱点实现了大幅压缩
格式层4:2:0 / 4:2:2 / 4:4:4 决定数据量,planar / semi-planar / packed 决定内存布局
实操层linesize决定你怎么拷贝数据,color_range + color_matrix决定颜色对不对
硬件层GPU靠大量简单核心并行处理像素,CPU靠复杂核心做精细决策,各司其职
工程层RGB→YUV转换用sws_scale,硬解输出记得hwdownload转格式,CPU/GPU按场景选

一句话总结:YUV就是把颜色和亮度拆开,狠狠压缩颜色——因为人眼根本看不出差别。这就是视频能压缩到原来几十分之一的理论基础。而工程中的所有坑,全都是在“怎么正确地操作这个YUV数据”这件事上。至于CPU和GPU的选择,本质是“速度优先还是画质优先”的取舍,两者配合才是最佳实践。

作者

老丹

关注我
其他文章
上一个

tcpdump 的 BPF 过滤语法:从入门到精通

下一个

深入解析x265:HEVC/H.265编码的核心引擎

关于博主

    老丹是一名C/C++后台开发工程师,信奉“无抽象不设计,无性能不生产”。

  • 技术栈:Modern C++、Linux环境编程、多线程/并发、网络编程等。
  • 信条:能用constexpr解决的问题绝不拖到运行时,能靠RAII避免的泄漏绝不写析构。
  • 正在填坑:从解封装到渲染的C++全链路实现,正在驯服FFmpeg与H.264/H.265。
  • 输出原则:这里的每一段代码都经过-Wall -Wextra -Werror -O2的洗礼。

近期文章

  • Ubuntu 防火墙迁移指南:从 UFW 到 firewalld 的完整实践 2026年9月12日
  • Nano 编辑器完全操作指南:从入门到熟练 2026年9月12日
  • SSCG:让自签名证书不再“危险”的生成工具 2026年9月12日
  • Ubuntu Samba 服务安装与配置完全指南 2026年9月12日
  • 从零开始:用 Docker 部署 Jellyfin 并启用英特尔核显硬件加速 2026年9月11日

文章分类

  • C/C++开发 (22)
  • Docker容器 (5)
  • Linux工具包 (17)
  • Linux服务配置 (50)
  • Linux系统 (16)
  • OpenWrt路由 (3)
  • Shell脚本 (3)
  • 代码管理 (1)
  • 安防技术 (4)
  • 数据安全 (36)
  • 未分类 (1)
  • 网络协议 (25)
  • 计算机理论 (23)
  • 音视频技术 (5)
联系我们:📍 地址:中国·广东省深圳市   |   ✉️ 邮箱:support@tanglinux.com   |   💬 QQ:870866607
版权所有:老丹的足迹粤ICP备2026061170号-1       公安备案图标 粤公网安备44030002013274号