第十三章|基础专题一:像素、颜色与原始帧
从像素、颜色分量、位深、RGB/RGBA、YUV420P、NV12、stride、颜色参数和 VideoFrame 生命周期入手,建立未压缩视频帧的底层心智模型。
第十三章|基础专题一:像素、颜色与原始帧
原稿学习节奏:第 1 周。
一、本周学习目标
完成本周学习后,你应当能够:
- 区分像素、颜色分量、采样点、平面和帧。
- 区分分辨率、帧率、码率、位深和每像素位数。
- 画出
RGB24、RGBA、I420/YUV420P、NV12的内存布局。 - 解释为什么数字视频领域常说的“YUV”通常实际是
Y′CbCr。 - 解释 4:4:4、4:2:2、4:2:0 的采样数量和画质差异。
- 根据宽高、像素格式、stride 和帧率计算内存及带宽。
- 区分 primaries、transfer、matrix、range 四类颜色参数。
- 排查视频偏色、画面发灰、黑位压死和透明边缘黑边。
- 在 WebCodecs 中正确读取
VideoFrame信息并及时释放资源。
二、本周安排
| 时间 | 学习内容 | 产出 |
|---|---|---|
| 第 1 天,1 小时 | 像素、分辨率、帧率、码率、位深 | 完成原始数据量计算 |
| 第 2 天,1.5 小时 | RGB、RGBA、Alpha | 能解释 straight 与 premultiplied alpha |
| 第 3 天,2 小时 | Y′CbCr、亮度与色度 | 手算一次 RGB → Y′CbCr |
| 第 4 天,1.5 小时 | 4:4:4、4:2:2、4:2:0 | 画出 2×2 像素的采样关系 |
| 第 5 天,1.5 小时 | Packed、Planar、NV12、I420 | 能根据地址读取任意像素 |
| 第 6 天,1.5 小时 | stride、alignment、颜色管理 | 完成偏色排查表 |
| 第 7 天,1 小时 | WebCodecs 实验与面试复习 | 完成练习和自测题 |
三、概念地图
本周只研究未压缩媒体数据:
相机 / 图片 / AI 模型 / Canvas
↓
原始像素帧
RGB / RGBA / I420 / NV12
↓
视频编码器
H.264 / H.265 / AV1
↓
基础码流
↓
MP4 / WebM
必须建立以下边界:
| 层次 | 示例 | 本周是否学习 |
|---|---|---|
| 原始像素 | RGBA、NV12、I420 | 是 |
| 视频编码标准 | H.264、H.265 | 否 |
| 编码器实现 | x264、x265 | 否 |
| 基础码流 | H.264 NALU | 否 |
| 容器 | MP4、WebM | 否 |
| 浏览器原始帧接口 | VideoFrame | 是 |
| 浏览器播放接口 | <video>、MSE | 只建立边界 |
原始帧不是图片文件,也不是视频码流。
一块 1920×1080 的 RGBA 内存没有文件头,不会自行告诉你:
- 宽度和高度;
- 每行有多少字节;
- 通道顺序;
- 是否有 padding;
- 使用什么颜色空间;
- Alpha 是否预乘。
这些信息必须由调用者另行保存。
四、像素、采样点与颜色分量
4.1 像素不等于一个字节
像素是图像空间中的一个位置。一个像素可能由多个颜色分量描述:
RGB 像素 = R + G + B
RGBA 像素 = R + G + B + A
8-bit RGBA 中每个分量占一个字节,所以一个像素通常占 4 字节。
但在 Y′CbCr 4:2:0 中:
- 每个像素都有自己的 Y′ 采样;
- 多个像素共享 Cb、Cr 采样;
- 因此不能简单认为每个像素都连续保存着 Y、Cb、Cr 三个字节。
更准确的术语是:
- Pixel:最终图像中的像素位置。
- Component:R、G、B、Y、Cb、Cr、A 等颜色分量。
- Sample:某个分量在某个位置的离散采样值。
- Plane:保存一个或一组分量的二维内存区域。
4.2 分辨率的三种含义
日常说“1920×1080”,通常指可见画面大小,但底层系统可能同时存在:
- Coded size:实际分配或编码的尺寸。
- Visible rectangle:真正有效的可见区域。
- Display size:考虑像素宽高比、旋转等因素后的显示尺寸。
例如,底层可能为硬件对齐分配 1920×1088,但只显示其中的 1920×1080。
WebCodecs 的 VideoFrame 因此分别提供:
codedWidth / codedHeight
visibleRect
displayWidth / displayHeight
不能默认这三组尺寸始终相等。(W3C)
4.3 帧率与码率不是一回事
帧率 Frame Rate 表示一秒有多少帧:
24 FPS
25 FPS
30 FPS
30000/1001 FPS ≈ 29.97 FPS
60 FPS
码率 Bitrate 表示每秒传输或存储多少比特:
8 Mbit/s
20 Mbit/s
原始视频也可以计算“原始数据率”:
[ \text{Raw data rate}
\text{bytes per frame} \times \text{frames per second} ]
但日常讨论视频码率时,通常指压缩后的码率。
4.4 位深不等于每像素位数
位深通常表示每个颜色分量使用多少有效位:
- 8-bit RGB:每个 R、G、B 分量 8 bit,总计 24 bit/pixel。
- 8-bit RGBA:总计 32 bit/pixel。
- 8-bit Y′CbCr 4:2:0:平均 12 bit/pixel。
- 10-bit Y′CbCr 4:2:0:有效信息平均 15 bit/pixel。
还要区分:
有效位深
≠
内存容器宽度
10-bit 数据经常存储在 16-bit 整数中。此时每个采样虽然只有 10 个有效位,却可能实际占用 2 字节。
五、RGB、RGBA 与 Alpha
5.1 RGB24
RGB24 通常表示一个像素连续保存 3 个 8-bit 分量:
地址递增 →
R0 G0 B0 | R1 G1 B1 | R2 G2 B2 | ...
紧密排列时:
[ S_{\text{RGB24}} = W \times H \times 3 ]
其中:
- (W):宽度;
- (H):高度;
- (S):字节数。
5.2 RGBA
8-bit RGBA 的字节布局为:
R0 G0 B0 A0 | R1 G1 B1 A1 | ...
紧密排列时:
[ S_{\text{RGBA}} = W \times H \times 4 ]
注意还存在:
BGRA
ARGB
ABGR
RGBX
BGRX
X 通常代表占位字节,图像被视为完全不透明。
WebCodecs 明确定义了 RGBA、BGRA、RGBX、BGRX 等格式的分量顺序和每像素字节数。(W3C)
5.3 Alpha 表示什么
Alpha 通常表示覆盖率或不透明度:
A = 0 完全透明
A = 1 完全不透明
A = 0.5 半透明
8-bit Alpha 使用 0~255 表示:
0 → 完全透明
255 → 完全不透明
5.4 Straight Alpha
Straight Alpha,也叫 unassociated alpha:
RGB 保存物体本身的颜色
A 单独保存覆盖率
例如半透明红色:
[ (R,G,B,A)=(1,0,0,0.25) ]
使用 source-over 合成时:
[ C_o=C_s\alpha_s+C_d(1-\alpha_s) ]
其中:
- (C_s):前景颜色;
- (C_d):背景颜色;
- (\alpha_s):前景 Alpha;
- (C_o):输出颜色。
5.5 Premultiplied Alpha
Premultiplied Alpha,即预乘 Alpha,先把 RGB 乘以 Alpha:
[ \widetilde C_s=C_s\alpha_s ]
刚才的半透明红色会存为:
[ (0.25,0,0,0.25) ]
合成公式变成:
[ \widetilde C_o
\widetilde C_s + \widetilde C_d(1-\alpha_s) ]
预乘表示通常更适合插值、缩放、模糊和图层合成;W3C 的颜色插值规则也采用先预乘、插值后再解除预乘的处理方式。(W3C)
常见错误
把预乘数据当成 straight 数据:
RGB 已经乘过一次 Alpha
渲染时又乘一次
→ 半透明边缘变黑
把 straight 数据当成预乘数据:
RGB 没乘 Alpha
渲染器却直接参与合成
→ 边缘过亮或出现白边
在线编辑器内部最好统一一种 Alpha 约定,并在素材导入、GPU 上传和导出阶段明确转换。
六、YUV 与 Y′CbCr
6.1 业界口语中的“YUV”
严格来说:
- YUV 原本用于模拟视频系统。
- Y′CbCr 是数字视频中常见的亮度与色差信号表示。
工程交流中,人们经常把 YUV420P、YUV frame 当作通用称呼,但真正讨论数字像素值时,通常指的是 Y′CbCr。
其中:
- (Y’):由非线性的 (R’G’B’) 计算得到的 luma;
- Cb:蓝色色差;
- Cr:红色色差;
- 撇号
'表示分量已经经过非线性传递函数处理。
Y′ 不等于物理意义上的线性亮度 Y。
6.2 为什么不直接一直使用 RGB
RGB 很适合显示和图形渲染,但视频压缩更常使用 Y′CbCr,主要因为:
- RGB 三通道之间存在较强相关性。
- 转换为亮度和色度后,更容易分别处理。
- 人眼通常对高频亮度细节比高频色度细节更敏感。
- 可以降低色度分辨率,同时保留较多主观细节。
- 大量视频编解码硬件和视频标准都围绕 Y′CbCr 工作。
注意:这不是说人眼“看不见颜色”,而是高频色度信息在很多自然画面中可以进行更强压缩。
6.3 RGB 到 Y′CbCr 的通用形式
令:
[ K_g=1-K_r-K_b ]
则:
[ Y’=K_rR’+K_gG’+K_bB’ ]
[ Cb=\frac{B’-Y’}{2(1-K_b)} ]
[ Cr=\frac{R’-Y’}{2(1-K_r)} ]
在归一化表示中:
R′、G′、B′ ∈ [0,1]
Y′ ∈ [0,1]
Cb、Cr 通常位于 [-0.5,0.5]
不同标准使用不同系数:
| 矩阵 | (K_r) | (K_g) | (K_b) | 常见关联 |
|---|---|---|---|---|
| BT.601 系列 | 0.2990 | 0.5870 | 0.1140 | SD 视频 |
| BT.709 | 0.2126 | 0.7152 | 0.0722 | HD 视频 |
| BT.2020 NCL | 0.2627 | 0.6780 | 0.0593 | UHD/WCG 视频 |
BT.601、BT.709 和 BT.2020 分别面向不同电视制作体系;但在真实文件中,不能只根据分辨率猜测颜色矩阵,仍应读取实际元数据。(ITU)
6.4 手算示例:BT.709 纯红色
令:
[ R’=1,\quad G’=0,\quad B’=0 ]
首先计算:
[ Y’=0.2126 ]
然后:
[ Cb
\frac{0-0.2126}{2(1-0.0722)} \approx -0.1146 ]
[ Cr
\frac{1-0.2126}{2(1-0.2126)} =0.5 ]
因此纯红色会表现为:
中等偏低的 Y′
略低于中心值的 Cb
最大的正 Cr
这说明 Y′CbCr 中三个分量不能被分别理解为三个独立颜色通道。
七、色度下采样
7.1 4:4:4
每个像素都有独立的 Y′、Cb、Cr:
像素 0:Y0 Cb0 Cr0
像素 1:Y1 Cb1 Cr1
像素 2:Y2 Cb2 Cr2
8-bit 时:
[ 24\text{ bit/pixel} ]
适合:
- 高质量中间处理;
- 颜色键控;
- 文字和图形素材;
- 对色度边缘要求高的内容。
7.2 4:2:2
水平方向两个像素共享一组 Cb、Cr:
Y0 Y1 共用 Cb0 Cr0
Y2 Y3 共用 Cb1 Cr1
每两个像素保存:
2 个 Y + 1 个 Cb + 1 个 Cr
8-bit 时平均:
[ 16\text{ bit/pixel} ]
7.3 4:2:0
普通 4:2:0 中,一个 2×2 亮度区域共享一组 Cb、Cr:
Y00 Y01 ┐
Y10 Y11 ┘ 共享 Cb00、Cr00
四个像素总共保存:
4 个 Y
1 个 Cb
1 个 Cr
因此每像素平均采样数为:
[ \frac{4+1+1}{4}=1.5 ]
8-bit 时:
[ 1.5 \times 8=12\text{ bit/pixel} ]
相对于 4:4:4:
[ \frac{12}{24}=50% ]
所以 8-bit 4:2:0 的紧密原始数据量只有 4:4:4 的一半。
WebCodecs 对 I420 的定义同样规定:U、V 平面在水平和垂直方向都相对于 Y 平面缩小 2 倍。(W3C)
7.4 色度下采样的副作用
4:2:0 对自然照片和普通视频通常很有效,但容易影响:
- 彩色小字号文字;
- 红色或蓝色细线;
- UI 屏幕录制;
- 高饱和度边缘;
- Chroma key;
- 多次缩放和重编码。
此外,仅知道“4:2:0”还不完整。不同系统中的 chroma location/chroma siting 可能不同,即色度采样点相对于亮度像素的位置不同。处理错误时会出现颜色边缘水平或垂直错位。
八、Packed、Planar 与 Semi-planar
8.1 Packed
多个分量交错保存在同一个平面中。
RGBA:
R G B A | R G B A | R G B A
YUYV 4:2:2:
Y0 U0 Y1 V0 | Y2 U1 Y3 V1
优点是单个像素或像素组读取直观;缺点是只处理某个分量时可能不够方便。
8.2 Planar
不同分量分别保存。
I420,也常对应 FFmpeg 中的 yuv420p:
Plane 0:YYYYYYYYYYYY
Plane 1:UUUUUU
Plane 2:VVVVVV
内存顺序:
Y plane
↓
U plane
↓
V plane
对于偶数宽高:
[ S_Y=W\times H ]
[ S_U=S_V=\frac W2\times\frac H2 ]
总大小:
[ S_{\text{I420}}
WH+\frac{WH}{4}+\frac{WH}{4}
1.5WH ]
对于奇数宽高,不能直接使用 (1.5WH),应计算:
[ S_{\text{I420}}
WH + 2 \left\lceil\frac W2\right\rceil \left\lceil\frac H2\right\rceil ]
8.3 Semi-planar
NV12 有两个平面:
Plane 0:YYYYYYYYYYYY
Plane 1:UVUVUVUVUVUV
它与 I420 的采样数量相同,但 U、V 不再分成两个独立平面。
I420:Y + U + V
NV12:Y + UV
WebCodecs 对 NV12 的定义也是一个 Y 平面加一个交错的 UV 平面。(W3C)
容易混淆的格式
| 格式 | 布局 |
|---|---|
| I420 | Y、U、V |
| YV12 | Y、V、U |
| NV12 | Y、UVUV |
| NV21 | Y、VUVU |
| YUYV | Y0 U0 Y1 V0 |
| UYVY | U0 Y0 V0 Y1 |
名称相似不代表内存可以直接互用。
九、原始数据量计算
以 1920×1080、8-bit、紧密排列为例:
[ W\times H=2{,}073{,}600 ]
| 格式 | 字节/帧 | 十进制 MB/帧 | 30 FPS 数据率 |
|---|---|---|---|
| RGB24 | 6,220,800 | 6.22 MB | 186.62 MB/s |
| RGBA | 8,294,400 | 8.29 MB | 248.83 MB/s |
| I420 | 3,110,400 | 3.11 MB | 93.31 MB/s |
| NV12 | 3,110,400 | 3.11 MB | 93.31 MB/s |
| YUV422P | 4,147,200 | 4.15 MB | 124.42 MB/s |
| YUV444P | 6,220,800 | 6.22 MB | 186.62 MB/s |
RGBA 在 30 FPS 时:
[ 8{,}294{,}400\times30
248{,}832{,}000\text{ bytes/s} ]
换成比特率:
[ 248{,}832{,}000\times8
1.99\text{ Gbit/s} ]
I420 在 30 FPS 时:
[ 3{,}110{,}400\times30
93.312\text{ MB/s} ]
这还没有计算:
- 对齐 padding;
- GPU 纹理;
- 解码器参考帧;
- JavaScript 复制缓冲区;
- Canvas 中间表面;
- 编码器输入队列。
所以编辑器同时缓存 60 帧 1080p RGBA,仅理论像素数据就需要:
[ 8.2944\text{ MB}\times60
497.664\text{ MB} ]
这正是浏览器编辑器不能无限缓存原始帧的原因。
十、Plane、Stride、Alignment 与 Padding
10.1 Plane
Plane 是保存二维采样数据的内存区域。
I420 有三个 plane:
Y plane
U plane
V plane
NV12 有两个:
Y plane
UV plane
RGBA 通常只有一个。
10.2 Stride
Stride,也叫 line size 或 row pitch,表示:
从当前行起始地址走到下一行起始地址,需要跨过多少字节。
它包括有效像素和行尾 padding。
像素地址的一般形式为:
[ \text{address}(x,y)
\text{planeBase} + y\times\text{stride} + x\times\text{bytesPerElement} ]
WebCodecs 将 stride 定义为每一行占用的字节数,包含 padding;每个 plane 可以拥有不同的 offset 和 stride。(W3C)
10.3 为什么需要 Padding
CPU SIMD、GPU 和硬件编解码器经常要求:
16 字节对齐
32 字节对齐
64 字节对齐
128 字节对齐
假设 RGBA 宽度为 1921:
[ 1921\times4=7684\text{ bytes} ]
若每行按 64 字节对齐:
[ \operatorname{align}(7684,64)=7744 ]
所以:
有效字节:7684
stride: 7744
padding: 60 字节/行
真实内存大小是:
[ 7744\times H ]
而不是:
[ 1921\times H\times4 ]
10.4 多平面格式的真实大小
通用公式:
[ S=\sum_i \text{stride}_i\times \text{planeHeight}_i ]
例如 I420:
[ S
\text{stride}_Y H + \text{stride}_U\left\lceil\frac H2\right\rceil + \text{stride}_V\left\lceil\frac H2\right\rceil ]
因此,width × height × 1.5 只适用于:
- 8-bit;
- 紧密排列;
- 没有额外对齐;
- 正确处理奇数尺寸;
- 没有额外 plane;
- CPU 可线性访问的普通 I420。
硬件表面还可能采用 tiled 或厂商私有布局,不能直接按普通线性内存解释。
十一、颜色管理的四个核心参数
仅知道像素是 yuv420p 还不足以正确显示颜色。
至少要知道:
Primaries
Transfer
Matrix
Range
还经常需要:
Chroma location
WebCodecs 的 VideoColorSpace 同样分别暴露 primaries、transfer、matrix 和 fullRange。(W3C)
11.1 Primaries:原色点
Primaries 定义:
- 红色原色在哪里;
- 绿色原色在哪里;
- 蓝色原色在哪里;
- 三个原色形成多大的色域。
常见值:
BT.709 / sRGB primaries
Display P3
BT.2020
原色点错误通常表现为:
- 整体饱和度不对;
- 某些颜色明显偏色;
- 广色域内容在普通画布中过饱和或失去饱和度。
11.2 Transfer:传递特性
Transfer function 描述:
非线性编码值
↔
线性光
之所以使用非线性表示,是为了更合理地分配数字精度,并匹配显示和感知系统。
常见传递特性包括:
sRGB
BT.709
PQ
HLG
Linear
传递函数错误可能表现为:
- 整体过暗或过亮;
- 阴影层次异常;
- 重复执行 gamma;
- HDR 内容灰暗或高光完全错误。
11.3 Matrix:RGB 与 Y′CbCr 的转换矩阵
Matrix 决定:
R′G′B′
↔
Y′CbCr
常见矩阵:
BT.601
BT.709
BT.2020 non-constant luminance
例如,把 BT.709 数据按 BT.601 矩阵转换,可能导致:
- 肤色变化;
- 红色、绿色和蓝色饱和度异常;
- 整体轻微但明显的色相偏移。
11.4 Range:Full 与 Limited
8-bit limited range 的常见标称范围:
Y′: 16~235
Cb/Cr: 16~240
色度中心:128
Full range 通常使用:
0~255
FFmpeg 对 limited range 和 full range 也分别定义了对应的数字范围。(FFmpeg)
解释错误的表现
| 实际数据 | 被错误解释为 | 结果 |
|---|---|---|
| Limited | Full | 黑色不够黑,白色不够白,画面发灰 |
| Full | Limited | 黑位和白位被压缩或裁剪,细节丢失 |
10-bit limited range 通常对应:
Y′: 64~940
Cb/Cr: 64~960
11.5 一个完整颜色描述
不能只写:
这是 BT.709 视频
更精确的描述应当类似:
Pixel format: I420 8-bit
Primaries: BT.709
Transfer: BT.709
Matrix: BT.709
Range: Limited
Chroma siting: Left
因为原色点、传递特性和矩阵在元数据中是相互独立的字段。
十二、偏色排查方法
| 现象 | 优先检查 |
|---|---|
| 画面发灰、对比度低 | Limited 被当作 Full |
| 黑位压死、白色溢出 | Full 被当作 Limited |
| 红绿蓝色相不正确 | BT.601/709/2020 matrix 错误 |
| 整体太暗或太亮 | Transfer function、重复 gamma |
| 广色域画面过饱和 | Primaries 或 gamut conversion |
| 透明边缘发黑 | Premultiplied 数据被再次乘 Alpha |
| 透明边缘发亮 | Straight 数据被当作 premultiplied |
| 彩色边缘位置错开 | Chroma siting 或色度缩放错误 |
| Canvas 正常但导出偏色 | 编码器输入格式或导出元数据错误 |
| 预览与最终文件颜色不同 | 浏览器渲染和服务端转换链路不一致 |
建议按照以下顺序排查:
确认输入像素格式
↓
确认各 plane 和 stride
↓
确认 range
↓
确认 matrix
↓
确认 transfer
↓
确认 primaries
↓
确认 Canvas / GPU 目标颜色空间
↓
确认编码输出元数据
十三、Web 实现视角
13.1 VideoFrame 的关键属性
function inspectFrame(frame: VideoFrame): void {
console.table({
format: frame.format,
codedSize: `${frame.codedWidth}x${frame.codedHeight}`,
visibleRect: frame.visibleRect
? `${frame.visibleRect.x},${frame.visibleRect.y},` +
`${frame.visibleRect.width}x${frame.visibleRect.height}`
: "unknown",
displaySize: `${frame.displayWidth}x${frame.displayHeight}`,
timestamp: frame.timestamp,
duration: frame.duration,
primaries: frame.colorSpace.primaries,
transfer: frame.colorSpace.transfer,
matrix: frame.colorSpace.matrix,
fullRange: frame.colorSpace.fullRange,
});
}
不要只打印 displayWidth,因为真实内存通常与 coded size 和 plane layout 有关。
13.2 正确复制 VideoFrame
不要自行猜测缓冲区大小,应使用 allocationSize():
async function copyFrameToCPU(
frame: VideoFrame,
): Promise<{ bytes: Uint8Array; layout: PlaneLayout[] }> {
if (frame.format === null) {
throw new Error("当前 VideoFrame 没有可复制的已知像素格式");
}
const size = frame.allocationSize();
const bytes = new Uint8Array(size);
const layout = await frame.copyTo(bytes);
return { bytes, layout };
}
allocationSize() 返回满足指定复制选项所需的最小字节数,copyTo() 返回实际 plane 的 offset 和 stride。(W3C)
13.3 及时关闭 VideoFrame
decoder.output = (frame: VideoFrame): void => {
try {
canvasContext.drawImage(frame, 0, 0);
} finally {
frame.close();
}
};
VideoFrame 可能引用 CPU 内存、GPU 内存或硬件媒体资源。规范建议在不再使用时立即调用 close(),以降低内存压力并促进资源复用。(W3C)
不要依赖 JavaScript 垃圾回收器及时释放媒体资源。
13.4 Canvas 不一定只是普通 8-bit RGBA
普通 ImageData 的 rgba-unorm8 使用 RGBA 顺序,每像素 4 字节;规范还定义了 rgba-float16,每像素使用四个 16-bit 浮点分量,即 8 字节。(html.spec.whatwg.org)
rgba-unorm8:4 bytes/pixel
rgba-float16:8 bytes/pixel
浏览器编辑器不能默认所有 Canvas 或 GPU 中间表面都只占 width × height × 4。
13.5 避免不必要的 CPU 转换
性能较差的链路:
硬件解码 NV12
↓
复制到 CPU
↓
转换 RGBA
↓
上传 GPU
↓
渲染
更理想的链路:
硬件解码 VideoFrame
↓
GPU 直接采样或浏览器直接绘制
↓
滤镜与合成
需要 CPU 读取像素时再执行 copyTo(),不要在每一帧无条件执行。
十四、实际项目中的应用
A. AI 生成视频网页
AI 模型常见输出是:
Float tensor
N × C × H × W
或
N × H × W × C
输出通常需要经过:
模型 RGB 浮点输出
↓
数值范围和 transfer 确认
↓
色域转换
↓
RGB → Y′CbCr
↓
4:2:0 色度下采样
↓
转换为 NV12 / I420 / P010
↓
视频编码
重点问题:
- 模型输出究竟是线性 RGB 还是类似 sRGB 的非线性 RGB。
- 输出值是
[0,1]、[-1,1]还是其他范围。 - 是否需要从模型工作色域转换到 BT.709 或 BT.2020。
- RGB → Y′CbCr 时使用哪套 matrix。
- 编码器要求 NV12、I420 还是其他格式。
- 是否发生 GPU → CPU → GPU 的重复复制。
- 生成速度是否超过编码和播放速度,需要 backpressure。
缩略图可使用 RGB/JPEG,但正式连续播放不应长期传输大量独立 PNG 帧。
B. 在线 Web 视频编辑器
推荐像素处理链路:
解码器输出 VideoFrame
↓
保留原始 Y′CbCr 或 GPU 表面
↓
按播放窗口缓存少量帧
↓
GPU 完成缩放、滤镜和合成
↓
转为编码器要求的像素格式
↓
VideoEncoder / 服务端编码
工程原则:
- 时间线只缓存当前播放位置附近的帧。
- 缩略图使用较低分辨率。
- 普通预览避免把每帧复制成 RGBA 数组。
- 透明贴纸、字幕和图层统一 Alpha 约定。
- 合成完成后再转换为最终 Y′CbCr 格式。
- 导出时明确写入 primaries、transfer、matrix 和 range。
- 取消拖动前产生的旧解码任务。
- 每个不再使用的
VideoFrame都应关闭。
十五、FFmpeg 小实验
15.1 查看颜色和像素格式
ffprobe -v error \
-select_streams v:0 \
-show_entries \
stream=width,height,pix_fmt,color_range,color_space,color_transfer,color_primaries \
-of default=noprint_wrappers=1 \
input.mp4
重点观察:
pix_fmt
color_range
color_space
color_transfer
color_primaries
15.2 导出原始 I420
ffmpeg -i input.mp4 \
-pix_fmt yuv420p \
-f rawvideo \
output.yuv
播放时必须重新提供格式和尺寸:
ffplay \
-f rawvideo \
-pixel_format yuv420p \
-video_size 1920x1080 \
-framerate 30 \
output.yuv
原始视频没有容器头,所以读取时必须明确给出像素格式、尺寸和帧率;FFmpeg 的 rawvideo 文档也明确要求调用方提供这些信息。(FFmpeg)
15.3 导出 RGBA
ffmpeg -i input.mp4 \
-pix_fmt rgba \
-f rawvideo \
output.rgba
ffplay \
-f rawvideo \
-pixel_format rgba \
-video_size 1920x1080 \
-framerate 30 \
output.rgba
对比两个文件大小,应接近:
RGBA / I420
= 4 / 1.5
≈ 2.67
十六、易错点
- 像素不等于字节。
- 位深不等于每像素总位数。
- YUV420P 不是每个像素保存 Y、U、V 三个字节。
- 数字视频中的“YUV”通常实际是 Y′CbCr。
- Y′ 不是线性物理亮度。
RGB24的 24 表示每像素 24 bit,不是每通道 24 bit。- I420、YV12、NV12、NV21 不能直接互换。
- 4:2:0 只描述采样数量还不够,还可能涉及 chroma siting。
width × height × 4只适用于紧密排列的 8-bit RGBA。- stride 的单位通常是字节,不是像素。
- coded size 不一定等于 visible size。
- BT.709 不只是一个 RGB → Y′CbCr 矩阵名称。
- BT.2020 不自动等于 HDR。
- sRGB 与 BT.709 虽然原色点相同,但不能笼统视为完全相同。
- Limited range 数据不是“动态范围比较差”的错误数据。
- Alpha 约定不一致会造成透明边缘黑边或白边。
VideoFrame被 JavaScript 引用并不意味着只占少量 JS 堆内存。copyTo()可能产生昂贵的格式转换和内存复制。- Canvas 预览正确不代表编码导出元数据正确。
- 原始帧体积与压缩后视频文件大小不是一个量级。
十七、面试题
1. 为什么视频通常不直接使用 RGB 编码?
标准回答:
RGB 适合显示,但三个通道存在相关性。转换到 Y′CbCr 后,可以分离亮度和色度,并对色度进行 4:2:2 或 4:2:0 下采样,从而减少输入数据量,同时较好地保持主观画质。
追问:
是不是所有视频编码器内部都必须使用 Y′CbCr?
高级回答:
不是绝对必须,具体标准和 profile 可能支持 RGB 或 4:4:4,但面向消费级视频的主流硬件链路通常围绕 4:2:0 Y′CbCr 优化。
2. 1920×1080 YUV420P 一帧有多大?
偶数宽高、8-bit、紧密排列时:
[ 1920\times1080\times1.5
3{,}110{,}400\text{ bytes} ]
约 3.11 MB。
3. 为什么乘以 1.5?
四个像素包含:
4 个 Y
1 个 U
1 个 V
平均每像素:
[ 6/4=1.5\text{ bytes} ]
4. I420 和 NV12 有什么区别?
两者都是 8-bit 4:2:0:
I420:Y + U + V,三个 plane
NV12:Y + UV,两个 plane
采样数量相同,内存组织不同。
5. stride 为什么可能大于 width × bytesPerPixel?
因为每行可能为了 SIMD、GPU 或硬件要求进行对齐,行尾存在 padding。
6. Full range 和 Limited range 错误会产生什么结果?
Limited 当 Full:画面发灰。
Full 当 Limited:黑白细节被压缩或裁剪。
7. Primaries、Transfer、Matrix 分别表示什么?
Primaries:定义色域
Transfer:定义信号值与线性光的关系
Matrix:定义 RGB 与 Y′CbCr 的转换
8. 为什么透明 PNG 缩放后可能出现黑边?
常见原因是 straight 与 premultiplied alpha 淌用,或在错误的 Alpha 表示下执行缩放、模糊和滤波。
9. 为什么 width × height × 4 不一定是真实占用?
因为还可能存在:
- stride padding;
- coded size padding;
- GPU 纹理对齐;
- 多缓冲;
- 硬件私有表面; -额外的 CPU 复制和中间表面。
10. 为什么需要调用 VideoFrame.close()?
因为 VideoFrame 可能引用较大的 CPU、GPU 或硬件编解码资源。关闭它可以立即释放当前对象对媒体资源的引用,而不是等待不确定的垃圾回收。
十八、练习
练习 1:720p I420
计算 1280×720、8-bit I420 在 25 FPS 时的原始数据率。
答案
[ 1280\times720\times1.5
1{,}382{,}400\text{ bytes/frame} ]
[ 1{,}382{,}400\times25
34{,}560{,}000\text{ bytes/s} ]
约:
34.56 MB/s
276.48 Mbit/s
练习 2:4K 10-bit 4:2:0
假设 10-bit 采样均存储在 16-bit 容器中,计算 3840×2160、60 FPS 的内存吞吐。
每像素实际存储:
Y:2 bytes/pixel
UV:平均 1 byte/pixel
合计:3 bytes/pixel
[ 3840\times2160\times3
24{,}883{,}200\text{ bytes/frame} ]
[ 24{,}883{,}200\times60
1{,}492{,}992{,}000\text{ bytes/s} ]
约:
1.493 GB/s
11.94 Gbit/s
练习 3:奇数尺寸 I420
计算 1921×1081 的紧密 I420 大小。
[ S_Y=1921\times1081=2{,}076{,}601 ]
色度尺寸:
[ W_C=\left\lceil\frac{1921}{2}\right\rceil=961 ]
[ H_C=\left\lceil\frac{1081}{2}\right\rceil=541 ]
[ S_U+S_V=961\times541\times2=1{,}039{,}802 ]
总大小:
[ S=3{,}116{,}403\text{ bytes} ]
这说明奇数尺寸不能直接使用 (W\times H\times1.5)。
练习 4:Alpha 排查
一个像素以 straight alpha 保存为:
[ (1,0,0,0.25) ]
转换为 premultiplied alpha:
[ (0.25,0,0,0.25) ]
若把后者错误地当作 straight,再乘一次 Alpha:
[ 0.25\times0.25=0.0625 ]
红色贡献被错误降低为原来的四分之一,边缘会明显变暗。
练习 5:系统排查题
现象:
浏览器 Canvas 预览正常
服务端导出 MP4 后整体发灰
排查顺序:
- 用 ffprobe 查看导出文件的
color_range。 - 检查编码器输入究竟是 full 还是 limited。
- 检查 RGB → Y′CbCr 转换时是否做了 range 映射。
- 检查编码器和容器是否写入正确的颜色元数据。
- 在不同播放器中交叉验证,区分文件问题与播放器问题。
十九、本周速查表
| 项目 | 结论 |
|---|---|
| RGB24 | 3 bytes/pixel |
| RGBA | 4 bytes/pixel |
| I420 | Y + U + V |
| NV12 | Y + UV |
| 8-bit 4:4:4 | 24 bit/pixel |
| 8-bit 4:2:2 | 16 bit/pixel |
| 8-bit 4:2:0 | 12 bit/pixel |
| I420 偶数尺寸 | (1.5WH) bytes |
| RGBA 紧密尺寸 | (4WH) bytes |
| 真实多平面尺寸 | (\sum stride_i \times height_i) |
| Limited 8-bit Y′ | 16~235 |
| Limited 8-bit Cb/Cr | 16~240 |
| 色度中心值 | 128 |
| Primaries | 色域 |
| Transfer | 信号值与线性光的关系 |
| Matrix | RGB 与 Y′CbCr 转换 |
| Range | 数字码值映射范围 |
| Straight Alpha | RGB 未乘 Alpha |
| Premultiplied Alpha | RGB 已乘 Alpha |
| WebCodecs 资源释放 | frame.close() |
本周通过标准
你能够独立回答以下四个问题,就可以进入第二周:
- 为什么 1920×1080 I420 一帧约为 3.11 MB?
- 为什么实际内存不能总按
width × height × bytesPerPixel计算? - 一段视频偏色时,如何区分 matrix、transfer、primaries 和 range 问题?
- 为什么 Web 视频编辑器应避免长期缓存大量 RGBA
VideoFrame?