把小画面换成纯蓝块,位置与尺寸就能一像素不差地量出来。 实测 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 秒实验
- 换颜色:把
c=blue改成c=red,改一下判据的通道号,看位置是否一样。 - 试奇数尺寸:把
80x45改成80x44,看量的结果有没有变化。 - 量边距:把
W-w-8里的 8 改成 16,看右边留的到底是不是 16 列。
常见误区
- 以为只有 y 要抹偶数。x 同样要抹,写 41 会落在 40。
- 以为奇数坐标会四舍五入。它是直接往下抹,13 落 12 而不是 13。
- 用固定比例当蓝块判据。小画面小于比例阈值时永远判不出来。
- 以为负坐标会被夹到 0。它不夹,越界部分直接不画。
自测三问(点开对答案)
1. 写 `overlay=70:11` 实际落在第几行? —— 第 10 行。y 被向下抹到偶数。 2. 写 `overlay=41:10` 实际落在第几列? —— 第 40 列。x 跟 y 一样被抹到偶数。 3. 为什么横竖都要抹到偶数? —— yuv420p 的色度是 2×2 采样,横竖都只有一半分辨率。- ☐ (本节完,下一节 7.9.3 使用 overlay 与 scale 的结合实现画中画)