🎬 第十三章 FFmpeg直播综合案例实战
📄 一句话概括

解码帧交给 OpenCV、处理完再交回来,四次倒手。 磨皮吃掉了三分之二的时间——这就是美颜必须上 GPU 的原因。

📝 本节和前面几节的关系

13.5 单独看 OpenCV,这一节把它塞进 13.3 那条链。 这是全书最后一篇,也是把前十三章拼成一件事的地方。

一、交接点在哪

两个库之间的边界只有一处:numpy 数组。 从 到 要做什么


解码帧 numpy 转 bgr24 再 to_ndarray numpy cv2 直接传,认连续 uint8 cv2 结果 新帧 from_ndarray 指定 bgr24 新帧 编码器 转回 yuv420p

四次倒手,其中两次是格式转换——这就是「用 OpenCV 处理视频」的固定开销。 有一个坑要记住:from_ndarray 要求数组是连续内存, bilateralFilter 返回的通常已经是连续的,但 addWeighted、切片、转置之后不一定。 保险写法是 np.ascontiguousarray(x) 再过一遍。

二、全链真跑 50 帧

解码、转格式、磨皮、混合、转回、编码,一路跑到底。 项 值


帧数 50 总耗时 203 毫秒 解码占 6 毫秒 转+磨皮占 134 毫秒 编码占 18 毫秒 磨皮后文件 9101 字节 不磨皮文件 7583 字节

磨皮之后文件反而大了 1518 字节。 为什么不是变小:这段素材本来就干净, 转 rgb 再转回 yuv 会引入舍入误差,双边与混合又添了一点「纹理」, 编码器要多花字节去记这些细节。 所以「磨皮能省码率」这句话只在真实人脸素材上成立,不能当定律。 真实场景里省码率的是「皮肤区域变平滑」,而不是「加了磨皮算法」这个动作本身。 「磨皮比编码还贵」这一行才是这一节的重点:134 对 18 毫秒,差七倍多。 所以实时直播里的美颜,要么降分辨率、要么用 GPU、要么换更快的近似算法。

三、三段耗时各占多少

先看账,再谈优化。 环节 耗时与占比


解封装加解码 6 毫秒(3%) 转格式加磨皮 134 毫秒(66%) 编码加封装 18 毫秒(9%) 其余建流与收尾 约 45 毫秒(22%)

「其余」那 22% 是建编码器、写文件头、收尾冲空包——料越短,这部分占比越大。 所以测长视频时,中间那一段的占比会比这里更高。 这张表最实用的地方是排错顺序: 要提速先看谁最大,别一上来就换编码器——常常是中间那一步最贵。

四、一帧要走几趟内存

这一节真正贵的不是算法,是搬运。 步 干了什么


转 bgr24 yuv→rgb,新分配一块内存 bilateralFilter 读一遍写一遍,算颜色差 addWeighted 再读两遍写一遍 from_ndarray 建帧再转回 yuv

一次磨皮,一帧在内存里至少走三趟。 1080p 一帧 6 MB,三趟就是 18 MB,一秒 25 帧就是 450 MB 的搬运量。 这就是为什么工程上会尽量让滤镜在 yuv 域里做,少绕一次 RGB。 这也解释了第九章那句话的另一面:滤镜住在库内部,能省掉进出的拷贝; 一旦把帧拿出来交给别的库,这笔拷贝就省不掉了。 所以「用 OpenCV 处理」与「用 FFmpeg 滤镜处理」的取舍,本质是「灵活」换「带宽」。

五、全书到这里收口

这一节把整本书的环节全串起来了。 环节 哪一章


编库搭环境 第一章 解码与编码 第八、四章 格式转换 第十一章 OpenCV 处理 本节 封装与传输 第七、十三章

能真跑的都跑了,跑不了的都写清了原因: 本机没有摄像头、没有编译器、没有 rtmp 协议、没有流媒体服务器。 但库这一层能做的事,这 99 篇已经一件一件跑过了。 最后留一句经验:这本书里所有「崩溃」与「不对」,九成出在三处。 时间基没换算、收尾没冲一次、改动共享内存没 copy。 这三条在不同章节都出现过,而且全都不报错。 知道这三处之后,再回头看那些 Demo 的报错, 会发现它们都是同一个主题的变体:「库替你缓存了什么」。 缓存是性能的来源,也是错位的来源。

六、动手 Demo:把 OpenCV 磨皮接进 FFmpeg 的链,推到 flv

解 50 帧、每帧转 bgr24 走一遍双边加混合、再编成 flv,同时量三段各花多少时间。

import os
import time
import numpy as np
import av
import cv2

t0 = time.perf_counter()
c = av.open("tiny.mp4")
st = c.streams.video[0]
o = av.open("smooth.flv", "w", format="flv")
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 pk.decode():
        # 1) 转成 OpenCV 认的格式
        bgr = fr.reformat(format="bgr24").to_ndarray()
        # 2) 磨皮:双边 + 半混合
        sm = cv2.bilateralFilter(bgr, 5, 40, 40)
        mix = cv2.addWeighted(bgr, 0.5, sm, 0.5, 0)
        # 3) 交回 FFmpeg 并编码
        nf = av.VideoFrame.from_ndarray(
            np.ascontiguousarray(mix), format="bgr24")
        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, round((time.perf_counter() - t0) * 1000))
print(os.path.getsize("smooth.flv"))

实跑输出:

--------------------------------------------------------------------
把 OpenCV 磨皮接进 FFmpeg 的链,推到 flv
--------------------------------------------------------------------

**① 交接点在哪**

  从           到         要做什么
  ------------------------------------------------------------
  解码帧       numpy      reformat 成 bgr24,再 to_ndarray
  numpy        cv2        直接传,cv2 认 uint8 连续数组
  cv2 结果     新帧       from_ndarray 指定 bgr24
  新帧         编码器     reformat 回 yuv420p

**四次倒手,其中两次是格式转换**——**这就是「用 OpenCV 处理视频」的固定开销。**

**② 全链真跑 50 帧**

  项             值
  ----------------------------
  帧数           50
  总耗时         203 毫秒
  解码占         6 毫秒
  转+磨皮占     134 毫秒
  编码占         18 毫秒
  磨皮后文件     9101 字节
  不磨皮文件     7583 字节

**磨皮之后文件反而大了 1518 字节。**
**为什么不是变小**:这段素材本来就干净,
**转 rgb 再转回 yuv 会引入舍入误差,双边与混合又添了一点「纹理」**,
**编码器要多花字节去记这些细节。**
**所以「磨皮能省码率」这句话只在真实人脸素材上成立,不能当定律。**
**但在这一小段上,磨皮花的时间比编码还多。**
**这就是实时直播里「美颜」必须用 GPU 或低分辨率做的原因。**

**③ 三段耗时各占多少**

  环节                   耗时与占比
  ------------------------------------------
  解封装加解码           6 毫秒(3%)
  转格式加磨皮           134 毫秒(66%)
  编码加封装             18 毫秒(9%)
  其余(建流与收尾)     45 毫秒(22%)

**要提速先看这张表里谁最大**,**别一上来就换编码器——常常是中间那一步最贵。**

**④ 一帧要走几趟内存**

  步                  干了什么
  ---------------------------------------------------------
  reformat bgr24      yuv→rgb,新分配一块内存
  bilateralFilter     读一遍写一遍,中间要算颜色差
  addWeighted         再读两遍写一遍
  from_ndarray        按 bgr24 建帧,再 reformat 回 yuv

**一次磨皮,一帧在内存里至少走三趟。**
**1080p 一帧 6 MB,三趟就是 18 MB,一秒 25 帧就是 450 MB 的搬运量。**
**这就是为什么工程上会尽量让滤镜在 yuv 域里做,少绕一次 RGB。**

**⑤ 全书到这里收口**

**这一节把整本书的四个环节全串起来了**:
**解码(第八章)、格式转换(第十一章)、OpenCV 处理(本节)。**
**再加上编码与封装(第七、八章)、传输(第十三章),就是一条完整直播链。**

**能真跑的都跑了,跑不了的都写清了原因**:
**本机没有摄像头、没有编译器、没有 rtmp 协议、没有流媒体服务器。**
**但库这一层能做的事,这 99 篇已经一件一件跑过了。**

磨皮占 134 毫秒、编码只占 18 毫秒,中间那一步才是瓶颈。 磨皮后文件反而更大:转 rgb 的舍入误差与混合引入的细节都要占字节。

30 秒实验

  1. 把 bilateralFilter 的 d 从 5 改成 9,看总耗时涨多少(提示:接近翻倍)。
  2. 把分辨率降到 80×60 再跑,看磨皮耗时是不是按面积降下来。
  3. 跳过磨皮只做 reformat 往返,量一次「纯转格式」的代价。

常见误区

  1. 以为磨皮一定省码率。 干净素材上反而更大。
  2. 以为编码最贵。 这一小段里磨皮是编码的七倍。
  3. 不改 pts 就直接编码。 时长会错,第九章那个坑。
自测三问(点开对答案) 1. FFmpeg 与 OpenCV 之间的交接点是什么? —— numpy 数组,格式名要写 bgr24。 2. 这一节里哪一步最贵? —— 转格式加磨皮,134 毫秒,占三分之二。 3. 磨皮后文件为什么可能变大? —— 转 rgb 的舍入误差与混合引入的细节都要额外占字节。

相关笔记