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

drawtext 做的事可以拆成两步:按字体把文字画成一张透明图,再把它贴到指定位置。 第一步靠 freetype,第二步和图片水印一模一样。

📝 本节和前面几节的关系

6.2 讲的是「贴一张现成的图」。这一节要贴的图是当场画出来的。 弄明白这两步,后面所有文字水印的参数都不再是黑箱。

一、文字其实是一张带透明通道的小图

「水印」两个字画出来是 56×31 的画布,字形只占 48×23, 非透明像素 481 个,只占整张图的 28%。

换句话说,这张图里有 72% 的像素是全透明的。 所以贴上去必须按 alpha 混合,不能整块覆盖。

整块覆盖会怎样?字的四周会挂一个黑色方框—— 因为透明区在 rgb24 里就是黑色,一覆盖就把底下的画面涂黑了。

二、贴上去:只碰了 3.3% 的画面

把这张图贴到 (94, 43),也就是右下角再往里缩 10 像素:

项 值


贴图位置 (94, 49) 被改动的像素 481 单通道最大变化 255 整幅像素 14400

改动像素 481 个,占整幅的 3.3%。 这就是水印「不碍事」的原因——它只碰了这么小一块地方。

「单通道最大变化 255」是指某些像素从全黑变成全白, 不是指整张图都被改了。这两件事很容易看混。

三、四条位置公式,值得背下来

drawtext 的 x、y 不是死坐标,是表达式。 w、h 是画面宽高,tw、th 是文字宽高。

位置 x 写什么 y 写什么


左上 10 10 右上 w-tw-10 10 左下 10 h-th-10 右下 w-tw-10 h-th-10

这四行与分辨率无关。换成 4K 素材,一个数字都不用改。 这是「写表达式」相对「写死坐标」最大的好处。

反过来说,如果你在命令里看到 x=1580:y=1020 这种数字, 基本可以断定这条命令只能在这一个分辨率上用。

四、命令行的写法

-vf "drawtext=text='水印':fontsize=24:fontcolor=white:x=10:y=10"

把 x=10:y=10 换成 x=w-tw-10:y=h-th-10,水印就跑到右下角。 不用自己去量文字多宽,tw 就是现成的。

本机没有 drawtext,所以这行命令没法在这里跑。 但它的两个动作(画字、贴图)上面都算过了,参数的实际效果不会跑偏。

五、动手 Demo:把 drawtext 拆成两步

用 Pillow 按字体把「水印」画成透明图,量出画布、包围盒与非透明像素。 再按 alpha 公式贴到右下角,数一数动了多少像素。

import numpy as np
from PIL import Image, ImageDraw, ImageFont

FONT = u"C:/Windows/Fonts/msyh.ttc"
f = ImageFont.truetype(FONT, 24)
bb = ImageDraw.Draw(Image.new(u"RGBA", (8, 8))).textbbox(
    (0, 0), u"水印", font=f)
pad = 4
im = Image.new(u"RGBA", (bb[2] - bb[0] + pad * 2,
                          bb[3] - bb[1] + pad * 2), (0, 0, 0, 0))
ImageDraw.Draw(im).text((pad - bb[0], pad - bb[1]), u"水印",
                          font=f, fill=(255, 255, 255, 255))
a = np.array(im)
n = int((a[:, :, 3] > 0).sum())
print(u"画布 %dx%d  字形 %dx%d  非透明 %d  占 %.0f%%"
      % (im.size[0], im.size[1], bb[2] - bb[0], bb[3] - bb[1],
         n, n * 100.0 / (im.size[0] * im.size[1])))

import av
c0 = av.open(u"s5.mp4")
bg = next(c0.decode(video=0)).to_ndarray(format=u"rgb24")
c0.close()
H, W = bg.shape[:2]
x, y = W - im.size[0] - 10, H - im.size[1] - 10
fg = a[:, :, :3].astype(np.float64)
al = a[:, :, 3:4].astype(np.float64) / 255.0
reg = bg[y:y + im.size[1], x:x + im.size[0]].astype(np.float64)
out = bg.copy()
out[y:y + im.size[1], x:x + im.size[0]] = np.clip(
    fg * al + reg * (1 - al) + 0.5, 0, 255)
d = np.abs(out.astype(np.int32) - bg.astype(np.int32))
print(u"贴到 (%d, %d)  改动 %d 像素  最大变化 %d  整幅 %d"
      % (x, y, int((d.max(axis=2) > 0).sum()), int(d.max()), W * H))

实跑输出:

--------------------------------------------------------------------
6.3.1 drawtext 添加固定文字水印——等价实验
--------------------------------------------------------------------

**本机没有 drawtext,但 drawtext 做的事可以拆成两步:**
**「按字体把文字画成图」和「把这张图贴到指定位置」。**
**第二步与 6.2 的图片水印是同一件事,所以只要把第一步算清楚就够了。**

**① 把「水印」两个字画成一张透明图**

  项                 值
  ----------------------------
  透明画布           56×31
  字形包围盒         48×23
  非透明像素         481
  占整张图的比例     28%

**最后一行是关键:文字图里有 72% 的像素是透明的,字形本身只占一小片。**
**所以贴的时候必须按 alpha 混合,不能整块覆盖——**
**整块覆盖的话,字的四周会挂一个黑色方框。**

**② 贴到右下角**

  项                 值
  -------------------------------
  贴图位置           (94, 49)
  被改动的像素       481
  单通道最大变化     255
  整幅像素           14400

**改动像素 481 个,占整幅的 3.3%。**
**水印之所以「不碍事」,就是因为它只碰了这么小一块地方。**

**③ 命令行怎么写**

**`-vf "drawtext=text='水印':fontsize=24:fontcolor=white:x=10:y=10"`**

**把 `x=10:y=10` 换成 `x=w-tw-10:y=h-th-10`,水印就跑到右下角去了。**

**`w`、`h` 是画面宽高,`tw`、`th` 是文字的宽高——**
**`w-tw-10` 就是「右边缘再往里缩 10 像素」,不用自己去算文字多宽。**
**这一点比「先量文字尺寸、再算坐标」省事得多,也是 drawtext 最好用的地方。**

**④ 水印放哪儿:四条常用公式**

  位置     x 写什么     y 写什么
  ----------------------------------
  左上     10           10
  右上     w-tw-10      10
  左下     10           h-th-10
  右下     w-tw-10      h-th-10

**这四行值得抄进笔记——它们与画面分辨率无关,**
**换成 4K 素材也不用改数字,这是写成表达式而不是写死坐标的最大好处。**

textbbox 返回的是 (左, 上, 右, 下),原点在左上。 它可能与 (0, 0) 差几个像素——字形有上伸部与下伸部,起点会偏。

pad 是我自己加的空边,给字形留一点余量。 真实 drawtext 也有个类似的 borderw,作用是加描边而不是留白。

fg * al + reg * (1 - al) 这一行就是 overlay 的公式。 下一章讲 overlay 时会发现,我们在这里已经把它写完了。

30 秒实验

  1. 换字号:把 24 改成 12 与 48,看非透明像素怎么变。
  2. 换位置:把 (94, 43) 换成左上角 (10, 10),比较视觉影响。
  3. 不混合:把公式改成直接赋值 out[...] = fg,看图里是不是挂了黑框。

常见误区

  1. 整块覆盖水印图。透明区变成黑色,字的四周挂一圈黑框。
  2. 以为字号就是文字高度。th 才是,而且它随字体变化。
  3. 把坐标写死。换个分辨率水印就跑到画外去了。
自测三问(点开对答案) 1. drawtext 可以拆成哪两步? —— 按字体把文字画成透明图;再把这张图按 alpha 贴到指定位置。 2. 为什么不能整块覆盖? —— 文字图大部分像素是全透明的,覆盖会把这些地方涂黑。 3. `w-tw-10` 里的 tw 是什么? —— 文字的包围宽度,由字体与文字内容决定,不用自己去量。

相关笔记