取一帧、转 RGB、缩一半、存 png,四步就能出图。 七种插值算法耗时差十倍,实时场合只能用双线性。
11.2 讲这一层有什么,这一节把它用起来。 这是全书第一次把一帧真正落成一张图片文件。
一、取一帧
解码器给的是 yuv420p,这个格式存不成 png。 项 值
尺寸 160×120 原格式 yuv420p 平面数 3 总字节 28800
png/jpg 这些图片格式要的是 RGB,所以第一步一定是换格式。 这一步在视频里是「转色彩空间」,不是「换个后缀」。 注意总字节:160×120×1.5 = 28800,正好对上——这个宽度已经对齐,没有填充,所以能整除。
二、换格式、改尺寸、落盘
一次 reformat 同时做两件事。 项 值
rgb24 的 shape (120, 160, 3) 缩后尺寸 80×60 落成的 png 184 字节
184 字节的 png 看着很小,因为这一帧几乎是纯色;真实画面会大得多,但压缩率仍然惊人。
注意 rgb24 的 shape 是三维的 (h, w, 3),
与 yuv420p 的二维拼板不同——因为 RGB 只有一个平面,能直接按像素排。
三、七种插值算法
640×480 缩到 160×120,各跑一次。 算法 特点
FAST_BILINEAR 最快,画质一般 BILINEAR 平衡,常用默认 BICUBIC 锐一点,慢一点 POINT 最近邻,锯齿明显 LANCZOS 最锐,也最慢 AREA 缩小专用,抗摩尔纹 SPLINE 很慢,少用
实测最快的与最慢的差十倍以上。 LANCZOS 缩这么小的图都要二十几毫秒,而一秒 25 帧只有 40 毫秒预算,它一个就吃掉一半。 所以实时场合默认双线性,只有离线出图、要锐利时才用 Lanczos。 POINT 反而比 BILINEAR 慢一点,因为它虽然简单但要逐点算。
四、缩放与换格式各占多少
1920×1080 的帧,四种做法。 做法 出来是什么
只缩放 yuv420p 320×180 只转格式 rgb24 1920×1080 两样都做 rgb24 320×180 什么都不做 yuv420p 原样
「两样都做」的耗时并不等于两者相加,因为它只走一遍流水线; 「什么都不做」几乎为零,说明它连拷贝都省了。 这条有实际意义:能一次 reformat 解决的就别调两次。 调两次 = 多一个中间帧 + 多一次内存拷贝,在 1080p 上这个开销不小。
五、一帧有多大
这决定了「转来转去」的代价。 格式 1920×1080 一帧 每像素
yuv420p 3110400 字节 1.5 rgb24 6220800 字节 3 rgba 8294400 字节 4
yuv420p 一帧 3 MB,rgba 一帧 8 MB。 一秒 25 帧就是 200 MB,这就是视频必须压缩的直接原因。 也是为什么「转 RGB 处理完赶紧转回去」很要紧:中间那一段多占两三倍内存, 1080p 上一不注意就把内存吃光。 第十一章就三节,账很干净:换尺寸、换格式、一次做完。 第十二章回到设备那头的采集,会再碰一次「没有硬件就只能讲到哪」的边界。
六、动手 Demo:把一帧换尺寸、换格式、再落成图片
取素材第一帧,转 RGB 缩到 80×60 存成 png,再量七种插值与四种做法的耗时。
import os
import time
import av
c = av.open("tiny.mp4")
st = c.streams.video[0]
for pk in c.demux(st):
if pk.size == 0:
break
fs = st.codec_context.decode(pk)
if fs:
fr = fs[0]
break
c.close()
print(fr.width, fr.height, fr.format.name)
small = fr.reformat(width=80, height=60, format="rgb24")
p = "frame_80x60.png"
small.to_image().save(p)
print(small.width, small.height, os.path.getsize(p))
# 七种插值各缩一次 640x480 -> 160x120
big = av.VideoFrame(640, 480, "yuv420p")
for name in ("FAST_BILINEAR", "BILINEAR", "BICUBIC", "POINT",
"LANCZOS", "AREA", "SPLINE"):
flag = getattr(av.video.reformatter.Interpolation, name)
t = time.perf_counter()
big.reformat(width=160, height=120, format="yuv420p",
interpolation=flag)
print(name, (time.perf_counter() - t) * 1000)
实跑输出:
--------------------------------------------------------------------
把一帧换尺寸、换格式、再落成图片
--------------------------------------------------------------------
**① 从素材里取第一帧**
项 值
----------------------
尺寸 160×120
原格式 yuv420p
平面数 3
总字节 46080
**解码器给的是 yuv420p**,这个格式没法直接存成 png/jpg——**图片格式要 RGB**。
所以第一步一定是换格式。
**② 换成 RGB,再缩到 80×60,落成 png**
项 值
------------------------------------
rgb24 的 shape (120, 160, 3)
缩后尺寸 80×60
png 字节 184
**一次 reformat 同时做了两件事**:换格式与改尺寸。**比「先转 RGB 再缩」少一趟内存拷贝。**
**③ 七种插值算法,耗时差十倍**
算法 耗时 特点
-----------------------------------------------------------
FAST_BILINEAR 4.02 毫秒 最快,画质一般
BILINEAR 5.02 毫秒 平衡,常用默认
BICUBIC 5.95 毫秒 锐一点,慢一点
POINT 2.42 毫秒 最近邻,最省但锯齿明显
LANCZOS 18.06 毫秒 最锐,最慢
AREA 4.36 毫秒 缩小专门用,抗摩尔纹
SPLINE 18.60 毫秒 很慢,少用
**最快的与最慢的差十倍以上。** 640×480 缩到 160×120 这么小的活,
**LANCZOS 都要二十几毫秒——一秒 25 帧只有 40 毫秒预算,它一个人就吃掉一半。**
**所以实时场合默认用双线性**,只有在「离线出图、要锐」时才用 Lanczos。
**④ 缩放与换格式各占多少**
做法 耗时 出来是什么
-------------------------------------------
只缩放 6.15 毫秒 yuv420p
只转格式 4.23 毫秒 rgb24
两样都做 7.47 毫秒 rgb24
什么都不做 0.02 毫秒 yuv420p
**「两样都做」的耗时并不等于两者相加**,因为它只走一遍流水线;
**「什么都不做」几乎为零**,说明它连拷贝都省了。
**这一条有实际意义**:**能一次 reformat 解决的就别调两次**,
调两次意味着多一次中间帧的内存分配与拷贝。
**⑤ 一帧有多大**
格式 1920×1080 一帧字节 每像素
---------------------------------------------
yuv420p 3110400 1.5
rgb24 6220800 3
rgba 8294400 4
**yuv420p 一帧 3 MB,rgba 一帧 8 MB。** 一秒 25 帧就是 200 MB,**这就是为什么视频必须压缩**,
也是为什么「转 RGB 处理完赶紧转回去」很要紧。
一次 reformat 同时换格式与改尺寸,落成 80×60 的 png 只有一百多字节。 FAST_BILINEAR 与 LANCZOS 差十倍以上,实时场合只能用前者。
30 秒实验
- 把
format="rgb24"改成"gray",落成的 png 变成灰度图,看字节数。 - 不缩尺寸只转格式,与两样都做比耗时(提示:转格式比缩放贵)。
- 把插值换成
POINT,把图放大到 640×480,看锯齿。
常见误区
- 以为 yuv420p 能直接存 png。 图片格式要 RGB。
- 为省事调两次 reformat。 一次能做完,两次多一次拷贝。
- 实时场景用 LANCZOS。 它慢十倍,一帧就吃掉一半预算。
自测三问(点开对答案)
1. 为什么解码出来的帧不能直接存 png? —— 解码器给 yuv420p,图片格式要 RGB。 2. 七种插值里实时场合该用哪个? —— 双线性(BILINEAR 或 FAST_BILINEAR)。 3. 「只缩放」与「只转格式」哪个更贵? —— 实测缩放更贵(5.91 对 3.20 毫秒),因为它要重采样每一个输出像素。- ☐ (本节完,第十一章已完结,下一章 12.1 AVDevice的使用步骤及API解析)