裸存成 .yuv,字节数完全由「宽×高×帧数」决定,一个字节都不多。 代价是:这种文件里没有任何说明,别人拿到打不开。
上一节讲的是「压缩」,这一节反着来——把画面解压成最原始的像素。 后面做算法、做分析、喂给别的库,走的都是这条路。
一、为什么先讲 YUV 不讲 RGB
视频解码出来的第一站就是 YUV,转 RGB 是后面的事。 所以「提取原始数据」这个需求,天然应该先拿到 YUV,需要了再转。
平面 存什么 尺寸
Y 亮度 160×90 U 蓝色差 80×45 V 红色差 80×45
第三列正是 420 的含义:色度只有一半宽高,也就是四分之一的样本数。 合起来每像素 1.5 字节,是 RGB 的一半。
所以「先拿 YUV 再按需处理」不只是习惯问题:省一半内存,也省一半拷贝。
二、字节数可以手算
无压缩的意思就是:没有任何一个字节是「根据内容」变的。
项 公式 值
宽×高 160×90 14400 单帧字节 宽×高×1.5 21600 50 帧合计 单帧×帧数 1080000 实测文件 — 1080000
第三行与第四行分毫不差。记住这个手感:以后拿到一个 .yuv 文件, 除以「单帧字节」就知道里面有几帧,不需要任何头信息。
反过来说,如果除不尽,那就是三件事之一:尺寸猜错了、格式猜错了、文件被截断了。
三、没有头信息的三个麻烦
.yuv 里没有宽、没有高、没有帧率、没有像素格式,全靠你自己记:
场景 结果 原因
ffplay 加参数 能播 参数里补上了尺寸与格式 普通播放器双击 打不开 没有任何可读的说明 写脚本按偏移切帧 能读 你自己算的偏移
第二行是最常见的坑:文件明明是好文件,只是没人告诉播放器它多宽多高。
命令行里对应 -s 160x90 -pix_fmt yuv420p,少一个都放不出来。
还有一个隐蔽的坑:解码器有内部缓冲。取完所有包还得再 flush 一次, 否则最后几帧永远拿不到——本次实跑第一遍就少了 2 帧,数对不上才发现。
四、动手 Demo:把画面裸存成 .yuv,再对一遍字节数
用 rawvideo 封装器把每一帧原样写出去,得到一个没有任何头信息的 .yuv 文件。 然后用手算的结果和实测的文件大小对一遍,看看差多少。
import av, os
c1 = av.open(u"o.yuv", u"w", format=u"rawvideo")
s1 = c1.add_stream(u"rawvideo", rate=25)
s1.width, s1.height, s1.pix_fmt = 160, 90, u"yuv420p"
c0 = av.open(u"s5.mp4")
cc = c0.streams.video[0].codec_context
n = 0
for pk in c0.demux(video=0):
if not pk.size:
continue
for f in cc.decode(pk):
n += 1
fr = f.reformat(width=160, height=90, format=u"yuv420p")
for o in s1.encode(fr):
c1.mux(o)
for f in cc.decode(None):
n += 1
fr = f.reformat(width=160, height=90, format=u"yuv420p")
for o in s1.encode(fr):
c1.mux(o)
for o in s1.encode(None):
c1.mux(o)
c1.close()
c0.close()
per = 160 * 90 * 3 // 2
print(u"帧数 %d" % n)
print(u"单帧 %d 应占 %d 实测 %d"
% (per, per * n, os.path.getsize(u"o.yuv")))
实跑输出:
--------------------------------------------------------------------
5.2.1 提取视频的 YUV 像素数据
--------------------------------------------------------------------
**① 裸存成 .yuv,字节数可以手算**
项 公式 值
------------------------------------
宽×高 160×90 14400
单帧字节 宽×高×1.5 21600
50 帧合计 单帧×帧数 1080000
实测文件 — 1080000
**第二行乘第三行正好等于第四行——YUV 是「无压缩」的,存多少字节只由尺寸与帧数决定。**
**这就是「原始数据」的含义:没有压缩,也就没有「画面复杂度」这个变量。**
**顺便记一笔:解码器有内部缓冲,取完包还得再 flush 一次才吐得出最后两帧——**
**本次实跑第一遍只拿到 48 帧,补上 flush 才是 50 帧。**
**② 这种文件谁能打开**
播放器 能不能播
----------------------------------------------
ffplay 加参数 能,但必须告诉它尺寸与格式
普通播放器双击 不能,没有任何头信息
自己写脚本 能,按字节偏移切帧
**第二行是重点:`.yuv` 文件里没有宽、没有高、没有帧率,连「一帧多长」都得你自己算。**
**所以这种格式只在「自己写的程序之间」流通,不适合发给别人。**
format=u"rawvideo" 指定的是「封装器」,里面的编码也是 rawvideo。
本质上就是「什么包装都不要,字节原样倒进文件」。
f.reformat(...) 不能省:解码出来的帧可能带着对齐填充,
直接写会把填充字节也写进去,字节数就对不上了。
最后两行如果对不上,先查帧数,再查尺寸,最后才怀疑代码。 这类「无头文件」的调试顺序永远是:数量、尺寸、格式,三步定死。
30 秒实验
- 自己看:
ffplay -f rawvideo -pixel_format yuv420p -video_size 160x90 文件.yuv。 - 反推帧数:
文件字节 ÷ (宽×高×1.5),得到的商就是帧数。 - 少给一个参数:故意把
-video_size去掉,看 ffplay 报什么错。
常见误区
- 把 .yuv 当成能双击播放的格式。它没有头,播放器不知道尺寸与格式。
- 忘了 flush 解码器。最后几帧会被内部缓冲扣住,帧数总是少两三帧。
- 用错像素格式算字节。按 420 算出来的尺寸去读 444 的文件,画面会斜着错位。
自测三问(点开对答案)
1. 160×90 的 yuv420p 单帧多少字节? —— 21600 字节。宽乘高乘 1.5,也就是亮度平面 14400 加两个色度平面各 3600。 2. 为什么 .yuv 文件普通播放器打不开? —— 文件里没有任何头信息,播放器不知道宽高、像素格式和帧率,无法解析。 3. 解码器和编码器为什么都要 flush? —— 它们内部有缓冲:解码器为处理 B 帧重排而压帧,编码器也要收尾。不 flush 就会丢尾部的帧。- ☐ (本节完,下一节 5.2.2 YUV444/YUV422/YUV420)