只想改一边,另一边写 -1 或 -2。 -1 只保证比例,-2 还保证能编码——所以几乎总该用 -2。
上一节五档都是宽高同时给。实际场合更多是「只定一个边」。 -1 与 -2 就是干这个的,而它俩的差别比想象中要紧。
一、两个写法,差别在一个字
scale=80:-1 与 scale=80:-2,只差最后那个数字。
-1 的意思是「按比例算,算出来是奇数也行」;
-2 的意思是「按比例算,但必须是偶数」。
看起来只是取整规则不同,实际影响的是「能不能编码」。
二、六组实测
拿三档宽度各试一次 -1 与 -2:
写法 实际输出 高是奇数还是偶数
80:-1 80×45 奇数 80:-2 80×46 偶数 101:-1 101×57 奇数 101:-2 101×56 偶数 200:-1 200×113 奇数 200:-2 200×112 偶数
第一对:80 宽时 90×80/160 得 45,是奇数。 -1 原样给 45,-2 往上补成 46。
第二对更能说明问题:101 宽时算出来是 56.8。 -1 四舍五入成 57(奇数),-2 往下取到 56。
注意两个的方向可能相反:45 那一档 -2 是往上补,56.8 那一档 -2 是往下取。 因为它只保证「是偶数」,具体就近取哪一边由内部算法定。
三、为什么几乎总该用 -2
因为下游编码器多半不接受奇数宽高:
编码或格式 奇数宽高会怎样
H.264 + yuv420p 直接报错,编不出来 libx265 直接报错 MJPEG 能编,但某些播放器花屏
根子在 4:2:0 的色度:它是两两合并的,奇数高度最后一行没有配对。 所以编码器宁可拒绝,也不肯编出个半截色度的文件。
这里要说清一件事:scale 自己并不在乎奇偶。
上面六行全部成功出图,包括三行奇数。
是下游的编码器在乎——所以 -1 只保证「按比例」,不保证「能编码」。
四、只给高度也一样
把 -2 放在前面,就变成「高度定了、宽度自己算」:
写法 实际输出
-2:45 80×45
160×90 把高定成 45,宽自动算出 80,而且是偶数。位置互换,规则不变。
但要注意:-2 只管它自己那一边。这里 45 是你写死的,它不会去改。
所以 -2:45 出来的高仍然是 45(奇数)——
要两个方向都保证偶数,得两个方向都用 -2,或者两个都写表达式。
所以 -2 是「算出来的那一边」的写法, 放在哪个位置,取决于你想让哪一边自动算。
五、要精确控制就自己写表达式
-1 与 -2 的处理规则是内部的,不同版本可能略有差别。 想完全可控,就自己写:
scale=trunc(iw/2)*2:trunc(ih/2)*2
trunc 是「截掉小数」,先除 2 再乘 2 就是把奇数砍成偶数。
这句的效果与 -2 类似,但两个方向都自己算,不依赖内部规则。
-2 只能用在「按比例算」的那一边;两个方向都要自己控制时,还是得写这句。
7.2.3 讲内置变量时会把 trunc 再拆一遍。
六、动手 Demo:-1 与 -2 各三档,看谁出奇数
下面这段代码把六组写法各跑一次,顺便标出高度是奇数还是偶数。 重点是看 -2 那三行,一次奇数都没出。
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()
for expr in (u"80:-1", u"80:-2", u"101:-1", u"101:-2",
u"200:-1", u"200:-2", u"-2:45"):
g = av.filter.Graph()
p = g.add_buffer(width=160, height=90, format=u"yuv420p", time_base=TB)
sc = g.add(u"scale", expr)
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=%-8s -> %d×%d %s"
% (expr, w, h, u"偶数" if h % 2 == 0 else u"奇数"))
print(u"--- 奇数高度送进编码器会怎样")
try:
c1 = av.open(u"_odd.mp4", u"w")
s1 = c1.add_stream(u"libx264", rate=25)
s1.width, s1.height, s1.pix_fmt = 80, 45, u"yuv420p"
print(u"建流成功")
for pkt in s1.encode(av.VideoFrame.from_ndarray(
base, format=u"yuv420p").reformat(width=80, height=45)):
c1.mux(pkt)
c1.close()
except Exception as e:
print(u"编码 80×45 失败:%s" % str(e)[:40])
实跑输出:
--------------------------------------------------------------------
7.2.2 使用 scale 保持宽高比缩放
--------------------------------------------------------------------
**① 只想改一个边、另一边自动算,就把自动算的那一边写 -1 或 -2。**
`scale=80:-1` 与 `scale=80:-2`
**② 两个写法的实测差别**
写法 实际输出 高是奇数还是偶数
--------------------------------------------
80:-1 80×45 奇数
80:-2 80×46 偶数
101:-1 101×57 奇数
101:-2 101×56 偶数
200:-1 200×113 奇数
200:-2 200×112 偶数
**前两行:80 宽时比例算出 45,是奇数。`-1` 原样给 45,`-2` 改成 46。**
**中间两行更能说明问题:101 宽时比例算出 56.8。**
**`-1` 四舍五入成 57(奇数),`-2` 往下取到 56。**
**③ 为什么几乎总该用 -2**
编码或格式 奇数宽高会怎样
----------------------------------------------
H.264 + yuv420p 直接报错,编不出来
libx265 直接报错
MJPEG 能编,但某些播放器花屏
**因为 4:2:0 的色度是两两合并的,奇数高度最后一行没有配对。**
**`-1` 只保证「按比例」,不保证「能编码」;`-2` 两个都保证。**
**④ 只给高度也一样**
写法 实际输出
----------------------
-2:45 80×45
**宽也按比例算出来了,而且是偶数。位置互换,规则不变。**
**⑤ 一个容易忽略的点:-1 与 -2 都不管「像素是不是整数」**
**它们只是把算出来的小数处理成奇数或偶数,四舍五入与向下取整的差别。**
**真要精确控制,得自己写表达式:`scale=trunc(iw/2)*2:trunc(ih/2)*2`。**
**这句在 7.2.3 讲内置变量时会再遇到。**
七行的结果:-1 那三行全是奇数,-2 那三行全是偶数。
最后一行 -2:45 也落在偶数上,说明调换位置不影响规则。
最后那段是反面对照:直接拿 80×45 去建 H.264 编码器。 它报的错说明一件重要的事—— filter 能吐出奇数尺寸,是编码器这一关不让过。
所以 -1 在「只做滤镜、不编码」的场合完全能用,比如抽帧做分析。
只要最后要出 MP4,就老老实实用 -2。
30 秒实验
- 换一个宽度:试
77:-2,看高度落在哪个偶数上。 - 两个都写 -2:
scale=-2:-2会怎样,是保持原样还是报错。 - 去掉编码:把最后那段删掉,看奇数尺寸是不是一路都通。
常见误区
- 以为 -1 与 -2 差不多。差的是「能不能编码」,不是取整方式。
- 以为 scale 会拒绝奇数。它不拒绝,是编码器拒绝。
- 两个方向都写 -2。两个都「自动算」,等于什么都没定。
自测三问(点开对答案)
1. `-1` 与 `-2` 的差别是什么? —— -1 算出什么就用什么,-2 会把结果调整成偶数。 2. 为什么 80 宽时 -2 给出 46 而不是 44? —— -2 只保证偶数,就近取;45 往上补一格到 46。 3. 谁不接受奇数宽高? —— 下游编码器(如 H.264、H.265),scale 本身是接受的。- ☐ (本节完,下一节 7.2.3 使用 FFmpeg 的内置变量进行缩放)