题记:前文介绍了着色器基础,本章可以轻松一些了。本文会介绍一些常用滤镜以及工作原理,结合GPUImage进入着色器的实战篇。 音视频学习Demo有OpenGL在相机、视频方面的应用,文章或代码若有错误,也希望大佬不吝赐教。
一、风格滤镜
风格滤镜是发布器或者美颜相机的常见功能,原理来说也是比较简单的,来一看GPUImage中的滤镜实现GPUImageLookupFilter
。
1.1 GPUImageLookupFilter原理
Lookup顾名思义,就是查找颜色的意思,那么再看提供的查找表,不同风格对应不同的图片,看一下lookup.png
原图是512X512
大小,由图像可知就是横竖8X8
的格子,把每个格子叠起来看如图。
这样结合上图就可以容易看出,其实就是RGB 3维的坐标系。
- 第一个格子的第一个像素点对应(0,0,0),就是黑色
- 最后一个格子的最后一个像素对应(1,1,1),就是白色
- 第一个格子的右上角对应(1,0,0),就是红色
- 第一个格子的左下角对应(0,1,0),就是绿色
- 最后一个格子的左上角对应(0,0,1),就是蓝色
这么看来就是找到颜色的对应位置,把原来的256X256X256
映射到64X64X64
即可。
1.2 GPUImageLookupFilter着色器
- 根据上面的描述,需要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上表现,如下图。
原理上比较简单,可以参考前文中OpenGL基础-坐标系变换,这里就不过多解释了。
2.1 分屏
分屏是相机特效中常见的功能,也是比较简单的应用,但它是理解很多形变例如瘦身的基础。
着色器中一般不推荐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 美型基础
美型是需要把图像按需求做局部的调整,例如下图将中间区域压缩。
2.3.1 着色器效果
来看下效果,原本均匀的横线,只处理了下半区(纹理坐标的【0, 0.5】),方便对比。
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的像素,那么图像会变稀疏。
2.3.2 其他
事实上我们可以尝试不同的变换,也不一定用抛物线函数,其它如正余弦函数都是可以的,看一下2次抛物线函数效果。具体要用哪种变换取决于实际设计效果。
对应的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));
}
例子很简单,但是如果结合身体点位就可以做出效果啦。
三、滤波
这里说滤波其实来源于傅里叶变换内容,在介绍编码原理中讲到过傅里叶变换的低通滤波。在图片处理上高频代表变化剧烈,也就是物体边界部分。那么高通就是提取特征,低通就是模糊。
注意这里不同于GPUImage中的GPUImageHighPassFilter
和GPUImageLowPassFilter
,GPU的高通低通指的是视频前后帧的处理,这里指图片自身的处理。 低通高通展示:
编码原理提供过低通实现,高通只需要稍微变动一下代码,调整threshold
即可
# 创建一个与dct_matrix形状相同的掩膜
mask = np.ones(dct_matrix.shape, dtype=np.float32)
# 将低频部分置为0
mask[:threshold, :threshold] = 0
而OpenGL滤波通常不是频率变化实现的,而是通过卷积,做法上更容易理解。
3.1 低通-高斯模糊
模糊的效果就是和周围颜色调和,如下图:
这里介绍高斯模糊,按照高斯分布(正态分布)的方式计算权重:
标准差取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边缘检测
效果经过了两层,亮度滤镜和SobelFilter,因为人眼对亮度更敏感,很多检测会选择使用亮度。SobelFilter和高斯模糊在算法都是卷积计算。Sobel算子如下,分为两个方向,再在两个方向求长度。
根据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);
}