音视频学习笔记十四——渲染与滤镜之着色器实战

88 阅读7分钟

题记:前文介绍了着色器基础,本章可以轻松一些了。本文会介绍一些常用滤镜以及工作原理,结合GPUImage进入着色器的实战篇。 音视频学习Demo有OpenGL在相机、视频方面的应用,文章或代码若有错误,也希望大佬不吝赐教。

一、风格滤镜

风格滤镜截图.jpg

风格滤镜是发布器或者美颜相机的常见功能,原理来说也是比较简单的,来一看GPUImage中的滤镜实现GPUImageLookupFilter

1.1 GPUImageLookupFilter原理

Lookup顾名思义,就是查找颜色的意思,那么再看提供的查找表,不同风格对应不同的图片,看一下lookup.png

lookup.png

原图是512X512大小,由图像可知就是横竖8X8的格子,把每个格子叠起来看如图。

滤镜查找表.jpg

这样结合上图就可以容易看出,其实就是RGB 3维的坐标系。

  • 第一个格子的第一个像素点对应(0,0,0),就是黑色
  • 最后一个格子的最后一个像素对应(1,1,1),就是白色
  • 第一个格子的右上角对应(1,0,0),就是红色
  • 第一个格子的左下角对应(0,1,0),就是绿色
  • 最后一个格子的左上角对应(0,0,1),就是蓝色

这么看来就是找到颜色的对应位置,把原来的256X256X256映射到64X64X64即可。

1.2 GPUImageLookupFilter着色器

  1. 根据上面的描述,需要2个纹理——输入图像和loopup图片。
uniform sampler2D inputImageTexture;
uniform sampler2D inputImageTexture2; // lookup texture

2. 对纹理进行采样

highp vec4 textureColor = texture2D(inputImageTexture, textureCoordinate);

3. 根据textureColor的颜色找到对应的映射颜色(由于读入图片存储左上为开始点,相当于读入的纹理是上下颠倒的,GPUImage处理时理解原点在左上

  • RBG的范围为0-1,分为64等分,相当于量化一下 r * 63,原图宽512,所以红色在小块上的分量为(r * 63 + 0.5) / 512,0.5是考虑四舍五入(离那个像素块更近)。

  • 绿色的计算同理 (g * 63 + 0.5) / 512

  • 蓝色的计算是需要考虑在哪个格子上,floor/ceil(b * 63) / 8.0得到行数,floor/ceil(b * 63) % 8得到列数。这里之所以用ceil或者floor,也是考虑量化时哪个更近一些。 - 更新纹理坐标,这里计算了2次,相当于考虑前后两个邻居格子

     highp vec2 texPos1;
     texPos1.x = (quad1.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.r);
     texPos1.y = (quad1.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.g);
     
     highp vec2 texPos2;
     texPos2.x = (quad2.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.r);
     texPos2.y = (quad2.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.g);
    
  • 最后获取两个邻居格子的颜色值,再用mix函数取到最后的值。fract为小数部分
    lowp vec4 newColor1 = texture2D(inputImageTexture2, texPos1);
    lowp vec4 newColor2 = texture2D(inputImageTexture2, texPos2);
    lowp vec4 newColor = mix(newColor1, newColor2, fract(blueColor));

4. 赋值输出,intensity表明滤镜的影响程度,0是原图,1是映射图

gl_FragColor = mix(textureColor, vec4(newColor.rgb, textureColor.w), intensity)

二、形变

形变就是改变原始图像的形状,例如旋转、分屏、瘦身等。如旋转只需要改变输入点坐标;分屏和瘦身可以在片元着色器处理,也可以改变顶点输入在顶点着色器处理。

2.1 旋转

旋转是在特效中常见的效果,可以在2D或3D上表现,如下图。

image.png

原理上比较简单,可以参考前文中OpenGL基础-坐标系变换,这里就不过多解释了。

2.1 分屏

分屏是相机特效中常见的功能,也是比较简单的应用,但它是理解很多形变例如瘦身的基础。

image.png

着色器中一般不推荐if,大家有兴趣可以使用step改写一下,另外可以使用uniform传入几分屏。这里为了让逻辑看起来清晰保留

precision highp float;
uniform sampler2D Texture;
varying highp vec2 TextureCoordsVarying;

void main() {
    vec2 uv = TextureCoordsVarying.xy;
    float y;
    if (uv.y >= 0.0 && uv.y <= 0.5) {
        y = uv.y + 0.25;
    } else {
        y = uv.y - 0.25;
    }
    gl_FragColor = texture2D(Texture, vec2(uv.x, y));
}

此段代码作用,上下半区显示范围都变成了【0.25,0.75】,当然也可以变为【0,1】,图片会变形。很显然如果用顶点着色器也是可以的,绘制三角形会从原来的2个变成4个,逻辑更清晰一些。这里抛砖引玉,会读者可以理解片元着色器的处理。

2.3 美型基础

美型是需要把图像按需求做局部的调整,例如下图将中间区域压缩。

压缩像素.jpg

2.3.1 着色器效果

来看下效果,原本均匀的横线,只处理了下半区(纹理坐标的【0, 0.5】),方便对比。

压缩区域2.jpg
void main (void) {
    vec2 uv = TextureCoordsVarying.xy;
    float y = uv.y;
    if (uv.y <= 0.5) {
        y = 2.0* (uv.y) * (uv.y);
    }
    gl_FragColor = texture2D(Texture, vec2(uv.x, y));
}

上述的变换实际上把原来线性的取值变成了抛物线。看曲线斜率可以知道,前半段斜率小,变换慢所以被拉长,后半段斜率大,变化快,所以被压缩。也可以具体计算值,在0-0.25的区域取了0-0.0625的像素,那么图像会变稀疏。

坐标分析.jpg

2.3.2 其他

事实上我们可以尝试不同的变换,也不一定用抛物线函数,其它如正余弦函数都是可以的,看一下2次抛物线函数效果。具体要用哪种变换取决于实际设计效果。

压缩区域.jpg

对应的shader的写法:

void main (void) {
    vec2 uv = TextureCoordsVarying.xy;
    float y = uv.y;
    if (uv.y <= 0.5) {
        if (uv.y <= 0.25) {
            y = -4.0*(uv.y - 0.25) *(uv.y - 0.25) + 0.25;
        } else {
            y = 4.0*(uv.y - 0.25) *(uv.y - 0.25) + 0.25;
        }
        
    }
    gl_FragColor = texture2D(Texture, vec2(uv.x, y));
}

例子很简单,但是如果结合身体点位就可以做出效果啦。

image.png

三、滤波

这里说滤波其实来源于傅里叶变换内容,在介绍编码原理中讲到过傅里叶变换的低通滤波。在图片处理上高频代表变化剧烈,也就是物体边界部分。那么高通就是提取特征,低通就是模糊。

2271737533703\_.pic.jpg

注意这里不同于GPUImage中的GPUImageHighPassFilterGPUImageLowPassFilter,GPU的高通低通指的是视频前后帧的处理,这里指图片自身的处理。 低通高通展示:

image.png

编码原理提供过低通实现,高通只需要稍微变动一下代码,调整threshold即可

# 创建一个与dct_matrix形状相同的掩膜
mask = np.ones(dct_matrix.shape, dtype=np.float32)
# 将低频部分置为0
mask[:threshold, :threshold] = 0

而OpenGL滤波通常不是频率变化实现的,而是通过卷积,做法上更容易理解。

3.1 低通-高斯模糊

模糊的效果就是和周围颜色调和,如下图:

模糊图片.jpg

这里介绍高斯模糊,按照高斯分布(正态分布)的方式计算权重:

image.png

标准差取2,均值为0,计算左右各4的共9个高斯权重(也可以是5个或者更多),再归一化就得到(列出0到4,-4到-1对称):

(0.227027, 0.1945946, 0.1216216, 0.054054, 0.016216)

由此,高斯模糊的公式就是如下:

void main() {
    vec4 sum = vec4(0.0);
    float weights[5] = float[](0.227027, 0.1945946, 0.1216216, 0.054054, 0.016216);
    float offsets[5] = float[](0.0, 1.0, 2.0, 3.0, 4.0);
    
    sum += texture2D(inputImageTexture, textureCoordinate) * weights[0];
    
    for (int i = 1; i < 5; i++) {
        float offset = offsets[i] * texelHeightOffset * blurRadius;
        sum += texture2D(inputImageTexture, textureCoordinate + vec2(offset, 0.0)) * weights[i];
        sum += texture2D(inputImageTexture, textureCoordinate - vec2(offset, 0.0)) * weights[i];
    }
    gl_FragColor = sum;
}

一般来说,高斯模糊需要水平和垂直方向,上述是水平方向,垂直只需要调整vec2(offset, 0.0))即可。高斯模糊就是磨皮的重要步骤。

高斯模糊就是低通滤波其中的一种,其他还有采用均值滤波、中值滤波等。

3.2 梯度-sobel边缘检测

image.png

效果经过了两层,亮度滤镜和SobelFilter,因为人眼对亮度更敏感,很多检测会选择使用亮度。SobelFilter和高斯模糊在算法都是卷积计算。Sobel算子如下,分为两个方向,再在两个方向求长度。

image.png

根据Sobel的原理,其实就是加权梯度的计算(除了Sobel还有拉普拉斯算子等)。根据原理,shader就很容易写出来了(GPUImage)

 void main()
 {
    float bottomLeftIntensity = texture2D(inputImageTexture, bottomLeftTextureCoordinate).r;
    float topRightIntensity = texture2D(inputImageTexture, topRightTextureCoordinate).r;
    float topLeftIntensity = texture2D(inputImageTexture, topLeftTextureCoordinate).r;
    float bottomRightIntensity = texture2D(inputImageTexture, bottomRightTextureCoordinate).r;
    float leftIntensity = texture2D(inputImageTexture, leftTextureCoordinate).r;
    float rightIntensity = texture2D(inputImageTexture, rightTextureCoordinate).r;
    float bottomIntensity = texture2D(inputImageTexture, bottomTextureCoordinate).r;
    float topIntensity = texture2D(inputImageTexture, topTextureCoordinate).r;
    float h = -topLeftIntensity - 2.0 * topIntensity - topRightIntensity + bottomLeftIntensity + 2.0 * bottomIntensity + bottomRightIntensity;
    float v = -bottomLeftIntensity - 2.0 * leftIntensity - topLeftIntensity + bottomRightIntensity + 2.0 * rightIntensity + topRightIntensity;
    
    float mag = length(vec2(h, v)) * edgeStrength;
    
    gl_FragColor = vec4(vec3(mag), 1.0);
 }