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

只想改一边,另一边写 -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 秒实验

  1. 换一个宽度:试 77:-2,看高度落在哪个偶数上。
  2. 两个都写 -2:scale=-2:-2 会怎样,是保持原样还是报错。
  3. 去掉编码:把最后那段删掉,看奇数尺寸是不是一路都通。

常见误区

  1. 以为 -1 与 -2 差不多。差的是「能不能编码」,不是取整方式。
  2. 以为 scale 会拒绝奇数。它不拒绝,是编码器拒绝。
  3. 两个方向都写 -2。两个都「自动算」,等于什么都没定。
自测三问(点开对答案) 1. `-1` 与 `-2` 的差别是什么? —— -1 算出什么就用什么,-2 会把结果调整成偶数。 2. 为什么 80 宽时 -2 给出 46 而不是 44? —— -2 只保证偶数,就近取;45 往上补一格到 46。 3. 谁不接受奇数宽高? —— 下游编码器(如 H.264、H.265),scale 本身是接受的。

相关笔记