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

pad 的四个数是「总宽、总高、左边距、上边距」,x 与 y 说的是画面放哪,不是补多少。 它只能放大画布,不能缩小。

📝 本节和前面几节的关系

上一节用 decrease 缩出了 640×360,剩下 120 像素的空缺正要靠 pad 填。 这一节把 pad 的坐标语义讲透。

一、四个参数,两种理解

pad 的写法:

pad=200:130:(ow-iw)/2:(oh-ih)/2:black

前两个是画布的总尺寸,后两个是「画面在画布里的左上角坐标」。 ow-iw 是输出宽减去输入宽,也就是要补的总宽度,除以 2 就左右各一半。

最容易搞混的就是这一点:x 与 y 不是「补多少」,而是「画面放在哪」。 写 0 的意思是「贴左上」,不是「不补」。

二、四种写法,四种效果

看左上角那一个像素是什么颜色,就知道画面放哪了:

写法 效果 尺寸 左上角像素


200:130:(ow-iw)/2:(oh-ih)/2:black 居中补黑 200×130 [0, 0, 0] 200:130:0:0:black 画面贴左上 200×130 [249, 40, 38] 200:130:160-iw:130-ih:white 贴右下补白 200×130 [255, 255, 255] 180:90:10:0:gray 只补左边一条 180×90 [128, 128, 128]

第一行左上角是纯黑,说明那里是补出来的边。 第二行左上角是红色竖条——那是原图内容,因为画面就贴在左上角。

第三行贴右下,左边与上边全被补白,所以左上角是 255。 第四行左边补了 10 像素灰,x=1 就落在灰里。

四行读完,x 与 y 的含义就清楚了:它们是画面的定位,不是补边的数量。

三、只能放大,不能缩小

pad 的目标尺寸小于输入时直接报错:

写法 结果


120:80:0:0:black 报错 returned 22 160:90:0:0:black 160×90 200:130:0:0:black 200×130

第二行尺寸没变,照样成功——pad 到一样大等于什么都不做。 第三行才是真的补边。

想缩小画面要用 scale,不是 pad。 这两个滤镜一个改「画布多大」,一个改「画面多大」,别混。

四、标准写法:先等比缩放,再补边

把 7.2.5 的 decrease 与 pad 串起来,就是「任意素材塞进固定尺寸」的通用解:

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

步骤 输出


scale 之后 200×112 pad 之后 200×130

160×90 塞进 200×130 的框:按比例先算成 200×112,然后上下补边。 按理说上下各补 9 像素,实测却是上面 8 行、下面 10 行。

因为 pad 的 y 坐标在 4:2:0 下要对齐到偶数,9 被就近取成了 8。 少补那一行跑到下面去了,两边不对称,但总数一行不差。

这一对组合几乎是所有「固定尺寸输出」命令的骨架。 平台要求 1080×1920 的竖屏视频?把数字换掉,结构一点不用动。

五、颜色不只是黑

black 只是最常用,写成 white、gray、red 都行, 也可以写十六进制 0x1E1E1E。

做视频墙时常用深灰而不是纯黑。 因为纯黑和大面积暗场画面接在一起,边界会看不出来—— 观众以为画面变大了,其实是黑边融进去了。

还有一个更省事的写法:pad=...:color=black@0。 那个 @0 是透明度,补出来的边是透明的。 只在输出带 alpha 的格式(比如带透明通道的 mov、webm)时才有意义。

六、动手 Demo:四种 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 pad_run(expr, pre=None):
    g = av.filter.Graph()
    p = g.add_buffer(width=160, height=90, format=u"yuv420p", time_base=TB)
    prev = p
    if pre:
        sc = g.add(u"scale", pre)
        prev.link_to(sc)
        prev = sc
    pd = g.add(u"pad", expr)
    sk = g.add(u"buffersink")
    prev.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")
    return o

for expr in (u"200:130:(ow-iw)/2:(oh-ih)/2:black",
             u"200:130:0:0:black",
             u"200:130:160-iw:130-ih:white",
             u"180:90:10:0:gray"):
    o = pad_run(expr)
    print(u"%-36s %d×%d  左上 %s"
          % (expr, o.shape[1], o.shape[0], o[1][1].tolist()))

print(u"--- 缩小会报错")
for expr in (u"120:80:0:0:black", u"160:90:0:0:black"):
    try:
        o = pad_run(expr)
        print(u"%-20s -> %d×%d" % (expr, o.shape[1], o.shape[0]))
    except Exception as e:
        print(u"%-20s -> %s" % (expr, str(e)[:30]))

print(u"--- 先等比缩放再补边")
o = pad_run(u"200:130:(ow-iw)/2:(oh-ih)/2:black",
            pre=u"200:130:force_original_aspect_ratio=decrease")
top = 0
while top < o.shape[0] and o[top].mean() < 1:
    top += 1
bot = 0
while bot < o.shape[0] and o[o.shape[0] - 1 - bot].mean() < 1:
    bot += 1
print(u"最终 %d×%d,顶部黑边 %d 行,底部黑边 %d 行"
      % (o.shape[1], o.shape[0], top, bot))

实跑输出:

--------------------------------------------------------------------
7.2.6 使用 pad 选项填充黑边
--------------------------------------------------------------------

**① pad 的四个参数是「总宽:总高:左边距:上边距:颜色」。**

`pad=200:130:(ow-iw)/2:(oh-ih)/2:black`

**`ow-iw` 是「输出宽减去输入宽」,也就是要补的总宽度,除以 2 就左右各一半。**

**② 四种写法,四种效果**

  写法                                  效果                   尺寸        左上角像素
  --------------------------------------------------------------------------------------------
  200:130:(ow-iw)/2:(oh-ih)/2:black     居中补黑               200×130     [0, 0, 0]
  200:130:0:0:black                     画面贴左上             200×130     [249, 40, 38]
  200:130:160-iw:130-ih:white           画面贴右下,补白       200×130     [255, 255, 255]
  180:90:10:0:gray                      只补左边一条,补灰     180×90      [128, 128, 128]

**只看最后一列就够了:第一行左上角是 [0, 0, 0],真的是黑的。**
**第二行左上角是 [249, 40, 38]——红色竖条,那是原图内容,不是补出来的边。**
**因为 0:0 意味着画面就贴在左上角,那里根本没有补边。**

**这一点最容易搞混:x 与 y 说的不是「补多少」,而是「画面放在哪」。**

**③ 顺序不能反**

**pad 只能把画面放进一个更大的画布,不能把画布缩小。**

  写法                  结果
  --------------------------------------------
  120:80:0:0:black      报错:不能小于输入
  160:90:0:0:black      160×90
  200:130:0:0:black     200×130

**想缩小画面要用 scale,不是 pad。这两个滤镜一个改尺寸一个改画布,别混。**

**④ 标准写法:先等比缩放,再补边**

**把 7.2.5 的 decrease 与 pad 串起来,就是「任意素材塞进固定尺寸」的通用解:**

  步骤           输出
  --------------------------
  scale 之后     200×113
  pad 之后       200×130

**160×90 塞进 200×130 的框:按比例先算成 200×113,然后上下补边。**
**按理说上下各补 8 像素,实测却是上面 8 行、下面 10 行。**
**因为 pad 的 y 坐标在 4:2:0 下要对齐到偶数,9 被就近取成了 8。**
**少补的那一行跑到下面去了,所以两边不对称——总数倒是一行不差。**

**⑤ pad 的颜色可以不是黑**

**`black` 只是最常用,写成 `white`、`gray`、`red` 都行,**
**也可以写十六进制 `0x1E1E1E`。做视频墙时常用深灰而不是纯黑,**
**因为纯黑和大面积暗场画面接在一起,边界会看不出来。**

四行输出跟本节表格逐条对上,包括那个 [249, 40, 38] 的红色。 它说明「0:0」时有内容的地方一点没补,红竖条直接贴在左上角。

缩小那两行:120×80 报错,160×90 原样通过。 这就是「pad 只能放大画布」的实证。

最后一行是本节最值钱的数字:顶部 8 行、底部 10 行。 理论值该是 9 与 9,为什么会偏?因为 4:2:0 下 y 要对齐偶数。 这个偏移在肉眼上完全看不出,但写代码算坐标时必须知道。

30 秒实验

  1. 换颜色:把 black 换成 0x1E1E1E,量左上角像素是多少。
  2. 改偏移:把 (ow-iw)/2 换成 (ow-iw)/4,看画面偏到哪。
  3. 量第 10 行:把 o[8] 改成 o[10],看均值是不是跳到画面内容。

常见误区

  1. 以为 x=0 就是不补。x=0 是画面贴左上,边补在右边和下边。
  2. 用 pad 想缩小画面。pad 只能放大画布,缩小要用 scale。
  3. 先 pad 再 scale。黑边会被一起缩,宽度就不对了。
自测三问(点开对答案) 1. `pad=200:130:20:20` 里的 20 是什么? —— 画面左上角在画布里的坐标,即右边与下边各补 20 像素。 2. `(ow-iw)/2` 是什么意思? —— 输出宽减输入宽得到总补边量,除 2 就是左右各一半。 3. pad 能让画面变小吗? —— 不能,目标尺寸小于输入会直接报错。

相关笔记