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

两秒片子,头尾各淡 0.5 秒,实测两头各 13 帧过渡。 前后要接别的片段时,得先垫一段黑场再淡入。

📝 本节和前面几节的关系

7.11.1 讲参数、7.11.2 讲写法,这一节把它们拼成一条能直接用的成品命令。 黑场那一步用到 7.4.6 的 color 滤镜。

一、头尾各淡 0.5 秒

完整命令(视频与音频都淡):

ffmpeg -i s7.mp4 -vf "fade=t=in:st=0:d=0.5,fade=t=out:st=1.5:d=0.5" \ -af "afade=t=in:st=0:d=0.5,afade=t=out:st=1.5:d=0.5" head.mp4

两秒片子上,0~0.5 秒淡入、1.5~2.0 秒淡出,中间 1 秒是干净的。

二、实测首尾曲线

把首尾各取六帧,看均值爬升与下坠:

位置 画面均值 位置 画面均值


开头第 0 帧 0 结尾第 38 帧 143 开头第 3 帧 33 结尾第 41 帧 110 开头第 6 帧 65 结尾第 44 帧 77 开头第 9 帧 99 结尾第 47 帧 43 开头第 12 帧 132 结尾第 49 帧 22 开头第 13 帧 144 — —

第 0 帧全黑、第 13 帧满幅,正好 13 个帧距;结尾从第 38 帧开始掉, 第 49 帧只剩 22,也是 12 个帧距。两边对称,都是 0.5 秒。

片尾没到 0 是因为片子结束得早:淡出从 1.5 秒起算,走到 2.0 秒才归零, 而第 50 帧已经到头了,差半帧。真做成品时把 d 收小一点,或者让素材多给几帧。

三、先垫黑场再淡入

如果这段前面还要接别的片子,直接淡入会在接缝处突跳。 标准做法是「黑场 0.3 秒 + 淡入 0.5 秒」:

ffmpeg -f lavfi -i color=c=black:s=160x90:r=25:d=0.3 -i s7.mp4 \ -filter_complex "[0:v][1:v]concat=n=2:v=1:a=0[v]" -map "[v]" out.mp4

color 就是 7.4.6 讲过的纯色源,d=0.3 造 0.3 秒黑场; concat=n=2 表示两段拼接,v=1:a=0 表示只接视频、不接音频。

为什么垫黑而不是直接淡:黑场给观众一个「这里该停一下」的信号, 两段片子之间不垫东西,观感上像被硬切了一刀。

四、三个容易踩的点

做片子时最常见的三个错:

现象 原因


淡入没生效 st 写大了,前几帧已经过了淡化期 淡出没到全黑 d 超出了片长,最后停在半黑 两头都在闪 两个 fade 的时间段重叠了

第二条最容易犯:两秒的片子写 st=1.5:d=1.0,淡化要跑到 2.5 秒, 可片子 2.0 秒就结束了,最后只淡到一半。写之前先算一遍 st+d 有没有超片长。

第三条的判断办法:把输出逐帧取均值,如果曲线是「先降后升」的 V 形, 那就是两个 fade 叠了——单次淡入只会单调上升。

五、动手 Demo:头尾曲线、偏移帧数统计

下面这段代码把「头淡入、尾淡出」跑一遍,首尾各取六帧看曲线; 最后统计有多少帧偏离了原帧,验证过渡长度是不是 0.5 秒。

import av
from fractions import Fraction

TB = Fraction(1, 25)
c0 = av.open(u"s7.mp4")
BASE = [fr.to_ndarray(format=u"yuv420p") for fr in c0.decode(video=0)]
c0.close()

def run(filters, n=50):
    g = av.filter.Graph()
    b = g.add_buffer(width=160, height=90, format=u"yuv420p", time_base=TB)
    p = b
    for name, args in filters:
        f = g.add(name, args) if args else g.add(name)
        p.link_to(f)
        p = f
    sk = g.add(u"buffersink")
    p.link_to(sk)
    g.configure()
    out = []
    for i in range(n):
        vf = av.VideoFrame.from_ndarray(BASE[i % len(BASE)], format=u"yuv420p")
        vf.pts = i
        vf.time_base = TB
        b.push(vf)
        while True:
            try:
                out.append(sk.pull().to_ndarray(format=u"rgb24"))
            except Exception:
                break
    b.push(None)
    while True:
        try:
            out.append(sk.pull().to_ndarray(format=u"rgb24"))
        except Exception:
            break
    return out

o = run([(u"fade", u"t=in:st=0:d=0.5"),
         (u"fade", u"t=out:st=1.5:d=0.5")], 50)

print(u"--- 片头:前 14 帧")
for i in (0, 3, 6, 9, 12, 13):
    print(u"  第 %2d 帧 %.0f" % (i, o[i].mean()))

print(u"--- 片尾:后 12 帧")
for i in (38, 41, 44, 47, 49):
    print(u"  第 %2d 帧 %.0f" % (i, o[i].mean()))

print(u"--- 头尾各有几帧在变")
ref = run([], 50)
def dev(i):
    return o[i].mean() / max(ref[i].mean(), 1e-6) < 0.98
n_in = sum(1 for i in range(25) if dev(i))
n_out = sum(1 for i in range(25, 50) if dev(i))
print(u"  前 25 帧偏离 %d 帧,后 25 帧偏离 %d 帧,合计 %d 帧"
      % (n_in, n_out, n_in + n_out))

实跑输出:

--------------------------------------------------------------------
7.11.3 fade 滤镜的案例
--------------------------------------------------------------------

**① 一条完整的片头片尾**

**两秒片子,头尾各淡 0.5 秒:**

`ffmpeg -i s7.mp4 -vf "fade=t=in:st=0:d=0.5,fade=t=out:st=1.5:d=0.5" \`
`-af "afade=t=in:st=0:d=0.5,afade=t=out:st=1.5:d=0.5" head.mp4`

**实测首尾各取六帧,看均值爬升与下坠:**

  位置             第几帧     画面均值
  ----------------------------------------
  开头第 0 帧      0          0
  开头第 3 帧      3          33
  开头第 6 帧      6          65
  开头第 9 帧      9          99
  开头第 12 帧     12         132
  开头第 13 帧     13         144
  结尾第 38 帧     38         143
  结尾第 41 帧     41         110
  结尾第 44 帧     44         77
  结尾第 47 帧     47         43
  结尾第 49 帧     49         22

**第 0 帧是 0、第 13 帧满幅;结尾从第 38 帧开始往下掉,第 49 帧只剩 22。**
**两头都留了大约 13 帧的过渡,正好是 0.5 秒。**

**② 为什么要留「黑场」而不是直接淡**

**纯淡入淡出是「从黑到画面」,本质上是拿一段黑盖上去。**
**如果前后还要接别的片段,一般写成「黑场 0.3 秒 + 淡入 0.5 秒」,**
**这样接缝处有黑垫着,转场不会突跳。**

**③ 用 color 生成纯黑场,再 concat 拼起来**

`ffmpeg -f lavfi -i color=c=black:s=160x90:r=25:d=0.3 -i s7.mp4 \`
`-filter_complex "[0:v][1:v]concat=n=2:v=1:a=0[v]" -map "[v]" out.mp4`

**`color` 那条就是 7.4.6 讲过的纯色源,`d=0.3` 造 0.3 秒黑场。**
**`concat=n=2` 表示两段拼接,`v=1:a=0` 表示只接视频不接音频。**

**④ 三个容易踩的点**

  现象             原因
  ------------------------------------------------------
  淡入没生效       `st` 写大了,前几帧已经过了淡化期
  淡出没到全黑     `d` 超出了片长,最后停在半黑
  两头都在闪       两个 fade 的时间段重叠了

**第二条最常见:两秒的片子写 `st=1.5:d=1.0`,1.0 秒的淡化要跑到 2.5 秒,**
**可片子 2.0 秒就结束了,最后只淡到一半。写之前先算一遍 `st+d` 有没有超片长。**

片头那六行是 0 → 33 → 65 → 99 → 132 → 144,标准的线性爬升。

片尾那五行是 143 → 110 → 77 → 43 → 22,同样是线性下坠, 只是起点在第 38 帧,还没走完片子就结束了。

最后一行把「偏离原帧超过 2%」的帧分段统计出来:前面 13 帧、后面 11 帧。 合计 24 帧,比「13+13」少了 2 帧,因为片尾只走到第 49 帧,淡出没走完。

30 秒实验

  1. 改时长:把两处 d=0.5 改成 d=0.3,看过渡是不是缩到 8 帧。
  2. 改成淡白:给淡出加 color=white,看片尾是不是变白。
  3. 加黑场:用 color 造 0.3 秒黑场再 concat,看总长变成多少。

常见误区

  1. st+d 超过片长。淡出到不了全黑,看着像没做完。
  2. 直接淡入不加黑场。接别的片段时接缝会突跳。
  3. 两个 fade 的时间段重叠。曲线会变成 V 形,不是单调的。
自测三问(点开对答案) 1. 0.5 秒淡入在 25 帧率上占几帧? —— 13 个帧距,第 0 帧到第 13 帧。 2. 前后要接别的片段,怎么处理接缝? —— 先垫 0.3 秒黑场,再淡入,用 `color` 加 `concat`。 3. 怎么判断两个 fade 叠了? —— 逐帧均值曲线变成 V 形——先降后升,而单次淡入只会单调上升。

相关笔记