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

时间:2023-02-07 20:30:34

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<textureSize;i++){
externalBitmapTextures[i]=new BitmapTexture();
}
externalTextureHandles=new int[textureSize];
for(int i=0;i<textureSize;i++){
externalTextureHandles[i] =
GLES20.glGetUniformLocation(glSimpleProgram.getProgramId(),"sTexture"+(i+2));
}
}

@Override
public void destroy() {
glSimpleProgram.onDestroy();
for(BitmapTexture bitmapTexture:externalBitmapTextures){
bitmapTexture.destroy();
}
}

@Override
public void onPreDrawElements() {
super.onPreDrawElements();
for (int i = 0; i < textureSize; i++) {
TextureUtils.bindTexture2D(
externalBitmapTextures[i].getImageTextureId(),
GLES20.GL_TEXTURE0+(i+1),
externalTextureHandles[i],
i+1
);
}
}

@Override
public void onDrawFrame(int textureId) {
onPreDrawElements();
TextureUtils.bindTexture2D(textureId, GLES20.GL_TEXTURE0,glSimpleProgram.getTextureSamplerHandle(),0);
GLES20.glViewport(0,0,surfaceWidth,surfaceHeight);
plane.draw();
}
}

如果我们的滤镜中顶点着色器还不一样,就意味着我们需要对于一些uniform和attribute变量进行配置,那么就需要不同的java代码了。

如果我们的滤镜代码和纹理素材来自网络怎么办?
其实并没有任何的区别,不是么?

回到目录