小画面取主画面的四分之一到九分之一最合适,超过一半就成了分屏。 缩放算法上 bilinear 最糊最稳,fast_bilinear 的「锐」是锯齿假高频。
上一节把位置量准了,这一节解决「缩多大、用哪种算法缩」。 scale 的四种算法在 7.2.7 讲过参数,这里给的是画中画这个具体场景的取舍。
一、小画面该多大
经验值:四分之一到九分之一。160×90 的主画面,取 80×45 或者 40×22。
小画面尺寸 占主画面 实际占多少
80×45 0.25 80×45 40×22 0.06 40×22 120×68 0.57 120×68 160×90 1.00 160×90
「实际占多少」这一列是把蓝块量出来的结果,四行都跟写的尺寸一模一样。
说明 scale 到 color 的 s= 上不会偷偷改尺寸——它本来就按给的数造。
120×68 已经过半,那就不是画中画而是分屏了。 超过一半,观众不知道该看哪一边,主次就没了。
二、四种缩放算法的锐度
同一个 80×45,换四种算法,边缘的锐度差得很远:
算法 小画面锐度
fast_bilinear 60482 bilinear 25003 bicubic 49137 lanczos 55687
数是从拉普拉斯方差算的:把画面转灰度,跟四邻域做差再求方差, 边缘越硬、方差越大。所以这列数就是「看着有多锐」。
fast_bilinear 最高,但那是假的:它省掉了色度插值,边缘一格一格的, 方差大是因为锯齿本身就是高频噪声,不是细节多。
bilinear 最低最匀,小画面本来像素就少,糊一点反而盖住锯齿, 所以画中画用 bilinear 是本分。lanczos 次高且是真细节,讲究的时候用它。
三、顺序:先 scale 还是先 overlay
两种写法,结果完全不同:
写法 结果
[1:v]scale=80:45[pip];[0:v][pip]overlay 小画面清晰、主画面原样 [0:v][1:v]overlay,scale=80:45 整幅一起缩,小画面更糊
第二种实际用途不大,但它说明一件事:滤镜链的顺序有后果。 先缩小的那一路,后面的滤镜只需要处理更少的像素,又快又清楚。
这也解释了为什么 -filter_complex 里要把 scale 挂在 [1:v] 后面:
挂错位置,缩的就是整个输出,主画面跟着一起废。
四、不缩直接贴会怎样
160×90 的小画面贴到 160×90 的主画面上,等于把主画面整个换掉。 所以「贴之前必须先缩」不是风格问题,是前提。
工程上的顺序是:先把主画面统一到目标分辨率(scale 加 pad), 再按固定比例算小画面尺寸,最后 overlay。
这样小画面的相对大小才稳定。 如果直接按绝对像素写死 80×45,换一个输入分辨率,占比就全变了。
五、动手 Demo:四档尺寸、四种算法
下面这段代码先量四档小画面各占多少,再比四种缩放算法的锐度。 锐度用拉普拉斯方差算,同一块区域同一个指标,才可比。
import av
import numpy as np
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 one(second, pre=None):
g = av.filter.Graph()
b = g.add_buffer(width=160, height=90, format=u"yuv420p", time_base=TB)
src = g.add(second[0], second[1]) if second[1] else g.add(second[0])
ov = g.add(u"overlay", u"0:0")
sk = g.add(u"buffersink")
b.link_to(ov, 0, 0)
p = src
for name, args in (pre or []):
f = g.add(name, args) if args else g.add(name)
p.link_to(f)
p = f
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")
def sharp(a):
g = a.astype(np.float64).mean(axis=2)
lap = (-4 * g[1:-1, 1:-1] + g[:-2, 1:-1] + g[2:, 1:-1]
+ g[1:-1, :-2] + g[1:-1, 2:])
return float(lap.var())
print(u"--- 小画面占主画面多大")
for sw, sh in ((80, 45), (40, 22), (120, 68), (160, 90)):
a = one((u"color", u"c=blue:s=%dx%d:r=25" % (sw, sh)))
m = (a[:, :, 2] > 180) & (a[:, :, 0] < 70) & (a[:, :, 1] < 70)
cy = m.sum(axis=1)
cx = m.sum(axis=0)
ys = np.where(cy >= cy.max() * 0.5)[0]
xs = np.where(cx >= cx.max() * 0.5)[0]
print(u" %3dx%-3d 占 %.2f 实得 %dx%d"
% (sw, sh, sw * sh / (160.0 * 90),
xs.max() - xs.min() + 1, ys.max() - ys.min() + 1))
print(u"--- 四种缩放算法的锐度")
for alg in (u"fast_bilinear", u"bilinear", u"bicubic", u"lanczos"):
a = one((u"movie", ESC), pre=[(u"scale", u"80:45:flags=%s" % alg)])
print(u" %-14s %.0f" % (alg, sharp(a[0:45, 0:80])))
实跑输出:
--------------------------------------------------------------------
7.9.3 使用 overlay 与 scale 的结合实现画中画
--------------------------------------------------------------------
**① 小画面多大合适**
**经验值是主画面的四分之一到九分之一。160×90 的主画面,**
**小画面取 80×45(四分之一)或者 40×22(约九分之一)。**
小画面尺寸 占主画面 实际占多少
------------------------------------------
80×45 0.25 80×45
40×22 0.06 40×22
120×68 0.57 120×68
160×90 1.00 160×90
**120×68 已经占了主画面的一半以上,那就不是画中画,是分屏了。**
**超过一半就开始抢主画面的戏,观众不知道该看哪儿。**
**② 缩放算法对小画面的影响**
**同一个 80×45,换四种算法,锐度差得不少:**
算法 小画面锐度
--------------------------------
fast_bilinear 60482
bilinear 25003
bicubic 49137
lanczos 55687
**fast_bilinear 的锐度最高,但那是锯齿带来的假高频——它省掉了色度插值,**
**边缘一格一格的,看着「锐」其实更难看。lanczos 次之,那才是真细节。**
**bilinear 最糊,糊得均匀;小画面本身像素就少,糊一点反而盖住锯齿,**
**所以画中画用 bilinear 是本分,用 lanczos 是讲究。**
**③ 先 scale 还是先 overlay**
写法 结果
----------------------------------------------------
[1:v]scale=80:45[pip] 小画面清晰、主画面原样
overlay 之后再 scale 整幅一起缩,小画面更糊
**第二种实际用途有限,但它说明一件事:滤镜链的顺序是有后果的。**
**先缩小的那一方,后面的滤镜只需要处理更少的像素,又快又清楚。**
**④ 两段尺寸不一样也能贴**
**小画面不缩直接贴上去行不行?行,但 160×90 贴到 160×90 上就是全覆盖,**
**等于把主画面整个换掉。所以「贴之前必须先缩」不是风格问题,是前提。**
**工程上一般先统一主画面尺寸,再做画中画,这样小画面的相对大小才稳定。**
第一段四行与本节那张表完全一致,占比 0.25 / 0.06 / 0.57 / 1.00。 注意 40×22 只有 0.06,比九分之一还小,已经接近「角标」的程度了。
第二段四种算法的数字跟正文对得上:fast_bilinear 最高、bilinear 最低。 但最高不等于最好——方差只认「边缘硬度」,不认「边缘干不干净」。
这段代码里 one() 的第二路既可以给 color 也可以给 movie,
用元组 (滤镜名, 参数) 统一描述,后面串 scale 也就一行。
30 秒实验
- 换尺寸:把
120x68改成80x44,看占比掉到多少。 - 换指标:把拉普拉斯换成「相邻像素差的绝对值和」,看四种算法的排序会不会变。
- 试 sinc:把算法换成
sinc或者lanczos,比较两者的锐度与观感差异。
常见误区
- 把 fast_bilinear 当高画质。它的锐是锯齿,不是细节。
- 小画面超过一半。那不是画中画,是分屏。
- 把 scale 挂在 overlay 之后。整幅一起缩,主画面也废了。
自测三问(点开对答案)
1. 画中画的小画面取多大合适? —— 主画面面积的四分之一到九分之一,超过一半就成了分屏。 2. 为什么 fast_bilinear 的锐度最高却不好看? —— 它省了色度插值,边缘有锯齿,方差大是噪声不是细节。 3. scale 应该挂在哪一路后面? —— 挂在 `[1:v]` 那一路后面,先缩小再贴。- ☐ (本节完,下一节 7.9.4 画中画的灵活位置)