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

scale 只认两个坐标:宽在前、高在后。 边长减一半,像素数只剩四分之一——面积和长度不是一回事。

📝 本节和前面几节的关系

7.1 讲的是怎么把滤镜连起来,从这一节开始逐个拆常用滤镜。 第一个就是 scale,它是这一章用得最多的一个。

一、两个坐标,一个滤镜

scale 的格式简单到不能再简单:

scale=宽:高

第一个数是宽,第二个数是高,中间冒号。写完就完了,没有别的必填项。 后面那一堆可选参数(算法、等比、整除)都在这两个坐标之后接。

二、五档尺寸实测

同一段 160×90 的素材,缩放到五个不同尺寸:

写进命令 意图 实际尺寸 像素数


320:180 2 倍 320×180 57600 160:90 原尺寸 160×90 14400 80:45 一半 80×45 3600 40:22 约四分之一 40×22 880 32:18 五分之一 32×18 576

第四行值得看一眼:宽正好是四分之一,高却只能给 22。 因为 90 除以 4 是 22.5,而 yuv420p 只接受偶数高度,往下取到 22。

差这半个像素在观感上没影响,但算像素数时会差一点: 40×22 是 880 个像素,而不是理论上的 900 个。

三、像素数掉得比尺寸快

把每一档换算成「相对原尺寸的像素比例」:

尺寸 相对原尺寸的像素


320×180 4.00 倍 160×90 1.00 倍 80×45 0.25 倍 40×22 0.06 倍 32×18 0.04 倍

边长减一半,像素数只剩四分之一——这是面积和长度的关系。 边长乘 2,面积乘 4;边长除 2,面积除 4,永远是平方。

所以「缩一半」在文件大小上省下的不是一半,而是一大半。 反过来,「放大一倍」的代价是四倍的计算量,这一点常被低估。

四、缩放也是要花时间的

每缩放一次,都要把整帧的每个像素重新采样一遍。 1080p 转 720p 的耗时,在实拍素材上能占到整个转码的两三成。

所以同一条命令里不要重复缩放: scale=1280:720,scale=640:360 等于白干一次。 要小尺寸就直接一次缩到位,别分两步走。

缩放放在链里的位置也有讲究: 先缩放再画框,框会跟着变小;先画框再缩放,框的线宽也会被缩。 想保住线宽,就把缩放放在最后。

五、两个数写反了会怎样

scale 的第一个数是宽、第二个是高。写反了不报错:

写法 输出 观感


scale=90:160 90×160 宽高互换,人被拉长

这种错误最难查:命令跑完了、文件也有、就是画面不对。 而且肉眼看小图不一定发现,得拿尺子量。

养成一个习惯:写完 scale= 先念一遍「宽在前、高在后」。 比事后一帧一帧找快得多。

六、动手 Demo:五档缩放,顺手抓出取偶那一档

下面这段代码把五档尺寸各跑一次,量真实输出与像素数。 最后一行是会取偶的那一档,看它跟理论值差多少。

import av
from fractions import Fraction

TB = Fraction(1, 25)
c0 = av.open(u"s7.mp4")
fr = next(c0.decode(video=0))
base = fr.to_ndarray()
c0.close()
print(u"源 %d×%d  %d 像素"
      % (fr.width, fr.height, fr.width * fr.height))

for tgt in (u"320:180", u"160:90", u"80:45", u"40:22", u"32:18"):
    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")
    w, h = o.shape[1], o.shape[0]
    print(u"scale=%-9s -> %d×%d  "
          u"%d 像素  占原 %.3f"
          % (tgt, w, h, w * h, w * h / float(160 * 90)))

实跑输出:

--------------------------------------------------------------------
7.2.1 使用 scale 实现缩放
--------------------------------------------------------------------

**① scale 是这一章用得最多的滤镜,格式就两个坐标:**

`scale=宽:高`

**② 五档尺寸实测**

  写进命令     意图           实际尺寸     像素数
  ---------------------------------------------------
  320:180      2 倍           320×180      57600
  160:90       原尺寸         160×90       14400
  80:45        一半           80×45        3600
  40:22        约四分之一     40×22        880
  32:18        五分之一       32×18        576

**注意 40:22 那一行:宽正好是四分之一,高却只能给 22。**
**因为 90 除以 4 是 22.5,而 yuv420p 只接受偶数高度,往下取到 22。**
**差这半个像素在观感上没影响,但算像素数时会差一点——**
**40×22 是 880 个像素,而不是理论上的 900 个。**

**③ 像素数掉得比尺寸快**

  尺寸        相对原尺寸的像素
  --------------------------------
  320×180     4.00 倍
  160×90      1.00 倍
  80×45       0.25 倍
  40×22       0.06 倍
  32×18       0.04 倍

**边长减一半,像素数只剩四分之一——这是面积和长度的关系,不是线性关系。**
**所以「缩一半」在文件大小上省下的不是一半,而是一大半。**

**④ 缩放也是开销**

**每缩放一次,都要把整帧的每个像素重新采样一遍。**
**1080p 转 720p 的耗时,在实拍素材上能占到整个转码的两三成——**
**所以同一条命令里不要重复缩放:`scale=1280:720,scale=640:360` 是白干一次。**

**⑤ 两个参数写反了会怎样**

**scale 的第一个数是宽、第二个是高。写反了不报错,只出来一张变形图:**

  写法             输出       观感
  --------------------------------------------------
  scale=90:160     90×160     宽高互换,人被拉长

**这种错误最难查——命令跑完了、文件也有、就是画面不对。**
**养成习惯:写完 `scale=` 先念一遍「宽在前、高在后」。**

第四行 40:22 的占比是 0.061,不是理论的 0.0625。 差的那一点就是 22.5 被取成 22 造成的——半个像素,乘上宽度就出来了。

第五行 32:18 是 0.040,正好是五分之一边长对应的二十五分之一。 它没被取偶影响,因为 90 除以 5 得 18,本来就是偶数。

这段代码里每次循环都重建一次图。 尺寸不同的滤镜不能共用一张图,因为 buffersink 的输出格式在建图时就定了。

30 秒实验

  1. 试奇数:把 80:45 改成 80:43,看 yuv420p 下出来是多少。
  2. 换格式:把 format=u"yuv420p" 换成 rgb24,看 40:22 还砍不砍。
  3. 写反了:把 320:180 写成 180:320,量输出与源的比例。

常见误区

  1. 以为缩放不花时间。大分辨率下它常是整条命令里最贵的一步。
  2. 写反宽高。不报错,只出一张变形的图。
  3. 连缩两次。中间那次纯属浪费,还可能多丢一次精度。
自测三问(点开对答案) 1. 160×90 缩到 80×45,像素数剩多少? —— 四分之一。边长减半,面积按平方减,是 1/4。 2. 为什么 `40:22` 出来的高度不是 22.5? —— yuv420p 要求高度为偶数,22.5 往下取到 22。 3. 链里同时有 scale 和 drawbox,谁先谁后? —— 看你要框跟着缩、还是框的线宽不缩,放在后面线宽不变。

相关笔记