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

把小画面换成纯蓝块,位置与尺寸就能一像素不差地量出来。 实测 x 与 y 都被向下抹成偶数,写奇数的代价是整块往左上挪一格。

📝 本节和前面几节的关系

上一节讲了「怎么接两路」,这一节解决「贴上去到底落在哪」。 量位置的这套办法,后面 7.14、7.15 都要接着用。

一、为什么要把小画面换成蓝块

沙箱里主画面与小画面取自同一个文件,比像素根本分不出谁是谁。 把第二路换成 color=c=blue 一块纯蓝,蓝块落在哪儿一眼就能量。

color 是 lavfi 源滤镜,本身就是「源头」,接 overlay 的槽 1 合法—— 跟 movie 一样是源,比 movie 更省事,连文件都不用读。

量的时候不能只看「有没有蓝」,要按行、按列数蓝像素的个数: 某一行蓝像素数达到全图最多那一行的一半,这一行才算「在块里」。

用固定比例当阈值会翻车:40 宽的小画面只占 160 的 0.25, 写 > 宽*0.4 就永远判不出来,函数返回空,代码当场崩。

二、量出来的位置

color=c=blue:s=80x45 贴 70:10,量出来是这样:

项 值


输出尺寸 160×90 蓝块左上角 (70, 10) 蓝块大小 80×45

跟命令里写的完全对上:左上角就是 70:10,尺寸就是 80x45。 量像素比肉眼看图可靠,因为肉眼看不出差一行的偏移。

五档位置一起量,能看出字面坐标与表达式的区别:

写法 实际左上角


0:0 (0, 0) 70:10 (70, 10) W-w-8:8 (72, 8) 0:H-h (0, 44) main_w-overlay_w-10:main_h-overlay_h-10 (70, 34)

W、H 是主画面宽高,w、h 是小画面宽高,四个变量名只差大小写。 W-w-8 的意思是「靠右边、留 8 像素」,换分辨率不用改数字。

注意 0:H-h 那行:90-45=45,落地却是 44。这不是笔误,下一条就讲它。

三、隐藏的对齐:坐标被抹成偶数

先看纵向,写奇数会怎样:

写的 y 实际落在第几行


11 10 10 10 13 12 12 12

奇数全被抹到下面那个偶数,偶数原样通过。

横向一模一样,这一点很容易想当然:

写的 x 实际落在第几列


41 40 42 42 43 42 70 70

41 落 40、43 落 42。原因还是一个:yuv420p 的色度平面是 2×2 采样, 横向和纵向都只有一半分辨率,所以两个方向都只能落在偶数格上。

结论只有一句:横竖都写偶数。写奇数的后果是整块往左上挪一格, 不报错、不告警,只能靠量才知道——跟 crop 的坐标是同一套规矩。

顺便说一句:蓝块的下边缘会有一条半蓝的过渡行, 那是色度插值糊出来的,不属于块本身——量尺寸时要靠「过半」把它剔掉。

四、负坐标切掉一部分

overlay=-10:-10 不会报错,它老实按你写的摆,越界部分直接不画:

写法 看得见的蓝块 被切掉多少


-10:-10 (0, 0) 70×35 左 10 列、上 10 行

80 减 10 是 70,45 减 10 是 35,切得干干净净。 做「从屏幕边缘滑进来」的动效时,就是靠这个特性:坐标一路从负数走到 0。

不过滑到一半的帧上,画面是不完整的。 要让动效好看,得给小画面配一个同色的底衬,否则缺口一眼就能看见。

五、动手 Demo:五档位置、四档 y、两档 x、一次负坐标

下面这段代码把第二路换成纯蓝块,再把蓝块的位置按像素量出来。 先量五档位置,再单独验 y 与 x 的对齐差异,最后试负坐标。

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()),
            int(xs.max() - xs.min() + 1), int(ys.max() - ys.min() + 1))

print(u"--- 蓝块 80x45 贴到哪")
for pos in (u"0:0", u"70:10", u"W-w-8:8", u"0:H-h",
            u"main_w-overlay_w-10:main_h-overlay_h-10"):
    print(u"  %-40s %s" % (pos, block(pip(pos))))

print(u"--- y 会不会被抹成偶数")
for yy in (11, 10, 13, 12):
    print(u"  写 y=%-3d 落在第 %d 行" % (yy, block(pip(u"70:%d" % yy))[1]))

print(u"--- x 也一样")
for xx in (41, 42, 43, 70):
    print(u"  写 x=%-3d 落在第 %d 列" % (xx, block(pip(u"%d:10" % xx))[0]))

print(u"--- 负坐标")
print(u"  -10:-10 看得见 %s" % (block(pip(u"-10:-10")),))

实跑输出:

--------------------------------------------------------------------
7.9.2 使用 overlay 实现画中画
--------------------------------------------------------------------

**① 量位置的办法:把小画面换成纯蓝块**

**沙箱里主画面与小画面取自同一个文件,光比像素分不出谁是谁。**
**干脆把第二路换成一块纯蓝——素材里没有纯蓝,蓝块落在哪儿一目了然。**


**`color=c=blue:s=80x45` 是个 lavfi 源,可以直接接 overlay 的槽 1。**

**② 贴上去,把蓝块的位置量出来**

  项             值
  ---------------------------
  输出尺寸       160×90
  蓝块左上角     (70, 10)
  蓝块大小       80×45

**左上角 `(70, 10)`、大小 `80×45`——跟命令里写的 `overlay=70:10` 与 `80x45` 完全对上。**
**量像素比肉眼看图可靠得多,后面所有位置都这么量。**

**③ 字面位置与表达式位置**

**先写字面坐标,再写表达式,五种写法一起量:**

  写法                                        实际左上角
  ----------------------------------------------------------
  0:0                                         (0, 0)
  70:10                                       (70, 10)
  W-w-8:8                                     (72, 8)
  0:H-h                                       (0, 44)
  main_w-overlay_w-10:main_h-overlay_h-10     (70, 34)

**`W` 与 `H` 是主画面宽高,`w` 与 `h` 是小画面宽高。**
**`W-w-8` 的意思是「贴右边、留 8 像素边」,换分辨率也不用改数字。**

**注意 `0:H-h` 那行:`90-45=45`,落地却是 `44`。这不是笔误,下一条就讲它。**

**④ 一个隐藏的对齐:y 会被抹成偶数**

  写的 y     实际落在第几行
  -----------------------------
  11         10
  10         10
  13         12

**写 11 落在 10,写 13 落在 12,写 10 就落在 10。**
**yuv420p 的色度是 2×2 采样,纵向坐标被向下对齐到偶数——跟 crop 一个脾气。**

**x 的脾气一模一样:**

  写的 x     实际落在第几列
  -----------------------------
  41         40
  42         42
  43         42
  70         70

**41 落 40、43 落 42,偶数原样通过。**
**横向的色度也是减半存的,所以 x 同样要对齐到偶数。**

**结论只有一句:横竖都写偶数。写奇数的后果是整块往左上挪一格,不报错,量了才知道。**

**⑤ 负坐标也能用**

**`overlay=-10:-10` 会让小画面露出来一部分、切掉一部分,**
**做「从屏幕边缘滑进来」的动效时正好用它。**

  写法        看得见的蓝块     被切掉多少
  ---------------------------------------------------
  -10:-10     (0, 0) 70×35     左 10 列、上 10 行

**蓝块只剩 `70×35`,左边与上边各被切掉一部分。**
**overlay 不会因为坐标是负的就报错,它老实按你写的摆,越界部分直接不画。**

第一段五行跟本节那张表一一对上,0:H-h 算出来 45 落在 44。 这个 44 就是「奇数被抹到下面那个偶数」的现场证据。

第二段四行更直接:11 落 10、13 落 12,而 10 与 12 本来就在偶数上,原样通过。

第三段是本节最容易被想当然的地方:x 也照抹不误,41 落 40、43 落 42。 只测过 y 的人很容易写成「横向随便写」,那是错的。

最后一行负坐标只剩 70×35,切掉的部分不是「隐藏」而是「根本没画」。 所以负坐标做滑入动效是安全的,只是露出来的那块要自己补底。

30 秒实验

  1. 换颜色:把 c=blue 改成 c=red,改一下判据的通道号,看位置是否一样。
  2. 试奇数尺寸:把 80x45 改成 80x44,看量的结果有没有变化。
  3. 量边距:把 W-w-8 里的 8 改成 16,看右边留的到底是不是 16 列。

常见误区

  1. 以为只有 y 要抹偶数。x 同样要抹,写 41 会落在 40。
  2. 以为奇数坐标会四舍五入。它是直接往下抹,13 落 12 而不是 13。
  3. 用固定比例当蓝块判据。小画面小于比例阈值时永远判不出来。
  4. 以为负坐标会被夹到 0。它不夹,越界部分直接不画。
自测三问(点开对答案) 1. 写 `overlay=70:11` 实际落在第几行? —— 第 10 行。y 被向下抹到偶数。 2. 写 `overlay=41:10` 实际落在第几列? —— 第 40 列。x 跟 y 一样被抹到偶数。 3. 为什么横竖都要抹到偶数? —— yuv420p 的色度是 2×2 采样,横竖都只有一半分辨率。

相关笔记