🎬 第十章 FFmpeg命令行在Linux系统中的应用
📄 一句话概括

滤镜在命令行里是一条链,逗号分层,从左往右一个个过。 滤镜分两种:只改像素的不动画布,改画布的会让后面所有坐标参数失效。

📝 本节和前面几节的关系

第七章把常用特效全过了一遍,这一章补三件当时没铺开的事: 引号到底是不是逗号逼出来的、可逆性怎么用来验证、滤镜顺序为什么能改结果。

一、滤镜是一条链,不是一堆开关

命令行里的滤镜长这样:

ffmpeg -i in.mp4 -vf "hflip,transpose=1,gblur=sigma=8" out.mp4

逗号把三件事串成一条流水线:先左右翻,再转 90 度,最后糊一下。 顺序是有意义的——hflip 在前跟在后,出来的画面完全不同。

这里顺手纠正一个流传很广的说法:逗号必须加引号。其实不是。 shell 眼里逗号就是个普通字符,-vf hflip,vflip 一样过。 真正需要引号的是空格、括号、分号、*、$ 这些—— 而滤镜串里恰好常有它们(scale=iw*0.5:ih*0.5 里的 * 是 shell 的通配符, 不加引号会先被 shell 抢去展开), 所以「一律加双引号」是省心的规矩,不是因为逗号。

二、六种滤镜,看它动像素还是动画布

同一个测试源的第一帧,过六条不同的链,量「跟原帧差多少」:

滤镜链 输出尺寸 平均差 它动的是什么


hflip 320×240 233.47 左右翻,尺寸不变 hflip + hflip 320×240 0.00 翻两次回到原样 transpose=1 240×320 没法比 顺时针转 90 度 transpose=1 + transpose=2 320×240 0.00 再转回来 gblur sigma=8 320×240 37.95 糊,尺寸不变 hue s=0 320×240 83.60 抽掉饱和度

「平均差」是处理后的第一帧跟原第一帧逐像素相减取绝对值再平均, 范围 0 到 255。0 就是一模一样。

注意 transpose 那一行的尺寸:320×240 变成了 240×320。 这是滤镜里最容易被忽略的一类——改画布的滤镜。 hflip、vflip、gblur、hue 只动像素,画布不动; transpose、crop、scale、pad 会动画布。

画布一变,后面所有按坐标算的参数全都要重算: overlay 的 x/y、drawtext 的 x/y、crop 的起点, 还有那些用 w、h 写的表达式。 这是滤镜链写错时最常见的根因—— 你以为 w 还是原来的宽,其实它早跟着上一环变了。

三、可逆性是最好的验证手段

表里两行 0.00 值得单独说:hflip 翻两次、transpose 转两次, 出来的像素跟原来一个不差。

这条性质在做实验时特别有用: 想确认「某个滤镜到底做了什么」,最省事的办法是找一个能把它抵消掉的操作, 做一遍再比——差 0 说明你理解的变换方向是对的。

反过来,如果两次翻完差得很多,那一定是中间有别的东西在动: 比如自动插值、比如 YUV 转 RGB 的取整、比如编码器先跑了一遍。 所以「可逆性检验」也是排查「为什么结果跟预期不一样」的第一把尺子。

四、音频那边三条,别混

视频是帧,音频是采样点,滤镜也换了一套名字:

参数 改之前 改之后 改的是什么


volume=0.5 峰值 0.250 峰值 0.125 只动幅度 atempo=2.0 88200 点 / 2.00 秒 44100 点 / 1.00 秒 重采样,不动音高 asetrate=22050 44100 Hz 22050 Hz 改采样率,音高跟着降

volume 就是乘一个系数,波形形状一点没变。

atempo 和 asetrate 最容易混,区别就在一句话: atempo 变时长不变音高(重采样之后又把音高拉回去), asetrate 变音高也跟着变(就是改「一秒记多少个点」)。

所以「把 2 分钟的录音压成 1 分钟」用 atempo=2.0; 「做一个花栗鼠音效」用 asetrate——用错了效果完全两码事。

五、滤镜顺序为什么能改结果

同一个 crop,放在 hflip 前面和后面,裁出来的不是同一块:

滤镜链 实际做了什么


crop=100:100:0:0,hflip 先裁左上角那块,再把它翻过来 hflip,crop=100:100:0:0 先整幅翻过来,再裁新画面的左上角

第二行裁到的是原图的右上角——因为整幅已经翻过了。 这就是「链」的含义:每一环看到的都是上一环的输出,不是原片。

所以写滤镜链有个固定思路:先想清楚每一环的输入是什么样。 先把尺寸统一(scale/pad),再做位置相关的效果(crop/overlay/drawtext), 最后才做整体像素调整(hue/gblur)—— 顺序一乱,坐标就全乱了。

六、动手 Demo:六条滤镜链,量出平均差

沙箱里没有 ffmpeg,滤镜用 PyAV 的 filter graph 真跑。 源用 lavfi 的 testsrc 现造,代码块自包含,不依赖任何外部文件。

import av
import numpy as np


def one(chain):
    """在 filter graph 里过一条链,返回 (rgb 数组, 宽, 高)。"""
    c = av.open("testsrc=size=320x240:rate=25", format="lavfi")
    g = av.filter.Graph()
    prev = g.add_buffer(template=c.streams[0])
    for nm, arg in chain:
        n2 = g.add(nm, arg) if arg else g.add(nm)
        prev.link_to(n2)
        prev = n2
    sink = g.add("buffersink")
    prev.link_to(sink)
    g.configure()
    for f in c.decode(c.streams[0]):
        g.push(f)
        o = g.pull()
        a = o.to_ndarray(format="rgb24")
        c.close()
        return a, o.width, o.height
    c.close()
    return None, 0, 0


b0, w0, h0 = one([("null", None)])
print("原帧 %dx%d" % (w0, h0))
for label, chain in (("hflip", [("hflip", None)]),
                     ("hflip x2", [("hflip", None), ("hflip", None)]),
                     ("transpose=1", [("transpose", "1")])):
    a, w, h = one(chain)
    if a is None:
        print("%-14s 失败" % label)
    elif a.shape != b0.shape:
        print("%-14s %dx%d 尺寸换了" % (label, w, h))
    else:
        d = float(np.abs(a.astype(np.int16) - b0.astype(np.int16)).mean())
        print("%-14s %dx%d 平均差 %.2f" % (label, w, h, d))

实跑输出:

--------------------------------------------------------------------
10.4 使用 FFmpeg 实现音视频特效
--------------------------------------------------------------------

**① 滤镜在命令行里是一串,用逗号接起来**

    ffmpeg -i in.mp4 -vf "hflip,transpose=1,gblur=sigma=8" out.mp4

**这里有个流传很广的说法:逗号必须加引号。其实不是。**
**shell 眼里逗号就是个普通字符,`-vf hflip,vflip` 一样过。**
**真正需要引号的是空格、括号、分号、`$` 这些——**
**而滤镜串里恰好常有它们(`scale=w=iw*0.5` 里的括号就不行),**
**所以「一律加双引号」是省心的规矩,不是因为逗号。**

**② 六种滤镜真过一遍,看它动的是像素还是尺寸**

  滤镜链                      输出尺寸    平均差      它动的是什么
  --------------------------------------------------------------------------
  hflip                       320×240     233.47      左右翻,尺寸不变
  hflip + hflip               320×240     0.00        翻两次回到原样
  transpose=1                 240×320     没法比      顺时针转 90 度
  transpose=1 + transpose=2   320×240     0.00        再转回来
  gblur sigma=8               320×240     37.95       糊,尺寸不变
  hue s=0                     320×240     83.60       抽掉饱和度

**「平均差」这一列是拿处理后的第一帧跟原第一帧逐像素相减取绝对值再平均,**
**范围 0 到 255。0 就是一模一样。**
**所以两行 0.00 是最有说服力的证据:`hflip` 翻两次、`transpose` 转两次,**
**出来的像素跟原来一个不差——滤镜只要可逆,来回一趟没有精度损失。**

**滤镜分两类,这是最容易被忽略的一类:改尺寸的滤镜。**
**`transpose` 那一行的输出尺寸从 320×240 变成了 240×320。**
**`hflip`、`vflip`、`gblur`、`hue` 都只改像素不动画布,**
**`transpose`、`crop`、`scale`、`pad` 会改画布。**
**画布一变,后面所有按坐标算的参数(`overlay` 的 x/y、`drawtext` 的 x/y)**
**全都要重算,这是滤镜链写错时最常见的根因。**

**③ 音频那边的两条常用参数**

  参数            改之前                改之后                改的是什么
  --------------------------------------------------------------------------------
  volume=0.5      峰值 0.250            峰值 0.125            只动幅度
  atempo=2.0      88200 点 / 2.00 秒    44100 点 / 1.00 秒    重采样,不动音高
  atempo=0.5      88200 点 / 2.00 秒    176400 点 / 4.00 秒   同上,往慢里拉

**`volume` 就是乘一个系数,波形形状一点没变;**
**`atempo` 是「按倍数重采样」,所以点数跟着变、时长跟着变,音高不变。**
**想改音高要用 `asetrate`,这两个别混。**

hflip 差 233 是因为 testsrc 那幅图本身左右不对称,差得越多说明它真的翻了。 hflip x2 差 0.00——同一个操作做两遍,正好抵消。

transpose=1 那行走的是另一个分支:尺寸变了,逐像素就没法比了。 这也说明一件事:比像素之前必须先确认两边尺寸一样, 尺寸不同时去比数组,报的是 broadcast 错误,跟滤镜一点关系都没有。

30 秒实验

  1. 把 hflip x2 换成 vflip 加 vflip:验证上下翻也是可逆的。
  2. 在链里插一个 scale=160:120:看平均差怎么变,理解缩放带来的失真。
  3. 把 hue s=0 换成 hue s=2:饱和度加倍,看差值是变大还是变小。

常见误区

  1. 以为逗号必须加引号。shell 不管逗号,真正要防的是括号、空格和 $。
  2. 把改画布的滤镜夹在中间。后面所有按 w、h 算的参数全跟着变。
  3. 把 atempo 和 asetrate 混着用。一个保音高、一个改音高,效果完全相反。
自测三问(点开对答案) 1. 哪几个滤镜会改画布尺寸? —— `transpose`、`crop`、`scale`、`pad`。翻、糊、调色都不改。 2. 怎么用最快的方式验证「某个滤镜做的是什么」? —— 找一个能抵消它的操作做一遍,差 0 就说明方向对了。 3. `atempo=2.0` 和 `asetrate` 差在哪? —— `atempo` 只改时长,音高不变;`asetrate` 直接改采样率,音高跟着变。

相关笔记