本机没有 drawtext,所以水印是直接在像素数组上画一块。 代价是两次格式转换,收益是「这活儿在哪儿都能干」。
12.5 把流拉下来解成 YUV,这一节把它画上水印再编回去。 这是「采→处理→编」这条链第一次完整闭环。
一、为什么不用 drawtext
打水印有三条路,本机只通两条。 做法 本机能不能用
滤镜 drawtext 不能,没编进来 滤镜 overlay 叠图 能,要另做一张水印图 直接改像素 能,最直接
drawtext 依赖 freetype 与 harfbuzz 两个外部库, 编译时没带就整个滤镜不存在(第九章查过本机的滤镜表)。这不是参数写错,是功能没有。 overlay 需要一张带 alpha 的水印图, 好处是能叠复杂图案(台标、二维码),坏处是多一个资源要管。 直接改像素最土,但它不依赖任何滤镜——只要能拿到帧,就能画。所以本篇走这条。
二、三步走:解码、画面、编码
中间那一步要用 RGB,所以两头各转一次。 步 做什么 容易错在哪
一 转 rgb24 不转就没法按像素改 二 数组上画 别越界,形状是 (h,w,3) 三 转回 yuv420p 编码器只吃 yuv
转来转去一共两次格式转换,这就是「直接改像素」的代价。
用 overlay 能省掉一次,但要多准备一张图。
注意形状是 (h, w, 3),不是 (3, h, w)——RGB 只有一个平面,行在前、列在后。
而 yuv420p 的 to_ndarray() 是平面竖着拼的,两套形状搞混,画出来的位置一定错。
「别越界」不是客套话:写 rgb[h-15:h, w-50:w] 最稳,
用绝对坐标在换分辨率之后就会写到画面外,而 numpy 的切片不会报错,它只会静默地少画一块。
三、真跑一遍
50 帧,右下角画一块红,编成 H.264。 项 值
帧数 50 裸包字节 9550 文件字节 10827 时长 2.00 秒 容器开销 1277 字节
时长 2.00 秒、帧数 50、帧率 25——三个数对得上,说明 pts 排对了。
这一步是第九章那个坑的复用:nf.pts = n 不能省。
省了的话,出来的文件要么 0 秒,要么时长翻倍。
裸包 9550 字节,平均一帧 191 字节,因为画面几乎是纯色,264 压这种内容非常狠。
换成真实摄像头画面,一帧会在几千字节这个量级。
容器开销 1277 字节:moov 索引、流信息、每帧的时间戳表,
这部分不随画质变,只随帧数与轨道数变。
所以短视频的「文件头占比」总是很大。
四、那个容易忽略的 copy
这一处不写 copy,程序不报错,但结果会错。 写法 结果
直接改 reformat 的数组 改到原帧的共享内存
先 .copy() 再改 安全,多一次拷贝
to_ndarray() 拿到的数组与帧共用内存,不是副本。
不 copy 就改,等于把解码缓存改了——轻则水印画到不该画的地方,重则下一帧解出来就是坏的。
这类 bug 最难查:程序不崩、不报错,只是画面偶尔花一下。
所以凡是「在帧上原地改」的地方,第一反应都该是 copy。
代价是一次整帧拷贝:160×120 才 57 KB,1080p 就是 6 MB,
一秒 25 帧就是 150 MB 的额外带宽。所以能用滤镜做的事,优先用滤镜——它能在内部避免这次拷贝。
五、这条链就是推流的雏形
把这一节的三段与直播里的一比,只差两头。 这一节做的 直播里换成
读文件 读摄像头或读流 画一块红 叠加台标或美颜 编成 h264 存文件 编成 h264 推到服务器
三段骨架一个字都不改,只换两头。 中间这段「解码→处理→编码」完全复用。 第十三章就是把这两头换掉:头换成 RTMP 推流,尾换成摄像头或 OpenCV。 换完之后,这一节学的东西一样有用。 第十二章到此为止。这一章能给的边界很清楚:设备这一层,沙箱只有 gdigrab 一条真路。 但「采到之后的处理」全都真跑过了——重采样、拉流、画图、编码。
六、动手 Demo:给每一帧打上水印,再编成 H.264
解 50 帧,每帧转 RGB 后在右下角画一块 15×50 的红,再转回 yuv420p 编码落盘。
import os
import av
c = av.open("_media32/tiny.mp4")
st = c.streams.video[0]
o = av.open("_media32/watermark.mp4", "w")
vs = o.add_stream("libx264", rate=25)
vs.width, vs.height, vs.pix_fmt = 160, 120, "yuv420p"
n = 0
for pk in c.demux(st):
if pk.size == 0:
break
for fr in st.codec_context.decode(pk):
# 1) 转到能按像素改的格式,注意 copy
rgb = fr.reformat(format="rgb24").to_ndarray().copy()
# 2) 画一块红(行在前、列在后)
rgb[100:115, 100:150] = (255, 0, 0)
# 3) 转回编码器要的格式,pts 一定要给
nf = av.VideoFrame.from_ndarray(rgb, format="rgb24")
nf = nf.reformat(format="yuv420p")
nf.pts = n
for op in vs.encode(nf):
o.mux(op)
n += 1
for op in vs.encode(None):
o.mux(op)
c.close()
o.close()
print(n, os.path.getsize("_media32/watermark.mp4"))
实跑输出:
--------------------------------------------------------------------
给每一帧打上水印,再编成 H.264
--------------------------------------------------------------------
**① 为什么不用 drawtext**
做法 本机能不能用
----------------------------------------------------
滤镜 drawtext **不能,这个滤镜没编进来**
滤镜 overlay 叠图 能,但要另做一张水印图
直接改像素 能,最直接
**本机没有 drawtext**(第九章查过)。**所以这里走第三条:把帧转成 RGB,直接在数组上画一块。**
**② 三步走:解码 → 画面 → 编码**
步 做什么 容易错在哪
------------------------------------------------------
一 转 rgb24 不转就没法按像素改
二 数组上画 别越界,注意形状是 (h,w,3)
三 转回 yuv420p 编码器只吃 yuv
**转来转去一共两次格式转换**,这就是「直接改像素」的代价。
**用 overlay 滤镜能省掉一次,但要多准备一张图。**
**③ 真跑一遍:50 帧,右下角一块红**
项 值
--------------------------
帧数 50
裸包字节 9550
文件字节 10827
时长 2.00 秒
容器开销 1277 字节
**时长 2.00 秒,帧数 50——说明 pts 对齐了。**
**这一步是第九章那个坑的复用:`nf.pts = n` 不能省。**
**④ 一个容易忽略的细节:copy**
写法 结果
------------------------------------------------------------------
`fr.reformat(...)` 直接改 改的是原帧的共享内存,会污染后续
先 `.copy()` 再改 安全,但多一次拷贝
**这里必须先 copy**,因为 `to_ndarray()` 拿到的数组与帧共用内存。
**不 copy 就改,等于把解码缓存改了**——下一帧可能就不对。
**⑤ 这条链就是推流的雏形**
这一节做的 直播里换成
---------------------------------------------
读文件 读摄像头或读流
画一块红 叠加台标或美颜
编成 h264 存文件 编成 h264 推到服务器
**三段骨架一个字都不改,只换两头。** 第十三章就是把这两头换掉,
**中间这段「解码→处理→编码」完全复用。**
50 帧、2.00 秒对得上,说明 nf.pts = n 起了作用。
to_ndarray() 不是副本,改之前必须 .copy(),否则污染解码缓存。
30 秒实验
- 把
rgb[100:115, 100:150]改成rgb[-15:, -50:],换台机器换分辨率也不会越界。 - 去掉
.copy(),把 50 帧画不同颜色的块,看后面几帧是否被污染。 - 把水印改成半透明(
rgb[...] = rgb[...] // 2 + 127),看压缩后还能不能看清。
常见误区
- 直接改
to_ndarray()的数组。 那是共享内存,要 copy。 - 忘了设
nf.pts。 编码出来要么 0 秒,要么时长翻倍。 - 把 rgba 的形状当
(3,h,w)。 rgb24 是(h,w,3),yuv 才是平面拼。
自测三问(点开对答案)
1. 本机为什么不能用 drawtext? —— 它依赖 freetype 等外部库,编译时没带,滤镜整个不存在。 2. 直接改像素打水印的代价是什么? —— 要多两次格式转换,还要多一次整帧拷贝。 3. `nf.pts = n` 省了会怎样? —— 时间戳全一样,出来的文件要么 0 秒要么时长翻倍。- ☐ (本节完,第十二章已完结,下一章 13.1 直播系统架构及流程简介)