图像处理|Android平台美颜相机/Camera实时滤镜/视频编解码/影像后期/人脸技术探索——2.4 滤镜以及配套代码的制作方法

Github项目地址
好久没有更新了,不行不行,怎么可以太监呢(`⌒′メ)
滤镜结构 滤镜主要是对于图像的处理,关于一款滤镜的制作方法可以看这里
既然是图像处理,那么滤镜的操作就主要是:卷积、像素映射、坐标映射,反映到具体效果上,就是模糊锐化,覆盖层(贴纸等),RGB曲线调整,旋转缩放扭曲之类的。
嗯,就这么简单。
图像处理可以使用CPU来进行,但是由于我们每次只对图像的一小部分进行处理,因此可以考虑用并行的方式进行加速,这是典型的单指令(滤镜)多数据(图像),这个时候GPU就派上用场了,在移动平台上,我们可以使用最通用的OpenGL来利用GPU的计算性能。而我们需要付出的代价就是将之前的图像处理算法使用OpenGL能够理解的方式进行重写,着色器语言(OpenGL Shading Language)就是我们的工具。
让一款滤镜可以使用 我们知道了滤镜是怎么制作的,但是要如何让滤镜可以使用呢?例如实时用这个滤镜处理相机的预览结果并且显示出来。
以Android平台和OpenGL ES2.0+为例,我们可以发现主流滤镜的结构大概是这个样子:

  • 顶点着色器(vertex_shader)
  • 片元着色器(fragment_shader)
  • 颜色映射表/素材纹理(texture)
  • 对应控制代码
一个个来看吧。
顶点着色器(vertex_shader)
顶点着色器在一款滤镜中往往是不变的,一个标准的顶点着色器(图像,2D纹理)长这样:
attribute vec4 aPosition; attribute vec4 aTextureCoord; varying vec2 vTextureCoord; void main() { gl_Position = aPosition; vTextureCoord = aTextureCoord.xy; }

是不是异常简单?aPosition是顶点坐标,aTextureCoord是纹理坐标,vTextureCoord是用来向片元着色器传递纹理坐标用的,片元着色器会根据这个坐标对图片进行取样,然后进行处理,然后我们就完成了图像一个小区域的处理,GPU会自动对于纹理的所有小区域进行处理,完成滤镜的操作。
片元着色器(fragment_shader)
片元着色器是一款滤镜的核心,这其实就是我们的图像处理算法的描述,只不过我们现在用glsl的方式表述出来。
如果我们不需要对于图像进行任何处理,可以这样写:
precision mediump float; varying vec2 vTextureCoord; uniform sampler2D sTexture; void main() { gl_FragColor=texture2D(sTexture, vTextureCoord); }

是不是更简单?vTextureCoord是片元着色器传递过来的纹理坐标,sTexture就是我们的源图像了(相机预览、视频播放)。gl_FragColor是OpenGL的内置变量,他是一个vec4类型,代表当前片元的RGBA值,每个元素都是0-1的浮点数。
片元不一定是像素,他可能是相邻的好几个像素的集合,纹理的坐标是浮点数,而片元的中心是两个像素之间的中点。不过如果我们不使用glsl来做通用计算,而只是做图像处理的话,不需要特别在意这一点。
再分析一个高斯模糊的代码:
precision lowp float; precision lowp int; varying vec2 vTextureCoord; uniform sampler2D sTexture; varying vec2 blurCoordinates[5]; void main() { vec4 original = texture2D(sTexture, vTextureCoord); lowp vec4 sum = vec4(0.0); sum += texture2D(sTexture, blurCoordinates[0]) * 0.204164; sum += texture2D(sTexture, blurCoordinates[1]) * 0.304005; sum += texture2D(sTexture, blurCoordinates[2]) * 0.304005; sum += texture2D(sTexture, blurCoordinates[3]) * 0.093913; sum += texture2D(sTexture, blurCoordinates[4]) * 0.093913; gl_FragColor = vec4(sum.xyz,1.0); gl_FragColor=vec4(mix(gl_FragColor.rgb, vec3(0.0), 0.02), gl_FragColor.a); }

因为这里我们需要进行卷积操作,blurCoordinates就是相邻片元的坐标,lowp 代表low precision,因为在这个片元着色器中我们不是很在意处理精度(本来就是模糊操作嘛),我们按照高斯函数的系数对于周围几个片元进行加权平均,就得到了当前片元应该有的像素。
颜色映射表/素材纹理(texture) 如果我们的滤镜有其他素材,例如贴纸,RGB映射表,要怎么传递给OpenGL呢?
常见的方法是使用纹理(Texture),纹理可以看成一幅图像,也可以看做是一个二维(或者更高)数组,里面存储我们需要的数据,一些常见的纹理素材像这样:
图像处理|Android平台美颜相机/Camera实时滤镜/视频编解码/影像后期/人脸技术探索——2.4 滤镜以及配套代码的制作方法
文章图片

图像处理|Android平台美颜相机/Camera实时滤镜/视频编解码/影像后期/人脸技术探索——2.4 滤镜以及配套代码的制作方法
文章图片

图像处理|Android平台美颜相机/Camera实时滤镜/视频编解码/影像后期/人脸技术探索——2.4 滤镜以及配套代码的制作方法
文章图片

当然,我们也不一定要使用图片作为纹理素材,也可以直接用byte数组进行编码,之后再转换成纹理,本质上是一样的,像这样:
public class MxProFilter extends MxOneHashBaseFilter { public MxProFilter(Context context) { super(context, "filter/fsh/mx/mx_pro.glsl"); rgbMap = new int[]{ 0, 1, 3, 4, 6, 7, 9, 10, 12, 13, 15, 17, 18, 20, 21, 23, 24, 26, 27, 29, 30, 32, 33, 35, 36, 38, 39, 41, 42, 44, 45, 47, 48, 50, 51, 53, 54, 56, 57, 59, 60, 61, 63, 64, 66, 67, 69, 70, 71, 73, 74, 75, 77, 78, 79, 81, 82, 83, 85, 86, 87, 89, 90, 91, 92, 94, 95, 96, 97, 99, 100, 101, 102, 103, 105, 106, 107, 108, 109, 111, 112, 113, 114, 115, 116, 117, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 167, 168, 169, 170, 171, 172, 173, 174, 174, 175, 176, 177, 178, 179, 179, 180, 181, 182, 183, 184, 184, 185, 186, 187, 188, 188, 189, 190, 191, 192, 192, 193, 194, 195, 195, 196, 197, 198, 198, 199, 200, 201, 202, 202, 203, 204, 204, 205, 206, 207, 207, 208, 209, 210, 210, 211, 212, 213, 213, 214, 215, 215, 216, 217, 217, 218, 219, 219, 220, 221, 222, 222, 223, 224, 224, 225, 226, 226, 227, 228, 228, 229, 230, 230, 231, 232, 232, 233, 234, 234, 235, 236, 236, 237, 238, 238, 239, 240, 240, 241, 242, 242, 243, 243, 244, 245, 245, 246, 247, 247, 248, 249, 249, 250, 251, 251, 252, 253, 253, 254, 255 }; } }

对应控制代码 前面提到的坐标和纹理都是需要我们自行设置的,什么时候绘制,绘制什么内容也需要我们自行控制,对应的控制代码可以是c也可以是Java,因为本来Android上的OpenGL ES就是一个简单的Java封装,所以实际上将Java的OpenGL代码换成C以后效率不会有明显的变化。
动态加载新滤镜 基于前面的讨论,我们发现每个滤镜最主要的不同就是片元着色器,而且着色器代码都是在运行的时候动态编译的。
(以下代码仅供参考)
如果我们要加载的滤镜只有片元着色器不同,那么就很简单了,我们的Java代码可以使用同一套,像这样:
package com.martin.ads.omoshiroilib.filter.base; import android.content.Context; import android.opengl.GLES20; import com.martin.ads.omoshiroilib.glessential.program.GLSimpleProgram; import com.martin.ads.omoshiroilib.util.TextureUtils; /** * Created by Ads on 2017/1/31. */public class SimpleFragmentShaderFilter extends AbsFilter {protected GLSimpleProgram glSimpleProgram; public SimpleFragmentShaderFilter(Context context, final String fragmentShaderPath) { super("SimpleFragmentShaderFilter"); glSimpleProgram=new GLSimpleProgram(context, "filter/vsh/base/simple.glsl",fragmentShaderPath); }@Override public void init() { glSimpleProgram.create(); }@Override public void onPreDrawElements() { super.onPreDrawElements(); glSimpleProgram.use(); plane.uploadTexCoordinateBuffer(glSimpleProgram.getTextureCoordinateHandle()); plane.uploadVerticesBuffer(glSimpleProgram.getPositionHandle()); }@Override public void destroy() { glSimpleProgram.onDestroy(); }@Override public void onDrawFrame(int textureId) { onPreDrawElements(); TextureUtils.bindTexture2D(textureId, GLES20.GL_TEXTURE0,glSimpleProgram.getTextureSamplerHandle(),0); GLES20.glViewport(0,0,surfaceWidth,surfaceHeight); //Log.d(TAG, "onDrawFrame: "+surfaceWidth+" "+surfaceHeight); plane.draw(); } }

如果我们还有多个纹理,那么我们的代码依然可以用同一套,像这样:
package com.martin.ads.omoshiroilib.filter.base; import android.content.Context; import android.opengl.GLES20; import com.martin.ads.omoshiroilib.glessential.texture.BitmapTexture; import com.martin.ads.omoshiroilib.util.TextureUtils; /** * Created by Ads on 2017/4/6. * Textures are numbered from 2-N */public abstract class MultipleTextureFilter extends SimpleFragmentShaderFilter { protected BitmapTexture[] externalBitmapTextures; protected int[] externalTextureHandles; protected int textureSize; protected Context context; public MultipleTextureFilter(Context context, String fragmentShaderPath) { super(context, fragmentShaderPath); this.context=context; textureSize=0; }@Override public void init() { super.init(); externalBitmapTextures=new BitmapTexture[textureSize]; for(int i=0; i

如果我们的滤镜中顶点着色器还不一样,就意味着我们需要对于一些uniform和attribute变量进行配置,那么就需要不同的java代码了。
如果我们的滤镜代码和纹理素材来自网络怎么办?
其实并没有任何的区别,不是么?
【图像处理|Android平台美颜相机/Camera实时滤镜/视频编解码/影像后期/人脸技术探索——2.4 滤镜以及配套代码的制作方法】回到目录

    推荐阅读