🎬 第七章 FFmpeg命令行实现音视频特效及复杂滤镜应用
📄 一句话概括

-vf reverse 把整段顺序翻过来,逐像素一致、没有重编码失真。 代价是全片缓存在内存里,而且必须先 flush 才会吐帧。

📝 本节和前面几节的关系

7.3 改的是时间轴快慢,这一节把时间轴整个倒过来。 倒放是「时间方向」这类特效里最纯粹的一个。

一、命令只有一个词

倒放就一个滤镜,没有参数:

ffmpeg -i s7.mp4 -vf reverse back.mp4

reverse 把整段片子的顺序翻过来:最后一帧变成第一帧,第一帧变最后一帧。 它没有参数,也没有倍数——倒放就是倒放。

想「倒放的同时变慢」,得在后面再接 setpts,两个滤镜串起来。 顺序有意义:先 reverse 再 setpts,得到的是倒放后的慢速版。

二、真翻了吗:逐帧比对

「看起来倒了」不算证据,把输出帧和输入帧逐一比一遍才算:

输出第几帧 最像输入第几帧 平均差


0 5 0 1 4 0 2 3 0 3 2 0 4 1 0 5 0 0

六行全对上,顺序整个翻了过来,而且是逐像素相等(平均差 0)。 这比「看起来倒了」强得多:像素级一致,说明它只是换了顺序,没重编码失真。

顺带一句:reverse 拿到的必须是「按显示顺序排好」的帧。 解码器会做重排序,所以直接从文件解出来的帧天然是对的;手工拼的裸流不一定。

三、不 flush 就一帧都拿不到

这是本节最重要的一条:

怎么取 出来几帧


边喂边取 0 喂完再 push(None) 取 6

因为 reverse 得先把整段收完,才知道最后一帧是哪一帧。 收完之前它一帧都不吐,看起来就像「reverse 不工作」。

网上那句「PyAV 里 reverse 出 0 帧」的结论就是这么来的—— 不是滤镜不行,是没告诉它「流结束了」。

这条经验对所有「要等全片」的滤镜都成立:reverse、areverse、palettegen、 还有一些统计类滤镜,都得 flush 之后才有输出。

四、代价是内存

它得把整段缓存在内存里,缓存多大可以算:

片长与分辨率 缓存的原始帧


160×90 两秒 1.0 MB 1280×720 三十秒 988.8 MB 1920×1080 六十秒 4449.5 MB

一分钟的 1080p 就要 4.3 GB,一小时的能到 261 GB,机器直接废掉。 所以 reverse 只适合短片头、短片段,长片要倒放得先切段再拼。

算法是「宽 × 高 × 1.5 × 帧率 × 秒数」——那个 1.5 是 yuv420p 的 Y 全采样加两个半采样的色度平面。换个像素格式,系数就得改。

五、声音也要一起倒

命令写成这样,画面与声音一起倒:

ffmpeg -i s7.mp4 -vf reverse -af areverse back.mp4

只写 reverse 不写 areverse,画面倒着放、声音正着放,嘴型全对不上。 音频那边叫 areverse,多一个 a 前缀,参数一样没有。

音频也是全缓存的,一小时的立体声 44.1 kHz 大约 1.2 GB,比视频省得多。 但两头都缓存,长片子依旧扛不住。

六、动手 Demo:六帧、两次取法、一次缓存估算

下面这段代码先试不 flush 与 flush 两种取法,再把输出帧逐一对回输入帧。 最后算三档片长的缓存占用。

import av
from fractions import Fraction

TB = Fraction(1, 25)
c0 = av.open(u"s7.mp4")
yv, rg = [], []
for fr in c0.decode(video=0):
    yv.append(fr.to_ndarray(format=u"yuv420p"))
    rg.append(fr.to_ndarray(format=u"rgb24"))
    if len(yv) >= 6:
        break
c0.close()

def run(flush):
    g = av.filter.Graph()
    b = g.add_buffer(width=160, height=90, format=u"yuv420p", time_base=TB)
    rv = g.add(u"reverse")
    sk = g.add(u"buffersink")
    b.link_to(rv)
    rv.link_to(sk)
    g.configure()
    out = []
    for i, arr in enumerate(yv):
        vf = av.VideoFrame.from_ndarray(arr, format=u"yuv420p")
        vf.pts = i
        vf.time_base = TB
        b.push(vf)
        while True:
            try:
                o = sk.pull()
            except Exception:
                break
            out.append(o.to_ndarray(format=u"rgb24"))
    if flush:
        b.push(None)
        while True:
            try:
                o = sk.pull()
            except Exception:
                break
            out.append(o.to_ndarray(format=u"rgb24"))
    return out

print(u"--- 不 flush 与 flush")
print(u"不 flush -> %d 帧" % len(run(False)))
out = run(True)
print(u"flush    -> %d 帧" % len(out))

print(u"--- 逐帧对照")
for i, o in enumerate(out):
    d = [float(abs(o.astype(int) - r.astype(int)).mean()) for r in rg]
    print(u"出 %d 最像入 %d 平均差 %d" % (i, d.index(min(d)), int(min(d))))

print(u"--- 缓存占用(原始帧,未压缩)")
for w, h, sec in ((160, 90, 2), (1280, 720, 30), (1920, 1080, 60)):
    print(u"%dx%d %d 秒 -> %.1f MB" % (w, h, sec, w * h * 1.5 * 25 * sec / 1048576.0))

实跑输出:

--------------------------------------------------------------------
7.5 视频倒放
--------------------------------------------------------------------

**① 命令只有一个词**

`ffmpeg -i s7.mp4 -vf reverse back.mp4`

**`reverse` 把整段片子的顺序翻过来:最后一帧变成第一帧,第一帧变最后一帧。**
**它没有参数,也没有倍数——倒放就是倒放。**

**② 真翻了吗?逐帧比对**

  输出第几帧     最像输入第几帧     平均差
  --------------------------------------------
  0              5                  0
  1              4                  0
  2              3                  0
  3              2                  0
  4              1                  0
  5              0                  0

**六行全对上,顺序整个翻了过来,而且是逐像素相等(平均差 0)。**
**这比「看起来倒了」强得多:像素级一致,说明它只是换了顺序,没重编码失真。**

**③ 一个必须知道的坑:不 flush 就一帧都拿不到**

  怎么取                   出来几帧
  -------------------------------------
  边喂边取                 0
  喂完再 push(None) 取     6

**因为 reverse 得先把整段收完,才知道最后一帧是哪一帧。**
**收完之前它一帧都不吐,看起来就像「reverse 不工作」。**
**网上那句「PyAV 里 reverse 出 0 帧」的结论就是这么来的——**
**不是滤镜不行,是没告诉它「流结束了」。**

**④ 代价是内存**

**它得把整段缓存在内存里,缓存多大可以算:**

  片长与分辨率         缓存的原始帧
  -------------------------------------
  160×90 两秒          1.0 MB
  1280×720 三十秒      988.8 MB
  1920×1080 六十秒     4449.5 MB

**一分钟的 1080p 就要 4.3 GB,一小时的能到 261 GB,机器直接废掉。**
**所以 reverse 只适合短片头、短片段,长片要倒放得先切段再拼。**

**⑤ 声音也要一起倒**

**命令写成这样,画面与声音一起倒:**

`ffmpeg -i s7.mp4 -vf reverse -af areverse back.mp4`

**只写 `reverse` 不写 `areverse`,画面倒着放、声音正着放,嘴型全对不上。**

第一段两行就是本节那张小表:不 flush 是 0 帧,flush 之后 6 帧。 这个 0 帧非常容易让人误判成「滤镜不支持」。

第二段六行完全对上:出 0 最像入 5、出 5 最像入 0,平均差全是 0。 说明它只是改了顺序,一个像素都没重算。

第三段三行是缓存估算,跟正文那张表一致。 数字大得吓人,但这就是「无损倒放」绕不过去的代价。

30 秒实验

  1. 改帧数:把 >= 6 改成 >= 20,看对照表是不是变成 0 对 19。
  2. 去掉 flush:把 run(True) 改成 run(False),看是不是 0 帧。
  3. 加音轨:给命令加 -af areverse,量两条流的时长。

常见误区

  1. 不 flush 就说 reverse 坏了。它要等流结束才吐帧。
  2. 拿它处理长片。全片缓存在内存里,一小时的 1080p 要几百 GB。
  3. 只倒画面不倒声音。要配 -af areverse。
自测三问(点开对答案) 1. reverse 为什么必须 flush 才有输出? —— 它要先看完最后一帧才知道倒过来的第一帧是谁,收完才吐。 2. 倒放会不会损失画质? —— 不会,实测逐像素平均差为 0,只是换了顺序。 3. 一小时的 1080p 倒放要多少内存? —— 按宽×高×1.5×帧率×秒数算,约 261 GB,根本放不下。

相关笔记