编码的唯一目标:用尽量少的字节,保住尽量多的信息。 这一节把最小的一条命令跑通,实测压缩比 324 倍,并拆开看这 324 倍是从哪来的。
第三章讲的是「采」,这一章讲「存」。 采集给你的是 YUV,存储要的是 H.264,中间这一道就是编码。 编码原理在 26 课,这里只讲怎么调。像素格式与 YUV 的常识也在 26 课。
一、一条最小可用的命令
YUV 裸文件没有任何头信息——没有宽、没有高、没有像素格式、没有帧率。 所以这四个参数必须在命令行里全给,一个不给 ffmpeg 就只能猜,猜错就是花屏。
ffmpeg -f rawvideo -pix_fmt yuv420p -s 320x240 -r 30 \
-i in.yuv -c:v libx264 -crf 23 out.h264
| 参数 | 管什么 | 不给会怎样 |
|---|---|---|
-f rawvideo |
告诉它这是裸流 | 当成图片序列或直接报错 |
-pix_fmt yuv420p |
每像素怎么排 | 按 yuv444 读,画面斜掉 |
-s 320x240 |
一帧多大 | 猜 640×480,整段错位 |
-r 30 |
一秒几帧 | 默认 25,时长算错 |
这四行里最容易漏的是 -r:它不影响画面,只影响时间戳,
所以漏了不花屏,只是播放速度快了 20%——这种错最难发现。
二、实测:30 帧压成多少
沙箱里没有 ffmpeg.exe,但 PyAV 用的就是同一套库(libavcodec/libx264),
所以下面的数是真跑出来的,不是抄的。
源 3.30 MB → 输出 10.4 KB,压缩比 324 倍,码率 85 kbps。
| 项 | 值 | 说明 |
|---|---|---|
| 包数 | 30 | 一帧一包(含 B 帧时也如此) |
| I 帧 | 1 | 关键帧,能独立解 |
| P 帧 | 8 | 参考前面的帧 |
| B 帧 | 21 | 参考前后两边的帧 |
注意 I/P/B 的分布:30 帧里只有 1 个 I 帧、21 个 B 帧。这个比例说明 x264 默认更愿意用 B 帧—— B 帧最省字节,但延迟最大。
三、324 倍是从哪来的
不是编码器魔法,是三件事叠起来:
| 来源 | 贡献 | 谁做的 |
|---|---|---|
| 色度减半采样 | 4:2:0 只存 1.5 字节/像素 | 采集阶段就已经这样 |
| 帧内 DCT 加量化 | 扔掉高频细节 | 编码器 |
| 帧间只存差值 | 不动的地方几乎不花字节 | 编码器 |
第三件是大头。我造的画面是缓慢移动的斜纹,帧与帧之间差别很小, 所以「差值」几乎全是零,熵编码一压就没了。 换成剧烈变化的画面,这个倍数会掉到几十倍——这也正是监控能省硬盘的原因:镜头前大部分时间没人动。
所以「压缩比」这个数没有普适值,看到别人报「压了 500 倍」先问他画面是什么。
四、编出来的裸流有什么毛病
out.h264 只是把编码后的 nal 单元按顺序摞在一起,没有任何索引。
| 缺什么 | 后果 | 在哪补 |
|---|---|---|
| 时长 | 播放器只能按帧率猜 | 下一章封装进 MP4 |
| 索引 | 不能拖进度条 | 同上 |
| 音频 | 只有画面没声音 | 后续章节 |
这就是为什么监控录像一定是 MP4/TS 而不是 .h264——裸流没法按时间检索, 而监控的第一需求恰恰是「回放某一段」。
五、动手 Demo:把一段 YUV 编成 H.264 裸流
Demo 用 PyAV 走完整条链:造 YUV → 编码 → 落盘 → 数包与帧类型。 数据全由代码算出来,压缩比不是估的。
c = av.open("out.h264", "w", format="h264")
s = c.add_stream("libx264", rate=30)
s.width, s.height, s.pix_fmt = 320, 240, "yuv420p"
s.options = {"crf": "23"}
for i in range(30):
fr = av.VideoFrame.from_ndarray(frame(i), format="yuv420p")
fr.pts = i
for pk in s.encode(fr):
c.mux(pk)
for pk in s.encode(None): # 收尾,必须冲空包
c.mux(pk)
c.close()
实跑输出:
--------------------------------------------------------------------
把一段 YUV 编成 H.264 裸流
--------------------------------------------------------------------
**编码这件事的目标只有一个:用更少的字节,保住尽量多的信息。**
**D44 先把这条链跑通,看压缩比到底是多少。**
源:320×240、30 帧、YUV420p → **3.30 MB**
每帧 115200 字节,30 帧正好 3.30 MB。
编码:**57 毫秒**,输出 **10.4 KB**
压缩比 **325 倍**,码率 **85 kbps**
项 值 说明
-----------------------------------------------
包数 30 一帧一包(含 B 帧时也如此)
I 帧 1 关键帧,能独立解
P 帧 8 参考前面的帧
B 帧 21 参考前后两边的帧
**压缩比 324 倍**,靠的是三件事:
一、色度已经减半采样(YUV420p 本身就是压缩过的一半);
二、帧内用 DCT 加量化,把高频细节扔掉;
三、帧间只存「和前一帧的差」,画面不动的地方几乎不花字节。
**第三件是压缩比的大头**:我造的画面是缓慢移动的斜纹,帧间差很小,所以压得特别狠。
**换成剧烈变化的画面,这个倍数会掉到几十倍**——这也是为什么监控能省硬盘:镜头前大部分时间没人动。
三个数要对得上:源 3.30 MB、输出 10.4 KB、30 帧。任何一个对不上,先查是不是漏了 fr.pts。
I 帧只有 1 个,这是默认 GOP 250 的结果——片子才 30 帧,够不着第二个 I 帧。
下一节(04.01.03)就专门拧这个旋钮。
30 秒实验
- 把
crf改成 40,重跑,看文件小了多少、PSNR 掉了多少。 - 把
crf改成 0(无损),看压缩比还剩多少——这时候帧间预测还在工作,所以仍然会比源小。 - 把
frame(i)换成纯色块,看压缩比会飙到多少,想想为什么。
常见误区
- 以为裸 YUV 能自己知道自己多大。 它不能,四个参数必须从外面给。
- 以为压缩比是个固定值。 同一套参数,画面一动倍数就掉,报压缩比必须连画面一起报。
- 以为
.h264是个能直接播的视频文件。 它是裸流,没时长没索引,播放器只能一帧帧顺着放。
自测三问(点开对答案)
1. 为什么 `-r` 漏了不花屏? —— 它只影响时间戳(每帧算多少秒),不影响像素怎么排。所以画面是对的,只是播放速度不对。 2. 压缩比 324 倍里,哪一部分是采集阶段就省下来的? —— 色度减半采样。 yuv420p 每像素只占 1.5 字节,而 rgb24 是 3 字节——**这一半在编码之前就省掉了**。 3. 为什么 30 帧只有 1 个 I 帧? —— 默认 gop_size 是 250,片子只有 30 帧,第一个 I 帧之后还没走到第二个。下一节把「质量」和「分辨率」这两个旋钮分开拧,看它们各自管什么。
- ☐ (本节完,下一节 04.01.02 控制视频的码率及分辨率)