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

iw、ih、a、sar、dar 这些变量,让同一条命令适配所有分辨率。 本机实测:ow 读不到,oh 反而读得到。

📝 本节和前面几节的关系

前两节写的都是固定数字,换一段素材就得改命令。 内置变量就是把这些数字换成「输入画面的实际值」。

一、七个变量,各有各的用处

表达式里能读到的输入信息:

变量 含义


iw / in_w 输入画面宽度 ih / in_h 输入画面高度 a 宽高比,即 iw/ih sar 像素宽高比 dar 显示宽高比 hsub / vsub 色度的水平、垂直取样系数

sar 是「一个像素有多宽」,dar 是「整幅画面看起来多宽」。 大多数素材 sar 是 1(方形像素),这时 dar 就等于 a。

hsub 与 vsub 是色度的取样系数。 4:2:0 时两个都是 2,4:2:2 时是 2 与 1,4:4:4 时都是 1。

二、把变量值算出来

这些变量不能直接打印,但可以让它们变成尺寸,再从尺寸反推。 写法是把宽度设成 iw*0+变量*1000,高度固定 2。

变量 探针写法 输出宽 除一千


iw scale=iw0+iw1000:2 160000 160.000 ih scale=ih0+ih1000:2 90000 90.000 a scale=iw0+a1000:2 1777 1.777 sar scale=iw0+sar1000:2 1000 1.000 hsub scale=iw0+hsub1000:2 2000 2.000

iw 与 ih 乘一千之后再除回去,正好是素材的 160 与 90。 为什么写成乘一千?因为尺寸只能是整数,直接取 1.777 会把小数丢掉。

放大一千倍再除,就等价于保留了三位小数——这个技巧调参时很有用。 想知道某个表达式的值,就把它变成尺寸量出来。

三、按比例写缩放

有了变量,比例缩放就不用自己算了:

写法 输出


iw/2:ih/2 80×45 iw1.5:ih1.5 240×135 iw0.8:ih0.8 128×72

换成 1920×1080 的素材,这三条命令照样成立,只是输出跟着变。 这就是内置变量最大的用处:写一次,适配所有分辨率。

第二行输出 240×135,135 是奇数—— 它照样出来了,因为这不是在编码,只是滤镜内部的一帧。 真要出 MP4,还得改成 iw*1.5:trunc(ih*1.5/2)*2。

四、取偶的表达式写法

trunc(iw/2)*2 是最常见的取偶写法:

scale=trunc(iw/2)*2:trunc(ih/2)*2

先除以 2 取整,再乘 2——奇数的最后一位就这么被砍掉了。 160×90 过一遍还是 160×90,因为两个数本来就是偶数。

floor 与 trunc 等价,两个都认。 但千万别照搬 Python 的写法——本机实测四种里只有两种成立:

表达式 本机结果


trunc(iw/2)2 160×90 floor(iw/2)2 160×90 iw//2*2 returned 22 iw-iw%2 returned 22

// 是 Python 的整除符,% 是取余——FFmpeg 的表达式里两个都没有。 它只认四则运算加一批函数名,写法看着像 C,实际语法挺窄。

五、ow 读不到,oh 反而读得到

输出宽高这两个变量,在 scale 里表现不对称:

写法 结果


iw0+ow1000:ih0+2 returned 22 iw0+oh1000:ih0+2 2000×2

第一行报错,第二行成功——这个不对称很说明问题。 因为 scale 先算高、再算宽:算宽的那一刻,高已经定下来了。

所以 oh 读得到(值是前面算出的 2),ow 读不到。 ow 正是「此刻正在算的东西」,自己引用自己,只能报错。

这条规律推得再远一点:表达式里用到某个变量之前,先确认它是不是已经算出来了。 在 pad 里 ow 与 oh 都能读,因为 pad 的总尺寸是你直接给的,不存在先后。

六、动手 Demo:拿尺寸当探针,把七个变量量出来

下面这段代码把七个变量各乘一千,变成尺寸量出来。 最后试 ow 与 oh,看哪一个报错。

import av
from fractions import Fraction

TB = Fraction(1, 25)
c0 = av.open(u"s7.mp4")
base = next(c0.decode(video=0)).to_ndarray()
c0.close()

def probe(expr):
    g = av.filter.Graph()
    p = g.add_buffer(width=160, height=90, format=u"yuv420p", time_base=TB)
    sc = g.add(u"scale", expr + u":ih*0+2")
    sk = g.add(u"buffersink")
    p.link_to(sc)
    sc.link_to(sk)
    g.configure()
    vf = av.VideoFrame.from_ndarray(base, format=u"yuv420p")
    vf.pts = 0
    vf.time_base = TB
    p.push(vf)
    return sk.pull().to_ndarray(format=u"rgb24").shape[1]

for name in (u"iw", u"ih", u"a", u"sar", u"dar", u"hsub", u"vsub"):
    try:
        n = probe(u"iw*0+" + name + u"*1000")
        print(u"%-6s %8d  除一千 %.3f" % (name, n, n / 1000.0))
    except Exception as e:
        print(u"%-6s %s" % (name, str(e)[:40]))

print(u"--- 输出尺寸这两个变量")
for name in (u"ow", u"oh"):
    try:
        n = probe(u"iw*0+" + name + u"*1000")
        print(u"%-4s 可读,值 %.1f" % (name, n / 1000.0))
    except Exception as e:
        print(u"%-4s %s" % (name, str(e)[:40]))

print(u"--- trunc 取偶")
for tgt in (u"80:45", u"trunc(iw/2)*2:trunc(ih/2)*2", u"101:trunc(101*90/160)"):
    try:
        g = av.filter.Graph()
        p = g.add_buffer(width=160, height=90,
                         format=u"yuv420p", time_base=TB)
        sc = g.add(u"scale", tgt)
        sk = g.add(u"buffersink")
        p.link_to(sc)
        sc.link_to(sk)
        g.configure()
        vf = av.VideoFrame.from_ndarray(base, format=u"yuv420p")
        vf.pts = 0
        vf.time_base = TB
        p.push(vf)
        o = sk.pull().to_ndarray(format=u"rgb24")
        print(u"%-28s -> %d×%d" % (tgt, o.shape[1], o.shape[0]))
    except Exception as e:
        print(u"%-28s -> %s" % (tgt, str(e)[:30]))

实跑输出:

--------------------------------------------------------------------
7.2.3 使用 FFmpeg 的内置变量进行缩放
--------------------------------------------------------------------

**① scale 的两个参数可以是表达式,表达式里能读到输入画面的信息。**
**这些信息就是内置变量,写死的数字换成了变量,同一条命令就能适配各种素材。**

  变量            含义
  --------------------------------------------
  iw / in_w       输入画面宽度
  ih / in_h       输入画面高度
  a               宽高比,即 iw/ih
  sar             像素宽高比
  dar             显示宽高比
  hsub / vsub     色度的水平、垂直取样系数

**② 把变量值「算出来」的办法**

**这些变量不能直接打印,但可以让它们变成尺寸,再从输出尺寸反推。**
**写法是把宽度设成 `iw*0+变量*1000`,高度固定 2,输出宽度就是变量的一千倍。**

  变量     探针写法                   输出宽     除一千
  ----------------------------------------------------------
  iw       scale=iw*0+iw*1000:2       160000     160.000
  ih       scale=ih*0+ih*1000:2       90000      90.000
  a        scale=iw*0+a*1000:2        1777       1.777
  sar      scale=iw*0+sar*1000:2      1000       1.000
  dar      scale=iw*0+dar*1000:2      1777       1.777
  hsub     scale=iw*0+hsub*1000:2     2000       2.000
  vsub     scale=iw*0+vsub*1000:2     2000       2.000

**前两行的输出宽是六位数,除一千正好是 160 与 90,跟素材一模一样。**
**`a` 与 `dar` 除一千都是 1.777,就是 16 比 9——160×90 的宽高比正是 16:9。**
**`sar` 是 1.000,说明像素是正方形;`hsub` 与 `vsub` 都是 2,正是 4:2:0 的特征。**

**③ 用变量写「按比例」的缩放**

`scale=iw/2:ih/2` 与 `scale=iw*1.5:ih*1.5`

  写法              输出
  -----------------------------
  iw/2:ih/2         80×45
  iw*1.5:ih*1.5     240×135
  iw*0.8:ih*0.8     128×72

**换了素材,这些命令照样成立——因为它们读的是输入,不是写死的数。**
**这就是内置变量最大的用处:写一次,适配所有分辨率。**

**④ 取偶的老规矩,用表达式写出来**

`scale=trunc(iw/2)*2:trunc(ih/2)*2`

**`trunc` 是截掉小数,乘 2 再除 2 就把奇数变成偶数。**
**很多人把它简化成 `-2`,效果一样,但 `-2` 只能用在「按比例算」的那一边。**
**要两个方向都自己算时,还是得写这句。**

**⑤ 输出尺寸这两个变量,一个能读、一个不能**

**`ow`(输出宽)与 `oh`(输出高)在 scale 里表现不一样,一个能读、一个不能:**

  写法                    结果
  ---------------------------------------
  iw*0+ow*1000:ih*0+2     returned 22
  iw*0+oh*1000:ih*0+2     2000×2

**第一行报错、第二行成功,这个不对称很说明问题。**
**因为 scale 先算高、再算宽——算宽的那一刻,高已经定下来了,所以 `oh` 读得到。**
**而 `ow` 正是「此刻正在算的东西」,自己引用自己,只能报错。**
**所以真要用输出尺寸,得先想清楚它在什么时候可用:算得早的能用,正在算的不能用。**

前七行全部有值,a 与 dar 都是 1.777,sar 是 1.000。 这一段素材是方形像素、16:9 显示,所以三个数正好对得上。

ow 那行报 returned 22,oh 那行给出 2.0。 因为高度这一轮写死成 2,所以 oh 就是 2;乘一千得 2000,除回去还是 2。 这个数字本身没意义,有意义的是它能出来——它证明了「先算高、再算宽」。

最后三行是取偶的对照:80:45 出 45(不取偶), trunc 那句原样 90(本来就偶),101 配一个表达式高度出 56。

30 秒实验

  1. 换素材:换成 16:9 之外的比例,看 a 与 dar 还相等吗。
  2. 读 ow:把 iw*0+ow*1000 换成 ih*0+ow*1000,看报错是否一样。
  3. 除一百:把探针里的 1000 换成 100,看小数位够不够用。

常见误区

  1. 以为变量能直接打印。它们只在表达式里存在,得转成尺寸才看得见。
  2. 以为 ow 与 oh 地位相同。算宽的时候 ow 还没定,读它就报错。
  3. 以为 sar 就是宽高比。sar 是单个像素的宽高比,画面比例看 a 或 dar。
自测三问(点开对答案) 1. `a` 与 `dar` 什么时候不相等? —— 像素不是方形时。`a` 只看像素个数,`dar` 还乘上 sar。 2. 为什么探针要乘一千? —— 尺寸只能是整数,不放大就丢掉小数部分。 3. `trunc(iw/2)*2` 干的是什么? —— 把奇数宽砍成偶数:先除二取整,再乘二。

相关笔记