三个数字说的是「每几个像素共用一组色度样本」,420 是四个像素共用一组。 砍掉的只有色度,亮度一个样本都不动。
上一节按 420 存了一份,这一节把 444、422 也存一遍,看字节怎么变、画质怎么变。 这两个数字将来会反复出现在编码参数里。
一、三个数字怎么读
第一个数字是亮度,后两个是色度——记法统一成「横向/纵向」:
格式 色度样本怎么取 每像素字节
yuv444p 每个像素各配一组 U、V 3.0 yuv422p 横向两个像素共用一组 2.0 yuv420p 横竖都是两个像素共用一组 1.5
第三列不用背,按「亮度 1 份 + 色度几分之一」推: 444 是 1+1+1,422 是 1+0.5+0.5,420 是 1+0.25+0.25。
名字里那个 p 是 planar,意思是三个平面分开存,而不是像素交错存。 同一族里还有不带 p 的打包格式,字节总量一样,只是排列不同。
二、三种取样各存一份
同一批 50 帧,同一个画面,只改像素格式:
像素格式 总字节 每像素字节
yuv420p 1080000 1.5 yuv422p 1440000 2.0 yuv444p 2160000 3.0
第三列与上表的推算完全吻合,说明这个系数只由格式决定,与画面无关。 所以「420 转 444 能省画质吗」这个问题有答案了:不能,只是把样本补回去。
反方向才有意义:444 转 420 是真的丢信息,而且丢了就找不回来。
三、丢的到底是哪一路
把同一帧分别按 444 存、按 420 往返一次,逐通道比误差:
区域 平均误差 最大误差
整幅 11.97 59 左半(平滑) 0.13 1 右半(细条) 23.82 59
亮度平面的最大误差是 0:420 一个亮度样本都没动。 误差全在色度上,而且与画面内容强相关——左半是渐变,几乎无损; 右半是 2 像素宽的蓝条,色度被两两平均,误差直接跳到二十几。
所以「420 省一半空间」的准确说法是「省的是色度」。 人眼对亮度敏感、对色度迟钝,这个取舍才成立;换成给机器看(比如做检测), 色度被平均掉可能就是致命的,那就该用 444。
四、动手 Demo:三种取样各存一份,再逐通道算误差
先按 420、422、444 各裸存一份,比较字节;再取同一帧做 444 与 420 的往返对比。 对比时分左右两半看:左半是渐变,右半是 2 像素宽的蓝条,正好说明「误差跟内容有关」。
import av, os, numpy as np
for pf in (u"yuv420p", u"yuv422p", u"yuv444p"):
c1 = av.open(pf + u".yuv", u"w", format=u"rawvideo")
s1 = c1.add_stream(u"rawvideo", rate=25)
s1.width, s1.height, s1.pix_fmt = 160, 90, pf
c0 = av.open(u"s5.mp4")
for f in c0.decode(video=0):
for o in s1.encode(f.reformat(width=160, height=90, format=pf)):
c1.mux(o)
for o in s1.encode(None):
c1.mux(o)
c1.close()
c0.close()
print(u"%s %d" % (pf, os.path.getsize(pf + u".yuv")))
c0 = av.open(u"s5.mp4")
f0 = next(c0.decode(video=0))
c0.close()
rgb = f0.reformat(format=u"rgb24")
a = rgb.reformat(format=u"yuv444p").to_ndarray()
b = rgb.reformat(format=u"yuv420p").reformat(format=u"yuv444p").to_ndarray()
d = np.abs(a.astype(np.int16) - b.astype(np.int16))
print(u"亮度最大误差 %d" % d[0].max())
print(u"左半色度平均误差 %.2f" % d[1:, :, :80].mean())
print(u"右半色度平均误差 %.2f" % d[1:, :, 80:].mean())
实跑输出:
--------------------------------------------------------------------
5.2.2 YUV444/422/420——差的是色度分辨率
--------------------------------------------------------------------
**① 三种取样各存一份,字节对比**
像素格式 总字节 每像素字节
------------------------------------
yuv420p 1080000 1.5
yuv422p 1440000 2.0
yuv444p 2160000 3.0
**第三列就是公式里的系数:420 是 1.5、422 是 2.0、444 是 3.0。**
**这个系数来自「每个像素配几个色度样本」,与画面内容无关。**
**② 色度减半到底丢了什么**
区域 平均误差 最大误差
---------------------------------------
整幅 11.97 59
左半(平滑) 0.13 1
右半(细条) 23.82 59
**亮度这一次一个样本都没被平均:亮度平面最大误差只有 0。420 砍的只有色度。**
**代价全在色度上,而且与画面内容直接相关:左半是渐变,误差 0.13;**
**右半是 2 像素宽的蓝条,色度被两两平均,误差跳到 23.82。**
**所以「420 省一半空间」的准确版本是「省的是色度,不是亮度」。**
**人眼对亮度敏感、对色度迟钝,这个取舍才成立——换成给机器看,就不该这么省。**
两路都从同一个 RGB 出发,才算公平比较。 如果先存 420 再和 444 比,比的其实是「解码器怎么补色度」,那是另一回事。
to_ndarray() 对 planar 格式返回的是「三个平面竖着接起来的一整块」,
所以索引 0 是亮度,1 与 2 是色度,形状是 3×90×160。
左半右半这个切法是为了说明问题,不是通用做法。 真实评估该看整幅的 PSNR,或者直接把两张图叠起来看差值图。
30 秒实验
- 盲测:把 444 与 420 两版放大到全屏,看能不能分出哪张是哪张。
- 改素材:换成纯色块画面再跑一遍,误差会接近 0,说明误差取决于内容。
- 命令行对照:
-pix_fmt yuv420p与-pix_fmt yuv444p各转一遍,比文件大小。
常见误区
- 以为 420 转 444 能提升画质。只是把样本补回去,补不出丢掉的信息。
- 以为 420 所有通道都减半。亮度是全分辨率的,减半的只有色度。
- 以为 444 一定更好。它确实更保真,但文件大一半,多数播放场景用不上。
自测三问(点开对答案)
1. yuv420 里的 4、2、0 分别是什么意思? —— 按「横向/纵向」读:亮度每像素一个样本,色度横竖都是每两个像素共用一组。 2. 为什么 420 的每像素字节是 1.5 而不是 1? —— 亮度占 1 份,两个色度平面各占 0.25 份,1 加 0.25 加 0.25 等于 1.5。 3. 420 的误差为什么跟画面内容有关? —— 它把相邻像素的色度平均了。画面越平滑,平均前后的差别越小;细密的彩色纹理差别就大。- ☐ (本节完,下一节 5.2.3 利用 FFmpeg 提取视频的 RGB 像素数据)