🎬 第六章 FFmpeg命令行实现图片水印及文字跑马灯
📄 一句话概括

本机六种 overlay 建图写法全部失败,报的都是含义含糊的 returned 22。 命令行那一侧照旧能写能用,公式也已经用两条独立实现验过。

📝 本节和前面几节的关系

上一节把公式讲清楚了,这一节讲「怎么把两路接起来」。 接法在命令行里很成熟,在 PyAV 这一层却有坑。

一、六种写法,全部失败

建图写法 结果 说明


两个 buffer + pad 号 returned 22 最常见的写法 两个 buffer + pad 名 an integer pad 名不是整数 rgb24 主 + rgba 副 returned 22 换格式也不行 用 template 建 buffer returned 22 换建法也不行 只接一路输入 returned 22 缺一路也报同一个错 换 hstack 双输入 returned 22 别的双输入滤镜一样

连「只接一路」都失败,说明这与参数写法无关—— 是这份 PyAV 与 libavfilter 之间的对接层,压根没把多输入接通。

returned 22 是 EINVAL,意思是「参数无效」。 它不告诉你是哪个参数无效,所以这类错误只能靠「换写法逐个排除」来定位。

二、换成手算,结果仍然对得上

把水印按公式贴上去,再让 Pillow 独立合成一次:

算法 平均误差 最大误差


手算 alpha 混合 0.00 0

两条独立实现的结果完全一致。 这说明「公式没算错」这件事已经验过了,剩下的只是「怎么把两路接起来」。

把「原理」与「接口」分开验,是本课一直在用的办法: 接口跑不通的时候,至少原理那一半是确定的,不会两头都糊涂。

三、命令行怎么写

ffmpeg -i in.mp4 -i logo.png -filter_complex "[0:v][1:v]overlay=10:10" out.mp4

[0:v] 是第一个输入的视频流,[1:v] 是第二个。 顺序就是 overlay 的「主画面」与「叠上去的图层」,写反了水印会盖住整个画面。

一定要加 shortest=1。 水印图 loop 起来是无限长,主视频放完它还在出帧,转码就卡在那儿不动了。

shortest=1 让输出跟着短的那一路结束,这是带图片水印的命令里最容易漏的一个参数。

四、动手 Demo:六种建图写法与一次手算

先用六种方式尝试建 overlay 的双输入图,逐个记录报错。 再用 numpy 手算一遍 alpha 混合,与 Pillow 的结果比对。

import av, numpy as np
from fractions import Fraction
from PIL import Image, ImageDraw, ImageFont

W, H = 160, 90
TB = Fraction(1, 25)

def attempt(fn):
    try:
        fn()
        return u"成功"
    except Exception as e:
        s = str(e)
        return u"returned 22" if u"returned 22" in s else s[:20]

def v1():
    g = av.filter.Graph()
    a = g.add_buffer(width=W, height=H, format=u"yuv420p", time_base=TB)
    b = g.add_buffer(width=40, height=20, format=u"yuva420p", time_base=TB)
    ov = g.add(u"overlay", u"x=10:y=10")
    s = g.add(u"buffersink")
    a.link_to(ov, 0)
    b.link_to(ov, 1)
    ov.link_to(s)
    g.configure()

def v2():
    g = av.filter.Graph()
    a = g.add_buffer(template=av.VideoFrame(W, H, format=u"yuv420p"),
                     time_base=TB)
    b = g.add_buffer(template=av.VideoFrame(40, 20, format=u"yuva420p"),
                     time_base=TB)
    ov = g.add(u"overlay", u"10:10")
    s = g.add(u"buffersink")
    a.link_to(ov, 0)
    b.link_to(ov, 1)
    ov.link_to(s)
    g.configure()

print(u"pad 号      %s" % attempt(v1))
print(u"template    %s" % attempt(v2))

f = ImageFont.truetype(u"C:/Windows/Fonts/msyh.ttc", 24)
im = Image.new(u"RGBA", (56, 31), (0, 0, 0, 0))
ImageDraw.Draw(im).text((4, 4), u"水印", font=f, fill=(255, 255, 255, 255))
bg = np.full((H, W, 3), 100, dtype=np.uint8)
x, y = W - 56 - 8, H - 31 - 8
a8 = np.array(im)[:, :, 3:4].astype(np.float64) / 255.0
fg = np.array(im)[:, :, :3].astype(np.float64)
reg = bg[y:y + 31, x:x + 56].astype(np.float64)
manual = np.clip(fg * a8 + reg * (1 - a8) + 0.5, 0, 255)
comp = np.array(Image.alpha_composite(
    Image.fromarray(bg).convert(u"RGBA").crop((x, y, x + 56, y + 31)), im)
    .convert(u"RGB"))
d = np.abs(manual.astype(np.int32) - comp.astype(np.int32))
print(u"手算 vs PIL  平均 %.2f  最大 %d" % (d.mean(), d.max()))

实跑输出:

--------------------------------------------------------------------
6.5.2 -filter_complex overlay 添加水印——建图为什么失败
--------------------------------------------------------------------

**① 六种建图写法,全部试一遍**

  建图写法                  结果                         说明
  -------------------------------------------------------------------------------------
  两个 buffer + pad 号      失败(returned 22)          双输入在这台机器上建不起来
  两个 buffer + pad 名      失败(an integer is re)     pad 名不是整数,接口不认
  rgb24 主 + rgba 副        失败(returned 22)          双输入在这台机器上建不起来
  用 template 建 buffer     失败(returned 22)          双输入在这台机器上建不起来
  只接一路输入              失败(returned 22)          双输入在这台机器上建不起来
  换 hstack 双输入          失败(returned 22)          双输入在这台机器上建不起来

**六种写法全部失败:五种是 `returned 22`,一种栽在 pad 名上。**
**连「只接一路输入」都不行——这说明报错与参数写法无关,**
**而是这份 PyAV 与 libavfilter 之间的对接层,压根没把多输入这条路接通。**
**所以这一节只能改成手算,但命令行那一侧照旧能写、能用。**

**② 换成手算,结果与公式一致**

  算法                平均误差     最大误差
  ---------------------------------------------
  手算 alpha 混合     0.00         0

**两条独立实现的结果最大只差 0,说明手算这一版就是 overlay 干的事。**

**③ 命令行怎么写**

**`ffmpeg -i in.mp4 -i logo.png -filter_complex "[0:v][1:v]overlay=10:10" out.mp4`**

**`[0:v]` 是第一个输入的视频流,`[1:v]` 是第二个——**
**顺序就是 overlay 的「主画面」与「叠上去的图层」,写反了水印会盖住整个画面。**
**`overlay` 后面还能跟 `shortest=1`,让它跟着短的那一路结束,**
**否则水印图 loop 起来无限长,主视频放完它还在出帧,转码就卡住了。**

attempt 只用两种结果描述:returned 22 或别的。 把报错分类,比打印一长串更有用——一眼就能看出「是不是同一个原因」。

av.filter.Graph 里 link_to(ov, 0) 的第二个参数是目标 pad 号。 不传的话默认连第一个 pad,两路都往 0 上连,配流必然失败。

Pillow 的 alpha_composite 要求两图尺寸一致,所以先 crop 出对应区域。 直接整图合成再裁,结果一样,只是多算了些像素。

30 秒实验

  1. 换滤镜:把 overlay 换成 hstack,看双输入是不是一样报错。
  2. 命令行验证:找一台有 ffmpeg 的机器,跑一遍 [0:v][1:v]overlay。
  3. 加 shortest:把 shortest=1 去掉,观察转码是不是卡住不出结果。

常见误区

  1. 以为 returned 22 是参数写错。它只是 EINVAL,不指到底是哪个参数。
  2. 两路都 link_to(pad 0)。第二路把第一路挤掉,配流失败。
  3. 忘了 shortest=1。水印无限循环,主视频放完后转码卡住。
自测三问(点开对答案) 1. returned 22 是什么错误? —— EINVAL,参数无效。它不指明具体哪一项,只能靠换写法排除。 2. `[0:v][1:v]` 的顺序意味着什么? —— 前者是主画面,后者是叠上去的图层,写反了水印会盖住整个画面。 3. 为什么一定要 shortest=1? —— 水印 loop 后无限长,不加这个,输出不会跟着主视频结束。

相关笔记