Y4M 是全世界最简单的视频格式:一个文本头加一堆裸像素。 没有压缩,没有索引,用文本编辑器都能看出一半。
2.1 到 2.6 都在用 ffprobe 去「猜」一个文件是什么。 这一节换一个完全看得懂的格式,把「视频到底是什么」摊开来看。
一、它简单到什么程度
先看一个能背下来的结构:
组成部分 内容 长度
文本头 一行 ASCII,以 YUV4MPEG2 开头 几十字节 帧标记 每帧前面一行 FRAME 6 字节 帧数据 Y、U、V 三个平面紧挨着排 宽×高×1.5
整个格式就这三样东西,帧一帧一帧往后排,没有目录、没有索引、没有时间戳。 要读第 100 帧,只能从文件头开始数一百次。
这跟 mp4 正好相反:mp4 是「索引在前,方便随机访问」, Y4M 是「顺序记录,只能顺着读」。一个为播放优化,一个为简单优化。
二、为什么需要这么「笨」的格式
既然又大又慢,为什么还留着它?三个用途都很实在:
用途 为什么不用 mp4 具体场合
中间交换 不带编码器差异,谁读都一样 两个工具之间传帧 算法测试 输入可控,能逐字节比对 验证缩放、滤镜是否正确 教学演示 拿文本编辑器都能看出一半 讲「视频是什么」
第一行是最关键的:做画质对比时,如果用 mp4 当中间文件, 你会分不清「差异是算法造成的」还是「差异是编码造成的」。
换成 Y4M 就干净了:输入就是纯像素,一个字节都不含糊。 这也是为什么很多图像算法的测试集都是 Y4M 或者类似的裸格式。
第三行也不是玩笑:Y4M 的头是一行 ASCII,head -c 100 就能看到分辨率帧率。
初学者理解「视频就是一堆同样大小的画面排在一起」,用它最快。
三、代价:大得离谱
算一下就明白它为什么不能用来存东西:
规格 一帧字节 一秒字节
160×90 21600 540000 720×576 622080 15552000 1920×1080 3110400 77760000
1080p 一秒 74 MB,一分钟 4.4 GB。这个量级下没人会拿它存东西。 它只在「跑一趟实验」的时候用,用完就删。
这也顺带说明了一件事:压缩不是可选项,是必需品。 没有压缩,一张蓝光碟连十秒都装不下。
四、动手 Demo:一个文本头加一堆裸像素
不用任何编码器,手写一个三帧的 Y4M 文件,再用 ffmpeg 的解析器读回来。 如果读回来的属性跟写的完全一致,就证明「视频格式」确实可以这么简单。
import av
import numpy as np
W, H, FPS, NF = 160, 90, 25, 3
# 一帧 yuv420p 有多大:Y 全采,U 和 V 各占四分之一
ysize = W * H
csize = (W // 2) * (H // 2)
fsize = ysize + csize * 2
# 头就一行 ASCII,字段用空格分隔
head = u"YUV4MPEG2 W%d H%d F%d:1 Ip A1:1 C420\n" % (W, H, FPS)
with open(u"hand.y4m", u"wb") as f:
f.write(head.encode(u"ascii"))
for i in range(NF):
f.write(b"FRAME\n")
y = np.full(ysize, i * 80, dtype=np.uint8)
u = np.full(csize, 128, dtype=np.uint8)
v = np.full(csize, 128, dtype=np.uint8)
f.write(y.tobytes() + u.tobytes() + v.tobytes())
# 读回来验证
c = av.open(u"hand.y4m")
st = c.streams.video[0]
print(u"容器 %s" % c.format.name)
print(u"编码 %s" % st.codec_context.name)
print(u"分辨率 %dx%d" % (st.width, st.height))
print(u"像素格式 %s" % st.codec_context.pix_fmt)
print(u"帧率 %s" % st.average_rate)
print(u"帧数 %d" % sum(1 for _ in c.decode(video=0)))
c.close()
实跑输出:
--------------------------------------------------------------------
Y4M 格式简介——一个文本头加一堆裸像素
--------------------------------------------------------------------
**① Y4M 是全世界最简单的视频格式:没有压缩,没有索引**
项 值
--------------------------------------------------------
文件大小 64856 字节
文本头 YUV4MPEG2 W160 H90 F25:1 Ip A1:1 C420
一帧裸数据 21600 字节
三帧合计 64800 字节(21600 × 3 + 头与标记)
**这个文件是我用二十行代码手写出来的——没有调任何编码器。**
**因为 Y4M 根本没有「编码」这一步,像素怎么排就怎么存。**
**② 读回来,验证它真的是一个合法视频**
读到的属性 值
-------------------------------
容器格式 yuv4mpegpipe
编码 rawvideo
分辨率 160 × 90
像素格式 yuv420p
帧率 25
解出帧数 3
**注意第二行:编码是 rawvideo,意思是「不编码」。**
**所以 Y4M 不是压缩格式,它是把内存里的 YUV 原样倒进文件。**
**③ 那它有什么用?三个用途,都很实在**
用途 为什么用 Y4M 而不是 mp4
-------------------------------------------
中间交换 不带编码器差异,谁读都一样
算法测试 输入可控,能逐字节比对
教学演示 拿文本编辑器都能看出一半
**第二行是关键:做画质对比时,如果用 mp4 当中间文件,你会分不清差异是算法造成的还是编码造成的。**
**换成 Y4M,输入就是纯像素,一切都干净了。**
**④ 代价也很直接:大**
**160×90 的 3 帧就 64856 字节。换成 1080p 一秒,那就是 77760000 字节(约 74.2 MB)。**
**所以 Y4M 从来不用来存东西,只在「跑一趟实验」的时候用。**
写这个文件只用了二十行,而且没有一行在「编码」。 因为 Y4M 根本没有编码这一步:像素怎么排,就怎么存。
读回来那一栏里,容器名是 yuv4mpegpipe、编码名是 rawvideo。
rawvideo 这个名字本身就说明了性质——它就是「不编码的视频」。
文件总大小 = 头 + 三帧 × (6 字节 FRAME 标记 + 一帧数据)。 自己算一遍再和实际字节数对一下,能确认「视频文件里到底装了什么」。
这也是本课唯一一个你能手写出来的视频格式。 手写过一次之后,再听到「解封装」「解码」这些词,脑子里就有具体画面了。
30 秒实验
- 看一眼头:
head -c 60 hand.y4m,直接看到那行 ASCII 头,不需要任何工具。 - 转一次:
ffmpeg -i hand.y4m hand.mp4,把裸数据压成 h264,比一下体积差多少倍。 - 算一次:用
-pix_fmt yuv422p再写一个,看一帧是不是从 21600 变成 28800。
常见误区
- 把 Y4M 当成一种压缩格式。它完全不压缩,一帧就是一个分辨率的裸数据,所以特别大。
- 以为 Y4M 能随机访问。没有索引,要读第 N 帧必须从头数。这是它「简单」的代价。
- 在做画质对比时用 mp4 当中间文件。这样分不清差异是算法造成的还是编码造成的,白做实验。
自测三问(点开对答案)
1. Y4M 文件里为什么没有「索引」这种东西? —— 因为它追求的是简单而不是随机访问。帧一帧一帧往后排,要读第 N 帧就从头上数过去。 2. 读到编码名是 rawvideo,说明了什么? —— 说明这一路数据没有经过编码,是原始像素直接排进文件的。raw 就是「生的、未加工的」。 3. 做画质对比为什么该用 Y4M 而不是 mp4? —— 因为 mp4 会引入编码损失,你分不清看到的差异是算法造成的还是编码造成的。Y4M 没有这一层。- ☐ (本节完,下一节 2.7.2 Y4M 格式规范)