scale 只认两个坐标:宽在前、高在后。 边长减一半,像素数只剩四分之一——面积和长度不是一回事。
7.1 讲的是怎么把滤镜连起来,从这一节开始逐个拆常用滤镜。 第一个就是 scale,它是这一章用得最多的一个。
一、两个坐标,一个滤镜
scale 的格式简单到不能再简单:
scale=宽:高
第一个数是宽,第二个数是高,中间冒号。写完就完了,没有别的必填项。 后面那一堆可选参数(算法、等比、整除)都在这两个坐标之后接。
二、五档尺寸实测
同一段 160×90 的素材,缩放到五个不同尺寸:
写进命令 意图 实际尺寸 像素数
320:180 2 倍 320×180 57600 160:90 原尺寸 160×90 14400 80:45 一半 80×45 3600 40:22 约四分之一 40×22 880 32:18 五分之一 32×18 576
第四行值得看一眼:宽正好是四分之一,高却只能给 22。 因为 90 除以 4 是 22.5,而 yuv420p 只接受偶数高度,往下取到 22。
差这半个像素在观感上没影响,但算像素数时会差一点: 40×22 是 880 个像素,而不是理论上的 900 个。
三、像素数掉得比尺寸快
把每一档换算成「相对原尺寸的像素比例」:
尺寸 相对原尺寸的像素
320×180 4.00 倍 160×90 1.00 倍 80×45 0.25 倍 40×22 0.06 倍 32×18 0.04 倍
边长减一半,像素数只剩四分之一——这是面积和长度的关系。 边长乘 2,面积乘 4;边长除 2,面积除 4,永远是平方。
所以「缩一半」在文件大小上省下的不是一半,而是一大半。 反过来,「放大一倍」的代价是四倍的计算量,这一点常被低估。
四、缩放也是要花时间的
每缩放一次,都要把整帧的每个像素重新采样一遍。 1080p 转 720p 的耗时,在实拍素材上能占到整个转码的两三成。
所以同一条命令里不要重复缩放:
scale=1280:720,scale=640:360 等于白干一次。
要小尺寸就直接一次缩到位,别分两步走。
缩放放在链里的位置也有讲究: 先缩放再画框,框会跟着变小;先画框再缩放,框的线宽也会被缩。 想保住线宽,就把缩放放在最后。
五、两个数写反了会怎样
scale 的第一个数是宽、第二个是高。写反了不报错:
写法 输出 观感
scale=90:160 90×160 宽高互换,人被拉长
这种错误最难查:命令跑完了、文件也有、就是画面不对。 而且肉眼看小图不一定发现,得拿尺子量。
养成一个习惯:写完 scale= 先念一遍「宽在前、高在后」。
比事后一帧一帧找快得多。
六、动手 Demo:五档缩放,顺手抓出取偶那一档
下面这段代码把五档尺寸各跑一次,量真实输出与像素数。 最后一行是会取偶的那一档,看它跟理论值差多少。
import av
from fractions import Fraction
TB = Fraction(1, 25)
c0 = av.open(u"s7.mp4")
fr = next(c0.decode(video=0))
base = fr.to_ndarray()
c0.close()
print(u"源 %d×%d %d 像素"
% (fr.width, fr.height, fr.width * fr.height))
for tgt in (u"320:180", u"160:90", u"80:45", u"40:22", u"32:18"):
g = av.filter.Graph()
p = g.add_buffer(width=160, height=90, format=u"yuv420p", time_base=TB)
sc = g.add(u"scale", tgt)
sk = g.add(u"buffersink")
p.link_to(sc)
sc.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")
w, h = o.shape[1], o.shape[0]
print(u"scale=%-9s -> %d×%d "
u"%d 像素 占原 %.3f"
% (tgt, w, h, w * h, w * h / float(160 * 90)))
实跑输出:
--------------------------------------------------------------------
7.2.1 使用 scale 实现缩放
--------------------------------------------------------------------
**① scale 是这一章用得最多的滤镜,格式就两个坐标:**
`scale=宽:高`
**② 五档尺寸实测**
写进命令 意图 实际尺寸 像素数
---------------------------------------------------
320:180 2 倍 320×180 57600
160:90 原尺寸 160×90 14400
80:45 一半 80×45 3600
40:22 约四分之一 40×22 880
32:18 五分之一 32×18 576
**注意 40:22 那一行:宽正好是四分之一,高却只能给 22。**
**因为 90 除以 4 是 22.5,而 yuv420p 只接受偶数高度,往下取到 22。**
**差这半个像素在观感上没影响,但算像素数时会差一点——**
**40×22 是 880 个像素,而不是理论上的 900 个。**
**③ 像素数掉得比尺寸快**
尺寸 相对原尺寸的像素
--------------------------------
320×180 4.00 倍
160×90 1.00 倍
80×45 0.25 倍
40×22 0.06 倍
32×18 0.04 倍
**边长减一半,像素数只剩四分之一——这是面积和长度的关系,不是线性关系。**
**所以「缩一半」在文件大小上省下的不是一半,而是一大半。**
**④ 缩放也是开销**
**每缩放一次,都要把整帧的每个像素重新采样一遍。**
**1080p 转 720p 的耗时,在实拍素材上能占到整个转码的两三成——**
**所以同一条命令里不要重复缩放:`scale=1280:720,scale=640:360` 是白干一次。**
**⑤ 两个参数写反了会怎样**
**scale 的第一个数是宽、第二个是高。写反了不报错,只出来一张变形图:**
写法 输出 观感
--------------------------------------------------
scale=90:160 90×160 宽高互换,人被拉长
**这种错误最难查——命令跑完了、文件也有、就是画面不对。**
**养成习惯:写完 `scale=` 先念一遍「宽在前、高在后」。**
第四行 40:22 的占比是 0.061,不是理论的 0.0625。
差的那一点就是 22.5 被取成 22 造成的——半个像素,乘上宽度就出来了。
第五行 32:18 是 0.040,正好是五分之一边长对应的二十五分之一。
它没被取偶影响,因为 90 除以 5 得 18,本来就是偶数。
这段代码里每次循环都重建一次图。 尺寸不同的滤镜不能共用一张图,因为 buffersink 的输出格式在建图时就定了。
30 秒实验
- 试奇数:把
80:45改成80:43,看 yuv420p 下出来是多少。 - 换格式:把
format=u"yuv420p"换成rgb24,看 40:22 还砍不砍。 - 写反了:把
320:180写成180:320,量输出与源的比例。
常见误区
- 以为缩放不花时间。大分辨率下它常是整条命令里最贵的一步。
- 写反宽高。不报错,只出一张变形的图。
- 连缩两次。中间那次纯属浪费,还可能多丢一次精度。
自测三问(点开对答案)
1. 160×90 缩到 80×45,像素数剩多少? —— 四分之一。边长减半,面积按平方减,是 1/4。 2. 为什么 `40:22` 出来的高度不是 22.5? —— yuv420p 要求高度为偶数,22.5 往下取到 22。 3. 链里同时有 scale 和 drawbox,谁先谁后? —— 看你要框跟着缩、还是框的线宽不缩,放在后面线宽不变。- ☐ (本节完,下一节 7.2.2 使用 scale 保持宽高比缩放)