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

倒影=画布加高+把画面上下翻+只留下半截贴到底部,再给一个从上到下的透明度渐变。 沙箱里第二路只能用 movie,用 split 接会静默出全黑。

📝 本节和前面几节的关系

7.6 的 vflip 与 7.7 的 pad 在这一节合体,做出一个真正能用的特效。 这也是本章第一个「多滤镜合成」的完整案例。

一、思路:三步拼出来

倒影本质是「下半块放一个上下翻的画面」,拆成三步:

第一步 pad 把画布加高,从 160×90 加到 160×134; 第二步把原画面 vflip;第三步只留下半截,贴到画布底部。

ffmpeg -i s7.mp4 -filter_complex "[0:v]pad=160:134:0:0[a]; [0:v]vflip,crop=160:44:0:0[b];[a][b]overlay=0:90" out.mp4

为什么裁掉上半:倒影只该映出靠下的那部分,映全了就成了「照镜子」。 44 这个高度正好是加高的那一截(134 减 90),一行不多一行不少。

二、实测:三块区域的亮度

倒影做得好不好,「淡不淡」是关键,拿区域均值量:

做法 主画面均值 倒影上段 倒影底段


倒影不加渐隐 142 165 167 加渐隐 142 140 26 渐隐加模糊 142 139 27

不加渐隐时倒影上下都是 165 左右,整块跟主画面一样亮,很假。 加了渐隐之后倒影底段只剩 26,上段还有 140,一个从亮到暗的过渡就出来了。

第三行数值跟第二行差不多,因为 gblur 只糊不压暗;糊开是为了模拟水面的散光。 要同时「淡」与「糊」,两个都要加,缺一个都不像。

三、渐隐靠的是 alpha 通道

关键那一句:

format=rgba,geq=r='r(X,Y)':g='g(X,Y)':b='b(X,Y)':a='255*(1-Y/44)'

geq 按坐标逐像素算新值。这里 r、g、b 原样不动,只改 a(透明度): 最上面一行 Y=0,a=255(不透明);最下面一行 Y=43,a 掉到个位数(几乎透明)。

倒影自然就「越往下越淡」,跟真实水面一个道理。 分母 44 不是随便写的,它跟倒影区的高度一致,写错了渐变就会提前到底或者到不了底。

format=rgba 不能省:yuv420p 没有 alpha 平面,geq 写了也存不下来。 这一步是「先换格式,再改透明度」,顺序反了等于白做。

四、沙箱里第二路只能用 movie

这一点必须说清楚,否则按命令行的思路写代码会得到全黑且不报错:

第二路怎么接 结果


从 split 再接一路 贴图区全黑,不报错 用 movie 读文件 正常贴上

这是 PyAV 的限制:overlay 的槽 1 只认「源滤镜」,从 split 过来的一律吃不到。 命令行里没这个毛病,同一条命令 [0:v]vflip...[b] 就够,不需要读两遍文件。

所以本节代码里 movie 把同一个文件又读了一遍—— 这只是迁就 PyAV 的写法,不是命令行的必需步骤。

五、水面效果还能再加两笔

在倒影那一路继续加滤镜,效果就出来了:

做法 加在哪


gblur=sigma=1.5 倒影那一路,做散光 noise=alls=20 倒影那一路,做细碎波纹 hflip 再贴 做成上下都翻的诡异效果

真正的水面还有横向抖动:那要靠 vflip 之后接 crop 逐帧移 x, 或者用 displace 拿另一张噪声图当位移场。这一节先做到「像那么回事」。

再加一步「只让倒影有颜色变化」也能出效果: 给倒影那一路接 hue=s=0.3,倒影变淡彩,主画面保持原色,看着像水面吸收了一部分光。

六、动手 Demo:三块区域、一条静默失败的路

下面这段代码把倒影做三遍:不加渐隐、加渐隐、渐隐加模糊,各量三块区域。 最后试一次「从 split 接第二路」,看它是不是静默全黑。

import av
from fractions import Fraction

TB = Fraction(1, 25)
ESC = u"s7.mp4".replace(u"\\", u"/").replace(u":", u"\\:")
c0 = av.open(u"s7.mp4")
base = next(c0.decode(video=0)).to_ndarray(format=u"yuv420p")
c0.close()

def mirror(padh, alpha=True, blur=None):
    hm = padh - 90
    g = av.filter.Graph()
    b = g.add_buffer(width=160, height=90, format=u"yuv420p", time_base=TB)
    pd = g.add(u"pad", u"160:%d:0:0:black" % padh)
    mv = g.add(u"movie", ESC)
    fl = g.add(u"vflip")
    cr = g.add(u"crop", u"160:%d:0:0" % hm)
    ov = g.add(u"overlay", u"0:90")
    sk = g.add(u"buffersink")
    b.link_to(pd)
    pd.link_to(ov, 0, 0)
    mv.link_to(fl)
    fl.link_to(cr)
    p = cr
    if alpha:
        fm = g.add(u"format", u"rgba")
        gq = g.add(u"geq",
                   u"r='r(X,Y)':g='g(X,Y)':b='b(X,Y)':a='255*(1-Y/%d)'" % hm)
        p.link_to(fm)
        fm.link_to(gq)
        p = gq
    if blur:
        bl = g.add(blur[0], blur[1])
        p.link_to(bl)
        p = bl
    p.link_to(ov, 0, 1)
    ov.link_to(sk)
    g.configure()
    vf = av.VideoFrame.from_ndarray(base, format=u"yuv420p")
    vf.pts = 0
    vf.time_base = TB
    b.push(vf)
    return sk.pull().to_ndarray(format=u"rgb24")

for tag, kw in ((u"不加渐隐", dict(alpha=False)),
                (u"加渐隐", dict(alpha=True)),
                (u"渐隐加模糊", dict(alpha=True, blur=(u"gblur", u"sigma=1.5")))):
    a = mirror(134, **kw)
    print(u"%-8s %dx%d  主 %3.0f  影上 %3.0f  影底 %3.0f"
          % (tag, a.shape[1], a.shape[0], a[0:90].mean(),
             a[90:105].mean(), a[-14:].mean()))

print(u"--- 第二路从 split 接(会静默失败)")
g = av.filter.Graph()
b = g.add_buffer(width=160, height=90, format=u"yuv420p", time_base=TB)
pd = g.add(u"pad", u"160:134:0:0")
sp = g.add(u"split", u"2")
fl = g.add(u"vflip")
cr = g.add(u"crop", u"160:44:0:0")
ov = g.add(u"overlay", u"0:90")
sk = g.add(u"buffersink")
b.link_to(pd)
pd.link_to(sp)
sp.link_to(ov, 0, 0)
sp.link_to(fl, 1, 0)
fl.link_to(cr)
cr.link_to(ov, 0, 1)
ov.link_to(sk)
g.configure()
vf = av.VideoFrame.from_ndarray(base, format=u"yuv420p")
vf.pts = 0
vf.time_base = TB
b.push(vf)
a = sk.pull().to_ndarray(format=u"rgb24")
print(u"split 第二路 -> 倒影区均值 %.0f" % a[90:134].mean())

实跑输出:

--------------------------------------------------------------------
7.8 视频倒影及镜面水面特效
--------------------------------------------------------------------

**① 倒影就是「下半块放一个上下翻的画面」**

**思路三步:**

**第一步 pad 把画布加高到 160×134;第二步把原画面 vflip;**
**第三步把翻过来的画面裁掉上半、只留下半,贴到画布底部。**

`ffmpeg -i s7.mp4 -filter_complex "[0:v]pad=160:134:0:0[a];`
`[0:v]vflip,crop=160:44:0:0[b];[a][b]overlay=0:90" out.mp4`

**为什么裁掉上半:倒影只该映出靠下的那部分,映全了就成了「照镜子」。**

**② 实测:三块区域的亮度**

  做法             主画面均值     倒影上段     倒影底段
  ---------------------------------------------------------
  倒影不加渐隐     142            165          167
  加渐隐           142            140          26
  渐隐加模糊       142            139          27

**不加渐隐时倒影上下都是 165 左右,整块跟主画面一样亮,很假。**
**加了渐隐之后倒影底段只剩 26,上段还有 140,一个从亮到暗的过渡就出来了。**
**第三行数值跟第二行差不多,因为 gblur 只糊不压暗;糊开是为了模拟水面的散光。**

**③ 渐隐靠的是 alpha 通道**

`format=rgba,geq=r='r(X,Y)':g='g(X,Y)':b='b(X,Y)':a='255*(1-Y/44)'`

**`geq` 按坐标逐像素算新值。这里只改 a(透明度):**
**最上面一行 Y=0,a=255(不透明);最下面一行 Y=43,a 掉到个位数(几乎透明)。**
**倒影自然就「越往下越淡」,跟真实水面一个道理。**

**④ 沙箱里第二路只能用 movie**

  第二路怎么接          结果
  --------------------------------------------
  从 split 再接一路     贴图区全黑,不报错
  用 movie 读文件       正常贴上

**这是 PyAV 的限制:overlay 的槽 1 只认「源滤镜」,从 split 过来的一律吃不到。**
**命令行里没这个毛病,同一条命令 `[0:v]vflip...[b]` 就够,不需要读两遍文件。**

**⑤ 水面效果还能再加两笔**

  做法                加在哪
  --------------------------------------------------
  gblur=sigma=1.5     倒影那一路,做散光
  noise=alls=20       倒影那一路,做细碎波纹
  hflip 再贴          做成「上下都翻」的诡异效果

**真正的水面还有横向抖动:那要靠 `vflip` 之后接 `crop` 逐帧移 x,**
**或者用 `displace` 拿另一张噪声图当位移场。这一节先做到「像那么回事」。**

前三行跟本节那张表一一对上:165/167、140/26、139/27。 底段从 167 掉到 26,渐隐是真的生效了,不是心理作用。

最后一行是那个静默失败的复现:倒影区均值 0, 而整段代码一个异常都没抛——这种坑只能靠量数据发现。

代码里 p = cr 之后按需串 format、geq、gblur, 用一个「当前末端」变量接力,比写三套分支短得多。

30 秒实验

  1. 改渐变:把 1-Y/44 改成 pow(1-Y/44,2),看底段是不是更淡。
  2. 换模糊:把 sigma=1.5 改成 4,看倒影糊到什么程度。
  3. 去掉 movie:改成 split 那条路,确认它真的静默出全黑。

常见误区

  1. 忘了 format=rgba。yuv420p 没有 alpha,透明度写了也存不住。
  2. 分母写错。44 要跟倒影区高度一致,写 90 渐变就提前到底了。
  3. 在 PyAV 里用 split 接第二路。静默全黑,必须用 movie。
自测三问(点开对答案) 1. 倒影为什么要先 pad 再加高? —— 画布不够高就没地方放倒影,pad 先把下面的空间留出来。 2. 渐隐为什么必须换 rgba? —— 只有带 alpha 平面的格式才能存透明度,yuv420p 没有。 3. split 接 overlay 会怎样? —— 不报错,但贴图区全黑——这是 PyAV 的限制,要用 movie。

相关笔记