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

min 封顶、max 保底,两个套起来就是夹逼。 本机实测:逗号不用转义也能跑,转义是命令行那一层的事。

📝 本节和前面几节的关系

上一节的变量让命令能适配不同分辨率。这一节再加一层条件: 不仅要跟着输入变,还要落在某个范围内。

一、两个函数,一个封顶一个保底

min 取两个数里小的,max 取大的。就这两句,没有别的。

scale=min(iw,320):-1——宽度取「原本宽度」与 320 里小的那个。

效果是:原本不到 320 的素材原样不动,超过 320 的缩到 320。 一个参数就实现了「大图缩小、小图不动」。

二、四组实测

同一个 160 宽的素材,四种写法四种结果:

写法 想干的事 实际输出


min(iw,320):-1 宽最多 320 160×90 max(iw,320):-1 宽至少 320 320×180 min(iw,80):-1 宽最多 80 80×45 max(iw,80):-1 宽至少 80 160×90

第一行原样不变:输入本来只有 160 宽,min 在两数里取了 160。 第二行放大到 320:max 挑大的,320 赢。

第三行缩到 80,第四行不动——同一个素材,四种结果。 决定结果的不是素材,是你用了哪个函数、跟哪个数比。

三、转义是命令行那一层的事

网上的命令几乎都写成 min(iw\,320),逗号前面多个反斜杠。 本机把四种写法都试了一遍,全部跑通,包括一个反斜杠都不加的:

写法 本机结果


min(iw,320):-1 160×90 min(iw\,320):-1 160×90 min(max(iw,80),320):-1 160×90 min(max(iw\,80)\,320):-1 160×90

因为这段代码直接调接口,参数串已经是一个整体,中间没有「先按逗号切」这一步。 写成 -vf scale=min(iw,320):-1 就不同了。

命令行里那一长串要先切成滤镜、再切成参数,逗号在两层都是分隔符。 所以那边必须转义,不转义就会被截成两段。

跟第六章 movie 的路径引号是同一件事: 转义是给「解析那一层」加的,不是滤镜自己的要求。

四、两个函数套起来就是夹逼

先保底、再封顶:

scale=min(max(iw,80),320):-1

读法是从里往外:先把宽度抬到至少 80,再压到最多 320。 结果是不管输入多大多小,宽度都落在 80 到 320 之间。

批量处理尺寸不一的素材时,这一句比写一堆 if 省事得多。 视频墙、缩略图批处理、用户上传的任意素材,都是这个场景。

顺序不能反:max(min(iw,320),80) 是「先封顶再保底」, 对 160 这种本来就在区间里的值结果一样,但对极小的输入就不同了。

五、两个方向都夹会变形

min 与 max 只管写进去的那一边,另一边还得靠 -1 或 -2 去算。 如果宽高各写一个 min:

scale=min(iw,320):min(ih,180)

160×90 过一遍还是 160×90,因为两个数都没超。 但换一段 640×480 的素材,宽被压到 320、高被压到 180, 比例从 4:3 变成 16:9——画面就被横向拉扁了。

两个方向各自独立地夹,等于各改各的比例。 要保住比例,只能夹一个方向,另一个交给 -2。

六、动手 Demo:四种写法、一次夹逼,再验一遍转义

下面这段代码把四种写法、夹逼、以及转义与否都试一遍。 最后两行是为了说明:转义在这条路上根本不需要。

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 run(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)
    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")
    return u"%d×%d" % (o.shape[1], o.shape[0])

for expr in (u"min(iw,320):-1", u"max(iw,320):-1",
             u"min(iw,80):-1", u"max(iw,80):-1",
             u"min(max(iw,80),320):-1"):
    print(u"%-26s -> %s" % (expr, run(expr)))

print(u"--- 带反斜杠的写法")
for expr in (u"min(iw\\,320):-1", u"min(max(iw\\,80)\\,320):-1"):
    print(u"%-26s -> %s" % (expr, run(expr)))

print(u"--- 两个方向都夹")
for expr in (u"min(iw,320):min(ih,180)", u"min(iw,80):min(ih,45)"):
    print(u"%-26s -> %s" % (expr, run(expr)))

实跑输出:

--------------------------------------------------------------------
7.2.4 使用 min 或 max 函数进行缩放
--------------------------------------------------------------------

**① 想让「大图缩小、小图不动」,就得有条件判断,min 与 max 就是最简的那种。**

`scale=min(iw,320):-1`——宽度取「原本宽度」与 320 里小的那个。

**② 四组实测**

  写法               想干的事       实际输出
  ----------------------------------------------
  min(iw,320):-1     宽最多 320     160×90
  max(iw,320):-1     宽至少 320     320×180
  min(iw,80):-1      宽最多 80      80×45
  max(iw,80):-1      宽至少 80      160×90

**第一行原样不变:输入本来就只有 160 宽,min 取到 160。**
**第二行放大到 320:max 在两数里挑大的,320 赢。**
**第三行缩到 80,第四行不动——同一个素材,四种结果,全看要用哪个函数。**

**③ 转义是命令行那一层的事**

**网上的命令几乎都写成 `min(iw\,320)`,逗号前面多个反斜杠。**
**但本机四种写法全部跑通,包括一个反斜杠都不加的:**

  写法                         本机结果
  -----------------------------------------
  min(iw,320):-1               160×90
  min(iw\,320):-1              160×90
  min(max(iw,80),320):-1       160×90
  min(max(iw\,80)\,320):-1     160×90

**因为这段代码直接调接口,参数串已经是一个整体,中间没有「先按逗号切」这一步。**
**写成 `-vf scale=min(iw,320):-1` 就不同了:那一长串要先切成滤镜、再切成参数,**
**逗号在那两层都是分隔符,所以必须转义。**

**跟第六章 movie 的路径引号是同一件事:转义是为「解析那一层」加的,不是滤镜自己的要求。**

**④ 两个函数套起来用**

`scale=min(max(iw,80),320):-1`——先保底 80,再封顶 320。

  写法                    实际输出
  ------------------------------------
  min(max(iw,80),320)     160×90

**这句就是「夹逼」:不管输入多大多小,宽度都落在 80 到 320 之间。**
**批量处理尺寸不一的素材时,这一句比写一堆 if 省事得多。**

**⑤ min 与 max 只管数,不管比例**

**它们只对写进去的那一边生效,另一边还得靠 -1 或 -2 去算。**
**如果两个方向都写 min,画面就会被拉变形——因为两个方向各自独立地夹。**

五行结果跟上一张表逐条对上,夹逼那一行落在 160×90,因为它在区间内。 带反斜杠的两行结果一模一样,证明转义对结果毫无影响。

最后两行是「两个方向都夹」: 第一行两数都没超,所以原样;第二行两个都被压,比例就变了。

为什么第二行会变比例?因为 160 压到 80 是砍一半,90 压到 45 也是砍一半, 这次正好比例相同所以没看出来。换一段 4:3 素材就会明显拉扁。

30 秒实验

  1. 换数值:把夹逼的上下限改成 100 与 200,看输出落在哪。
  2. 换素材:拿一段 4:3 的素材跑「两个方向都夹」,量比例差多少。
  3. 去掉 -1:写作 min(iw,320):ih,看与 -1 有没有区别。

常见误区

  1. 以为 min 与 max 会自动保持比例。它们只改一个数,不管另一个。
  2. 在直接调接口时也加反斜杠。多转义一层反而可能出错。
  3. 两个方向都写 min。比例保不住,画面会被拉扁。
自测三问(点开对答案) 1. min 与 max 分别干什么? —— min 取小的(封顶),max 取大的(保底)。 2. 为什么命令行里要写 `min(iw\,320)`? —— 逗号在命令行那层是参数分隔符,必须转义;直接调接口时不需要。 3. 两个方向都写 min 会怎样? —— 两个方向各自被夹,比例可能改变,画面变形。

相关笔记