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

四角定位靠 W-w-8 这类表达式,换分辨率不用改数字。 但 H-h-8 算出来是奇数,落地会被抹掉一行,底边反而比上边窄。

📝 本节和前面几节的关系

7.9.2 量准了坐标语义,7.9.3 定好了尺寸,这一节把位置做成可复用的表达式。 把主画面也缩成格子,就是下一节 7.10 的九宫格。

一、四角定位

四种角落写法,一律用变量拼,不写死像素:

角落 写法 实际落点


左上 8:8 (8, 8) 右上 W-w-8:8 (72, 8) 左下 8:H-h-8 (8, 36) 右下 W-w-8:H-h-8 (72, 36)

左右是对称的:8 与 72,两边都留 8 列。 上下看着不对称——上是 8,下只剩 9 行。

原因是 H-h-8 算出 37,奇数被抹成 36,底边就多留了一行。 想上下真的一样,要么把小画面高度取成奇数,要么干脆直接写偶数坐标。

这也是为什么工程上宁可直接写 36 而不写 H-h-8: 表达式看着优雅,但一遇到奇数就得回头查表。

二、居中一行搞定

居中不用算,一行表达式:

(main_w-overlay_w)/2:(main_h-overlay_h)/2

实测落在 (40, 22)。手算:(160-80)/2=40,(90-45)/2=22.5, 直接把小数抹掉就是 22,跟量出来的完全一致。

这里又出现「往下取」——不是四舍五入。 要是四舍五入,22.5 会变 23,但实测是 22,说明它只做截断。

main_w 与 overlay_w 是 W 与 w 的长名字,两套都认。 表达式里还能写中文以外的任何算术:+ - * /、括号、min/max 都行。

三、让它动起来

x 里能写帧号 n 与时间 t,三种典型写法:

写法 效果


10+5n:10 每帧右移 5 像素 x='10+20t':y=10 每秒右移 20 像素 'if(lt(t,1),0,80)':10 第一秒在左,之后跳到 80

n 是帧序号,t 是秒。写 t 的写法跨帧率更稳: 换个帧率,n 的增长速度就变了,t 不会。

含逗号的表达式必须整体用单引号包起来,否则逗号会被当成参数分隔符。 if(lt(t,1),0,80) 这种不带引号写,滤镜会以为你多传了参数。

四、表达式里能用哪些变量

常用的就这几个:

变量 含义


W / main_w 主画面宽 w / overlay_w 小画面宽 n 帧序号 t 时间(秒)

H、h、main_h、overlay_h 同理,管高。 大写是主画面、小写是小画面,这个区别必须记牢。

写反了位置就整个错位,而且一声不吭——这是 overlay 最常见的坑。 W-w 写成 w-W 会得到负数,画面直接被推到左边看不见。

五、下一步:多宫格

画中画是「一主一小」。把主画面也缩成小格,就是多宫格。 下一节 7.10 会用 xstack 一次排出四格、九格。

xstack 的思路跟这里完全一样:每个格子的坐标都用 w、h 拼出来。 只不过它一次接收九路输入,比叠 8 次 overlay 干净得多,也不会一层层变糊。

所以这一节的表达式功夫是下一节的地基,别急着跳到 xstack。

六、动手 Demo:四角、居中、边距实测、负边距

下面这段代码把四角与居中的落点都量一遍,再把四条边距算出来, 最后试一次负的边距(小画面整体出界)。

import av
import numpy as np
from fractions import Fraction

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

def pip(pos, w=80, h=45):
    g = av.filter.Graph()
    b = g.add_buffer(width=160, height=90, format=u"yuv420p", time_base=TB)
    cl = g.add(u"color", u"c=blue:s=%dx%d:r=25" % (w, h))
    ov = g.add(u"overlay", pos)
    sk = g.add(u"buffersink")
    b.link_to(ov, 0, 0)
    cl.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 block(a):
    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]
    return (int(xs.min()), int(ys.min()))

print(u"--- 四角与居中")
for tag, pos in ((u"左上", u"8:8"), (u"右上", u"W-w-8:8"),
                 (u"左下", u"8:H-h-8"), (u"右下", u"W-w-8:H-h-8"),
                 (u"居中", u"(main_w-overlay_w)/2:(main_h-overlay_h)/2")):
    print(u"  %-4s %-40s %s" % (tag, pos, block(pip(pos))))

print(u"--- 右下角的边距")
x0, y0 = block(pip(u"W-w-8:H-h-8"))
print(u"  右 %d 列  下 %d 行(写法里写的是 8)"
      % (160 - 80 - x0, 90 - 45 - y0))

print(u"--- 负边距也能写")
print(u"  -20:8   %s" % (block(pip(u"-20:8")),))

实跑输出:

--------------------------------------------------------------------
7.9.4 画中画的灵活位置
--------------------------------------------------------------------

**① 四角定位用表达式,不写死数字**

**四种角落写法,换分辨率也不用改:**

  角落     写法            实际落点
  -------------------------------------
  左上     8:8             (8, 8)
  右上     W-w-8:8         (72, 8)
  左下     8:H-h-8         (8, 36)
  右下     W-w-8:H-h-8     (72, 36)

**左右两行是对称的:`8` 与 `72`,两边各留 `8` 像素。**
**上下两行看着不对称——`8` 与 `36`,底边只剩 `9` 像素。**

**原因是 `H-h-8` 算出来是 `37`,写进去是奇数,被抹成 `36`。**
**想上下真的一样,得把小画面的高度取成奇数,或者干脆直接写偶数坐标。**

**② 居中也是一行表达式**

`(main_w-overlay_w)/2:(main_h-overlay_h)/2`

**实测落在 `(40, 22)`。**
**手算:`(160-80)/2=40`、`(90-45)/2=22.5`,取整 22,正好对上。**
**这里又出现「往下取」——不是四舍五入,是直接抹掉小数。**

**③ 让它动起来**

**x 里能写帧号 `n` 与时间 `t`:**

  写法                      效果
  -----------------------------------------------------
  10+5*n:10                 每帧右移 5 像素
  x='10+20*t':y=10          每秒右移 20 像素
  'if(lt(t,1),0,80)':10     第一秒在左,之后跳到 80

**`n` 是帧序号,`t` 是秒。写 `t` 的写法跨帧率更稳:**
**换个帧率,`n` 的增长速度就变了,`t` 不会。**

**④ 表达式里能用哪些变量**

  变量              含义
  --------------------------------
  W / main_w        主画面宽
  w / overlay_w     小画面宽
  n                 帧序号
  t                 时间(秒)

**记住 `W` 与 `w` 的大小写区别:大写是主画面,小写是小画面。**
**写反了位置就整个错位,而且一声不吭——这是 overlay 最常见的坑。**

**⑤ 下一步:多宫格**

**画中画是「一主一小」,把主画面也缩成小格,就是九宫格。**
**7.10 会用 xstack 一次排出四格、九格,比叠 8 次 overlay 干净得多。**

第一段五行跟正文那张表完全一致:(8, 8)、(72, 8)、(8, 36)、(72, 36)、(40, 22)。

第二段把边距算出来了:右边 8 列、下边 9 行。 写法里 x 与 y 都减了 8,右边界刚好 8,下边界多出一行—— H-h-8 算成 37 被抹到 36 的结果。

第三段 -20:8 量出来的 x 是 0,因为左边 20 列已经被切掉了。 overlay 照写的坐标摆,看不见的部分就是不画。

注意这一整段的前提:x 也得是偶数。W-w-8 算出 72 刚好是偶数, 要是写的边距是 7,右边就变成 9 了。

30 秒实验

  1. 改边距:把四角里的 8 全改成 16,看边距是不是真的变成 16。
  2. 试奇数尺寸:把 80x45 改成 80x44,看 H-h-8 还会不会被抹。
  3. 做动效:把 x 写成 -80+2*n,看小画面是不是从左边滑进来。

常见误区

  1. 以为 H-h-8 会给出对称边距。算出奇数就被抹掉一行。
  2. 把 W 与 w 写反。位置整个错位,而且不报错。
  3. 含逗号的表达式不加引号。逗号会被当参数分隔符。
自测三问(点开对答案) 1. `H-h-8` 算出 37,实际落在第几行? —— 第 36 行,奇数被向下抹成偶数。 2. `(main_h-overlay_h)/2` 算出 22.5,落地是几? —— 22,只截断不进位。 3. 为什么建议四角直接写偶数坐标? —— 表达式算出奇数会被抹掉一行,边距就不对称了。

相关笔记