倒影=画布加高+把画面上下翻+只留下半截贴到底部,再给一个从上到下的透明度渐变。 沙箱里第二路只能用 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-Y/44改成pow(1-Y/44,2),看底段是不是更淡。 - 换模糊:把
sigma=1.5改成 4,看倒影糊到什么程度。 - 去掉 movie:改成 split 那条路,确认它真的静默出全黑。
常见误区
- 忘了 format=rgba。yuv420p 没有 alpha,透明度写了也存不住。
- 分母写错。44 要跟倒影区高度一致,写 90 渐变就提前到底了。
- 在 PyAV 里用 split 接第二路。静默全黑,必须用 movie。
自测三问(点开对答案)
1. 倒影为什么要先 pad 再加高? —— 画布不够高就没地方放倒影,pad 先把下面的空间留出来。 2. 渐隐为什么必须换 rgba? —— 只有带 alpha 平面的格式才能存透明度,yuv420p 没有。 3. split 接 overlay 会怎样? —— 不报错,但贴图区全黑——这是 PyAV 的限制,要用 movie。- ☐ 7.8 完,下一节 7.9.1 画中画技术简介