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

-vf 是给这条路加工序,-filter_complex 是重新画一张路网。 本机实测:多路合成靠 movie 补位才建得起来。

📝 本节和前面几节的关系

前两节各讲了一半,这一节把两者的区别收成一张表,再拿三次实测验证。 其中「两路怎么接」这条,是后面画中画、九宫格的地基。

一、一张表看全区别

先给结论,再逐条验:

对比项 简单滤镜 -vf 复杂滤镜


输入路数 只能 1 任意 输出路数 只能 1 任意 能不能分叉 不能 能 命令行位置 跟在 -i 后面 可放任意位置 要不要 -map 不用 多半要

五行里最要紧的是前两行——路数。 后面三行都是这条约束带来的副作用。

二、实测一:单链只有一个出口

在代码里,链的末端必须接一个 buffersink 才能把帧取出来。 一条单链只有一个末端,所以只能拉到一个 sink——这是硬约束。

写法 滤镜数 末端尺寸


scale,crop 2 60×40

两环串起来,仍然是一路进一路出,所以它依然是简单滤镜。 滤镜的个数不影响「简单」还是「复杂」——影响的是分不分叉。

三、实测二:两路怎么接才通

想接第二路输入时,本机只认一种组合:

建图写法 结果


两个 add_buffer 接 overlay returned 22 buffer + movie 接 overlay 成功

两个「内存缓冲」当输入时,PyAV 的对接层建不起来,报的是一句干巴巴的 22。 换成一路 buffer 加一路 movie,立刻就通了。

还有一个细节:接第二路必须显式给槽位号。 link_to(ov, 0, 1) 里的最后那个 1,意思是「接到 overlay 的第 1 号入口」。

不给槽位号、按顺序连,一样是 returned 22。 这条限制在第六章没露过面,因为那边全是单输入链。

四、实测三:三路、四路都行

同样的办法可以往上堆:一路 buffer,后面全用 movie 补。

滤镜 输入路数 输出尺寸


hstack 2 320×90 vstack 2 160×180 xstack 4 320×180 xstack 9 480×270

四宫格的尺寸是两倍宽、两倍高,正好四个 160×90 拼成 320×180。 九宫格是三倍宽、三倍高,480×270。

这个能力第七章后面要大用——画中画、九宫格、多路拼接全靠它。 所以本节这两条实测,是整章的技术地基。

五、动手 Demo:两路、四路、九路,一路一路加上去

下面这段代码按「一路 buffer + 其余 movie」的办法,从两路试到九路。 每一步都量输出尺寸,看是不是真的按倍数长。

import av
from fractions import Fraction

TB = Fraction(1, 25)
SRC = u"s7.mp4"
ESC = SRC.replace(u"\\", u"/").replace(u":", u"\\:")

def build(name, args, extra, w=160, h=90):
    g = av.filter.Graph()
    b = g.add_buffer(width=w, height=h, format=u"yuv420p", time_base=TB)
    f = g.add(name, args)
    sk = g.add(u"buffersink")
    b.link_to(f, 0, 0)
    for i in range(extra):
        g.add(u"movie", ESC + u":loop=0").link_to(f, 0, i + 1)
    f.link_to(sk)
    g.configure()
    return g, b, sk

LAY9 = u"0_0|w0_0|w0+w1_0|0_h0|w0_h0|w0+w1_h0|0_h0+h1|w0_h0+h1|w0+w1_h0+h1"

for tag, name, args, extra in (
        (u"hstack", u"hstack", u"", 1),
        (u"vstack", u"vstack", u"", 1),
        (u"xstack 4", u"xstack", u"inputs=4:layout=0_0|w0_0|0_h0|w0_h0", 3),
        (u"xstack 9", u"xstack", u"inputs=9:layout=" + LAY9, 8)):
    g, b, sk = build(name, args, extra)
    c0 = av.open(SRC)
    arr = next(c0.decode(video=0)).to_ndarray()
    c0.close()
    vf = av.VideoFrame.from_ndarray(arr, format=u"yuv420p")
    vf.pts = 0
    vf.time_base = TB
    b.push(vf)
    o = sk.pull().to_ndarray(format=u"rgb24")
    print(u"%-10s %d 路 -> %d×%d" % (tag, extra + 1, o.shape[1], o.shape[0]))

# 对照:两路全用内存缓冲
try:
    g = av.filter.Graph()
    a = g.add_buffer(width=160, height=90, format=u"yuv420p", time_base=TB)
    c = g.add_buffer(width=160, height=90, format=u"yuv420p", time_base=TB)
    ov = g.add(u"overlay", u"10:10")
    sk = g.add(u"buffersink")
    a.link_to(ov)
    c.link_to(ov)
    ov.link_to(sk)
    g.configure()
    print(u"两个内存缓冲 -> 成功")
except Exception as e:
    print(u"两个内存缓冲 -> %s" % str(e)[:40])

实跑输出:

--------------------------------------------------------------------
7.1.3 简单滤镜和复杂滤镜的区别
--------------------------------------------------------------------

**① 这一节把上两节的话收成一张对照表,然后实测三件事。**

  对比项          简单滤镜 -vf     复杂滤镜 -filter_complex
  -------------------------------------------------------------
  输入路数        只能 1           任意
  输出路数        只能 1           任意
  能不能分叉      不能             能
  命令行位置      跟在 -i 后面     可放在任意位置
  要不要 -map     不用             多半要

**② 实测一:简单滤镜只有一个出口**

**PyAV 里给 buffersink 拉帧,一条单链只能拉到一个 sink,这是硬约束。**
**想拉两路,就得在链里插一个 split——一插,它就不再是简单滤镜了。**

  写法           滤镜数     末端尺寸
  --------------------------------------
  scale,crop     2          60×40

**两环串起来仍然是一路进一路出,所以它依然是简单滤镜。**
**滤镜的个数不影响「简单」还是「复杂」——影响的是分不分叉。**

**③ 实测二:多输入在没有 movie 补位时根本建不起来**

  建图写法                       结果
  ----------------------------------------------
  两个 add_buffer 接 overlay     returned 22
  buffer + movie 接 overlay      成功

**两个「内存缓冲」当输入时,PyAV 的对接层建不起来,报的是一句干巴巴的 22。**
**换成一路 buffer 加一路 movie,就能跑——这是本机实测出来的唯一可行组合。**

**④ 实测三:三路合成也能跑**

  滤镜       输入路数     输出尺寸
  ------------------------------------
  hstack     2            320×90
  xstack     4            320×180

**四宫格出来了:宽是两倍、高是两倍,正好四个 160×90 拼成 320×180。**
**这个能力第七章后面要大用——画中画、九宫格全靠它。**

**⑤ 一句话记住区别**

**`-vf` 是「给这条路加几道工序」,`-filter_complex` 是「重新画一张路网」。**
**要改的是工序,就用前者;要改的是路网,就用后者。**

四行尺寸全部按倍数长:两路是 2 倍宽或 2 倍高,四路是 2×2,九路是 3×3。 最后一行是反面对照,同样的两路,换成两个内存缓冲就建不起来。

movie 在这里的角色是「无输入的源」:它自己去读文件,不需要别人喂。 所以一路 buffer 加 N 个 movie,就凑出了 N+1 路。

代价是每一次 movie 都真的打开一次文件。九宫格就是九次打开, 在实际工程里对 IO 有压力——能用 split 复制的场合,优先用 split。

30 秒实验

  1. 加一路:把 extra 从 1 改成 2,看 hstack 建得起来还是报错。
  2. 换个源:把 movie 换成 color=red:s=160x90:d=1,看能不能当第二路。
  3. 打乱顺序:把两次 link 的顺序对调,看那句 22 是不是又回来了。

常见误区

  1. 以为两路都要喂帧。movie 自己读文件,不用管它。
  2. 忘了给槽位号。按顺序 link 会得到 returned 22,报错完全看不出原因。
  3. 用九宫格做实时处理。九次开文件,延迟会明显上去。
自测三问(点开对答案) 1. 为什么两个内存缓冲接不起来? —— PyAV 与 libavfilter 的对接层在这一组合上建图失败,只报 returned 22。 2. movie 补位要显式给什么? —— 槽位号,即 `link_to(目标, 0, 第几号入口)`。 3. 九宫格为什么是 480×270? —— 九个 160×90 拼成 3×3,宽高各三倍。

相关笔记