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 秒实验
- 把
bilateralFilter的第三个参数从 75 改成 15,看平坦区抖动与耗时怎么变。 - 用
COLOR_BGR2HSV转一遍,打印某个像素的 H 值,看肤色大致落在哪个区间。 - 把
addWeighted的系数改成 0.7 与 0.7(加起来不等于 1),看整体亮度变化。
常见误区
- 把 bgr24 写成 rgb24。 不报错,只是红蓝互换。
- 用高斯磨皮。 它把边缘一起糊了,人脸会发虚。
- 以为磨皮强度越高越好。 强度高抖动低,但也越假。
自测三问(点开对答案)
1. OpenCV 为什么用 BGR? —— 早期为了配合摄像头硬件的顺序,改会打断所有已有代码,就一直留着。 2. 高斯与双边最本质的差别? —— 双边除了距离还看颜色差,颜色差得远就不参与平均,所以保住边缘。 3. 磨皮强度在代码里是什么? —— `addWeighted` 的混合系数,两个系数和为 1。- ☐ (本节完,下一节 13.6 OpenCV4 磨皮美颜并结合 FFmpeg 直播推流)