目录
1、概述
本文介绍一个在Android上面实现的一个实时调整滤镜参数的播放器例子。市面上一些美颜,画质增强等播放器的大致原理都是如此。通过调整图1中的三个参数就可以实时看到画面颜色、明暗、艳丽程度的变化。

2、模块原理
这个例子可以分为三个模块:解码播放、显示渲染、颜色参数调整转化。本文重点是颜色滤镜参数的调整。
2.1、 播放器
这里采用的是系统的媒体播放器MediapPayer播放一个放在工程raw资源路径下的MP4文件。播放代码如下:
private void setupPlayer() {
try {
mMediaPlayer = MediaPlayer.create(this, R.raw.testfile);
mMediaPlayer.setSurface(mSurface);
mMediaPlayer.setLooping(true);
} catch (Exception e) {
e.printStackTrace();
}
mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mediaPlayer) {
mMediaPlayer.start();
}
});
}
这个播放器的输入是testfile.mp4,解码出来的画面会通过Surface传给渲染层。
2.2、 显示渲染
这里渲染采用的是OpenGl ES 2.0,由于跨平台,兼容性好被广泛应用。Android对OpenGL ES支持非常好,可以在JNI中开发也有对应的JAVA接口,本文采用的是JAVA。
下面是使用OpenGL ES的步骤:
1、在layout文件中配置一个GLSurfaceView
<android.opengl.GLSurfaceView
android:id="@+id/video_view"
android:layout_width="match_parent"
android:layout_height="320dp" />
2、在Activity文件中设置GlSurfaceView。这里主要是设置Render回调,onSurfaveCreate是初始化一些值,onDrawFrame是画每一帧。
mVideoView = findViewById(R.id.video_view);
mVideoView.setEGLContextClientVersion(2);
mVideoView.setRenderer(new GLSurfaceView.Renderer() {
@Override
public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
mReactShape = new RectShape();
int textureId = GLUtil.generateOESTexture();
mSurfaceTexture = new SurfaceTexture(textureId);
mSurfaceTexture.setOnFrameAvailableListener(new SurfaceTexture.OnFrameAvailableListener() {
@Override
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
mFrameAvailable = true;
}
});
mSurface = new Surface(mSurfaceTexture);
mReactShape.setTextureId(textureId);
setupPlayer();
}
@Override
public void onSurfaceChanged(GL10 gl10, int i, int i1) {
}
@Override
public void onDrawFrame(GL10 gl10) {
if (mFrameAvailable) {
mSurfaceTexture.updateTexImage();
mFrameAvailable = false;
}
float[] colorFilter = mColorFilterMatrixUtil.getColorFilterArray16();
mReactShape.setColorFilterArray(colorFilter);
mReactShape.draw();
}
});
3、在第二步是配置渲染的线程(GLSurfaceView 有自己独立的线程),具体内容显示还需要一个载体。这里采用的是一个矩形的ReactShape来画每一帧。
package com.test.videocolorfilter;
import android.opengl.GLES11Ext;
import android.opengl.GLES20;
import android.opengl.Matrix;
import android.util.Log;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
public class RectShape {
private float width = 2;
private float height = 2;
private int mTextureId = 0;
private int mProgram = -1;
private int mPositionHandle;
private int mTexCoordHandle;
private int mMVPHandle;
private int mColorFilterHandle;
private FloatBuffer mVertices, mTexCoord;
private float[] mModelProjection = new float[16];
private float[] mColorFilterArray = new float[16];
private final static String VERTEX_SHADER =
"uniform mat4 uMVPMatrix;\n" +
"attribute vec4 a_Position;\n" +
"attribute vec2 aTexCoor;\n" +
"varying vec2 vTextureCoord;\n" +
"uniform float uAngle;\n" +
"void main(){\n" +
" gl_Position = uMVPMatrix * a_Position;\n" +
" vTextureCoord = aTexCoor;\n" +
"}\n";
private final static String FRAGMENT_SHADER =
"#extension GL_OES_EGL_image_external : require\n" +
"precision highp float;\n" +
"varying vec2 vTextureCoord;\n" +
"uniform samplerExternalOES uTexture;\n" +
"uniform mat4 uColorFilterMatrix;\n" +
"void main() {\n" +
" vec4 val = texture2D(uTexture, vTextureCoord); \n" +
" gl_FragColor = val*uColorFilterMatrix; \n" +
"}\n";
public RectShape() {
init();
}
protected void init() {
Matrix.setIdentityM(mModelProjection, 0);
Matrix.setIdentityM(mColorFilterArray, 0);
float[] vertices = new float[]{
-width / 2, -height / 2, 0,
width / 2, height / 2, 0,
-width / 2, height / 2, 0,
width / 2, height / 2, 0,
-width / 2, -height / 2, 0,
width / 2, -height / 2, 0,
};
ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4);
vbb.order(ByteOrder.nativeOrder());
mVertices = vbb.asFloatBuffer();
mVertices.put(vertices);
mVertices.position(0);
// create program
mProgram = GLUtil.createProgram(VERTEX_SHADER, FRAGMENT_SHADER);
mPositionHandle = GLES20.glGetAttribLocation(mProgram, "a_Position");
mTexCoordHandle = GLES20.glGetAttribLocation(mProgram, "aTexCoor");
mMVPHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
mColorFilterHandle = GLES20.glGetUniformLocation(mProgram, "uColorFilterMatrix");
//setup buffers for the texture 2D coordinates
float[] texCoord = new float[]{
0.0f, 1.0f,
1.0f, 0.0f,
0.0f, 0.0f,
1.0f, 0.0f,
0.0f, 1.0f,
1.0f, 1.0f,
};
ByteBuffer cbb = ByteBuffer.allocateDirect(texCoord.length * 4);
cbb.order(ByteOrder.nativeOrder()); //set the byte order
mTexCoord = cbb.asFloatBuffer(); //convert to byte buffer
mTexCoord.put(texCoord); //load data to byte buffer
mTexCoord.position(0); //set the start point
}
public void setTextureId(int textureId) {
mTextureId = textureId;
}
public void draw() {
draw(mTextureId);
}
public void draw(int textureId) {
// Log.d("RectShape", "draw "+ textureId);
GLES20.glUseProgram(mProgram);
GLES20.glUniformMatrix4fv(mMVPHandle, 1, false, mModelProjection, 0);
GLES20.glUniformMatrix4fv(mColorFilterHandle, 1, false, mColorFilterArray, 0);
if (textureId > 0) {
// render texture
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId);
}
// draw triangles
GLES20.glVertexAttribPointer(mPositionHandle, 3, GLES20.GL_FLOAT, false, 0, mVertices);
GLES20.glEnableVertexAttribArray(mPositionHandle);
GLES20.glVertexAttribPointer(mTexCoordHandle, 2, GLES20.GL_FLOAT, false, 0, mTexCoord);
GLES20.glEnableVertexAttribArray(mTexCoordHandle);
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, mVertices.capacity() / 3);
}
public void setColorFilterArray(float filter[]) {
System.arraycopy(filter, 0, mColorFilterArray, 0, 16);
}
public void release() {
if (mProgram >= 0) {
GLES20.glDeleteProgram(mProgram);
mProgram = -1;
}
}
}
这个类最主要的是部分是两个shader和draw()函数。
shader采用的是GLSL语言,在openGL ES 2.0以后开始使用,分为顶点着色器和片元着色器;顶点着色器指定了顶点转化关系和几何姿态的调整以及投影关系;片元着色器指定的是像素色彩以及光照等变化,这部分通过编译最终执行在GPU中。
draw()函数是将纹理,几何坐标,纹理坐标、姿态矩阵、颜色矩阵等值传给shader。这些值可以每一帧都不一样。
2.3、 颜色滤镜
看道这篇文章的同学应该都比较喜欢或熟悉用RGB来描述图像,RGB三基色描述在图像数字化确实比较方便。但是对于眼睛或者设计师调整却并不方便。在滤镜调整过程中大家其实更偏向另外一种颜色模型HSL(hue,saturation,lightness)下图是HSL的原理模型:

Android对HSL调整有很好的支持,在SDK的android.graphic包中有一个ColorMatrix类。这个类时管理一个5x4的颜色矩阵,支持色相、饱和度、明度调整接口。ColorMatrix T表示如下:
颜色C表示如下:
新颜色C`
R' = a*R + b*G + c*B + d*A + e;
G' = f*R + g*G + h*B + i*A + j;
B' = k*R + l*G + m*B + n*A + o;
A' = p*R + q*G + r*B + s*A + t;
从上图模型可以看出,hue(色相)是调整颜色属性,直观地说就是让颜色偏绿点还是偏红点,效果如下图:

saturation(饱和度)指的是色彩纯度。是色彩的构成要素之一。纯度越高,表现越鲜明,纯度较低,表现则较黯淡。效果如下图;

lightness(明度)是调整颜色的能量程度,直白说就是黑还是白,调整效果如下图:

3、总结
- 本文的滤镜只是采用的颜色调整,还没有实现锐化、模糊、马赛克等像素位置变化的效果。
- 播放器采用的最简单的本地MediaPlayer对于一些直播可能需要替换支持跟多协议的播放器如ijkplayer。
- 本文的配套的工程github地址https://github.com/liyang-hello/VideoColorFilter,如有兴趣可以下载。