19、OpenGL ES视频滤镜:Lookup Table(LUT)滤镜、卷积滤镜(模糊/锐化)、美颜滤镜基础

视频滤镜,说白了就是给每一帧画面“化妆”。

我在做视频SDK的时候,发现很多同学一上来就怼美颜算法,结果连最基础的LUT和卷积都没搞明白。嗯,今天我们就从这三个核心方向入手,把视频滤镜的底子打扎实。

19.1 Lookup Table(LUT)滤镜:颜色映射的“作弊器”

LUT滤镜,本质上是一个颜色映射表。你输入一个颜色值,它输出另一个颜色值。整个过程就是一个查表操作,速度极快。

核心原理:

对于RGBA每个通道(通常是0-255),LUT表预先存储了映射后的结果。渲染时,像素的原始颜色作为索引,直接取出目标颜色。

我个人习惯把LUT分为两类:

  • 1D LUT:每个通道独立映射。简单,但无法处理通道间的交叉影响。
  • 3D LUT:将RGB三个通道组合成一个三维坐标,查表得到新的RGB。效果更丰富,电影级调色基本都用它。

在OpenGL ES中实现LUT滤镜,核心就是一张纹理。我们把LUT数据(通常是64x64或16x16的格子图)加载到纹理中,然后在片段着色器里用原始颜色去采样这张纹理。

我曾经在项目里踩过一个坑:LUT纹理的采样精度。如果你用GL_NEAREST,颜色过渡会有明显的断层。后来我改成了GL_LINEAR,效果就平滑多了。

避坑指南:

我曾经在低端机上用3D LUT,发现帧率掉得厉害。后来排查发现是纹理采样次数太多。优化方案:把3D LUT预计算成2D纹理,或者降低LUT的分辨率(比如从64x64降到32x32),肉眼几乎看不出区别。

19.2 卷积滤镜:模糊与锐化的数学魔法

卷积滤镜,说白了就是用一个“小窗口”在图像上滑动,每个像素的新值由它周围像素加权平均得到。这个“小窗口”就是卷积核(Kernel)。

为什么卷积能实现模糊和锐化?你想想看:

  • 模糊:卷积核里所有权重都是正数,且加起来等于1。相当于把周围像素“拉”过来平均,边缘自然就模糊了。
  • 锐化:卷积核中心权重很大,周围权重为负。这样能突出像素与周围的差异,边缘就变得更锐利。

在OpenGL ES里实现卷积,我一般用texelFetch或者textureOffset来采样周围像素。但要注意,纹理坐标的偏移量需要根据纹理尺寸计算,否则会采样到错误的位置。

下面是一个3x3高斯模糊的片段着色器核心代码:

// 高斯模糊 3x3 卷积核
const float kernel[9] = float[](
    1.0/16, 2.0/16, 1.0/16,
    2.0/16, 4.0/16, 2.0/16,
    1.0/16, 2.0/16, 1.0/16
);

void main() {
    vec2 texOffset = 1.0 / uTextureSize; // 根据纹理尺寸计算偏移
    vec3 color = vec3(0.0);
    for(int i = 0; i < 3; i++) {
        for(int j = 0; j < 3; j++) {
            vec2 coord = vTexCoord + vec2(i-1, j-1) * texOffset;
            color += texture2D(uTexture, coord).rgb * kernel[i*3+j];
        }
    }
    gl_FragColor = vec4(color, 1.0);
}

注意:

卷积核越大,性能开销越大。5x5的卷积需要采样25次,7x7就是49次。我在项目中通常把大卷积核拆成两个1D卷积(水平和垂直),这样采样次数从N²降到2N,性能提升非常明显。

19.3 美颜滤镜基础:从磨皮到美白

美颜滤镜,说白了就是模糊+颜色调整的组合拳。但直接全图模糊会丢失细节,所以需要“保边滤波”。

我刚开始做美颜时,直接上了高斯模糊,结果人脸像塑料一样。后来才明白,真正的美颜需要做三件事:

  1. 磨皮(皮肤平滑):用保边滤波(如双边滤波、引导滤波)只平滑皮肤区域,保留眼睛、眉毛等细节。
  2. 美白(肤色调整):通过颜色映射或曲线调整,让皮肤更白皙。
  3. 祛痘/去瑕疵:用中值滤波或更高级的修复算法。

在OpenGL ES里实现基础美颜,我一般用双边滤波。它和高斯模糊的区别在于:不仅考虑空间距离,还考虑像素值的差异。这样在皮肤区域平滑,在边缘处停止。

下面是一个简化的双边滤波片段着色器:

uniform sampler2D uTexture;
uniform vec2 uTexOffset;
uniform float uSigmaSpatial;  // 空间标准差
uniform float uSigmaRange;    // 值域标准差

void main() {
    vec3 center = texture2D(uTexture, vTexCoord).rgb;
    vec3 sum = vec3(0.0);
    float totalWeight = 0.0;
    
    for(int i = -2; i <= 2; i++) {
        for(int j = -2; j <= 2; j++) {
            vec2 coord = vTexCoord + vec2(i, j) * uTexOffset;
            vec3 sample = texture2D(uTexture, coord).rgb;
            
            float spatialDist = float(i*i + j*j);
            float rangeDist = length(sample - center);
            
            float weight = exp(-spatialDist / (2.0 * uSigmaSpatial * uSigmaSpatial))
                         * exp(-rangeDist / (2.0 * uSigmaRange * uSigmaRange));
            
            sum += sample * weight;
            totalWeight += weight;
        }
    }
    
    gl_FragColor = vec4(sum / totalWeight, 1.0);
}

个人经验:

双边滤波在GPU上性能开销较大。我后来改用“快速双边滤波”方案:先降采样,再滤波,最后上采样。这样在1080p视频上也能跑到60fps。另外,美颜强度不要拉满,否则会失去真实感。我一般把磨皮强度控制在0.3-0.6之间。

19.4 本章知识体系

下面这张图总结了三种滤镜的核心逻辑和关系:

视频滤镜核心知识体系 LUT滤镜 卷积滤镜 美颜滤镜 1D LUT / 3D LUT 纹理采样 + 颜色映射 线性插值避免断层 模糊(高斯/均值) 锐化(拉普拉斯/USM) 分离卷积优化性能 双边滤波(保边平滑) 肤色调整 + 美白 降采样加速方案 核心:纹理采样 → 像素运算 → 性能优化

从图中可以看出,三种滤镜虽然应用场景不同,但底层都离不开纹理采样和像素运算。LUT是查表映射,卷积是加权平均,美颜是保边滤波+颜色调整。理解了这些,你就能组合出更复杂的滤镜效果。

好了,这一章的内容就到这里。LUT、卷积、美颜这三个方向,每一个都值得深入。下一章我们会继续深入OpenGL ES的视频渲染实战,敬请期待。


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