改帧率有三条完全不同的路:丢帧、补帧、只改时间轴。 前两条改画面数量,第三条只改解释方式——它的结果是「快放」。
音频那边改的是采样率,视频这边对应的是帧率。 但视频多一个岔路:帧率既是「每秒放几张」,也可以当成「时间轴怎么算」。
一、降帧率就是丢帧
25 fps 降到 10 fps:
输出帧率 帧数 字节 时长
25 fps 50 4478 2.00 秒 10 fps 20 3558 2.00 秒
第二行帧数掉到 20,时长没变——这就是丢帧:时间轴不动,只是取样的次数少了。 字节也降了,但只降了两成,不是按帧数比例降的。
代价是运动不连贯。原来每个位置都有画面,现在两个位置之间只能靠「保持上一帧」过渡。 所以动画片用 12 fps 还行,体育比赛用 12 fps 就没法看了。
二、升帧率补不出新画面
把每一帧重复一次,文件就变成 50 fps:
做法 帧数 字节
25 fps 原样 50 4478 每帧重复一次变 50 100 6201
字节只涨了 38%,因为没有新画面——每一对相邻帧完全一样,编码器几乎不花字节。 这就是「假高帧率」:文件写着 50 fps,实际信息量还是 25 fps 的。
真要提升流畅度得靠插帧算法去「猜」中间那一帧的运动,代价是算力。 简单重复只改标签,一秒的活,效果也是零。
三、只改时间基:不丢不补,改速度
帧数不变,只把时间轴压一半:
做法 帧数 字节 实际时长
50 帧当 50 fps 写 50 4217 1.00 秒
帧数还是 50,但时长变成 1.0 秒——这是「快放一倍」,不是「提高帧率」。 画面一张没少,只是每张停留的时间短了一半。
命令行里对应两条完全不同的路:setpts=0.5*PTS 是快放,fps=50 是真补帧。
搞混这两条,做出来的片子要么动作变快,要么多了几十张重复帧。
四、动手 Demo:同一段素材,丢帧、补帧、改时间各来一次
先按 25 fps 原样编一遍当基准,再按每 2.5 帧取一帧的方式降到 10 fps。 然后把每帧重复两次变成 50 fps,最后试「50 帧当 50 fps 写」的快放。
import av, os
from fractions import Fraction
def reenc(dst, rate, keep=None, dup=1):
c1 = av.open(dst, u"w")
s1 = c1.add_stream(u"libx264", rate=rate)
s1.width, s1.height, s1.pix_fmt = 160, 90, u"yuv420p"
s1.options = {u"crf": u"26"}
c0 = av.open(u"s5.mp4")
idx = 0
for i, f in enumerate(c0.decode(video=0)):
if keep is not None and not keep(i):
continue
arr = f.to_ndarray()
for k in range(dup):
nf = av.VideoFrame.from_ndarray(arr, format=u"yuv420p")
nf.pts = idx
nf.time_base = Fraction(1, rate)
idx += 1
for p in s1.encode(nf):
c1.mux(p)
for p in s1.encode(None):
c1.mux(p)
c1.close()
c0.close()
return os.path.getsize(dst), idx
b25, n25 = reenc(u"fps25.mp4", 25)
b10, n10 = reenc(u"fps10.mp4", 10,
keep=lambda i: (i * 2) // 5 != ((i - 1) * 2) // 5)
print(u"25 fps %d 帧 %d 字节 %.2f 秒" % (n25, b25, n25 / 25.0))
print(u"10 fps %d 帧 %d 字节 %.2f 秒" % (n10, b10, n10 / 10.0))
b50, n50 = reenc(u"fps50.mp4", 50, dup=2)
print(u"每帧重复 50 fps %d 帧 %d 字节 涨 %.0f%%"
% (n50, b50, (b50 - b25) * 100.0 / b25))
bf, nf = reenc(u"fpsfake.mp4", 50)
print(u"50 帧当 50 fps 写 %d 帧 %d 字节 %.2f 秒" % (nf, bf, nf / 50.0))
实跑输出:
--------------------------------------------------------------------
5.6.1 控制视频的帧率——丢帧、补帧、还是只改时间
--------------------------------------------------------------------
**① 25 帧降到 10 帧:丢帧**
输出帧率 帧数 字节 时长
---------------------------------------
25 fps 50 4478 2.00 秒
10 fps 20 3558 2.00 秒
**第二行帧数掉到 20,时长没变——这就是丢帧:时间轴不动,只是取样的次数少了。**
**代价是运动不连贯。原来每个位置都有画面,现在两个位置之间只能靠「保持上一帧」过渡。**
**② 25 帧升到 50 帧:补帧**
做法 帧数 字节
------------------------------------
25 fps 原样 50 4478
每帧重复一次变 50 100 6201
**字节只涨了 38%,因为没有新画面——每一对相邻帧完全一样,编码器几乎不花字节。**
**这就是「假高帧率」:文件写着 50 fps,实际信息量还是 25 fps 的。**
**③ 只改时间基:不丢不补,改速度**
做法 帧数 字节 实际时长
-------------------------------------------------
50 帧当 50 fps 写 50 4217 1.00 秒
**帧数还是 50,但时长变成 1.0 秒——这是「快放一倍」,不是「提高帧率」。**
**命令行里对应 `setpts=0.5*PTS`(快放)与 `fps=50`(真补帧),两条路完全不同。**
重复帧必须重建 VideoFrame 并显式给 pts 与 time_base。
直接复用同一个帧对象两次,两个包的时间戳会撞车,封装器报 returned 22。
那个 keep 表达式是「每 2.5 帧取一帧」的写法:
(i * 2) // 5 每次跨过整数就取,50 帧正好取出 20 帧。
reenc 返回的输出帧数用的是自己数的 idx,不是 ffprobe 的结果。
两者应当一致;不一致说明有包被封装器丢了。
30 秒实验
- 命令行对照:
ffmpeg -i 源.mp4 -r 10 出.mp4,这就是丢帧。 - 快放:
ffmpeg -i 源.mp4 -vf setpts=0.5*PTS 出.mp4,时长减半、帧数不变。 - 插帧:
minterpolate=fps=50,这才是真补帧,慢得多。
常见误区
- 以为
-r 50能提升流畅度。源只有 25 帧,多出来的都是重复帧。 - 把
setpts当成改帧率。它改的是时间轴,结果是快放或慢放。 - 重复帧不重建对象。时间戳撞车,封装器拒绝写入。
自测三问(点开对答案)
1. 降帧率时时长为什么不变? —— 丢帧只减少取样次数,时间轴还是原来那么长,画面靠保持上一帧过渡。 2. 每帧重复一次,字节为什么只涨 38%? —— 相邻两帧完全一样,编码器用极少的比特就能表示,没有新信息。 3. `setpts=0.5*PTS` 做的是什么? —— 把时间轴压一半,帧数不变、时长减半,效果是快放一倍。- ☐ (本节完,下一节 5.6.2 控制视频的码率及分辨率)