19、实时美颜与滤镜:GPUShader皮肤平滑、人脸关键点瘦脸/大眼、LUT滤镜、性能与画质平衡

实时美颜,说白了就是让摄像头里的你「看起来更好看」。这可不是简单的加个滤镜就完事。我做了这么多年音视频,见过太多产品——美颜一开,脸是白了,但背景糊成一片,或者人脸变形得像外星人。

这一章,咱们就聊聊WebRTC里怎么实现真正的实时美颜。核心就四个字:GPU加速。CPU做美颜?别想了,实时视频流每秒30帧,CPU根本扛不住。

19.1 皮肤平滑:高斯模糊与双边滤波

皮肤平滑,也叫「磨皮」。原理很简单——把皮肤上的小瑕疵模糊掉。但问题来了:怎么只模糊皮肤,不模糊眼睛、眉毛这些细节?

最简单的做法是高斯模糊。它把每个像素和周围像素做加权平均,权重由高斯分布决定。代码长这样:

// 片元着色器 - 高斯模糊
uniform sampler2D uTexture;
uniform vec2 uTexelSize;
uniform float uBlurRadius;

void main() {
    vec2 uv = gl_FragCoord.xy * uTexelSize;
    vec4 color = vec4(0.0);
    float totalWeight = 0.0;
    
    for (int x = -4; x <= 4; x++) {
        for (int y = -4; y <= 4; y++) {
            vec2 offset = vec2(float(x), float(y)) * uBlurRadius * uTexelSize;
            float weight = exp(-float(x*x + y*y) / (2.0 * uBlurRadius * uBlurRadius));
            color += texture2D(uTexture, uv + offset) * weight;
            totalWeight += weight;
        }
    }
    
    gl_FragColor = color / totalWeight;
}

但高斯模糊有个致命问题——它会把整张图都糊掉。你想想看,眼睛、眉毛这些需要保留细节的地方也被模糊了,效果就很假。

所以我更推荐双边滤波。它比高斯模糊多了一个「像素值差异」的判断。如果两个像素颜色差异太大,就不参与模糊。这样皮肤区域被平滑,边缘细节却保留了下来。

核心区别:

  • 高斯模糊:只看距离,不看颜色差异
  • 双边滤波:距离 + 颜色差异,双重判断

我在项目中遇到过一个问题:双边滤波在GPU上实现时,循环次数太多,性能直接崩了。后来怎么解决的?我把模糊半径缩小到3像素以内,然后用两次pass(先水平再垂直)来优化。嗯,性能一下就上来了。

19.2 人脸关键点:瘦脸与大眼

瘦脸和大眼,靠的是人脸关键点。WebRTC里可以用TensorFlow.js或者MediaPipe的人脸检测模型,输出68个或106个关键点。

有了关键点,剩下的就是「图像变形」。说白了,就是把某些区域的像素「推」到新的位置。

瘦脸原理

瘦脸的核心是局部缩放变形。以脸颊两侧的关键点为圆心,把周围的像素向圆心方向压缩。这样脸就「瘦」了。

伪代码大概是这样:

// 瘦脸 - 片元着色器
uniform vec2 uLeftCheek;   // 左脸颊关键点
uniform vec2 uRightCheek;  // 右脸颊关键点
uniform float uStrength;   // 瘦脸强度

void main() {
    vec2 uv = gl_FragCoord.xy / uResolution;
    vec2 delta = uv - uLeftCheek;
    float dist = length(delta);
    float radius = 0.15;  // 影响半径
    
    if (dist < radius) {
        float scale = 1.0 - uStrength * (1.0 - dist / radius);
        vec2 newUV = uLeftCheek + delta * scale;
        gl_FragColor = texture2D(uTexture, newUV);
    } else {
        gl_FragColor = texture2D(uTexture, uv);
    }
}

大眼的原理类似,只不过是把眼睛区域的像素向外拉伸。注意,拉伸的时候要保证眼睛形状自然,别变成「外星人眼」。

我的经验:瘦脸和大眼的强度不要超过0.3。超过这个值,人脸比例会失调,看起来很不自然。我曾经见过一个产品,瘦脸开到0.8,结果人脸变成了「蛇精脸」……用户反馈很糟糕。

19.3 LUT滤镜:一秒切换风格

LUT(Look-Up Table)滤镜,是业界最常用的调色方案。它本质上是一个颜色映射表——输入一个颜色值,输出另一个颜色值。

为什么用LUT?因为快。你想想看,如果每帧都做复杂的颜色计算,GPU扛不住。但LUT只需要一次纹理采样,性能开销几乎为零。

LUT文件通常是一个512x512的图片,里面分成8x8的格子,每个格子代表不同的亮度层级。GPU着色器里这样用:

// LUT滤镜 - 片元着色器
uniform sampler2D uTexture;
uniform sampler2D uLUT;

void main() {
    vec4 color = texture2D(uTexture, gl_FragCoord.xy / uResolution);
    
    // 将RGB映射到LUT坐标
    float blue = color.b * 63.0;
    vec2 quad1 = vec2(floor(blue) / 8.0, 0.0);
    vec2 quad2 = vec2(ceil(blue) / 8.0, 0.0);
    
    float offset = fract(blue);
    vec2 lutPos1 = (color.rg * 63.0 / 512.0) + quad1;
    vec2 lutPos2 = (color.rg * 63.0 / 512.0) + quad2;
    
    vec4 lutColor = mix(texture2D(uLUT, lutPos1), 
                        texture2D(uLUT, lutPos2), offset);
    
    gl_FragColor = mix(color, lutColor, uLUTStrength);
}

LUT的好处是:你可以提前在Photoshop里调好颜色,导出LUT文件。运行时直接加载,一秒切换风格。暖色调、冷色调、复古风、日系风……随便换。

注意:LUT文件不要太大。512x512的LUT已经够用,再大就是浪费显存。另外,LUT的精度是8位,如果你需要更高的色彩精度,可以考虑10位LUT,但移动端支持有限。

19.4 性能与画质的平衡

实时美颜最头疼的问题是什么?性能和画质的平衡。你美颜效果再好,如果掉帧到15fps,用户一样骂娘。

我总结了几条经验:

策略 说明 适用场景
降低分辨率 美颜在低分辨率上做,最后再上采样 移动端、低端设备
减少pass次数 能一个pass搞定就别用两个 所有场景
纹理复用 不要每帧创建新纹理,用FBO复用 所有场景
精度降级 从float16降到float8 移动端GPU
关键点缓存 人脸关键点不用每帧都检测,可以隔帧检测 低端设备

我个人习惯的做法是:先检测设备性能。如果是高端手机,直接上全精度、多pass。如果是低端设备,把分辨率降到720p,美颜强度也调低一些。

还有一个坑:纹理上传。CPU到GPU的纹理上传是很慢的。我曾经在项目里每帧都上传LUT纹理,结果性能直接腰斩。后来改成初始化时一次性上传,后面只传uniform参数,问题就解决了。

19.5 整体架构图

下面这张图展示了实时美颜的完整流程。从摄像头采集到最终渲染,每一步都在GPU上完成。

实时美颜与滤镜 GPU 管线架构 摄像头采集 YUV → RGB 纹理 人脸关键点检测 68/106 关键点 美颜处理 皮肤平滑 / 瘦脸 / 大眼 LUT 滤镜 性能优化策略 降低分辨率 减少pass次数 纹理复用(FBO) 精度降级 关键点缓存 最终渲染输出

从图上你能看到,整个管线是串行的。摄像头采集后,先做人脸检测,再根据关键点做美颜,最后套LUT滤镜。性能优化策略贯穿始终,每个环节都可以做降级处理。

19.6 避坑指南

最后,分享几个我踩过的坑:

  • 纹理坐标搞反:OpenGL ES的纹理坐标原点在左下角,而摄像头数据通常是从左上角开始的。不处理好这个映射,美颜效果会歪掉。
  • 多线程同步:人脸检测在CPU上做,美颜在GPU上做。两者之间的数据同步要用信号量或者Fence,别用简单的sleep等待。
  • 内存泄漏:每帧都创建新的纹理对象,但不释放。跑几分钟后内存就爆了。记得用FBO(帧缓冲对象)复用纹理。
  • LUT精度:有些LUT文件是16位的,但WebGL只支持8位纹理。加载前要做精度转换,否则颜色会偏。

我曾经在一个项目里,因为纹理坐标搞反,美颜效果变成了「镜像脸」——左脸瘦了,右脸胖了。调试了整整一天才发现是坐标映射的问题。嗯,从那以后我每次写纹理采样都会先检查坐标方向。

好了,这一章的内容就到这里。实时美颜的核心就是GPU加速 + 人脸关键点 + LUT滤镜。性能优化是永远的主题,没有银弹,只有根据设备做权衡。

公众号:蓝海资料掘金营,微信deep3321