一条 FFmpeg 命令同时完成缩放、加水印、拼画面——这不是三个命令的拼接,而是滤镜图(filter graph)在起作用。filter_complex 是 FFmpeg 最强大也最劝退的部分:方括号、垫、链,符号一堆。这篇用图解的方式把它拆开,看完你就能写出自己的复合命令。
先分清:滤镜链 vs 滤镜图
**滤镜链(filter chain)**是滤镜的串行流水线,前一个的输出是后一个的输入,用逗号连接:
scale=1280:-1,format=yuv420p,vflip
数据像流水线一样依次流过:先缩放 → 再转像素格式 → 再垂直翻转。
**滤镜图(filter graph)**允许多条链存在,链之间可以分叉、汇合、交叉连接,用分号分隔链、方括号标记垫(pad):
[0:v]scale=640:480[a];[1:v]scale=640:480[b];[a][b]hstack
一句话总结:单路串行用 -vf(滤镜链),多路或需要拆分合流用 -filter_complex(滤镜图)。-filter_complex 是超集——滤镜链只是没有分叉的滤镜图。
理解"垫":滤镜图的连线
垫(pad)是理解滤镜图的钥匙。方括号里的标签就是一条虚拟的连线,把上游输出接到下游输入:
输入0 ──[0:v]──> scale ──[a]──┐
├──> hstack ──> 输出
输入1 ──[1:v]──> scale ──[b]──┘
对应命令:
ffmpeg -i left.mp4 -i right.mp4 -filter_complex \
"[0:v]scale=640:480[left];[1:v]scale=640:480[right];[left][right]hstack" \
out.mp4
逐段拆解:
[0:v]:第 0 个输入的视频流(0是-i的顺序,v表示 video;音频是a)scale=640:480[left]:处理后打上left标签,供下游引用[left][right]hstack:hstack滤镜吃两个输入垫,横向拼接
没打标签的输出会自动接到滤镜图的最终输出;hstack 的输出没有标签,所以它就是结果。
-vf 与 -filter_complex 的关系
-vf | -filter_complex | |
|---|---|---|
| 输入流 | 固定单路 | 任意多路 |
| 能否引用第 2 个输入 | 不能(-i 只当数据源) | 能,[1:v] 直接引用 |
| 输出 | 自动映射到输出文件 | 可用 -map 精确选择 |
| 音频 | 不经手 | 可一并处理 |
-vf 本质是 -filter_complex 的单路语法糖。但注意:同一个命令里 -vf 和 -filter_complex 不要混用,容易产生两个互不知情的滤镜管线。
实战一:缩放 + 水印(最常见的复合需求)
给视频右下角加图片水印,同时限制到 720p:
ffmpeg -i input.mp4 -i logo.png -filter_complex \
"[1:v]scale=120:-1[wm];[0:v][wm]overlay=W-w-20:H-h-20,scale=-2:720" \
out.mp4
拆开看:
[1:v]scale=120:-1[wm]:水印图缩到 120 宽(-1表示按比例自动算高)[0:v][wm]overlay=...:把水印叠到主画面上。W/H是主画面尺寸,w/h是水印尺寸,所以W-w-20:H-h-20= 右下角留 20px 边距,scale=-2:720:接在 overlay 后面的链尾缩放,-2让宽度自动对齐到偶数(H.264 要求宽高为偶数,用-1会报错)
顺带记住 overlay 的位置速记:0:0 左上、(W-w)/2:(H-h)/2 居中、0:H-h 左下。
实战二:多路合成与画面布局
四宫格拼画面——每个输入缩到半宽半高,再用 xstack 摆位:
ffmpeg -i a.mp4 -i b.mp4 -i c.mp4 -i d.mp4 -filter_complex \
"[0:v]scale=iw/2:ih/2[a];[1:v]scale=iw/2:ih/2[b];\
[2:v]scale=iw/2:ih/2[c];[3:v]scale=iw/2:ih/2[d];\
[a][b][c][d]xstack=inputs=4:layout=0_0|w0_0|0_h0|w0_h0" \
out.mp4
layout 里 w0 表示"第 0 个输入的宽度"、h0 同理——垫与垫之间的相对位置关系直接写进布局表达式。
分叉也很自然:一个源同时出预览图和正式输出:
ffmpeg -i input.mp4 -filter_complex \
"[0:v]split=2[v1][v2];[v1]scale=320:-1[thumb];[v2]scale=1280:-2[main]" \
-map "[thumb]" thumb.jpg -map "[main]" main.mp4
split=2 把一路复制成两路,各自走不同的链,最后用 -map 指定哪个垫写入哪个输出文件。这是滤镜图相对 -vf 的真正威力所在。
调试技巧
滤镜图写长了必然出错,几个救命习惯:
- 逐段验证。先跑通
[0:v]scale=640:480[v],确认无误后再加第二段——一口气写完再调试是效率最低的方式。 -v error减少日志噪音。FFmpeg 默认日志很吵,只看 error 能立刻定位是哪个滤镜在抱怨。- 报错会指名滤镜和垫。
Invalid argument后面跟的垫标签就是断点,顺着它检查上游输出参数。 - 分辨率不匹配是头号错误源。
hstack/vstack要求输入同高/同宽,overlay像素格式不一致会静默变慢——拼接前先统一scale+format=yuv420p。
写在最后
滤镜图的思路其实是数据流编程:每个滤镜是一个节点,垫是连线,你设计的是图而不是命令。掌握 [x] 标签的连接逻辑后,剩下的只是查滤镜参数表。
不想手写的话,视频加水印和视频转 GIF把本文用到的 overlay、scale 滤镜做成了即拿即用的界面;想自由组合参数,去 FFmpeg 命令实验室直接试跑——都在浏览器本地完成。
FAQ
Q:滤镜链的顺序影响结果吗?
A:影响,而且影响体积。先 scale 缩小再做后续滤镜,处理的数据量更小;但去噪类滤镜通常要放在缩放前,顺序要按需求定。
Q:-1 和 -2 在 scale 里有什么区别?
A:-1 表示保持比例自动计算,-2 是自动计算且对齐到偶数。H.264/H.265 编码要求宽高为偶数,实际项目里基本都应该用 -2。
Q:为什么我的 overlay 是逐帧处理但速度极慢?
A:通常是主视频与水印的像素格式不一致,FFmpeg 在每一帧做隐式格式转换。给两路显式加 format=yuv420p 可以消除。