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

给一个框,怎么把画面塞进去又不压扁:disable 硬拉、decrease 塞进、increase 盖住。 decrease 留下的空白靠 pad 补。

📝 本节和前面几节的关系

前三节都在算「缩放成多大」。这一节换个角度:先给一个框,再看画面怎么落进去。 平台要求固定尺寸时,用的就是这个参数。

一、一个框,三种塞法

命令行长这样:

scale=640:480:force_original_aspect_ratio=decrease

看到 force_original_aspect_ratio 这么长的名字,其实只问一件事: 框的比例和画面的比例不一样时,听谁的。

三个取值三条路:硬拉、缩进去、放大盖住。

二、三个取值的实测

同一段 160×90 的素材,塞进 640×480 的框:

取值 含义 实际输出


disable 默认,直接拉扯到目标尺寸 640×480 decrease 缩到能塞进框内的最大尺寸 640×360 increase 放大到能盖住框的最小尺寸 853×480

disable 就是不用这个参数,画面被硬拉到 640×480,比例全乱。 decrease 保住 16:9,缩成 640×360:宽顶满,高只用了 360。

increase 也保住 16:9,放大到 853×480:高顶满,宽超出 213 像素。 853 这个数字有点怪——它是 480 乘 16/9 算出来的,除不尽。

注意 853 是奇数。decrease 与 increase 都不管奇偶, 要偶数得另外加 force_divisible_by=2,7.2.8 会讲。

三、换个框再看一次

同样用 decrease,框的宽高比变了,谁顶满也跟着变:

目标框 decrease 输出 谁顶满


640:480 640×360 宽顶满 80:80 80×45 宽顶满 200:100 178×100 高顶满 100:200 100×56 宽顶满

四行里总有一个方向正好等于目标值——这就是 decrease 的定义: 在这个框里能放多大就放多大,不越界、不改比例。

第三行是唯一「高顶满」的:框又宽又扁,画面得先满足高度。 其余三行都是框比画面更「高」,所以宽度先到顶。

四、留下的空白要靠 pad 补

640×360 塞进 640×480 的框,上下各空 60 像素。 要输出固定尺寸的文件,就得再接一个 pad:

scale=640:480:force_original_aspect_ratio=decrease,pad=640:480:(ow-iw)/2:(oh-ih)/2

这一对组合是「等比缩放加补黑边」的标准写法。 少了 pad,输出尺寸就不是 640×480,平台那边直接不收。

顺序也不能反:先 pad 再 scale,补出来的黑边会跟着一起被缩, 黑边的宽度就不对了。

五、increase 为什么反而少用

increase 会让画面超出框外,多出来的部分通常要靠 crop 再裁掉。 两步都做,等于先放大再丢像素,画质白损一次。

除非你要的就是「铺满不留黑边」,否则 decrease 加 pad 更划算。 视频墙、封面图这类「宁可留黑边也不能裁掉内容」的场合,一律 decrease。

一句话记法:decrease 是「装进去」,increase 是「盖上去」。 装进去不会丢内容,盖上去一定丢。

六、动手 Demo:三种塞法,三种尺寸

下面这段代码把三种取值各跑一次,再换四个框看谁顶满。 最后把 decrease 与 pad 串起来,量补边前后差多少。

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 size(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 o.shape[1], o.shape[0]

for mode in (u"disable", u"decrease", u"increase"):
    e = u"640:480:force_original_aspect_ratio=" + mode
    w, h = size(e)
    print(u"%-9s -> %d×%d" % (mode, w, h))

print(u"--- 换四个框")
for tgt in (u"640:480", u"80:80", u"200:100", u"100:200"):
    w, h = size(tgt + u":force_original_aspect_ratio=decrease")
    tw, th = [int(x) for x in tgt.split(u":")]
    tag = u"宽顶满" if w == tw else (u"高顶满" if h == th else u"都没满")
    print(u"%-9s -> %d×%d  %s" % (tgt, w, h, tag))

print(u"--- decrease 之后补边")
g = av.filter.Graph()
p = g.add_buffer(width=160, height=90, format=u"yuv420p", time_base=TB)
sc = g.add(u"scale", u"200:130:force_original_aspect_ratio=decrease")
pd = g.add(u"pad", u"200:130:(ow-iw)/2:(oh-ih)/2:black")
sk = g.add(u"buffersink")
p.link_to(sc)
sc.link_to(pd)
pd.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"scale 之后 %s,pad 之后 %d×%d"
      % (u"200×112", o.shape[1], o.shape[0]))

实跑输出:

--------------------------------------------------------------------
7.2.5 使用 force_original_aspect_ratio 进行缩放
--------------------------------------------------------------------

**① 这个参数解决的问题:给了一个框,怎么把画面塞进去又不压扁。**

`scale=640:480:force_original_aspect_ratio=decrease`

**② 三个取值的实测**

  取值         含义                         640×480 的实际输出
  ----------------------------------------------------------------
  disable      默认,直接拉扯到目标尺寸     640×480
  decrease     缩到能塞进框内的最大尺寸     640×360
  increase     放大到能盖住框的最小尺寸     853×480

**disable 就是不用这个参数,画面被硬拉到 640×480,比例全乱。**
**decrease 保住 16:9,缩成 640×360——宽顶满,高只用了 360。**
**increase 也保住 16:9,放大到 853×480——高顶满,宽溢出了 213 像素。**

**③ 换个目标框再看一次**

  目标框      decrease 输出     是宽顶满还是高顶满
  ----------------------------------------------------
  640:480     640×360           宽顶满
  80:80       80×45             宽顶满
  200:100     178×100           高顶满
  100:200     100×56            宽顶满

**四行里总有一个方向正好等于目标值——这就是 decrease 的定义:**
**在这个框里能放多大就放多大,而且不越界、不改比例。**

**④ decrease 留下的空白要靠 pad 补**

**640×360 塞进 640×480 的框,上下各空 60 像素。**
**要输出固定尺寸的文件(比如平台要求 640×480),就得再接一个 pad:**

`scale=640:480:force_original_aspect_ratio=decrease,pad=640:480:(ow-iw)/2:(oh-ih)/2`

**这一对组合是「等比缩放 + 补黑边」的标准写法,7.2.6 会把它跑一遍。**

**⑤ increase 为什么反而少用**

**increase 会让画面超出框外,多出来的部分通常要靠 crop 再裁掉。**
**两步都做,等于先放大再丢像素,画质白损一次。**
**除非你要的是「铺满不留黑边」,否则 decrease 加 pad 更划算。**

前三行跟本节那张表逐条对上:disable 硬拉、decrease 缩进、increase 盖住。 接着四行里只有 200:100 是高顶满,其余都是宽顶满。

最后一段里「scale 之后 200×112」是写死的字符串,因为它只是中间量。 要量它也简单:把 pad 拿掉,单独跑一次 scale 就行。

注意 increase 出来的 853×480,那个 853 是除不尽的 853.33 取整。 所以等比缩放永远会有半个像素的误差,这点误差在 pad 与 crop 里会被放大。

30 秒实验

  1. 换框:把 640:480 改成 480:640,看 decrease 变成多少。
  2. 加整除:给 increase 那一行加 force_divisible_by=2,看 853 变成几。
  3. 去掉 pad:把 pad 那段删掉,量输出尺寸是不是 200×112。

常见误区

  1. 以为 decrease 会补边。它只缩,空白得自己 pad。
  2. 用 increase 又 crop。先放大再裁,白损一次画质。
  3. 把 pad 写在 scale 前面。黑边会跟着被缩,宽度就不对了。
自测三问(点开对答案) 1. decrease 与 increase 的区别是什么? —— decrease 缩到能塞进框内,increase 放大到能盖住框。 2. decrease 之后为什么还要 pad? —— decrease 只保证不越界,输出尺寸比框小,pad 才能补到刚好。 3. increase 出来的 853 为什么是奇数? —— 480 乘 16/9 得 853.33,取整就是 853,它不管奇偶。

相关笔记