pad 的四个数是「总宽、总高、左边距、上边距」,x 与 y 说的是画面放哪,不是补多少。 它只能放大画布,不能缩小。
上一节用 decrease 缩出了 640×360,剩下 120 像素的空缺正要靠 pad 填。 这一节把 pad 的坐标语义讲透。
一、四个参数,两种理解
pad 的写法:
pad=200:130:(ow-iw)/2:(oh-ih)/2:black
前两个是画布的总尺寸,后两个是「画面在画布里的左上角坐标」。
ow-iw 是输出宽减去输入宽,也就是要补的总宽度,除以 2 就左右各一半。
最容易搞混的就是这一点:x 与 y 不是「补多少」,而是「画面放在哪」。 写 0 的意思是「贴左上」,不是「不补」。
二、四种写法,四种效果
看左上角那一个像素是什么颜色,就知道画面放哪了:
写法 效果 尺寸 左上角像素
200:130:(ow-iw)/2:(oh-ih)/2:black 居中补黑 200×130 [0, 0, 0] 200:130:0:0:black 画面贴左上 200×130 [249, 40, 38] 200:130:160-iw:130-ih:white 贴右下补白 200×130 [255, 255, 255] 180:90:10:0:gray 只补左边一条 180×90 [128, 128, 128]
第一行左上角是纯黑,说明那里是补出来的边。 第二行左上角是红色竖条——那是原图内容,因为画面就贴在左上角。
第三行贴右下,左边与上边全被补白,所以左上角是 255。 第四行左边补了 10 像素灰,x=1 就落在灰里。
四行读完,x 与 y 的含义就清楚了:它们是画面的定位,不是补边的数量。
三、只能放大,不能缩小
pad 的目标尺寸小于输入时直接报错:
写法 结果
120:80:0:0:black 报错 returned 22 160:90:0:0:black 160×90 200:130:0:0:black 200×130
第二行尺寸没变,照样成功——pad 到一样大等于什么都不做。 第三行才是真的补边。
想缩小画面要用 scale,不是 pad。 这两个滤镜一个改「画布多大」,一个改「画面多大」,别混。
四、标准写法:先等比缩放,再补边
把 7.2.5 的 decrease 与 pad 串起来,就是「任意素材塞进固定尺寸」的通用解:
scale=200:130:force_original_aspect_ratio=decrease,pad=200:130:(ow-iw)/2:(oh-ih)/2
步骤 输出
scale 之后 200×112 pad 之后 200×130
160×90 塞进 200×130 的框:按比例先算成 200×112,然后上下补边。 按理说上下各补 9 像素,实测却是上面 8 行、下面 10 行。
因为 pad 的 y 坐标在 4:2:0 下要对齐到偶数,9 被就近取成了 8。 少补那一行跑到下面去了,两边不对称,但总数一行不差。
这一对组合几乎是所有「固定尺寸输出」命令的骨架。 平台要求 1080×1920 的竖屏视频?把数字换掉,结构一点不用动。
五、颜色不只是黑
black 只是最常用,写成 white、gray、red 都行,
也可以写十六进制 0x1E1E1E。
做视频墙时常用深灰而不是纯黑。 因为纯黑和大面积暗场画面接在一起,边界会看不出来—— 观众以为画面变大了,其实是黑边融进去了。
还有一个更省事的写法:pad=...:color=black@0。
那个 @0 是透明度,补出来的边是透明的。
只在输出带 alpha 的格式(比如带透明通道的 mov、webm)时才有意义。
六、动手 Demo:四种 pad,量左上角那一个像素
下面这段代码把四种写法各跑一次,每次都打印左上角像素。 这一列颜色就是「画面放哪」的直接证据。
import av
from fractions import Fraction
TB = Fraction(1, 25)
c0 = av.open(u"s7.mp4")
base = next(c0.decode(video=0)).to_ndarray()
c0.close()
def pad_run(expr, pre=None):
g = av.filter.Graph()
p = g.add_buffer(width=160, height=90, format=u"yuv420p", time_base=TB)
prev = p
if pre:
sc = g.add(u"scale", pre)
prev.link_to(sc)
prev = sc
pd = g.add(u"pad", expr)
sk = g.add(u"buffersink")
prev.link_to(pd)
pd.link_to(sk)
g.configure()
vf = av.VideoFrame.from_ndarray(base, format=u"yuv420p")
vf.pts = 0
vf.time_base = TB
p.push(vf)
o = sk.pull().to_ndarray(format=u"rgb24")
return o
for expr in (u"200:130:(ow-iw)/2:(oh-ih)/2:black",
u"200:130:0:0:black",
u"200:130:160-iw:130-ih:white",
u"180:90:10:0:gray"):
o = pad_run(expr)
print(u"%-36s %d×%d 左上 %s"
% (expr, o.shape[1], o.shape[0], o[1][1].tolist()))
print(u"--- 缩小会报错")
for expr in (u"120:80:0:0:black", u"160:90:0:0:black"):
try:
o = pad_run(expr)
print(u"%-20s -> %d×%d" % (expr, o.shape[1], o.shape[0]))
except Exception as e:
print(u"%-20s -> %s" % (expr, str(e)[:30]))
print(u"--- 先等比缩放再补边")
o = pad_run(u"200:130:(ow-iw)/2:(oh-ih)/2:black",
pre=u"200:130:force_original_aspect_ratio=decrease")
top = 0
while top < o.shape[0] and o[top].mean() < 1:
top += 1
bot = 0
while bot < o.shape[0] and o[o.shape[0] - 1 - bot].mean() < 1:
bot += 1
print(u"最终 %d×%d,顶部黑边 %d 行,底部黑边 %d 行"
% (o.shape[1], o.shape[0], top, bot))
实跑输出:
--------------------------------------------------------------------
7.2.6 使用 pad 选项填充黑边
--------------------------------------------------------------------
**① pad 的四个参数是「总宽:总高:左边距:上边距:颜色」。**
`pad=200:130:(ow-iw)/2:(oh-ih)/2:black`
**`ow-iw` 是「输出宽减去输入宽」,也就是要补的总宽度,除以 2 就左右各一半。**
**② 四种写法,四种效果**
写法 效果 尺寸 左上角像素
--------------------------------------------------------------------------------------------
200:130:(ow-iw)/2:(oh-ih)/2:black 居中补黑 200×130 [0, 0, 0]
200:130:0:0:black 画面贴左上 200×130 [249, 40, 38]
200:130:160-iw:130-ih:white 画面贴右下,补白 200×130 [255, 255, 255]
180:90:10:0:gray 只补左边一条,补灰 180×90 [128, 128, 128]
**只看最后一列就够了:第一行左上角是 [0, 0, 0],真的是黑的。**
**第二行左上角是 [249, 40, 38]——红色竖条,那是原图内容,不是补出来的边。**
**因为 0:0 意味着画面就贴在左上角,那里根本没有补边。**
**这一点最容易搞混:x 与 y 说的不是「补多少」,而是「画面放在哪」。**
**③ 顺序不能反**
**pad 只能把画面放进一个更大的画布,不能把画布缩小。**
写法 结果
--------------------------------------------
120:80:0:0:black 报错:不能小于输入
160:90:0:0:black 160×90
200:130:0:0:black 200×130
**想缩小画面要用 scale,不是 pad。这两个滤镜一个改尺寸一个改画布,别混。**
**④ 标准写法:先等比缩放,再补边**
**把 7.2.5 的 decrease 与 pad 串起来,就是「任意素材塞进固定尺寸」的通用解:**
步骤 输出
--------------------------
scale 之后 200×113
pad 之后 200×130
**160×90 塞进 200×130 的框:按比例先算成 200×113,然后上下补边。**
**按理说上下各补 8 像素,实测却是上面 8 行、下面 10 行。**
**因为 pad 的 y 坐标在 4:2:0 下要对齐到偶数,9 被就近取成了 8。**
**少补的那一行跑到下面去了,所以两边不对称——总数倒是一行不差。**
**⑤ pad 的颜色可以不是黑**
**`black` 只是最常用,写成 `white`、`gray`、`red` 都行,**
**也可以写十六进制 `0x1E1E1E`。做视频墙时常用深灰而不是纯黑,**
**因为纯黑和大面积暗场画面接在一起,边界会看不出来。**
四行输出跟本节表格逐条对上,包括那个 [249, 40, 38] 的红色。
它说明「0:0」时有内容的地方一点没补,红竖条直接贴在左上角。
缩小那两行:120×80 报错,160×90 原样通过。 这就是「pad 只能放大画布」的实证。
最后一行是本节最值钱的数字:顶部 8 行、底部 10 行。 理论值该是 9 与 9,为什么会偏?因为 4:2:0 下 y 要对齐偶数。 这个偏移在肉眼上完全看不出,但写代码算坐标时必须知道。
30 秒实验
- 换颜色:把 black 换成
0x1E1E1E,量左上角像素是多少。 - 改偏移:把
(ow-iw)/2换成(ow-iw)/4,看画面偏到哪。 - 量第 10 行:把
o[8]改成o[10],看均值是不是跳到画面内容。
常见误区
- 以为 x=0 就是不补。x=0 是画面贴左上,边补在右边和下边。
- 用 pad 想缩小画面。pad 只能放大画布,缩小要用 scale。
- 先 pad 再 scale。黑边会被一起缩,宽度就不对了。
自测三问(点开对答案)
1. `pad=200:130:20:20` 里的 20 是什么? —— 画面左上角在画布里的坐标,即右边与下边各补 20 像素。 2. `(ow-iw)/2` 是什么意思? —— 输出宽减输入宽得到总补边量,除 2 就是左右各一半。 3. pad 能让画面变小吗? —— 不能,目标尺寸小于输入会直接报错。- ☐ (本节完,下一节 7.2.7 使用 scale 的指定算法进行缩放)