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

OpenCV 用的是 BGR,格式名写错就会红蓝互换。 磨皮的难点不是「糊」,是「只糊皮肤、不糊边缘」。

📝 本节和前面几节的关系

13.4 讲的是代码怎么组织,这一节换一个库,讲像素怎么处理。 这一节三个实测:通道顺序、转换函数、三种磨皮的代价。

一、第一件事:OpenCV 用 BGR

同一个三元组,两套理解,颜色完全相反。 看谁 值


numpy 里的 [0,0] [10, 20, 240] 当 bgr24 交出去 [10, 20, 240] 转成 rgb24 后 [240, 20, 10]

同一个三元组 (10, 20, 240),当 BGR 理解是「蓝少红多」,当 RGB 理解正好反过来。 所以 FFmpeg 与 OpenCV 之间倒手,格式名一定要写对: from_ndarray(a, format="bgr24") 里那个 bgr24 写成 rgb24,人脸就会变蓝。 而且它不报错、不崩,只是颜色不对——这类 bug 最难查。 记法:OpenCV 从最早支持摄像头那会儿就按 BGR 存, 为了与当时的硬件一致,后来一直没改(改了会打断所有人的代码)。 所以这个「反着来」不是设计失误,是历史包袱。

二、第二件事:转换都在 cvtColor

一个函数、两百多个转换码,方向写在名字里。 要什么 怎么写


转灰度 cv2.COLOR_BGR2GRAY 转 YUV cv2.COLOR_BGR2YUV 转 HSV cv2.COLOR_BGR2HSV 转 RGB cv2.COLOR_BGR2RGB

名字里第一个永远是「从什么」,第二个是「到什么」,方向写反不会报错,只会出一张怪图。 这与第十一章那次「YUV 与 RGB 换算」是同一件事,只是换了个库来做。 转灰度那一行最常用,也最容易掉坑:cvtColor 出来的灰度是单通道二维数组, 再交回 FFmpeg 时必须指定 gray 格式,不能还写 bgr24。 转 HSV 是做美颜的常见起点:肤色在 HSV 里集中在一个色相区间, 按区间筛一遍就是「皮肤区域」,比在 RGB 里凑阈值稳得多。

三、第三件事:磨皮三种做法的代价

造一张左暗右亮、加了噪声的图,三种做法各跑五遍取平均。 做法 耗时 边缘落差 平坦区抖动


原图 0.00 199.6 12.29 高斯 5×5 0.11 184.6 3.28 双边 d=9 10.50 199.6 1.98 双边 d=15 29.29 199.6 1.36

高斯把边缘也糊了:边缘落差从 199.6 掉到 184.6, 因为它是「按距离加权平均」,不区分这是边缘还是噪点。 双边几乎不动边缘:落差还是 199.6,因为它除了看距离,还看颜色差——颜色差得远就不许平均。 代价是慢:10.50 对 0.11 毫秒,差两个数量级。 平坦区抖动那一列才是磨皮的本职:从 12.29 压到 1.98,脸上的小噪点就是被这一项干掉的。 所以看磨皮效果要看两列:抖动降了多少、边缘掉了多少。

四、磨皮的标准三件套

真实的美颜不是一步,是三步。 步 做什么 为什么


一 双边滤波 把皮肤上的小噪点抹平 二 与原图混合 保留质感,不然像塑料 三 锐化五官 把该清晰的地方抢回来

只做第一步就是「糊」不是「美」:真实算法会先找皮肤区域、再做混合、最后局部锐化。 这一节只做到第一步与第二步,第三步要人脸关键点, 而本机这套 OpenCV 里没有人脸模块(cv2.face 不存在,xphoto 也没有)。 该说的限制要说清,不能含糊过去。

五、混合这一步一行就够

「磨皮强度」这个滑块,本质上就是一个混合系数。 项 值


混合系数 0.6 混合后的边缘 199.6 混合后的抖动 5.51

混合系数 0.6 的意思是「六成用磨过的、四成留原样」, 这个数就是产品里那句「磨皮强度」滑块。 注意抖动从 1.98 回到 5.51:因为混回去的四成原图带着噪点。 所以强度越高越干净、也越假——这就是那个滑块的取舍,不是参数调错了。 addWeighted 的两个系数加起来必须是 1,否则整体亮度会变。 写成 0.6 与 0.4 是混合,写成 1.0 与 0.6 是提亮。

六、动手 Demo:OpenCV 的三件事——通道顺序、转换、磨皮

先看同一个三元组在 BGR 与 RGB 下各是什么,再造一张带硬边缘的噪声图量三种磨皮的代价。

import numpy as np
import av
import cv2

# 1) BGR 与 RGB 差在哪
img = np.zeros((60, 80, 3), dtype=np.uint8)
img[:, :] = (10, 20, 240)
f = av.VideoFrame.from_ndarray(img, format="bgr24")
print(img[0, 0].tolist())
print(f.reformat(format="rgb24").to_ndarray()[0, 0].tolist())

# 2) 造一张左暗右亮、带噪声的图
big = np.zeros((240, 320, 3), dtype=np.uint8)
big[:, :160] = (40, 40, 40)
big[:, 160:] = (240, 240, 240)
rng = np.random.default_rng(7)
noisy = np.clip(big.astype(np.int16) + rng.normal(0, 12, big.shape),
                0, 255).astype(np.uint8)

def edge(a):                 # 左右各取 10 列,比均值差
    return float(abs(a[:, 150:160].mean() - a[:, 160:170].mean()))

# 3) 三种磨皮:耗时、边缘、平坦区抖动
for name, fn in (("原图", lambda a: a),
                 ("高斯 5x5", lambda a: cv2.GaussianBlur(a, (5, 5), 0)),
                 ("双边 d=9", lambda a: cv2.bilateralFilter(a, 9, 75, 75)),
                 ("双边 d=15", lambda a: cv2.bilateralFilter(a, 15, 75, 75))):
    t = time.perf_counter()
    for _ in range(5):
        out = fn(noisy)
    ms = (time.perf_counter() - t) * 1000 / 5
    print(name, round(ms, 2), round(edge(out), 1),
          round(float(out[60, 20:80].std()), 2))

# 4) 混合:磨皮强度就是这个系数
sm = cv2.bilateralFilter(noisy, 9, 75, 75)
print(cv2.addWeighted(noisy, 0.4, sm, 0.6, 0)[60, 20:80].std())

实跑输出:

--------------------------------------------------------------------
OpenCV 的三件事——通道顺序、转换、磨皮
--------------------------------------------------------------------

**① 第一件事:OpenCV 用的是 BGR**

  看谁                 值
  --------------------------------------
  numpy 里的 [0,0]     [10, 20, 240]
  当 bgr24 交出去      [10, 20, 240]
  转成 rgb24 后        [240, 20, 10]

**同一个三元组 `(10, 20, 240)`,当 BGR 理解是「蓝多红少」,当 RGB 理解正好反过来。**
**所以 FFmpeg 与 OpenCV 之间倒手,格式名一定要写对**:**写成 `rgb24` 就会红蓝互换,人脸会变蓝。**

**② 第二件事:转换几乎都在 cvtColor 一个函数里**

  要什么     怎么写
  ---------------------------------
  转灰度     cv2.COLOR_BGR2GRAY
  转 YUV     cv2.COLOR_BGR2YUV
  转 HSV     cv2.COLOR_BGR2HSV
  转 RGB     cv2.COLOR_BGR2RGB

**名字里第一个永远是「从什么」,第二个是「到什么」**,**方向写反不会报错,只会出一张怪图。**

**③ 第三件事:磨皮——三种做法差得很远**

  做法          耗时(毫秒)     边缘落差     平坦区抖动
  ----------------------------------------------------------
  原图          0.00             199.6        12.29
  高斯 5×5      0.08             184.6        3.28
  双边 d=9      10.28            199.6        1.98
  双边 d=15     30.35            199.6        1.36

**高斯把边缘也糊了**:边缘落差从 199.6 掉到 184.6,
**因为它是「按距离加权平均」,不区分这是边缘还是噪点。**
**双边几乎不动边缘**:落差还是 199.6,**因为它除了看距离,还看颜色差——颜色差得远就不许平均。**
**代价是慢**:双边 d=9 是高斯的上百倍(10.50 对 0.11 毫秒),**d=15 又快三倍**。
**这就是磨皮在实时场合的第一矛盾:越保边越慢。**

**④ 磨皮的标准三件套**

  步     做什么             为什么
  ------------------------------------------------------
  一     双边滤波           把皮肤上的小噪点抹平
  二     与原图混合         保留一点质感,不然像塑料
  三     锐化眼睛与嘴唇     把该清晰的地方抢回来

**只做第一步就是「糊」不是「美」**:**真实的磨皮算法都会先找皮肤区域、再做混合、最后局部锐化。**
**这一节只做到第一步与第二步**,第三步要人脸关键点,
**本机这套 OpenCV 里没有人脸模块**(`cv2.face` 不存在)。

**⑤ 混合这一步一行就够**

  项               值
  --------------------------
  混合系数         0.6
  混合后的边缘     199.6
  混合后的抖动     5.51

**混合系数 0.6 的意思是「六成用磨过的、四成留原样」**,**这个数就是产品里那句「磨皮强度」滑块。**

BGR 与 RGB 是同一组数的两种读法,格式名写错就红蓝互换。 高斯把边缘也糊了(199.6 到 184.6)、双边保住边缘但慢两个数量级。

30 秒实验

  1. 把 bilateralFilter 的第三个参数从 75 改成 15,看平坦区抖动与耗时怎么变。
  2. 用 COLOR_BGR2HSV 转一遍,打印某个像素的 H 值,看肤色大致落在哪个区间。
  3. 把 addWeighted 的系数改成 0.7 与 0.7(加起来不等于 1),看整体亮度变化。

常见误区

  1. 把 bgr24 写成 rgb24。 不报错,只是红蓝互换。
  2. 用高斯磨皮。 它把边缘一起糊了,人脸会发虚。
  3. 以为磨皮强度越高越好。 强度高抖动低,但也越假。
自测三问(点开对答案) 1. OpenCV 为什么用 BGR? —— 早期为了配合摄像头硬件的顺序,改会打断所有已有代码,就一直留着。 2. 高斯与双边最本质的差别? —— 双边除了距离还看颜色差,颜色差得远就不参与平均,所以保住边缘。 3. 磨皮强度在代码里是什么? —— `addWeighted` 的混合系数,两个系数和为 1。

相关笔记