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

小画面取主画面的四分之一到九分之一最合适,超过一半就成了分屏。 缩放算法上 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 秒实验

  1. 换尺寸:把 120x68 改成 80x44,看占比掉到多少。
  2. 换指标:把拉普拉斯换成「相邻像素差的绝对值和」,看四种算法的排序会不会变。
  3. 试 sinc:把算法换成 sinc 或者 lanczos,比较两者的锐度与观感差异。

常见误区

  1. 把 fast_bilinear 当高画质。它的锐是锯齿,不是细节。
  2. 小画面超过一半。那不是画中画,是分屏。
  3. 把 scale 挂在 overlay 之后。整幅一起缩,主画面也废了。
自测三问(点开对答案) 1. 画中画的小画面取多大合适? —— 主画面面积的四分之一到九分之一,超过一半就成了分屏。 2. 为什么 fast_bilinear 的锐度最高却不好看? —— 它省了色度插值,边缘有锯齿,方差大是噪声不是细节。 3. scale 应该挂在哪一路后面? —— 挂在 `[1:v]` 那一路后面,先缩小再贴。

相关笔记